PluginProbe ʕ •ᴥ•ʔ
Email Encoder – Protect Email Addresses and Phone Numbers / trunk
Email Encoder – Protect Email Addresses and Phone Numbers vtrunk
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 / Encoding.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
Encoding.php
620 lines
1 <?php
2
3 namespace OnlineOptimisation\EmailEncoderBundle\Validate;
4
5 use OnlineOptimisation\EmailEncoderBundle\Traits\PluginHelper;
6
7 class Encoding
8 {
9 use PluginHelper;
10
11 private string $at_identifier;
12
13 public function boot(): void
14 {
15 $this->at_identifier = $this->settings()->get_at_identifier();
16 }
17
18
19 /**
20 * ######################
21 * ###
22 * #### ENCODINGS
23 * ###
24 * ######################
25 */
26
27 /**
28 * @param string $content
29 * @param bool $decode
30 * @return string
31 */
32 public function temp_encode_at_symbol( string $content, bool $decode = false )
33 {
34 if ( $decode ) {
35 return str_replace( $this->at_identifier, '@', $content );
36 }
37
38 return str_replace( '@', $this->at_identifier, $content );
39 }
40
41 /**
42 * ASCII method
43 *
44 * @param string $value
45 * @param string $protection_text
46 * @return string
47 */
48 public function encode_ascii( $value, $protection_text )
49 {
50 $mail_link = $value;
51
52 // first encode, so special chars can be supported
53 $mail_link = $this->helper()->encode_uri_components( $mail_link );
54
55 $mail_letters = '';
56
57 for ( $i = 0; $i < strlen( $mail_link ); $i++ ) {
58 $l = substr($mail_link, $i, 1);
59
60 if (strpos($mail_letters, $l) === false) {
61 $p = wp_rand(0, strlen($mail_letters));
62 $mail_letters = substr($mail_letters, 0, $p) .
63 $l . substr($mail_letters, $p, strlen($mail_letters));
64 }
65 }
66
67 $mail_letters_enc = str_replace( "\\", "\\\\", $mail_letters );
68 $mail_letters_enc = str_replace( "\"", "\\\"", $mail_letters_enc );
69
70 $mail_indices = '';
71 for ( $i = 0; $i < strlen( $mail_link ); $i++ ) {
72 $index = strpos( $mail_letters, substr( $mail_link, $i, 1 ) );
73 $index += 48;
74 $mail_indices .= chr( $index );
75 }
76
77 $mail_indices = str_replace("\\", "\\\\", $mail_indices);
78 $mail_indices = str_replace("\"", "\\\"", $mail_indices);
79
80 $element_id = 'eeb-' . wp_rand( 0, 1000000 ) . '-' . wp_rand(0, 1000000);
81
82 return '<span id="' . $element_id . '"></span>'
83 . '<script type="text/javascript">'
84 . '(function() {'
85 . 'var ml="' . $mail_letters_enc . '",mi="' . $mail_indices . '",o="";'
86 . 'for(var j=0,l=mi.length;j<l;j++) {'
87 . 'o+=ml.charAt(mi.charCodeAt(j)-48);'
88 . '}document.getElementById("' . $element_id . '").innerHTML = decodeURIComponent(o);' // decode at the end, this way special chars can be supported
89 . '}());'
90 . '</script><noscript>'
91 . esc_html( $protection_text )
92 . '</noscript>'
93 ;
94 }
95
96 /**
97 * Escape encoding method
98 *
99 * @param string $value
100 * @param string $protection_text
101 * @return string
102 */
103 public function encode_escape( $value, $protection_text )
104 {
105 $element_id = 'eeb-' . wp_rand( 0, 1000000 ) . '-' . wp_rand( 0, 1000000 );
106
107 //Validate escape sequences
108 $string = preg_replace('/\s+/S', " ", $value) ?? '';
109
110 // break string into array of characters, we can't use string_split because its php5 only
111 $split = preg_split( '||', $string );
112 $out = '<span id="' . esc_attr( $element_id ) . '"></span>'
113 . '<script type="text/javascript">'
114 . 'document.getElementById("' . $element_id . '").innerHTML = decodeURIComponent("';
115
116 if ( is_array( $split ) )
117 foreach ( $split as $c ) {
118 // preg split will return empty first and last characters, check for them and ignore
119 if ( ! empty( $c ) || $c === '0' ) {
120 $out .= '%' . dechex( ord( $c ) );
121 }
122 }
123
124 $out .= '");'
125 . '</script><noscript>'
126 . esc_html( $protection_text )
127 . '</noscript>';
128
129 return $out;
130 }
131
132 /**
133 * Encode email in input field
134 * @param string $input
135 * @param string $email
136 * @param bool $strongEncoding
137 * @return string
138 */
139 public function encode_input_field( $input, $email, $strongEncoding = false )
140 {
141
142 $show_encoded_check = (bool) $this->getSetting( 'show_encoded_check', true );
143
144 if ( $strongEncoding === false ) {
145 // encode email with entities (default wp method)
146 $sub_return = str_replace( $email, antispambot( $email ), $input );
147
148 if ( current_user_can( $this->getAdminCap( 'frontend-display-security-check' ) ) && $show_encoded_check ) {
149 $sub_return .= $this->get_encoded_email_icon();
150 }
151
152 return $sub_return;
153 }
154
155 // add data-enc-email after "<input"
156 $inputWithDataAttr = substr( $input, 0, 6 );
157 $inputWithDataAttr .= ' data-enc-email="' . esc_attr( $this->get_encoded_email( $email ) ) . '"';
158 $inputWithDataAttr .= substr( $input, 6 );
159
160 // mark link as successfullly encoded (for admin users)
161 if ( current_user_can( $this->getAdminCap( 'frontend-display-security-check' ) ) && $show_encoded_check ) {
162 $inputWithDataAttr .= $this->get_encoded_email_icon();
163 }
164
165 // remove email from value attribute
166 $encInput = str_replace( $email, '', $inputWithDataAttr );
167
168 return $encInput;
169 }
170
171 /**
172 * Get encoded email, used for data-attribute (translate by javascript)
173 *
174 * @param string $email
175 * @return string
176 */
177 public function get_encoded_email( $email )
178 {
179 $encEmail = $email;
180
181 // decode entities
182 $encEmail = html_entity_decode( $encEmail );
183
184 // rot13 encoding
185 // phpcs:ignore Generic.PHP.ForbiddenFunctions.Found -- str_rot13 is intentional for email obfuscation
186 $encEmail = str_rot13( $encEmail );
187
188 // replace @
189 $encEmail = str_replace( '@', '[at]', $encEmail );
190
191 return $encEmail;
192 }
193
194 /**
195 * Get the ebcoded email icon
196 *
197 * @param string $text
198 * @return string
199 */
200 public function get_encoded_email_icon( $text = 'Email encoded successfully!' )
201 {
202
203 $html = '<i class="eeb-encoded dashicons-before dashicons-lock" title="' . esc_attr( $text ) . '"></i>';
204
205 return apply_filters( 'eeb/validate/get_encoded_email_icon', $html, $text );
206 }
207
208 /**
209 * Create a protected email
210 *
211 * @param string $display
212 * @param array< string, string > $attrs
213 * @param string $protection_method
214 * @return string
215 */
216 public function create_protected_mailto( $display, $attrs = [], $protection_method = null )
217 {
218 $email = '';
219 $class_ori = ( empty( $attrs['class'] ) ) ? '' : $attrs['class'];
220 $custom_class = (string) $this->getSetting( 'class_name', true );
221 $show_encoded_check = (string) $this->getSetting( 'show_encoded_check', true );
222
223 if ( ! empty( $attrs['href'] ) && stripos( $attrs['href'], 'mailto:' ) === 0 ) {
224 $email = substr( $attrs['href'], 7 );
225 }
226
227 // set user-defined class
228 if ( $custom_class !== '' && strpos( $class_ori, $custom_class ) === false ) {
229 $attrs['class'] = ( empty( $attrs['class'] ) ) ? $custom_class : $attrs['class'] . ' ' . $custom_class;
230 }
231
232 // check title for email address
233 if ( ! empty( $attrs['title'] ) ) {
234 $attrs['title'] = $this->filterPlainEmails( $attrs['title'], '{{email}}' ); // {{email}} will be replaced in javascript
235 }
236
237 // set ignore to data-attribute to prevent being processed by WPEL plugin
238 $attrs['data-wpel-link'] = 'ignore';
239
240 // create element code
241 $link = '<a ';
242
243 foreach ( $attrs as $key => $value ) {
244 if ( strtolower( $key ) === 'href' ) {
245 if ( $protection_method === 'without_javascript' ) {
246 $link .= $key . '="' . antispambot( $value ) . '" ';
247 } else {
248 $encoded_email = $this->get_encoded_email( $email );
249
250 // set attrs
251 $link .= 'href="javascript:;" ';
252 $link .= 'data-enc-email="' . esc_attr( $encoded_email ) . '" ';
253 }
254
255 } else {
256 $link .= esc_attr( $key ) . '="' . esc_attr( $value ) . '" ';
257 }
258 }
259
260 // remove last space
261 $link = substr( $link, 0, -1 );
262
263 $link .= '>';
264
265 // Only scramble the display when it IS the email (classic <a href="mailto:x">x</a> shape).
266 // When the display holds richer content (e.g. a builder-wrapped <span>Email us at x</span>),
267 // keep the markup intact — the final filterPlainEmails pass below entity-encodes any emails
268 // still embedded in it so bots can't harvest them.
269 $display_is_just_email = ( $email !== '' && trim( strip_tags( (string) $display ) ) === $email );
270
271 if ( $display_is_just_email ) {
272 $link .= $this->get_protected_display( $display, $protection_method );
273 } else {
274 $link .= $display;
275 }
276
277 $link .= '</a>';
278
279 // filter
280 $link = apply_filters( 'eeb_mailto', $link, $display, $email, $attrs );
281
282 // just in case there are still email addresses f.e. within title-tag
283 $link = $this->filterPlainEmails( $link, null, 'char_encode' );
284
285 // mark link as successfullly encoded (for admin users)
286 if ( current_user_can( $this->getAdminCap( 'frontend-display-security-check' ) ) && $show_encoded_check !== '' ) {
287 $link .= $this->get_encoded_email_icon();
288 }
289
290
291 return $link;
292 }
293
294 /**
295 * Create a protected custom attribute
296 *
297 * @param string $display
298 * @param array< string, string > $attrs Optional
299 * @param string $protection_method
300 * @return string
301 */
302 public function create_protected_href_att( $display, $attrs = [], $protection_method = null )
303 {
304 $email = '';
305 $class_ori = ( empty( $attrs['class'] ) ) ? '' : $attrs['class'];
306 $custom_class = (string) $this->getSetting( 'class_name', true );
307 $show_encoded_check = (string) $this->getSetting( 'show_encoded_check', true );
308
309 // set user-defined class
310 if ( $custom_class !== '' && strpos( $class_ori, $custom_class ) === false ) {
311 $attrs['class'] = ( empty( $attrs['class'] ) ) ? $custom_class : $attrs['class'] . ' ' . $custom_class;
312 }
313
314 // check title for email address
315 if ( ! empty( $attrs['title'] ) ) {
316 $attrs['title'] = antispambot( $attrs['title'] );
317 }
318
319 // set ignore to data-attribute to prevent being processed by WPEL plugin
320 $attrs['data-wpel-link'] = 'ignore';
321
322 // create element code
323 $link = '<a ';
324
325 foreach ( $attrs as $key => $value ) {
326 if ( strtolower( $key ) === 'href' ) {
327 $link .= $key . '="' . antispambot( $value ) . '" ';
328 } else {
329 $link .= esc_attr( $key ) . '="' . esc_attr( $value ) . '" ';
330 }
331 }
332
333 // remove last space
334 $link = substr( $link, 0, -1 );
335
336 $link .= '>';
337
338 $link .= $this->get_protected_display( $display, $protection_method );
339
340 $link .= '</a>';
341
342 // filter
343 $link = apply_filters( 'eeb_custom_href', $link, $display, $email, $attrs );
344
345 // mark link as successfullly encoded (for admin users)
346 if ( current_user_can( $this->getAdminCap( 'frontend-display-security-check' ) ) && $show_encoded_check ) {
347 $link .= $this->get_encoded_email_icon( 'Custom attribute encoded successfully!' );
348 }
349
350
351 return $link;
352 }
353
354 /**
355 * Create protected display combining these 3 methods:
356 * - reversing string
357 * - adding no-display spans with dummy values
358 * - using the wp antispambot function
359 *
360 * @param string|array<string> $display
361 * @param string $protection_method
362 * @return string Protected display
363 */
364 public function get_protected_display( $display, $protection_method = null )
365 {
366
367 $convert_plain_to_image = (bool) $this->getSetting( 'convert_plain_to_image', true, 'filter_body' );
368 $protection_text = (string) $this->getSetting( 'protection_text', true );
369 $raw_display = $display;
370
371 // get display out of array (result of preg callback)
372 if ( is_array( $display ) ) {
373 $display = $display[0];
374 }
375
376 if ( $convert_plain_to_image ) {
377 // generate_email_image_url() requires a bare email address (it runs is_email()
378 // on its input and returns false otherwise). Page builders like WPForms, Divi,
379 // and Elementor commonly wrap the email in HTML (<span>x</span>) or surround
380 // it with copy ("Contact us at x"), which previously produced <img src="">
381 // — the broken image rendered invisibly on the frontend.
382 $email_for_image = $display;
383 if ( ! is_email( (string) $display ) ) {
384 $stripped = wp_strip_all_tags( (string) $display );
385 $email_match = [];
386 if ( preg_match( $this->settings()->get_email_regex(), $stripped, $email_match ) ) {
387 $email_for_image = $email_match[0];
388 }
389 }
390 $display = '<img src="' . $this->generate_email_image_url( $email_for_image ) . '" />';
391 } elseif ( $protection_method !== 'without_javascript' ) {
392 $display = $this->dynamic_js_email_encoding( $display, $protection_text );
393 } else {
394 $display = $this->encode_email_css( $display );
395 }
396
397 return apply_filters( 'eeb/validate/get_protected_display', $display, $raw_display, $protection_method, $protection_text );
398 }
399
400 /**
401 * Dynamic email encoding with certain javascript methods
402 *
403 * @param string $email
404 * @param string $protection_text
405 * @return string the encoded email
406 */
407 public function dynamic_js_email_encoding( $email, $protection_text = '' )
408 {
409 $return = $email;
410 $rand = apply_filters( 'eeb/validate/random_encoding', wp_rand( 0, 2 ), $email, $protection_text );
411
412 switch ( $rand ) {
413 case 2:
414 $return = $this->encode_escape( $return, $protection_text );
415 break;
416 case 1:
417 $return = $this->encode_ascii( $return, $protection_text );
418 break;
419 default:
420 $return = $this->encode_ascii( $return, $protection_text );
421 break;
422 }
423
424 return $return;
425 }
426
427 /**
428 * @param string $display
429 * @return string
430 */
431 public function encode_email_css( $display )
432 {
433 $deactivate_rtl = (bool) $this->getSetting( 'deactivate_rtl', true, 'filter_body' );
434
435 // $this->log( 'display: ' . $display );
436 $stripped_display = wp_strip_all_tags( $display );
437 $stripped_display = html_entity_decode( $stripped_display );
438
439 $length = strlen( $stripped_display );
440 $interval = (int) ceil( min( 5, $length / 2 ) );
441 $offset = 0;
442 $dummy_data = time();
443 $protected = '';
444 $protection_classes = 'eeb';
445
446 if ( $deactivate_rtl ) {
447 $rev = $stripped_display;
448 $protection_classes .= ' eeb-nrtl';
449 } else {
450 // reverse string ( will be corrected with CSS )
451 $rev = strrev( $stripped_display );
452 $protection_classes .= ' eeb-rtl';
453 }
454
455
456 while ( $offset < $length ) {
457 $protected .= '<span class="eeb-sd">' . antispambot( substr( $rev, $offset, $interval ) ) . '</span>';
458
459 // Dummy content between real segments confuses scrapers. It's kept hidden from
460 // humans via CSS, but we also inline display:none so it never leaks as visible
461 // text if the plugin's stylesheet fails to load (page builders that defer/strip
462 // CSS, caching layers, or other plugins dropping the eeb-css-frontend handle).
463 $protected .= '<span class="eeb-nodis" style="display:none">' . $dummy_data . '</span>';
464 $offset += $interval;
465 }
466
467 // Inline the bidi-override / word-break styles for the same reason the dummy spans
468 // inline display:none — the email must still render correctly (forward, not reversed)
469 // when the stylesheet isn't loaded.
470 $wrapper_style = $deactivate_rtl
471 ? 'word-break:break-all'
472 : 'unicode-bidi:bidi-override;direction:rtl';
473
474 $protected = '<span class="' . $protection_classes . '" style="' . $wrapper_style . '">' . $protected . '</span>';
475
476 return $protected;
477 }
478
479
480 /**
481 * @return string
482 */
483 public function email_to_image( string $email, string $image_string_color = 'default', string $image_background_color = 'default', int $alpha_string = 0, int $alpha_fill = 127, int $font_size = 4 )
484 {
485
486 $setting_image_string_color = (string) $this->getSetting( 'image_color', true, 'image_settings' );
487 $setting_image_background_color = (string) $this->getSetting( 'image_background_color', true, 'image_settings' );
488 $image_text_opacity = (int) $this->getSetting( 'image_text_opacity', true, 'image_settings' );
489 $image_background_opacity = (int) $this->getSetting( 'image_background_opacity', true, 'image_settings' );
490 $image_font_size = (int) $this->getSetting( 'image_font_size', true, 'image_settings' );
491 $border_height = (int) $this->getSetting( 'image_underline', true, 'image_settings' );
492 $border_padding = 0;
493 $border_offset = 2;
494
495 if ( $image_background_color === 'default' ) {
496 $image_background_color = $setting_image_background_color;
497 } else {
498 $image_background_color = '0,0,0';
499 }
500
501 $colors = explode( ',', $image_background_color );
502 $bg_red = max( 0, min( 255, (int) $colors[0] ) );
503 $bg_green = max( 0, min( 255, (int) $colors[1] ) );
504 $bg_blue = max( 0, min( 255, (int) $colors[2] ) );
505
506 if ( $image_string_color === 'default' ) {
507 $image_string_color = $setting_image_string_color;
508 } else {
509 $image_string_color = '0,0,0';
510 }
511
512 $colors = explode( ',', $image_string_color );
513 $string_red = max( 0, min( 255, (int) $colors[0] ) );
514 $string_green = max( 0, min( 255, (int) $colors[1] ) );
515 $string_blue = max( 0, min( 255, (int) $colors[2] ) );
516
517 if (
518 ! empty( $image_text_opacity )
519 && $image_text_opacity >= 0
520 && $image_text_opacity <= 127
521 ) {
522 $alpha_string = intval( $image_text_opacity );
523 }
524 $alpha_string = max( 0, min( 127, $alpha_string ) );
525
526 if (
527 ! empty( $image_background_opacity )
528 && $image_background_opacity >= 0
529 && $image_background_opacity <= 127
530 ) {
531 $alpha_fill = intval( $image_background_opacity );
532 }
533 $alpha_fill = max( 0, min( 127, $alpha_fill ) );
534
535 if ( ! empty( $image_font_size ) && $image_font_size >= 1 && $image_font_size <= 5 ) {
536 $font_size = intval( $image_font_size );
537 }
538
539 $img_width = max( 1, imagefontwidth( $font_size ) * strlen( $email ) );
540 $img_height = imagefontheight( $font_size );
541
542 if ( ! empty( $border_height ) ) {
543 $img_real_height = max( 1, $img_height + $border_offset + $border_height );
544 } else {
545 $img_real_height = max( 1, $img_height );
546 }
547
548 $img = imagecreatetruecolor( $img_width, $img_real_height );
549 imagesavealpha( $img, true );
550 imagefill( $img, 0, 0, max( 0, imagecolorallocatealpha($img, $bg_red, $bg_green, $bg_blue, $alpha_fill ) ) );
551 imagestring(
552 $img,
553 $font_size,
554 0,
555 0,
556 $email,
557 max( 0, imagecolorallocatealpha( $img, $string_red, $string_green, $string_blue, $alpha_string ) )
558 );
559
560
561 if ( ! empty( $border_height ) ) {
562 $border_fill = imagecolorallocatealpha ($img, $string_red, $string_green, $string_blue, $alpha_string );
563 imagefilledrectangle(
564 $img,
565 0,
566 $border_offset + $img_height + $border_height - 1,
567 $border_padding + $img_width,
568 $border_offset + $img_height,
569 max( 0, $border_fill )
570 );
571 }
572
573 ob_start();
574 imagepng( $img );
575 imagedestroy( $img );
576
577 return (string) ob_get_clean();
578 }
579
580
581 /**
582 * @param string $email
583 * @param string $secret
584 * @return string|bool
585 */
586 public function generate_email_signature( string $email, string $secret )
587 {
588
589 if ( ! $secret ) {
590 return false;
591 }
592
593 $hash_signature = apply_filters( 'eeb/validate/email_signature', 'sha256', $email );
594
595 return base64_encode( hash_hmac( $hash_signature, $email, $secret, true ) );
596 }
597
598 /**
599 * @param string $email
600 * @return string|bool
601 */
602 public function generate_email_image_url( ?string $email )
603 {
604 if ( ! function_exists( 'imagefontwidth' ) || empty( $email ) || ! is_email( $email ) ) {
605 return false;
606 }
607
608 $secret = $this->settings()->get_email_image_secret();
609 $signature = (string) $this->generate_email_signature( $email, $secret );
610 $url = home_url();
611 $url .= '?eeb_mail=' . urlencode( base64_encode( $email ) );
612 $url .= '&eeb_hash=' . urlencode( $signature );
613
614 $url = apply_filters( 'eeb/validate/generate_email_image_url', $url, $email );
615
616 return $url;
617 }
618
619 }
620