PluginProbe ʕ •ᴥ•ʔ
Email Encoder – Protect Email Addresses and Phone Numbers / 2.5.0
Email Encoder – Protect Email Addresses and Phone Numbers v2.5.0
2.5.0 2.4.8 trunk 0.10 0.11 0.12 0.20 0.21 0.22 0.30 0.31 0.32 0.40 0.41 0.42 0.50 0.60 0.70 0.71 0.80 1.0.0 1.0.1 1.0.2 1.1.0 1.2.0 1.2.1 1.3.0 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.5 1.5.2 1.51 1.53 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 2.1.0 2.1.1 2.1.10 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.3.0 2.3.1 2.3.3 2.3.4 2.3.5 2.3.6 2.3.7 2.3.8 2.3.9 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 2.4.6 2.4.7
email-encoder-bundle / src / Validate / Filters.php
email-encoder-bundle / src / Validate Last commit date
EncoderForm.php 1 month ago Encoding.php 2 months ago Filters.php 2 months ago Validate.php 5 months ago
Filters.php
517 lines
1 <?php
2
3 namespace OnlineOptimisation\EmailEncoderBundle\Validate;
4
5 use DOMDocument;
6 use OnlineOptimisation\EmailEncoderBundle\Traits\PluginHelper;
7
8 class Filters
9 {
10 use PluginHelper;
11
12 public function boot(): void
13 {
14 }
15
16
17 /**
18 * ######################
19 * ###
20 * #### FILTERS
21 * ###
22 * ######################
23 */
24
25 /**
26 * The main page filter function
27 *
28 * @param string $content - the content that needs to be filtered
29 * @param string $protect_using
30 * @return string - The filtered content
31 */
32 public function filter_page( $content, $protect_using )
33 {
34
35 //Added in 2.0.6
36 $content = apply_filters( 'eeb/validate/filter_page_content', $content, $protect_using );
37
38 $content = $this->filter_soft_dom_attributes( $content, 'char_encode' );
39
40 $htmlSplit = preg_split( '/(<body(([^>]*)>))/is', $content, -1, PREG_SPLIT_DELIM_CAPTURE );
41
42 if ( ! is_array( $htmlSplit) || count( $htmlSplit ) < 4 ) {
43 return $content;
44 }
45
46 switch ( $protect_using ) {
47 case 'with_javascript':
48 case 'without_javascript':
49 case 'char_encode':
50 $head_encoding_method = 'char_encode';
51 break;
52 default:
53 $head_encoding_method = 'default';
54 break;
55 }
56
57 //Filter head area
58 $filtered_head = $this->filter_plain_emails( $htmlSplit[0], null, $head_encoding_method );
59
60 //Filter body
61 //Soft attributes always need to be protected using only the char encode method since otherwise the logic breaks
62 $filtered_body = $this->filter_soft_attributes( $htmlSplit[4], 'char_encode' );
63 $filtered_body = $this->filter_content( $filtered_body, $protect_using );
64
65 $filtered_content = $filtered_head . $htmlSplit[1] . $filtered_body;
66
67 //Revalidate filtered emails that should not bbe encoded
68 $filtered_content = $this->tempEncodeAtSymbol( $filtered_content, true );
69
70 return $filtered_content;
71 }
72
73 /**
74 * Filter content
75 *
76 * @param string $content
77 * @param string $protect_using
78 * @return string
79 */
80 public function filter_content( $content, $protect_using )
81 {
82 $filtered = $content;
83 $self = $this;
84 $encode_mailtos = (bool) $this->getSetting( 'encode_mailtos', true, 'filter_body' );
85 $convert_plain_to_image = (bool) $this->getSetting( 'convert_plain_to_image', true, 'filter_body' );
86
87 //Added in 2.0.6
88 $filtered = apply_filters( 'eeb/validate/filter_content_content', $filtered, $protect_using );
89
90 //Soft attributes always need to be protected using only the char encode method since otherwise the logic breaks
91 $filtered = $this->filter_soft_attributes( $filtered, 'char_encode' );
92
93 // <option>, <textarea>, <title> can only contain text — any <span>/<script>/<img> inside
94 // them is stripped or rendered inconsistently across browsers (Firefox dropdowns show only
95 // the noscript fallback). For modes that emit HTML wrappers, pre-encode emails in those
96 // zones as entities and stash them behind placeholders so the main filter pass skips them.
97 $text_only_tokens = [];
98 $guard_text_only_zones = in_array( $protect_using, [ 'with_javascript', 'without_javascript' ], true );
99 if ( $guard_text_only_zones ) {
100 $filtered = $this->isolate_text_only_zones( $filtered, $text_only_tokens );
101 }
102
103 switch ( $protect_using ) {
104 case 'char_encode':
105 $filtered = $this->filter_plain_emails( $filtered, null, 'char_encode' );
106 break;
107 case 'strong_method':
108 $filtered = $this->filter_plain_emails( $filtered );
109 break;
110 case 'without_javascript':
111 $filtered = $this->filter_input_fields( $filtered, $protect_using );
112 $filtered = $this->filter_mailto_links( $filtered, 'without_javascript' );
113 $filtered = $this->filter_custom_links( $filtered, 'without_javascript' );
114
115 if ( $convert_plain_to_image ) {
116 $replace_by = 'convert_image';
117 } else {
118 $replace_by = 'use_css';
119 }
120
121 if ( $encode_mailtos ) {
122 if ( ! ( function_exists( 'et_fb_enabled' ) && et_fb_enabled() ) ) {
123 $filtered = $this->filter_plain_emails( $filtered, function ( $match ) use ( $self ) {
124 return $self->createProtectedMailto( $match[0], array( 'href' => 'mailto:' . $match[0] ), 'without_javascript' );
125 }, $replace_by);
126 } else {
127 $filtered = $this->filter_plain_emails( $filtered, null, $replace_by );
128 }
129 } else {
130 $filtered = $this->filter_plain_emails( $filtered, null, $replace_by );
131 }
132
133 break;
134 case 'with_javascript':
135 $filtered = $this->filter_input_fields( $filtered, $protect_using );
136 $filtered = $this->filter_mailto_links( $filtered );
137 $filtered = $this->filter_custom_links( $filtered );
138
139 if ( $convert_plain_to_image ) {
140 $replace_by = 'convert_image';
141 } else {
142 $replace_by = 'use_javascript';
143 }
144
145 if ( $encode_mailtos ) {
146 if ( ! ( function_exists( 'et_fb_enabled' ) && et_fb_enabled() ) ) {
147 $filtered = $this->filter_plain_emails( $filtered, function ( $match ) use ( $self ) {
148 return $self->createProtectedMailto( $match[0], array( 'href' => 'mailto:' . $match[0] ), 'with_javascript' );
149 }, $replace_by);
150 } else {
151 $filtered = $this->filter_plain_emails( $filtered, null, $replace_by );
152 }
153 } else {
154 $filtered = $this->filter_plain_emails( $filtered, null, $replace_by );
155 }
156
157 break;
158 }
159
160 //Revalidate filtered emails that should not be encoded
161 $filtered = $this->tempEncodeAtSymbol( $filtered, true );
162
163 if ( $guard_text_only_zones && ! empty( $text_only_tokens ) ) {
164 $filtered = strtr( $filtered, $text_only_tokens );
165 }
166
167 return $filtered;
168 }
169
170 /**
171 * Pre-encode emails inside text-only HTML zones (<option>, <textarea>, <title>) using
172 * entity encoding and replace each zone with an opaque placeholder. The main filter pass
173 * leaves the placeholders alone; the caller restores them at the end.
174 *
175 * @param string $content
176 * @param array<string, string> $tokens Populated with placeholder => replacement pairs.
177 * @return string Content with text-only zones replaced by placeholder tokens.
178 */
179 private function isolate_text_only_zones( string $content, array &$tokens ): string
180 {
181 $result = preg_replace_callback(
182 '/(<(option|textarea|title)\b[^>]*>)(.*?)(<\/\2\s*>)/is',
183 function ( array $match ) use ( &$tokens ) {
184 $inner_encoded = $this->filter_plain_emails( $match[3], null, 'char_encode', false );
185 $token = "\x00EEB_TEXT_ONLY_" . count( $tokens ) . "\x00";
186 $tokens[ $token ] = $match[1] . $inner_encoded . $match[4];
187
188 return $token;
189 },
190 $content
191 );
192
193 return $result ?? $content;
194 }
195
196 /**
197 * Emails will be replaced by '*protected email*'
198 * @param string $content
199 * @param string|callable $replace_by Optional
200 * @param string $protection_method Optional
201 * @param mixed $show_encoded_check Optional
202 * @return string
203 */
204 public function filter_plain_emails( $content, $replace_by = null, $protection_method = 'default', $show_encoded_check = 'default' )
205 {
206
207 if ( $show_encoded_check === 'default' ) {
208 $show_encoded_check = (bool) $this->getSetting( 'show_encoded_check', true );
209 }
210
211 if ( $replace_by === null ) {
212 $replace_by = (string) $this->getSetting( 'protection_text', true );
213 }
214
215 $self = $this;
216
217 return preg_replace_callback( $this->settings()->get_email_regex(), function ( $matches ) use ( $replace_by, $protection_method, $show_encoded_check, $self ) {
218 // workaround to skip responsive image names containing @
219 $extention = strtolower( $matches[4] );
220 $excludedList = array(
221 '.jpg',
222 '.jpeg',
223 '.png',
224 '.gif',
225 '.svg',
226 '.webp',
227 '.bmp',
228 '.tiff',
229 '.avif',
230 );
231
232 //Added in 2.1.1
233 $excludedList = apply_filters( 'eeb/validate/excluded_image_urls', $excludedList );
234
235 if ( in_array( $extention, $excludedList ) ) {
236 return $matches[0];
237 }
238
239 if ( is_callable( $replace_by ) ) {
240 return call_user_func( $replace_by, $matches, $protection_method );
241 }
242
243 if ( $protection_method === 'char_encode' ) {
244 $protected_return = antispambot( $matches[0] );
245 } elseif ( $protection_method === 'convert_image' ) {
246
247 $image_link = $self->generateEmailImageUrl( $matches[0] );
248 if ( ! empty( $image_link ) ) {
249 $protected_return = '<img src="' . $image_link . '" />';
250 } else {
251 $protected_return = antispambot( $matches[0] );
252 }
253
254 } elseif ( $protection_method === 'use_javascript' ) {
255 $protection_text = (string) $this->getSetting( 'protection_text', true );
256 $protected_return = $this->dynamicJsEmailEncoding( $matches[0], $protection_text );
257 } elseif ( $protection_method === 'use_css' ) {
258 $protection_text = (string) $this->getSetting( 'protection_text', true );
259 // $protected_return = $this->validate()->encoding->encode_email_css( $matches[0], $protection_text );
260 $protected_return = $this->validate()->encoding->encode_email_css( $matches[0] );
261 } elseif ( $protection_method === 'no_encoding' ) {
262 $protected_return = $matches[0];
263 } else {
264 $protected_return = $replace_by;
265 }
266
267 // mark link as successfully encoded (for admin users)
268 if ( current_user_can( $this->getAdminCap( 'frontend-display-security-check' ) ) && $show_encoded_check ) {
269 $protected_return .= $this->getEncodedEmailIcon();
270 }
271
272 return $protected_return;
273 }, $content ) ?? '';
274 }
275
276 /**
277 * Filter passed input fields
278 *
279 * @param string $content
280 * @return string
281 */
282 public function filter_input_fields( string $content, string $encoding_method = 'default' )
283 {
284 $strong_encoding = (bool) $this->getSetting( 'input_strong_protection', true, 'filter_body' );
285
286 $callback_encode_input_fields = function ( $match ) use ( $encoding_method, $strong_encoding ) {
287 $input = $match[0];
288 $email = $match[2];
289
290 //Only allow strong encoding if javascript is supported
291 if ( $encoding_method === 'without_javascript' ) {
292 $strong_encoding = false;
293 }
294
295 return $this->validate()->encoding->encode_input_field( $input, $email, $strong_encoding );
296 };
297
298 $regexpInputField = '/<input([^>]*)value=["\'][\s+]*' . $this->settings()->get_email_regex( true ) . '[\s+]*["\']([^>]*)>/is';
299
300 return preg_replace_callback( $regexpInputField, $callback_encode_input_fields, $content ) ?? '';
301 }
302
303 /**
304 * @param string $content
305 * @param string $protection_method
306 * @return string
307 */
308 public function filter_mailto_links( string $content, ?string $protection_method = null )
309 {
310 $self = $this;
311
312 $callbackEncodeMailtoLinks = function ( $match ) use ( $self, $protection_method ) {
313 $attrs = $this->helper()->parse_html_attributes( $match[1] );
314 return $self->createProtectedMailto( $match[4], $attrs, $protection_method );
315 };
316
317 $regexpMailtoLink = '/<a[\s+]*(([^>]*)href=["\']mailto\:([^>]*)["\' ])>(.*?)<\/a[\s+]*>/is';
318
319 return preg_replace_callback( $regexpMailtoLink, $callbackEncodeMailtoLinks, $content ) ?? '';
320 }
321
322 /**
323 * @param string $content
324 * @param string $protection_method
325 * @return string
326 */
327 public function filter_custom_links( string $content, ?string $protection_method = null )
328 {
329 $self = $this;
330 $custom_href_attr = (string) $this->getSetting( 'custom_href_attr', true );
331
332 if ( ! empty( $custom_href_attr ) ) {
333 $custom_attr_list = explode( ',', $custom_href_attr );
334 foreach ( $custom_attr_list as $s_attr ) {
335 $attr_name = trim( $s_attr );
336
337 $callbackEncodeCustomLinks = function ( $match ) use ( $self, $protection_method ) {
338 $attrs = shortcode_parse_atts( $match[1] );
339 return $self->createProtectedHrefAtt( $match[4], $attrs, $protection_method );
340 };
341
342 $regexpMailtoLink = '/<a[\s+]*(([^>]*)href=["\']' . addslashes( $attr_name ) . '\:([^>]*)["\' ])>(.*?)<\/a[\s+]*>/is';
343
344 $content = preg_replace_callback( $regexpMailtoLink, $callbackEncodeCustomLinks, $content ) ?? '';
345 }
346 }
347
348 return $content;
349 }
350
351 /**
352 * Emails will be replaced by '*protected email*'
353 *
354 * @param string $content
355 * @param string $protection_type
356 * @return string
357 */
358 public function filter_rss( string $content, ?string $protection_type )
359 {
360
361 if ( $protection_type === 'strong_method' ) {
362 $filtered = $this->filter_plain_emails( $content );
363 } else {
364 $filtered = $this->filter_plain_emails( $content, null, 'char_encode' );
365 }
366
367 return $filtered;
368 }
369
370 /**
371 * Filter plain emails using soft attributes
372 *
373 * @param string $content - the content that should be soft filtered
374 * @param string $protection_method - The method (E.g. char_encode)
375 * @return string
376 */
377 public function filter_soft_attributes( string $content, string $protection_method )
378 {
379 $soft_attributes = (array) $this->settings()->get_soft_attribute_regex();
380
381 foreach ( $soft_attributes as $ident => $regex ) {
382
383 // $attributes = array();
384 preg_match_all( $regex, $content, $attributes );
385
386 // if ( is_array( $attributes ) && isset( $attributes[0] ) ) {
387 foreach ( $attributes[0] as $single ) {
388
389 if ( empty( $single ) ) {
390 continue;
391 }
392
393 $content = str_replace( $single, $this->filter_plain_emails( $single, null, $protection_method, false ), $content );
394 }
395 // }
396
397 }
398
399 return $content;
400 }
401
402 /**
403 * Filter plain emails using soft dom attributes
404 *
405 * @param string $content - the content that should be soft filtered
406 * @param string $protection_method - The method (E.g. char_encode)
407 * @return string
408 */
409 public function filter_soft_dom_attributes( $content, $protection_method )
410 {
411 $content = (string) $content;
412
413 $no_script_tags = (bool) $this->getSetting( 'no_script_tags', true, 'filter_body' );
414 $no_attribute_validation = (bool) $this->getSetting( 'no_attribute_validation', true, 'filter_body' );
415
416 if ( $content !== '' ) {
417
418 if ( class_exists( 'DOMDocument' ) ) {
419 $dom = new DOMDocument();
420 @$dom->loadHTML($content);
421
422 //Filter html attributes
423 if ( ! $no_attribute_validation ) {
424 $allNodes = $dom->getElementsByTagName('*');
425 foreach ( $allNodes as $snote ) {
426 if ( $snote->hasAttributes() ) {
427 foreach ( $snote->attributes as $attr ) {
428 if ( $attr->nodeName == 'href' || $attr->nodeName == 'src' ) {
429 continue;
430 }
431
432 if ( $attr->nodeValue !== null && strpos( $attr->nodeValue, '@' ) !== false ) {
433 // $single_tags = array();
434 preg_match_all( '/' . $attr->nodeName . '=["\']([^"]*)["\']/i', $content, $single_tags );
435
436 // if ( is_array( $single_tags ) && isset( $single_tags[0] ) ) {
437 foreach ( $single_tags[0] as $single ) {
438
439 if ( empty( $single ) ) {
440 continue;
441 }
442
443 $content = str_replace( $single, $this->filter_plain_emails( $single, null, $protection_method, false ), $content );
444 }
445 // }
446
447 }
448 }
449 }
450 }
451 }
452
453 //Keep for now
454 //Soft-encode scripts
455 // $script = $dom->getElementsByTagName('script');
456 // if ( ! empty( $script ) ) {
457 // $scripts_encoded = true;
458
459 // if ( ! $no_script_tags ) {
460 // foreach( $script as $item ) {
461 // $content = str_replace( $item->nodeValue, $this->filter_plain_emails( $item->nodeValue, null, $protection_method, false ), $content );
462 // }
463 // } else {
464 // foreach( $script as $item ) {
465 // $content = str_replace( $item->nodeValue, $this->temp_encode_at_symbol( $item->nodeValue ), $content );
466 // }
467 // }
468 // }
469
470 }
471
472 //Validate script tags for better encoding
473 $pattern = '/<script\b[^>]*>(.*?)<\/script>/is';
474
475 preg_match_all($pattern, $content, $matches);
476 if ( ! empty( $matches[1] ) ) {
477 if ( ! $no_script_tags ) {
478 foreach ( $matches[1] as $key => $item ) {
479
480 //Don't do anything if something doesn't add up
481 if ( ! isset( $matches[0][ $key ] ) ) {
482 continue;
483 }
484
485 $org_script = $matches[0][ $key ];
486
487 //Only encode emails when a CDATA is given to not cause any break within the scripts
488 if ( strpos( $item, '<![CDATA' ) !== false ) {
489 $validated_script = str_replace( $item, $this->filter_plain_emails( $item, null, $protection_method, false ), $org_script );
490 } else {
491 $validated_script = str_replace( $item, $this->tempEncodeAtSymbol( $item ), $org_script );
492 }
493
494 $content = str_replace( $org_script, $validated_script, $content );
495 }
496 } else {
497 foreach ( $matches[1] as $key => $item ) {
498
499 //Don't do anything if something doesn't add up
500 if ( ! isset( $matches[0][ $key ] ) ) {
501 continue;
502 }
503
504 $org_script = $matches[0][ $key ];
505 $validated_script = str_replace( $item, $this->tempEncodeAtSymbol( $item ), $org_script );
506
507 $content = str_replace( $org_script, $validated_script, $content );
508 }
509 }
510 }
511
512 }
513
514 return $content;
515 }
516 }
517