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'] );
}
)
);
}
}