HEX
Server: LiteSpeed
System: Linux server315.web-hosting.com 4.18.0-553.54.1.lve.el8.x86_64 #1 SMP Wed Jun 4 13:01:13 UTC 2025 x86_64
User: globfdxw (6114)
PHP: 8.1.34
Disabled: NONE
Upload Files
File: /home/globfdxw/www/wp-content/plugins/wpforms-user-journey/src/View.php
<?php

namespace WPFormsUserJourney;

use WPForms\Emails\Helpers;

/**
 * Class View.
 *
 * @since 1.0.3
 */
class View {

	/**
	 * Inline styles of the User Journey table in emails.
	 *
	 * @since 1.0.3
	 *
	 * @var string
	 */
	const TABLE_STYLE = 'width: 100%; border-top: 1px solid #cccccc; border-left: 1px solid #cccccc; border-collapse: collapse;';

	/**
	 * Inline styles of the User Journey table cell in emails.
	 *
	 * @since 1.0.3
	 *
	 * @var string
	 */
	const CELL_STYLE = 'border-right: 1px solid #cccccc; border-bottom: 1px solid #cccccc; padding: 6px; vertical-align: top;';

	/**
	 * Default email text color used as fallback for Legacy template.
	 *
	 * @since 1.7.0
	 *
	 * @var string
	 */
	private const DEFAULT_EMAIL_TEXT_COLOR = '#333333';

	/**
	 * Default email body color used as a fallback for Legacy template.
	 *
	 * @since 1.7.0
	 *
	 * @var string
	 */
	private const DEFAULT_EMAIL_BODY_COLOR = '#ffffff';

	/**
	 * Default email links color used as fallback for Legacy template.
	 *
	 * @since 1.7.0
	 *
	 * @var string
	 */
	private const DEFAULT_EMAIL_LINKS_COLOR = '#e27730';

	/**
	 * Computed email colors for the current render cycle.
	 *
	 * @since 1.7.0
	 *
	 * @var array
	 */
	private $email_colors = [];

	/**
	 * Blend a foreground color onto a background color at the opacity given.
	 *
	 * Email clients (especially Outlook) don't reliably support rgba(),
	 * so we compute the actual hex value.
	 *
	 * @since 1.7.0
	 *
	 * @param string $fg_hex  Foreground color hex (e.g. '#333333').
	 * @param string $bg_hex  Background color hex (e.g. '#ffffff').
	 * @param float  $opacity Opacity from 0.0 to 1.0.
	 *
	 * @return string Computed hex color (e.g. '#999999').
	 */
	private static function blend_color( string $fg_hex, string $bg_hex, float $opacity ): string {

		$fg = wpforms_hex_to_rgb( $fg_hex, false );
		$bg = wpforms_hex_to_rgb( $bg_hex, false );

		if ( empty( $fg ) || empty( $bg ) ) {
			return $fg_hex;
		}

		$r = (int) round( $fg['R'] * $opacity + $bg['R'] * ( 1 - $opacity ) );
		$g = (int) round( $fg['G'] * $opacity + $bg['G'] * ( 1 - $opacity ) );
		$b = (int) round( $fg['B'] * $opacity + $bg['B'] * ( 1 - $opacity ) );

		return sprintf( '#%02x%02x%02x', $r, $g, $b );
	}

