PluginProbe ʕ •ᴥ•ʔ
MailPoet – Newsletters, Email Marketing, and Automation / 5.36.0
MailPoet – Newsletters, Email Marketing, and Automation v5.36.0
5.36.0 5.35.1 5.35.0 5.34.3 5.34.2 5.34.1 5.34.0 5.33.1 5.33.0 5.32.0 5.31.0 5.30.0 5.29.0 5.28.1 5.28.0 5.27.0 5.26.0 5.26.1 5.25.0 5.24.0 4.43.0 4.43.1 4.44.0 4.44.1 4.45.0 4.46.0 4.47.0 4.48.0 4.48.1 4.48.2 4.49.0 4.49.1 4.5.0 4.5.1 4.5.2 4.50.0 4.50.1 4.51.0 4.51.1 4.51.2 4.52.0 4.53.0 4.54.0 4.55.0 4.56.0 4.57.0 4.58.0 4.58.1 4.58.2 4.6.0 4.6.1 4.6.2 4.7.0 4.7.1 4.8.0 4.8.1 4.9.0 5.0.0 5.0.1 5.0.2 5.1.0 5.1.1 5.10.0 5.10.1 5.11.0 5.12.0 5.12.1 5.12.10 5.12.11 5.12.12 5.12.13 5.12.2 5.12.3 5.12.4 5.12.5 5.12.6 5.12.7 5.12.8 5.12.9 5.13.0 5.13.1 5.13.2 5.14.0 5.14.1 5.14.2 5.14.3 5.15.0 5.15.1 5.16.0 5.16.1 5.16.2 5.16.3 5.16.4 5.17.0 5.17.1 5.17.2 5.17.3 5.17.4 5.17.5 5.17.6 5.18.0 5.19.0 5.2.0 5.2.1 5.2.2 5.2.3 5.20.0 5.21.0 5.21.1 5.21.2 5.21.3 5.22.0 5.22.1 5.22.2 5.22.3 5.22.4 5.23.0 5.23.1 5.23.2 5.3.0 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.3.7 5.4.0 5.4.1 5.4.2 5.5.0 5.5.1 5.5.2 5.6.0 5.6.1 5.6.2 5.6.3 5.6.4 5.7.0 5.7.1 5.8.0 5.8.1 5.9.0 3.0.0-beta.15 3.7.1 3.0.0-beta.16 3.7.2 3.0.0-beta.17 3.7.3 3.0.0-beta.18 3.7.4 3.0.0-beta.19 3.7.5 3.0.0-beta.2 3.7.6 3.0.0-beta.20 3.7.8 3.0.0-beta.21 3.70.0 3.0.0-beta.22 3.71.0 3.0.0-beta.23 3.71.1 3.0.0-beta.23.1 3.71.2 3.0.0-beta.23.2 3.71.3 3.0.0-beta.24 3.72.0 3.0.0-beta.25 3.73.0 3.0.0-beta.26 3.73.1 3.0.0-beta.27 3.73.2 3.0.0-beta.28 3.74.0 3.0.0-beta.29 3.74.1 3.0.0-beta.3 3.74.2 3.0.0-beta.30 3.74.3 3.0.0-beta.31 3.75.0 3.0.0-beta.32 3.75.1 3.0.0-beta.33 3.76.0 3.0.0-beta.33.1 3.77.0 3.0.0-beta.34.0.0 3.77.1 3.0.0-beta.36.0.0 3.78.0 3.0.0-beta.36.0.1 3.79.0 3.0.0-beta.36.2.0 3.8 3.0.0-beta.36.3.0 3.8.1 3.0.0-beta.36.3.1 3.8.2 3.0.0-beta.37.0.0 3.8.3 3.0.0-beta.4 3.8.4 3.0.0-beta.5 3.8.5 3.0.0-beta.6 3.8.6 3.0.0-beta.7 3.80.0 3.0.0-beta.7.1 3.81.0 3.0.0-beta.8 3.82.0 3.0.0-beta.9 3.83.0 3.0.0-rc.1.0.0 3.84.0 3.0.0-rc.1.0.1 3.84.1 3.0.0-rc.1.0.2 3.85.0 3.0.0-rc.1.0.3 3.85.1 3.0.0-rc.1.0.4 3.86.0 3.0.0-rc.2.0.0 3.87.0 3.0.0-rc.2.0.1 3.87.1 3.0.0-rc.2.0.2 3.87.2 3.0.0-rc.2.0.3 3.88.0 3.0.1 3.88.1 3.0.2 3.88.2 3.0.3 3.89.0 3.0.4 3.89.1 3.0.5 3.89.2 3.0.6 3.89.3 3.0.7 3.89.4 3.0.8 3.9.0 3.0.9 3.9.1 3.1.0 3.90.0 3.10 3.90.1 3.10.1 3.90.2 3.100.0 3.91.0 3.100.1 3.91.1 3.100.2 3.92.0 3.101.0 3.92.1 3.101.1 3.93.0 3.102.0 3.93.1 3.102.1 3.94.0 3.103.0 3.95.0 3.103.1 3.95.1 3.11.0 3.96.0 3.11.1 3.96.1 3.11.2 3.97.0 3.11.3 3.98.0 3.11.4 3.98.1 3.11.5 3.99.0 3.12.0 3.99.1 3.12.1 4.0.0 3.13.0 4.0.1 3.14.0 4.1.0 3.14.1 4.1.1 3.15.0 4.10.0 3.16.0 4.11.0 3.16.1 4.11.1 3.16.2 4.12.0 3.16.3 4.12.1 3.17.0 4.12.2 3.17.1 4.13.0 3.17.2 4.14.0 3.18.0 4.15.0 3.18.1 4.16.0 3.18.2 4.17.0 3.19.0 4.17.1 3.19.1 4.18.0 3.19.2 4.18.1 3.19.3 4.19.0 3.2.0 4.2.0 3.2.1 4.20.0 3.2.2 4.20.1 3.2.3 4.20.2 3.2.4 4.21.0 3.2.5 4.22.0 3.20.0 4.22.1 3.21.0 4.22.2 3.21.1 4.23.0 3.22.0 4.24.0 3.23.0 4.25.0 3.23.1 4.26.0 3.23.2 4.26.1 3.24.0 4.27.0 3.25.0 4.28.0 3.25.1 4.29.0 3.26.0 4.3.0 3.26.1 4.3.1 3.27.0 4.30.0 3.28.0 4.31.0 3.29.0 4.31.1 3.3.0 4.32.0 3.3.1 4.33.0 3.3.2 4.34.0 3.3.3 4.35.0 3.3.4 4.35.1 3.3.5 4.36.0 3.3.6 4.37.0 3.30.0 4.38.0 3.31.0 4.39.0 3.31.1 4.4.0 3.32.0 4.40.0 3.32.1 4.41.0 3.32.2 4.41.1 3.33.0 4.41.2 3.34.0 4.41.3 3.34.1 4.42.0 3.34.2 4.42.1 3.34.3 3.34.4 3.35.0 3.35.1 3.35.3 3.35.4 3.36.0 3.37.0 3.37.1 3.37.2 3.37.3 3.38.0 3.38.1 3.39.0 3.39.1 3.39.2 3.4.0 3.4.1 3.4.2 3.4.3 3.4.4 3.40.0 3.40.1 3.41.0 3.41.1 3.41.2 3.42.0 3.42.1 3.42.2 3.42.3 3.43.0 3.43.1 3.44.0 3.45.0 3.45.1 3.46.0 3.46.1 3.46.10 3.46.11 3.46.12 3.46.13 3.46.14 3.46.2 3.46.3 3.46.4 3.46.5 3.46.6 3.46.7 3.46.8 3.46.9 3.47.0 3.47.1 3.47.10 3.47.11 3.47.2 3.47.3 3.47.5 3.47.6 3.47.7 3.47.9 3.48.0 3.48.1 3.49.0 3.49.1 3.5.0 3.5.1 3.50.0 3.51.0 3.51.1 3.51.2 3.52.0 3.53.0 3.54.0 3.54.1 3.54.2 3.54.3 3.55.0 3.55.1 3.56.0 3.56.1 3.56.2 3.57.0 3.57.1 3.58.0 3.59.0 3.59.1 3.59.2 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.60.0 3.60.1 3.60.10 3.60.11 3.60.12 3.60.2 3.60.3 3.60.4 3.60.6 3.60.7 3.60.8 3.60.9 3.61.0 3.62.0 3.62.1 3.63.0 3.64.0 3.64.1 3.64.2 3.64.3 3.65.0 trunk 3.65.1 3.0.0 3.66.0 3.0.0-beta.1 3.67.0 3.0.0-beta.10 3.67.1 3.0.0-beta.11 3.68.0 3.0.0-beta.12 3.69.0 3.0.0-beta.13 3.69.1 3.0.0-beta.14 3.7.0
mailpoet / vendor / woocommerce / email-editor / src / Engine / class-personalizer.php
mailpoet / vendor / woocommerce / email-editor / src / Engine Last commit date
Logger 11 months ago Patterns 11 months ago PersonalizationTags 3 days ago Renderer 3 days ago Templates 4 months ago class-assets-manager.php 5 months ago class-dependency-check.php 11 months ago class-email-api-controller.php 6 months ago class-email-editor.php 3 days ago class-email-styles-schema.php 11 months ago class-personalizer.php 3 days ago class-send-preview-email.php 5 months ago class-settings-controller.php 3 months ago class-site-style-sync-controller.php 4 months ago class-theme-controller.php 3 months ago class-user-theme.php 11 months ago content-editor.css 2 weeks ago content-shared.css 11 months ago index.php 11 months ago theme.json 3 months ago
class-personalizer.php
175 lines
1 <?php
2 declare(strict_types = 1);
3 namespace Automattic\WooCommerce\EmailEditor\Engine;
4 if (!defined('ABSPATH')) exit;
5 use Automattic\WooCommerce\EmailEditor\Engine\PersonalizationTags\HTML_Tag_Processor;
6 use Automattic\WooCommerce\EmailEditor\Engine\PersonalizationTags\Personalization_Tag;
7 use Automattic\WooCommerce\EmailEditor\Engine\PersonalizationTags\Personalization_Tags_Registry;
8 class Personalizer {
9 private const TAG_NAME_PATTERN = '[a-zA-Z0-9\-\/]+';
10 public const RENDERING_CONTEXT_HTML = 'html';
11 public const RENDERING_CONTEXT_TEXT = 'text';
12 public const RENDERING_CONTEXT_HREF = 'href';
13 public const RENDERING_CONTEXT_KEY = 'rendering_context';
14 private Personalization_Tags_Registry $tags_registry;
15 private array $context;
16 public function __construct( Personalization_Tags_Registry $tags_registry ) {
17 $this->tags_registry = $tags_registry;
18 $this->context = array();
19 }
20 public function set_context( array $context ) {
21 $this->context = $context;
22 }
23 public function get_context(): array {
24 return $this->context;
25 }
26 public function personalize_content( string $content, string $rendering_context = self::RENDERING_CONTEXT_HTML ): string {
27 if ( ! in_array( $rendering_context, array( self::RENDERING_CONTEXT_HTML, self::RENDERING_CONTEXT_TEXT ), true ) ) {
28 $rendering_context = self::RENDERING_CONTEXT_HTML;
29 }
30 $content_processor = new HTML_Tag_Processor( $content );
31 while ( $content_processor->next_token() ) {
32 if ( $content_processor->get_token_type() === '#comment' ) {
33 $modifiable_text = $content_processor->get_modifiable_text();
34 $token = $this->parse_token( $modifiable_text );
35 $tag = $this->tags_registry->get_by_token( $token['token'] );
36 if ( ! $tag ) {
37 continue;
38 }
39 $value = $tag->execute_callback( $this->get_callback_context( $rendering_context ), $token['arguments'] );
40 if ( self::RENDERING_CONTEXT_HTML === $rendering_context && Personalization_Tag::VALUE_TYPE_TEXT === $tag->get_value_type() ) {
41 $value = esc_html( $value );
42 }
43 $content_processor->replace_token( $value );
44 } elseif ( $content_processor->get_token_type() === '#tag' && $content_processor->get_tag() === 'TITLE' ) {
45 // The title tag contains the subject of the email which should be personalized. HTML_Tag_Processor does parse the header tags.
46 // The title content is effectively plain text, so it is personalized in the text rendering context.
47 $modifiable_text = $content_processor->get_modifiable_text();
48 $title = $this->personalize_content( $modifiable_text, self::RENDERING_CONTEXT_TEXT );
49 $content_processor->set_modifiable_text( $title );
50 } elseif ( $content_processor->get_token_type() === '#tag' && $content_processor->get_tag() === 'A' && $content_processor->get_attribute( 'data-link-href' ) ) {
51 // The anchor tag contains the data-link-href attribute which should be personalized.
52 $href = (string) $content_processor->get_attribute( 'data-link-href' );
53 $token = $this->parse_token( $href );
54 $tag = $this->tags_registry->get_by_token( $token['token'] );
55 if ( ! $tag ) {
56 continue;
57 }
58 $value = $tag->execute_callback( $this->get_callback_context( self::RENDERING_CONTEXT_HREF ), $token['arguments'] );
59 $value = $this->replace_link_href( $href, $tag->get_token(), $value );
60 if ( '' !== $value ) {
61 $content_processor->set_attribute( 'href', $value );
62 $content_processor->remove_attribute( 'data-link-href' );
63 $content_processor->remove_attribute( 'contenteditable' );
64 }
65 } elseif ( $content_processor->get_token_type() === '#tag' && $content_processor->get_tag() === 'A' ) {
66 $href = $content_processor->get_attribute( 'href' );
67 if ( ! is_string( $href ) ) {
68 continue;
69 }
70 $personalized_href = $this->personalize_href_tokens( $href );
71 if ( null !== $personalized_href ) {
72 $content_processor->set_attribute( 'href', $personalized_href );
73 }
74 }
75 }
76 $content_processor->flush_updates();
77 return $content_processor->get_updated_html();
78 }
79 private function personalize_href_tokens( string $href ): ?string {
80 // Decode both URL encoding (%XX) and HTML entities (&#039;) to handle various encoding scenarios.
81 $decoded_href = html_entity_decode( urldecode( $href ), ENT_QUOTES, 'UTF-8' );
82 if ( ! preg_match_all( '/\[' . self::TAG_NAME_PATTERN . '(?:\s+[^\]]+)?\]/', $decoded_href, $matches ) ) {
83 return null;
84 }
85 // Resolve every replaceable token first.
86 $replacements = array();
87 foreach ( array_unique( $matches[0] ) as $token_string ) {
88 $token = $this->parse_token( $token_string );
89 $tag = $this->tags_registry->get_by_token( $token['token'] );
90 if ( ! $tag ) {
91 continue;
92 }
93 $value = $tag->execute_callback( $this->get_callback_context( self::RENDERING_CONTEXT_HREF ), $token['arguments'] );
94 if ( '' !== $value ) {
95 $replacements[ $token_string ] = $value;
96 }
97 }
98 if ( ! $replacements ) {
99 return null;
100 }
101 // Prefer the original attribute value as the replacement base so legitimate
102 // percent-encoding in the surrounding URL is preserved; fall back to the decoded
103 // form when a replaced token occurrence exists only there (e.g. URL-encoded tokens).
104 // Only tokens that are actually replaced matter here — an unregistered bracket
105 // sequence that exists purely in the decoded form must not force the decoded base.
106 // Known tradeoff: the base is chosen for the whole href, so when the decoded form
107 // is used, unrelated percent-encoding elsewhere in the URL is decoded too.
108 $base = $href;
109 foreach ( array_keys( $replacements ) as $token_string ) {
110 if ( substr_count( $href, $token_string ) !== substr_count( $decoded_href, $token_string ) ) {
111 $base = $decoded_href;
112 break;
113 }
114 }
115 // The editor forces a protocol prefix when a tag is used as the whole URL
116 // ("http://[tag]"). Strip it only when the token directly after it is being
117 // replaced, so the tag value is used as-is while any suffix (e.g. appended
118 // query parameters) is kept.
119 if ( preg_match( '#^https?://(\[' . self::TAG_NAME_PATTERN . '(?:\s+[^\]]+)?\])#i', $base, $prefix_match ) && isset( $replacements[ $prefix_match[1] ] ) ) {
120 $base = (string) preg_replace( '#^https?://#i', '', $base );
121 }
122 // Single-pass replacement — tag values are never re-scanned for other tokens,
123 // and a regex replacement would interpret `$` and `\` in them.
124 return strtr( $base, $replacements );
125 }
126 private function get_callback_context( string $rendering_context ): array {
127 // array_replace() (unlike array_merge()) preserves integer keys in the consumer's context.
128 return array_replace( $this->context, array( self::RENDERING_CONTEXT_KEY => $rendering_context ) );
129 }
130 private function parse_token( string $token ): array {
131 $result = array(
132 'token' => '',
133 'arguments' => array(),
134 );
135 // Step 1: Separate the tag and attributes.
136 if ( preg_match( '/^\[(' . self::TAG_NAME_PATTERN . ')\s*(.*?)\]$/', trim( $token ), $matches ) ) {
137 $result['token'] = "[{$matches[1]}]"; // The tag part (e.g., "[mailpoet/subscriber-firstname]").
138 $attributes_string = $matches[2]; // The attributes part (e.g., 'default="subscriber"').
139 // Step 2: Extract attributes from the attribute string.
140 // Match quoted values (double or single quotes separately to avoid mixing) and unquoted values.
141 // Unquoted values can occur when esc_url() strips quotes from personalization tags.
142 // For unquoted values with spaces, capture until the next key= pattern or closing bracket.
143 // The negative lookahead (?!\w+=) is critical for preventing ReDoS:
144 // it ensures the inner loop terminates as soon as the next key= pattern appears,
145 // preventing excessive backtracking despite the nested quantifiers.
146 if ( preg_match_all( '/(\w+)=(?:"([^"]*)"|\'([^\']*)\'|([^\s\]]+(?:\s+(?!\w+=)[^\s\]]+)*))/', $attributes_string, $attribute_matches, PREG_SET_ORDER ) ) {
147 foreach ( $attribute_matches as $attribute ) {
148 // $attribute[2] is double-quoted value, $attribute[3] is single-quoted value,
149 // $attribute[4] is unquoted value (may contain spaces).
150 // Use null coalescing as only one of these will be populated depending on which pattern matched.
151 $double_quoted_value = $attribute[2] ?? '';
152 $single_quoted_value = $attribute[3] ?? '';
153 $unquoted_value = $attribute[4] ?? '';
154 if ( '' !== $double_quoted_value ) {
155 $result['arguments'][ $attribute[1] ] = $double_quoted_value;
156 } elseif ( '' !== $single_quoted_value ) {
157 $result['arguments'][ $attribute[1] ] = $single_quoted_value;
158 } else {
159 $result['arguments'][ $attribute[1] ] = $unquoted_value;
160 }
161 }
162 }
163 }
164 return $result;
165 }
166 private function replace_link_href( string $content, string $token, string $replacement ) {
167 // Escape the shortcode name for safe regex usage and strip the brackets.
168 $escaped_shortcode = preg_quote( substr( $token, 1, strlen( $token ) - 2 ), '/' );
169 // Create a regex pattern dynamically.
170 $pattern = '/\[' . $escaped_shortcode . '(?:\s+[^\]]+)?\]/';
171 // Escape `$` and `\` so they are inserted literally instead of being interpreted as backreferences.
172 return trim( (string) preg_replace( $pattern, addcslashes( $replacement, '\\$' ), $content ) );
173 }
174 }
175