	/**
	 * Get computed email colors for the User Journey table.
	 *
	 * Returns an array of hex colors derived from the email template's
	 * text color at different opacities, for both light and dark modes.
	 *
	 * @since 1.7.0
	 *
	 * @return array {
	 *     Color arrays for light and dark modes.
	 *
	 *     @type array $light {
	 *         @type string $text_100 Timestamp, page title, date heading text.
	 *         @type string $text_50  Duration (time on page), summary text.
	 *         @type string $border   Row bottom borders (15% opacity).
	 *         @type string $date_bg  Date heading background (5% opacity).
	 *         @type string $links    Link color for internal page titles.
	 *         @type string $body     Body background color.
	 *     }
	 *     @type array $dark Same keys as $light, for dark mode.
	 * }
	 */
	public function get_email_colors(): array {

		$overrides = $this->get_style_overrides();

		$text_color       = ! empty( $overrides['email_text_color'] ) ? $overrides['email_text_color'] : self::DEFAULT_EMAIL_TEXT_COLOR;
		$body_color       = ! empty( $overrides['email_body_color'] ) ? $overrides['email_body_color'] : self::DEFAULT_EMAIL_BODY_COLOR;
		$links_color      = ! empty( $overrides['email_links_color'] ) ? $overrides['email_links_color'] : self::DEFAULT_EMAIL_LINKS_COLOR;
		$text_color_dark  = ! empty( $overrides['email_text_color_dark'] ) ? $overrides['email_text_color_dark'] : '#dddddd';
		$body_color_dark  = ! empty( $overrides['email_body_color_dark'] ) ? $overrides['email_body_color_dark'] : '#1f1f1f';
		$links_color_dark = ! empty( $overrides['email_links_color_dark'] ) ? $overrides['email_links_color_dark'] : self::DEFAULT_EMAIL_LINKS_COLOR;

		return [
			'light' => [
				'text_100' => $text_color,
				'text_50'  => self::blend_color( $text_color, $body_color, 0.5 ),
				'border'   => self::blend_color( $text_color, $body_color, 0.15 ),
				'date_bg'  => self::blend_color( $text_color, $body_color, 0.05 ),
				'links'    => $links_color,
				'body'     => $body_color,
			],
			'dark'  => [
				'text_100' => $text_color_dark,
				'text_50'  => self::blend_color( $text_color_dark, $body_color_dark, 0.5 ),
				'border'   => self::blend_color( $text_color_dark, $body_color_dark, 0.15 ),
				'date_bg'  => self::blend_color( $text_color_dark, $body_color_dark, 0.05 ),
				'links'    => $links_color_dark,
				'body'     => $body_color_dark,
			],
		];
	}

	/**
	 * Get email template style overrides.
	 *
	 * Wraps the core Helpers method with a fallback for Legacy template
	 * or when the Helpers class is not available.
	 *
	 * @since 1.7.0
	 *
	 * @return array
	 */
	private function get_style_overrides(): array {

		if ( class_exists( Helpers::class ) && method_exists( Helpers::class, 'get_current_template_style_overrides' ) ) {
			return Helpers::get_current_template_style_overrides();
		}

		// Fallback for Legacy template or older WPForms versions.
		return [
			'email_text_color'       => self::DEFAULT_EMAIL_TEXT_COLOR,
			'email_body_color'       => self::DEFAULT_EMAIL_BODY_COLOR,
			'email_links_color'      => self::DEFAULT_EMAIL_LINKS_COLOR,
			'email_text_color_dark'  => '#dddddd',
			'email_body_color_dark'  => '#1f1f1f',
			'email_links_color_dark' => self::DEFAULT_EMAIL_LINKS_COLOR,
		];
	}

	/**
	 * Get Entry User Journey table.
	 *
	 * @since 1.0.3
	 *
	 * @param object $entry   Entry data object.
	 * @param string $context Context. Values: [ 'entries' | 'confirmation' | 'email' ]. Default: 'email'.
	 *
	 * @return string
	 * @noinspection HtmlUnknownAttribute
	 * @noinspection HtmlDeprecatedAttribute
	 */
	public function get_entry_journey_table( $entry, $context = 'email' ) {

		$form       = wpforms()->obj( 'form' )->get( $entry->form_id );
		$form_title = ! empty( $form->post_title ) ? $form->post_title : sprintf(
			/* translators: %d - form id. */
			esc_html__( 'Form (#%d)', 'wpforms-user-journey' ),
			$entry->form_id
		);

		$timestamp_prev = false;
		$style          = '';

		if ( $context === 'email' ) {
			$this->email_colors = $this->get_email_colors();

			$style = sprintf(
				' style="width: 100%%; border-collapse: collapse; border: 1px solid %s;"',
				esc_attr( $this->email_colors['light']['border'] )
			);
		}

		$class = $context === 'email' ? ' class="wpforms-uj-table"' : '';

		$output = sprintf(
			'<table width="100%%" cellspacing="0" cellpadding="0"%s%s>',
			$class,
			$style
		);

		foreach ( $entry->user_journey as $record ) {

			$output        .= $this->get_entry_journey_record( $record, $context, $timestamp_prev );
			$timestamp_prev = strtotime( $record->date );
		}

		$output .= $this->get_entry_journey_summary( $entry, $form_title, $context, $timestamp_prev );
		$output .= '</table>';

		// Reset email colors after render cycle.
		$this->email_colors = [];

		return $output;
	}

	/**
	 * Get Entry User Journey plain text with decoded URLs.
	 *
	 * @since 1.5.0
	 *
	 * @param object $entry Entry data object.
	 *
	 * @return string
	 */
	public function get_entry_journey_plain_text_urls_decoded( object $entry ): string {

		if ( empty( $entry->user_journey ) ) {
			return '';
		}

		$timestamp_prev = false;
		$output         = '';

		foreach ( $entry->user_journey as $record ) {
			$output        .= $this->get_entry_journey_plain_text_record( $record, $timestamp_prev, true );
			$timestamp_prev = strtotime( $record->date );
		}

		return $output;
	}

	/**
	 * Get Entry User Journey record row.
	 *
	 * @since 1.0.3
	 *
	 * @param object   $record         Journey record data.
	 * @param string   $context        Context. Values: [ 'entries' | 'confirmation' | 'email' ]. Default: 'email'.
	 * @param int|bool $timestamp_prev Previous record timestamp. Default: false.
	 *
	 * @return string
	 */
	private function get_entry_journey_record( $record, $context = 'email', $timestamp_prev = false ) {

		$timestamp = isset( $record->date ) ? strtotime( $record->date ) : time();
		$tpl_dir   = sanitize_key( $context );
		$params    = isset( $record->parameters ) ? json_decode( $record->parameters, true ) : [];

		// Render abandoned session marker row.
		if ( ! empty( $params['_wpforms_abandoned_session'] ) ) {
			return $this->get_abandoned_session_marker( $record, $context, $timestamp, $timestamp_prev );
		}

		$url         = urldecode( $record->url ?? '' );
		$is_external = ! empty( $record->external );

		$output  = $this->get_entry_journey_date( $timestamp, $context, $timestamp_prev );
		$output .= wpforms_render(
			$tpl_dir . '/journal-record',
			[
				'time'        => wpforms_time_format( $timestamp, '', true ),
				'title'       => $record->title ?? '',
				'url'         => $url,
				'path'        => str_replace( home_url(), '', $url ),
				'params'      => $params,
				'duration'    => ! empty( $record->duration ) ? human_time_diff( $timestamp - $record->duration, $timestamp ) : 0,
				'status'      => 'visit',
				'is_external' => $is_external,
				'colors'      => ! empty( $this->email_colors ) ? $this->email_colors['light'] : [],
				'dark_colors' => ! empty( $this->email_colors ) ? $this->email_colors['dark'] : [],
				'summary'     => '',
			],
			true
		);

		return $output;
	}

	/**
	 * Render an abandoned session marker row.
	 *
	 * @since 1.7.0
	 *
	 * @param object   $record         Journey record data.
	 * @param string   $context        Context. Values: [ 'entries' | 'confirmation' | 'email' ]. Default: 'email'.
	 * @param int      $timestamp      Record timestamp.
	 * @param int|bool $timestamp_prev Previous record timestamp. Default: false.
	 *
	 * @return string
	 */
	private function get_abandoned_session_marker( $record, $context, $timestamp, $timestamp_prev ) {

		$form       = wpforms()->obj( 'form' )->get( $record->form_id );
		$form_title = ! empty( $form->post_title ) ? $form->post_title : '';
		$tpl_dir    = sanitize_key( $context );

		$output  = $this->get_entry_journey_date( $timestamp, $context, $timestamp_prev );
		$output .= wpforms_render(
			$tpl_dir . '/journal-record',
			[
				'time'        => wpforms_time_format( $timestamp, '', true ),
				'title'       => sprintf( '%s %s', $form_title, __( 'abandoned', 'wpforms-user-journey' ) ),
				'url'         => '',
				'path'        => '',
				'params'      => [],
				'duration'    => ! empty( $timestamp_prev ) ? human_time_diff( $timestamp_prev, $timestamp ) : '',
				'status'      => 'abandon',
				'is_external' => false,
				'colors'      => ! empty( $this->email_colors ) ? $this->email_colors['light'] : [],
				'dark_colors' => ! empty( $this->email_colors ) ? $this->email_colors['dark'] : [],
				'summary'     => '',
			],
			true
		);

		return $output;
	}

	/**
	 * Get Entry User Journey record date row.
	 *
	 * @since 1.0.3
	 *
	 * @param int      $timestamp      Record timestamp.
	 * @param string   $context        Context. Values: [ 'entries' | 'confirmation' | 'email' ]. Default: 'email'.
	 * @param int|bool $timestamp_prev Previous record timestamp. Default: false.
	 *
	 * @return string
	 * @noinspection HtmlUnknownAttribute
	 */
	private function get_entry_journey_date( $timestamp, $context, $timestamp_prev = false ) {

		if ( ! empty( $timestamp_prev ) && gmdate( 'd', $timestamp ) === gmdate( 'd', $timestamp_prev ) ) {
			return '';
		}

		$style = '';

		if ( $context === 'email' && ! empty( $this->email_colors ) ) {
			$colors = $this->email_colors['light'];

			$style = sprintf(
				' style="padding: 7px 12px; font-weight: bold; font-size: 14px; color: %s; background-color: %s; border-bottom: none;"',
				esc_attr( $colors['text_100'] ),
				esc_attr( $colors['date_bg'] )
			);
		}

		// Email template has 2 columns, entries/confirmation have 3.
		$colspan = $context === 'email' ? 2 : 3;

		return sprintf(
			'<tr>
				<td colspan="%1$d" class="date wpforms-uj-date"%2$s>%3$s</td>
			</tr>',
			$colspan,
			$style,
			esc_html( wpforms_date_format( $timestamp, '', true ) )
		);
	}

	/**
	 * Get Entry User Journey summary record.
	 *
	 * @since 1.0.3
	 *
	 * @param object   $entry          Entry data object.
	 * @param string   $form_title     Form title.
	 * @param string   $context        Context. Values: [ 'entries' | 'confirmation' | 'email' ]. Default: 'email'.
	 * @param int|bool $timestamp_prev Previous record timestamp. Default: false.
	 *
	 * @return string
	 */
	private function get_entry_journey_summary( $entry, $form_title, $context = 'email', $timestamp_prev = false ) {

		$step_count = $this->get_journey_step_count( $entry->user_journey );

		$summary = sprintf(
			/* translators: %1$s - number of steps; %2$s - total time spent. */
			__( 'User took %1$s over %2$s', 'wpforms-user-journey' ),
			sprintf(
				/* translators: Total number of steps taken. */
				_n( '%s step', '%s steps', $step_count, 'wpforms-user-journey' ),
				$step_count
			),
			human_time_diff( strtotime( $entry->user_journey[0]->date ), strtotime( $entry->date ) )
		);
		$tpl_dir = sanitize_key( $context );

		return wpforms_render(
			$tpl_dir . '/journal-record',
			[
				'time'        => wpforms_time_format( $entry->date, '', true ),
				'title'       => sprintf(
					'%s %s',
					$form_title,
					$entry->status === 'abandoned' ? __( 'abandoned', 'wpforms-user-journey' ) : __( 'submitted', 'wpforms-user-journey' )
				),
				'url'         => '',
				'path'        => $summary,
				'params'      => [],
				'duration'    => human_time_diff( $timestamp_prev, strtotime( $entry->date ) ),
				'status'      => $entry->status === 'abandoned' ? 'abandon' : 'submit',
				'is_external' => false,
				'colors'      => ! empty( $this->email_colors ) ? $this->email_colors['light'] : [],
				'dark_colors' => ! empty( $this->email_colors ) ? $this->email_colors['dark'] : [],
				'summary'     => $summary,
			],
			true
		);
	}

	/**
	 * Get Entry User Journey plain text.
	 *
	 * @since 1.0.3
	 *
	 * @param object $entry Entry data object.
	 *
	 * @return string
	 */
	public function get_entry_journey_plain_text( $entry ) {

		if ( empty( $entry->user_journey ) ) {
			return '';
		}

		$form       = wpforms()->obj( 'form' )->get( $entry->form_id );
		$form_title = ! empty( $form->post_title ) ? $form->post_title : sprintf(
			/* translators: %d - form id. */
			esc_html__( 'Form (#%d)', 'wpforms-user-journey' ),
			$entry->form_id
		);

		$output         = '';
		$timestamp_prev = false;

		foreach ( $entry->user_journey as $record ) {

			$output        .= $this->get_entry_journey_plain_text_record( $record, $timestamp_prev );
			$timestamp_prev = strtotime( $record->date );
		}

		$step_count = $this->get_journey_step_count( $entry->user_journey );

		// Summary row.
		$summary = wp_strip_all_tags(
			sprintf(
				/* translators: %1$s - number of steps; %2$s - total time spent. */
				__( 'User took %1$s over %2$s', 'wpforms-user-journey' ),
				sprintf(
					/* translators: Total number of steps taken. */
					_n( '%s step', '%s steps', $step_count, 'wpforms-user-journey' ),
					$step_count
				),
				human_time_diff( strtotime( $entry->user_journey[0]->date ), strtotime( $entry->date ) )
			)
		);

		$time     = wpforms_time_format( $entry->date, '', true );
		$title    = sprintf(
			'%s %s',
			$form_title,
			wp_strip_all_tags( __( 'submitted', 'wpforms-user-journey' ) )
		);
		$duration = human_time_diff( $timestamp_prev, strtotime( $entry->date ) );

		$output .= "- {$time} - {$title} - {$summary} - {$duration}" . PHP_EOL;

		return $output;
	}

	/**
	 * Get Entry User Journey plain text record row.
	 *
	 * @since 1.0.3
	 *
	 * @param object   $record         Journey record data.
	 * @param int|bool $timestamp_prev Previous record timestamp. Default: false.
	 * @param bool     $urls_decoded   Whether to decode the URLs. Default: false.
	 *
	 * @return string
	 */
	private function get_entry_journey_plain_text_record( object $record, $timestamp_prev = false, bool $urls_decoded = false ): string { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh

		$timestamp = isset( $record->date ) ? strtotime( $record->date ) : time();
		$params    = isset( $record->parameters ) ? json_decode( $record->parameters, true ) : [];

		// Render abandoned session marker as plain text.
		if ( ! empty( $params['_wpforms_abandoned_session'] ) ) {
			return $this->get_abandoned_session_marker_plain_text( $record, $timestamp, $timestamp_prev );
		}

		$url    = isset( $record->url ) ? ' - ' . ( $urls_decoded ? urldecode( $record->url ) : $record->url ) : '';
		$url    = esc_html( $url );
		$output = '';

		if ( empty( $timestamp_prev ) || gmdate( 'd', $timestamp ) !== gmdate( 'd', $timestamp_prev ) ) {
			$output .= esc_html( wpforms_date_format( $timestamp, '', true ) ) . PHP_EOL;
		}

		$time     = wpforms_time_format( $timestamp, '', true );
		$title    = $record->title ?? wp_strip_all_tags( __( 'No title', 'wpforms-user-journey' ) );
		$duration = ! empty( $record->duration ) ? ' - ' . human_time_diff( $timestamp - $record->duration, $timestamp ) : '';

		$output .= "- {$time} - {$title}{$duration}{$url}" . PHP_EOL;

		return $output;
	}

	/**
	 * Render an abandoned session marker as plain text.
	 *
	 * @since 1.7.0
	 *
	 * @param object   $record         Journey record data.
	 * @param int      $timestamp      Record timestamp.
	 * @param int|bool $timestamp_prev Previous record timestamp. Default: false.
	 *
	 * @return string
	 */
	private function get_abandoned_session_marker_plain_text( $record, $timestamp, $timestamp_prev ) {

		$form       = wpforms()->obj( 'form' )->get( $record->form_id );
		$form_title = ! empty( $form->post_title ) ? $form->post_title : '';
		$output     = '';

		if ( empty( $timestamp_prev ) || gmdate( 'd', $timestamp ) !== gmdate( 'd', $timestamp_prev ) ) {
			$output .= esc_html( wpforms_date_format( $timestamp, '', true ) ) . PHP_EOL;
		}

		$time     = wpforms_time_format( $timestamp, '', true );
		$title    = sprintf( '%s %s', $form_title, wp_strip_all_tags( __( 'abandoned', 'wpforms-user-journey' ) ) );
		$duration = ! empty( $timestamp_prev ) ? ' - ' . human_time_diff( $timestamp_prev, $timestamp ) : '';

		$output .= "- {$time} - {$title}{$duration}" . PHP_EOL;

		return $output;
	}

	/**
	 * Count journey steps, excluding abandoned session marker rows.
	 *
	 * @since 1.7.0
	 *
	 * @param array $journey Journey records.
	 *
	 * @return int
	 */
	private function get_journey_step_count( array $journey ): int {

		return count(
			array_filter(
				$journey,
				static function ( $record ) {
					$params = isset( $record->parameters ) ? json_decode( $record->parameters, true ) : [];

					return empty( $params['_wpforms_abandoned_session'] );
				}
			)
		);
	}
}