From a42e8ce6eaabdb18f1b6822970e665b24ba0a744 Mon Sep 17 00:00:00 2001 From: alexandergull Date: Wed, 21 May 2025 15:14:41 +0500 Subject: [PATCH 1/3] New. EmailEncoder. Shortcode to exclude data from encoding. Lot of refactoring. --- .../{ => EmailEncoder}/EmailEncoder.php | 937 +++++++----------- .../EmailEncoder/EmailEncoderHelper.php | 155 +++ .../Antispam/{ => EmailEncoder}/Encoder.php | 2 +- .../EmailEncoder/ExclusionsService.php | 230 +++++ .../{ => EmailEncoder}/Obfuscator.php | 2 +- .../ObfuscatorEmailData.php | 2 +- .../Shortcodes/EmailEncoderShortCode.php | 57 ++ .../Shortcodes/EncodeContentSC.php | 95 ++ .../Shortcodes/ShortCodesService.php | 42 + .../Shortcodes/SkipContentFromEncodeSC.php | 109 ++ .../ApbctWP/Antispam/EmailEncoder.php | 6 +- lib/Cleantalk/ApbctWP/ShortCode.php | 46 + tests/Antispam/TestEmailEncoder.php | 2 +- .../testEmailEncoderShortCodeEncode.php | 68 ++ .../testEmailEnocderShortcodeSkip.php | 67 ++ 15 files changed, 1235 insertions(+), 585 deletions(-) rename lib/Cleantalk/Antispam/{ => EmailEncoder}/EmailEncoder.php (69%) create mode 100644 lib/Cleantalk/Antispam/EmailEncoder/EmailEncoderHelper.php rename lib/Cleantalk/Antispam/{ => EmailEncoder}/Encoder.php (99%) create mode 100644 lib/Cleantalk/Antispam/EmailEncoder/ExclusionsService.php rename lib/Cleantalk/Antispam/{ => EmailEncoder}/Obfuscator.php (99%) rename lib/Cleantalk/Antispam/{ => EmailEncoder}/ObfuscatorEmailData.php (96%) create mode 100644 lib/Cleantalk/Antispam/EmailEncoder/Shortcodes/EmailEncoderShortCode.php create mode 100644 lib/Cleantalk/Antispam/EmailEncoder/Shortcodes/EncodeContentSC.php create mode 100644 lib/Cleantalk/Antispam/EmailEncoder/Shortcodes/ShortCodesService.php create mode 100644 lib/Cleantalk/Antispam/EmailEncoder/Shortcodes/SkipContentFromEncodeSC.php create mode 100644 lib/Cleantalk/ApbctWP/ShortCode.php create mode 100644 tests/Antispam/testEmailEncoderShortCodeEncode.php create mode 100644 tests/Antispam/testEmailEnocderShortcodeSkip.php diff --git a/lib/Cleantalk/Antispam/EmailEncoder.php b/lib/Cleantalk/Antispam/EmailEncoder/EmailEncoder.php similarity index 69% rename from lib/Cleantalk/Antispam/EmailEncoder.php rename to lib/Cleantalk/Antispam/EmailEncoder/EmailEncoder.php index 78874821d..808a75543 100644 --- a/lib/Cleantalk/Antispam/EmailEncoder.php +++ b/lib/Cleantalk/Antispam/EmailEncoder/EmailEncoder.php @@ -1,73 +1,54 @@ [attributes]. - * @var array[] + * @var ExclusionsService */ - private $attribute_exclusions_signs = array( - 'input' => array('placeholder', 'value'), - 'img' => array('alt', 'title'), - ); - /** - * Attributes with possible email-like content to drop from the content to avoid unnecessary encoding. - * Key is a tag we want to find, value is an attribute with email to drop. - * @var array + * @var string[] */ - private static $attributes_to_drop = array( - 'a' => 'title', - ); - + public $decoded_emails_array; /** * @var string[] */ - protected $decoded_emails_array; + public $encoded_emails_array; /** - * @var string[] + * @var bool */ - protected $encoded_emails_array; - + public $has_connection_error; + /** + * @var ExclusionsService + */ + private $exclusions; + /** + * @var EmailEncoderHelper + */ + private $helper; + /** + * @var ShortCodesService + */ + private $shortcodes; /** * @var string */ private $response; - /** * Temporary content to use in regexp callback * @var string @@ -76,19 +57,23 @@ class EmailEncoder /** * @var bool */ - protected $has_connection_error; + private $privacy_policy_hook_handled = false; /** - * @var bool + * @var string */ - protected $privacy_policy_hook_handled = false; + private $aria_regex = '/aria-label.?=.?[\'"].+?[\'"]/'; /** - * @var string + * @var array */ - protected $aria_regex = '/aria-label.?=.?[\'"].+?[\'"]/'; + private $aria_matches = array(); /** + * Attributes with possible email-like content to drop from the content to avoid unnecessary encoding. + * Key is a tag we want to find, value is an attribute with email to drop. * @var array */ - protected $aria_matches = array(); + private static $attributes_to_drop = array( + 'a' => 'title', + ); /** * @var string */ @@ -98,10 +83,6 @@ class EmailEncoder */ private $global_replacing_text; - /** - * @var Encoder - */ - public $encoder; /** * @inheritDoc @@ -110,22 +91,24 @@ protected function init() { global $apbct; + $this->exclusions = new ExclusionsService(); + $this->shortcodes = new ShortCodesService(); + $this->helper = new EmailEncoderHelper(); + $this->encoder = new Encoder(md5($apbct->api_key)); - if ( ! apbct_api_key__is_correct() || ! $apbct->key_is_ok ) { + if ( $this->exclusions->doSkipBeforeAnything() ) { return; } - $this->registerShortcodeForEncoding(); + $this->shortcodes->registerAll(); + $this->shortcodes->addActionsAfterModify('the_content', 11); + $this->shortcodes->addActionsAfterModify('the_title', 11); + add_action('the_title', array($this->shortcodes->exclude, 'clearTitleContentFromShortcodeConstruction'), 12); $this->registerHookHandler(); - if ( ! $apbct->settings['data__email_decoder'] ) { - return; - } - - // Excluded request - if ($this->isExcludedRequest()) { + if ( $this->exclusions->doSkipBeforeModifyingHooksAdded() ) { return; } @@ -149,7 +132,8 @@ protected function init() 'render_block', ); foreach ( $hooks_to_encode as $hook ) { - add_filter($hook, array($this, 'modifyContent')); + $this->shortcodes->addActionsBeforeModify($hook, 9); + add_filter($hook, array($this, 'modifyContent'), 10); } // Search data to buffer @@ -157,12 +141,17 @@ protected function init() add_action('wp', 'apbct_buffer__start'); add_action('shutdown', 'apbct_buffer__end', 0); add_action('shutdown', array($this, 'bufferOutput'), 2); + $this->shortcodes->addActionsAfterModify('shutdown', 3); } // integration with Business Directory Plugin add_filter('wpbdp_form_field_display', array($this, 'modifyFormFieldDisplay'), 10, 4); } + /* + * =============== MODIFYING =============== + */ + /** * @param string $html * @param object $field @@ -182,7 +171,7 @@ public function modifyFormFieldDisplay($html, $field, $display_context, $post_id } /** - * @param $content string + * @param string $content * * @return string * @psalm-suppress PossiblyUnusedReturnValue @@ -196,84 +185,28 @@ public function modifyContent($content) $do_encode_emails = (bool)$apbct->settings['data__email_decoder_encode_email_addresses']; $do_encode_phones = (bool)$apbct->settings['data__email_decoder_encode_phone_numbers']; - if (!$do_encode_emails && !$do_encode_phones ) { + if ( $this->exclusions->doReturnContentBeforeModify($content) ) { return $content; } - if ( apbct_is_user_logged_in() && !apbct_is_in_uri('options-general.php?page=cleantalk') ) { - return $content; - } - - //skip empty or invalid content - if ( empty($content) || !is_string($content) ) { - return $content; - } - - // skip encoding if the content is already encoded with hook - // Extract shortcode content to protect it from email encoding - $shortcode_pattern = '/\[apbct_encode_data\](.*?)\[\/apbct_encode_data\]/s'; - $shortcode_replacements = []; - $shortcode_counter = 0; - $content = preg_replace_callback($shortcode_pattern, function ($matches) use (&$shortcode_replacements, &$shortcode_counter) { - $placeholder = '%%APBCT_SHORTCODE_' . ($shortcode_counter++) . '%%'; - if (isset($matches[0])) { - $shortcode_replacements[$placeholder] = $matches[0]; - } - return $placeholder; - }, $content); - - if ( static::skipEncodingOnHooks() ) { - return $content; - } - - if ( $this->hasContentExclusions($content) ) { + if ($this->shortcodes->exclude->isContentExcluded($content)) { return $content; } // modify content to prevent aria-label replaces by hiding it - $content = $this->modifyAriaLabelContent($content); + $content = $this->handleAriaLabelContent($content); - //will use this in regexp callback + // will use this in regexp callback $this->temp_content = $content; $content = self::dropAttributesContainEmail($content, self::$attributes_to_drop); + // Main logic + $do_encode_emails && $content = $this->modifyGlobalEmails($content); $do_encode_phones && $content = $this->modifyGlobalPhoneNumbers($content); - // Restore shortcodes - foreach ($shortcode_replacements as $placeholder => $original) { - $content = str_replace($placeholder, $original, $content); - } - - return $content; - } - - /** - * Drop attributes contains email from tag in the content to avoid unnecessary encoding. - * - * Example: Email - * Will be turned to Email - * - * @param string $content The content to process. - * @return string The content with attributes removed. - */ - private static function dropAttributesContainEmail($content, $tags) - { - $attribute_content_chunk = '[\s]{0,}=[\s]{0,}[\"\']\b[_A-Za-z0-9-\.]+@[_A-Za-z0-9-\.]+\..*\b[\"\']'; - foreach ($tags as $tag => $attribute) { - // Regular expression to match the attribute without the tag - $regexp_chunk_without_tag = "/{$attribute}{$attribute_content_chunk}/"; - // Regular expression to match the attribute with the tag - $regexp_chunk_with_tag = "/<{$tag}.*{$attribute}{$attribute_content_chunk}/"; - // Find all matches of the attribute with the tag in the content - preg_match_all($regexp_chunk_with_tag, $content, $matches); - if (!empty($matches[0])) { - // Remove the attribute without the tag from the content - $content = preg_replace($regexp_chunk_without_tag, '', $content, count($matches[0])); - } - } return $content; } @@ -295,24 +228,32 @@ public function modifyGlobalEmails($content) } //chek if email is placed in excluded attributes and return unchanged if so - if ( isset($matches[0][0]) && $this->hasAttributeExclusions($matches[0][0]) ) { + if ( isset($matches[0][0]) && $this->helper->hasAttributeExclusions($matches[0][0], $this->temp_content) ) { return $matches[0][0]; } // skip encoding if the content in script tag - if ( isset($matches[0][0]) && $this->isInsideScriptTag($matches[0][0], $content) ) { + if ( isset($matches[0][0]) && $this->helper->isInsideScriptTag($matches[0][0], $content) ) { return $matches[0][0]; } - if ( isset($matches[0][0]) && $this->isMailto($matches[0][0]) ) { + if ( isset($matches[0][0]) && $this->helper->isMailto($matches[0][0]) ) { return $this->encodeMailtoLinkV2($matches[0], $content); } - if ( isset($matches[0]) && $this->isMailtoAdditionalCopy($matches[0], $content) ) { + if ( + isset($matches[0]) && + is_array($matches[0]) && + $this->helper->isMailtoAdditionalCopy($matches[0], $content) + ) { return ''; } - if ( isset($matches[0], $matches[0][0]) && $this->isEmailInLink($matches[0], $content) ) { + if ( + isset($matches[0], $matches[0][0]) && + is_array($matches[0]) && + $this->helper->isEmailInLink($matches[0], $content) + ) { return $matches[0][0]; } @@ -333,11 +274,11 @@ public function modifyGlobalEmails($content) } //chek if email is placed in excluded attributes and return unchanged if so - if ( isset($matches[0]) && $this->hasAttributeExclusions($matches[0]) ) { + if ( isset($matches[0]) && $this->helper->hasAttributeExclusions($matches[0], $this->temp_content) ) { return $matches[0]; } - if ( isset($matches[0]) && $this->isMailto($matches[0]) ) { + if ( isset($matches[0]) && $this->helper->isMailto($matches[0]) ) { return $this->encodeMailtoLink($matches[0]); } @@ -352,7 +293,7 @@ public function modifyGlobalEmails($content) } // modify content to turn back aria-label - $replacing_result = $this->modifyAriaLabelContent($replacing_result, true); + $replacing_result = $this->handleAriaLabelContent($replacing_result, true); //please keep this var (do not simplify the code) for further debug return $replacing_result; @@ -373,17 +314,17 @@ public function modifyGlobalPhoneNumbers($content) $replacing_result = preg_replace_callback($pattern, function ($matches) use ($content) { if ( isset($matches[0][0]) && is_array($matches[0])) { - if ($this->isTelTag($matches[0][0])) { + if ($this->helper->isTelTag($matches[0][0])) { return $this->encodeTelLinkV2($matches[0], $content); } $item_length = strlen(str_replace([' ', '(', ')', '-', '+'], '', $matches[0][0])); if ($item_length > 12 || $item_length < 8) { return $matches[0][0]; } - if ($this->hasAttributeExclusions($matches[0][0])) { + if ($this->helper->hasAttributeExclusions($matches[0][0], $this->temp_content)) { return $matches[0][0]; } - if ($this->isInsideScriptTag($matches[0][0], $content)) { + if ($this->helper->isInsideScriptTag($matches[0][0], $content)) { return $matches[0][0]; } } @@ -406,7 +347,7 @@ public function modifyGlobalPhoneNumbers($content) if ( version_compare(phpversion(), '7.4.0', '<') ) { $replacing_result = preg_replace_callback($pattern, function ($matches) { if ( isset($matches[0]) ) { - if ($this->isTelTag($matches[0]) ) { + if ($this->helper->isTelTag($matches[0]) ) { return $this->encodeTelLink($matches[0]); } @@ -415,7 +356,7 @@ public function modifyGlobalPhoneNumbers($content) return $matches[0]; } - if ($this->hasAttributeExclusions($matches[0][0])) { + if ($this->helper->hasAttributeExclusions($matches[0][0], $this->temp_content)) { return $matches[0]; } } @@ -436,12 +377,18 @@ public function modifyGlobalPhoneNumbers($content) } // modify content to turn back aria-label - $replacing_result = $this->modifyAriaLabelContent($replacing_result, true); + $replacing_result = $this->handleAriaLabelContent($replacing_result, true); //please keep this var (do not simplify the code) for further debug return $replacing_result; } + /** + * Wrapper. Encode any string. + * @param $string + * + * @return string + */ public function modifyAny($string) { $encoded_string = $this->encodeAny($string); @@ -450,150 +397,20 @@ public function modifyAny($string) return $encoded_string; } - /** - * Ajax handler for the apbct_decode_email action - * - * @return void returns json string to the JS - */ - public function ajaxDecodeEmailHandler() - { - if (! defined('REST_REQUEST') && !apbct_is_user_logged_in()) { - AJAXService::checkPublicNonce(); - } - - // use non ssl mode for logged in user on settings page - if ( apbct_is_user_logged_in() ) { - $this->decoded_emails_array = $this->ignoreOpenSSLMode()->decodeEmailFromPost(); - $this->response = $this->compileResponse($this->decoded_emails_array, true); - wp_send_json_success($this->response); - } - - $this->decoded_emails_array = $this->decodeEmailFromPost(); - - if ( $this->checkRequest() ) { - //has error response from cloud - if ( $this->has_connection_error ) { - $this->response = $this->compileResponse($this->decoded_emails_array, false); - wp_send_json_error($this->response); - } - //decoding is allowed by cloud - $this->response = $this->compileResponse($this->decoded_emails_array, true); - wp_send_json_success($this->response); - } - //decoding is not allowed by cloud - $this->response = $this->compileResponse($this->decoded_emails_array, false); - //important - frontend waits success true to handle response - wp_send_json_success($this->response); - } - - /** - * Main logic of the decoding the encoded data. - * - * @return string[] array of decoded email - */ - public function decodeEmailFromPost() - { - $encoded_emails_array = Post::get('encodedEmails') ? Post::get('encodedEmails') : false; - if ( $encoded_emails_array ) { - $encoded_emails_array = str_replace('\\', '', $encoded_emails_array); - $this->encoded_emails_array = json_decode($encoded_emails_array, true); - } - - foreach ( $this->encoded_emails_array as $_key => $encoded_email) { - $this->decoded_emails_array[$encoded_email] = $this->encoder->decodeString($encoded_email); - } - - return $this->decoded_emails_array; - } - - /** - * Ajax handler for the apbct_decode_email action - * - * @return bool returns json string to the JS - */ - protected function checkRequest() - { - return true; - } - - /** @psalm-suppress PossiblyUnusedParam */ - protected function compileResponse($decoded_emails_array, $is_allowed) - { - $result = array(); - - if ( empty($decoded_emails_array) ) { - return false; - } - - foreach ( $decoded_emails_array as $_encoded_email => $decoded_email ) { - $result[] = strip_tags($decoded_email, ''); - } - return $result; - } - - /** - * Check if the given email is inside a script tag - * @param string $email The email to check - * @param string $content The full content - * @return bool - */ - private function isInsideScriptTag($email, $content) - { - // Find position of the email in content - $pos = strpos($content, $email); - if ($pos === false) { - return false; - } - - // Find the last script opening tag before the email - $last_script_start = strrpos(substr($content, 0, $pos), '', $last_script_start); - if ($script_end === false) { - return false; - } - - // The email is inside a script tag if its position is between the opening and closing tags - return ($pos > $last_script_start && $pos < $script_end); - } - - /** - * @param string $email_str - * - * @return string - */ - public function getObfuscatedEmailString($email_str) + public function bufferOutput() { - $obfuscator = new Obfuscator(); - $chunks = $obfuscator->getEmailData($email_str); - return $obfuscator->obfuscate_success ? $chunks->getFinalString() : $email_str; + global $apbct; + echo $this->modifyContent($apbct->buffer); } - /** - * @param $email_str - * - * @return string + /* + * =============== ENCODE ENTITIES =============== */ - private function addMagicBlurEmail($email_str) - { - $obfuscator = new Obfuscator(); - $chunks = $obfuscator->getEmailData($email_str); - $chunks_data = $obfuscator->obfuscate_success ? $chunks : false; - - return false !== $chunks_data - ? $this->addMagicBlurViaChunksData($chunks_data) - : $this->addMagicBlurToString($email_str) - ; - } /** * Method to process plain email * - * @param $email_str string + * @param string $email_str * * @return string */ @@ -650,175 +467,29 @@ private function encodeAny($string, $mode = 'blur', $replacing_text = null, $is_ } /** - * @param bool $is_email - * @param string $obfuscated_string - * @param string $mode - * @param string $replacing_text - * @param ObfuscatorEmailData|false $email_chunks_data + * Method to process mailto: links. For PHP < 7.4 + * + * @param string $mailto_link_str * * @return string */ - private function applyEffectsOnMode($is_email, $obfuscated_string, $mode, $replacing_text = null, $email_chunks_data = null) + private function encodeMailtoLink($mailto_link_str) { - switch ($mode) { - case 'blur': - $handled_string = $is_email && $email_chunks_data - ? $this->addMagicBlurEmail($obfuscated_string) - : $this->addMagicBlurToString($obfuscated_string); - break; - case 'obfuscate': - $handled_string = $is_email - ? $this->getObfuscatedEmailString($obfuscated_string) - : $obfuscated_string; - break; - case 'replace': - $handled_string = !empty($replacing_text) ? $replacing_text : static::getDefaultReplacingText(); - $handled_string = '' . $handled_string . ''; - break; - default: - return $obfuscated_string; + // Get inner tag text and place it in $matches[1] + preg_match('/mailto\:(\b[_A-Za-z0-9-\.]+@[_A-Za-z0-9-\.]+\.[A-Za-z]{2,})/', $mailto_link_str, $matches); + if ( isset($matches[1]) ) { + $mailto_inner_text = preg_replace_callback('/\b[_A-Za-z0-9-\.]+@[_A-Za-z0-9-\.]+\.[A-Za-z]{2,}/', function ($matches) { + if (isset($matches[0])) { + return $this->getObfuscatedEmailString($matches[0]); + } + }, $matches[1]); } - return $handled_string; - } - - private function constructEncodedSpan($encoded_string, $obfuscated_string) - { - return "" . $obfuscated_string . ""; - } - - /** - * @param ObfuscatorEmailData $email_chunks - * - * @return string - */ - private function addMagicBlurViaChunksData($email_chunks) - { - return $email_chunks->chunk_raw_left - . '' . $email_chunks->chunk_obfuscated_left . '' - . $email_chunks->chunk_raw_center - . '' . $email_chunks->chunk_obfuscated_right . '' - . $email_chunks->chunk_raw_right - . $email_chunks->domain; - } - - /** - * @param string $obfuscated_string with ** symbols - * - * @return string - */ - private function addMagicBlurToString($obfuscated_string) - { - //preparing data to blur - $regex = '/^([^*]+)(\*+)([^*]+)$/'; - preg_match_all($regex, $obfuscated_string, $matches); - if (isset($matches[1][0], $matches[2][0], $matches[3][0])) { - $first = $matches[1][0]; - $middle = $matches[2][0]; - $end = $matches[3][0]; - } else { - return $obfuscated_string; - } - return $first . '' . $middle . '' . $end; - } - - /** - * Checking if the string contains mailto: link - * - * @param $string string - * - * @return bool - */ - private function isMailto($string) - { - return strpos($string, 'mailto:') !== false; - } - - /** - * Checking if the string contains tel: link - * - * @param $string string - * - * @return bool - */ - private function isTelTag($string) - { - return strpos($string, 'tel:') !== false; - } - - /** - * Checking if the string contains mailto: link - * - * @param $match array - * @param $content string - * - * @return bool - */ - private function isMailtoAdditionalCopy($match, $content) - { - $position = $match[1]; - - $cc_position = strrpos(substr($content, 0, $position), 'cc='); - if ( $cc_position !== false && $cc_position + 3 == $position ) { - return true; - } - - $bcc_position = strrpos(substr($content, 0, $position), 'bcc='); - if ( $bcc_position !== false && $bcc_position + 4 == $position ) { - return true; - } - - return false; - } - - /** - * Checking if email in link - * - * @param $match array - * @param $content string - * - * @return bool - */ - private function isEmailInLink($match, $content) - { - $email = $match[0]; - $position = $match[1]; - - $href_position = strrpos(substr($content, 0, $position), 'href='); - - if ( $href_position !== false && $href_position + 6 == $position ) { - return true; - } - - return strpos($email, 'mailto:') !== false; - } - - /** - * Method to process mailto: links. For PHP < 7.4 - * - * @param $mailto_link_str string - * - * @return string - */ - private function encodeMailtoLink($mailto_link_str) - { - // Get inner tag text and place it in $matches[1] - preg_match('/mailto\:(\b[_A-Za-z0-9-\.]+@[_A-Za-z0-9-\.]+\.[A-Za-z]{2,})/', $mailto_link_str, $matches); - if ( isset($matches[1]) ) { - $mailto_inner_text = preg_replace_callback('/\b[_A-Za-z0-9-\.]+@[_A-Za-z0-9-\.]+\.[A-Za-z]{2,}/', function ($matches) { - if (isset($matches[0])) { - return $this->getObfuscatedEmailString($matches[0]); - } - }, $matches[1]); - } - $mailto_link_str = str_replace('mailto:', '', $mailto_link_str); - $encoded = $this->encoder->encodeString($mailto_link_str); - - $text = isset($mailto_inner_text) ? $mailto_inner_text : $mailto_link_str; - - return 'mailto:' . $text . '" data-original-string="' . $encoded . '" title="' . esc_attr($this->getTooltip()); + $mailto_link_str = str_replace('mailto:', '', $mailto_link_str); + $encoded = $this->encoder->encodeString($mailto_link_str); + + $text = isset($mailto_inner_text) ? $mailto_inner_text : $mailto_link_str; + + return 'mailto:' . $text . '" data-original-string="' . $encoded . '" title="' . esc_attr($this->getTooltip()); } /** @@ -918,184 +589,226 @@ private function encodeTelLinkV2($match, $content) } /** - * Get text for the title attribute + * @param string $email_str * * @return string */ - private function getTooltip() + public function getObfuscatedEmailString($email_str) { - global $apbct; - return sprintf( - esc_html__('This contact has been encoded by %s. Click to decode. To finish the decoding make sure that JavaScript is enabled in your browser.', 'cleantalk-spam-protect'), - esc_html__($apbct->data['wl_brandname']) - ); + $obfuscator = new Obfuscator(); + $chunks = $obfuscator->getEmailData($email_str); + return $obfuscator->obfuscate_success ? $chunks->getFinalString() : $email_str; } + /* + * =============== VISUALS =============== + */ + /** - * Check content if it contains exclusions from exclusion list - * @param $content - content to check - * @return bool - true if exclusions found, else - false + * @param $email_str + * + * @return string */ - private function hasContentExclusions($content) + private function addMagicBlurEmail($email_str) { - if ( is_array($this->content_exclusions_signs) ) { - foreach ( array_values($this->content_exclusions_signs) as $_signs_array => $signs ) { - //process each of subarrays of signs - $signs_found_count = 0; - if ( isset($signs) && is_array($signs) ) { - //chek all the signs in the sub-array - foreach ( $signs as $sign ) { - if ( is_string($sign) ) { - if ( strpos($content, $sign) === false ) { - continue; - } else { - $signs_found_count++; - } - } - } - //if each of signs in the sub-array are found return true - if ( $signs_found_count === count($signs) ) { - if (in_array('et_pb_contact_form', $signs) && !is_admin()) { - return false; - } - return true; - } - } - } - } - //no signs found - return false; + $obfuscator = new Obfuscator(); + $chunks = $obfuscator->getEmailData($email_str); + $chunks_data = $obfuscator->obfuscate_success ? $chunks : false; + + return false !== $chunks_data + ? $this->addMagicBlurViaChunksData($chunks_data) + : $this->addMagicBlurToString($email_str) + ; } /** - * Excluded requests + * @param bool $is_email + * @param string $obfuscated_string + * @param string $mode + * @param string $replacing_text + * @param ObfuscatorEmailData|false $email_chunks_data + * + * @return string */ - private function isExcludedRequest() + private function applyEffectsOnMode($is_email, $obfuscated_string, $mode, $replacing_text = null, $email_chunks_data = null) { - // Excluded request by alt cookie - $apbct_email_encoder_passed = Cookie::get('apbct_email_encoder_passed'); - if ( $apbct_email_encoder_passed === apbct_get_email_encoder_pass_key() ) { - return true; - } - - if ( - apbct_is_plugin_active('ultimate-member/ultimate-member.php') && - isset($_POST['um_request']) && - array_key_exists('REQUEST_METHOD', $_SERVER) && - strtoupper($_SERVER['REQUEST_METHOD']) === 'POST' && - empty(Post::get('encodedEmail')) - ) { - return true; + switch ($mode) { + case 'blur': + $handled_string = $is_email && $email_chunks_data + ? $this->addMagicBlurEmail($obfuscated_string) + : $this->addMagicBlurToString($obfuscated_string); + break; + case 'obfuscate': + $handled_string = $is_email + ? $this->getObfuscatedEmailString($obfuscated_string) + : $obfuscated_string; + break; + case 'replace': + $handled_string = !empty($replacing_text) ? $replacing_text : static::getDefaultReplacingText(); + $handled_string = '' . $handled_string . ''; + break; + default: + return $obfuscated_string; } + return $handled_string; + } - return false; + /** + * @param $encoded_string + * @param $obfuscated_string + * + * @return string + */ + private function constructEncodedSpan($encoded_string, $obfuscated_string) + { + return "" . $obfuscated_string . ""; } /** - * Check if email is placed in the tag that has attributes of exclusions. - * @param $email_match - email - * @return bool + * @param ObfuscatorEmailData $email_chunks + * + * @return string */ - private function hasAttributeExclusions($email_match) + private function addMagicBlurViaChunksData($email_chunks) { - $email_match = preg_quote($email_match); - foreach ( $this->attribute_exclusions_signs as $tag => $array_of_attributes ) { - foreach ( $array_of_attributes as $attribute ) { - //do not remove IDE highlighted unnecessary escape! - $pattern = '/<' - . $tag - . '+\s+[^>]*\b' - . $attribute - . '=((\\\')|")?[^"]*\b' - . $email_match - . '\b[^"]*((\\\')|")?"[^>]*>/'; - preg_match($pattern, $this->temp_content, $attr_match); - if ( !empty($attr_match) ) { - return true; - } - } - } - return false; + return $email_chunks->chunk_raw_left + . '' . $email_chunks->chunk_obfuscated_left . '' + . $email_chunks->chunk_raw_center + . '' . $email_chunks->chunk_obfuscated_right . '' + . $email_chunks->chunk_raw_right + . $email_chunks->domain; } /** - * Modify content to skip aria-label cases correctly. - * @param string $content - * @param bool $reverse + * @param string $obfuscated_string with ** symbols * * @return string */ - private function modifyAriaLabelContent($content, $reverse = false) + private function addMagicBlurToString($obfuscated_string) { - if ( !$reverse ) { - $this->aria_matches = array(); - //save match - preg_match($this->aria_regex, $content, $this->aria_matches); - if (empty($this->aria_matches)) { - return $content; - } - //replace with temp - return preg_replace($this->aria_regex, 'ct_temp_aria', $content); - } - if ( !empty($this->aria_matches[0]) ) { - //replace temp with match - return preg_replace('/ct_temp_aria/', $this->aria_matches[0], $content); + //preparing data to blur + $regex = '/^([^*]+)(\*+)([^*]+)$/'; + preg_match_all($regex, $obfuscated_string, $matches); + if (isset($matches[1][0], $matches[2][0], $matches[3][0])) { + $first = $matches[1][0]; + $middle = $matches[2][0]; + $end = $matches[3][0]; + } else { + return $obfuscated_string; } - return $content; + return $first . '' . $middle . '' . $end; } - public function bufferOutput() + /** + * Get text for the title attribute + * + * @return string + */ + private function getTooltip() { global $apbct; - echo $this->modifyContent($apbct->buffer); + return sprintf( + esc_html__('This contact has been encoded by %s. Click to decode. To finish the decoding make sure that JavaScript is enabled in your browser.', 'cleantalk-spam-protect'), + esc_html__($apbct->data['wl_brandname']) + ); } - private function handlePrivacyPolicyHook() + + /* + * =============== DECODING =============== + */ + + + /** + * Ajax handler for the apbct_decode_email action + * + * @return void returns json string to the JS + */ + public function ajaxDecodeEmailHandler() { - if ( !$this->privacy_policy_hook_handled && current_action() === 'the_title' ) { - add_filter('the_privacy_policy_link', function ($link) { - return wp_specialchars_decode($link); - }); - $this->privacy_policy_hook_handled = true; + if (! defined('REST_REQUEST') && !apbct_is_user_logged_in()) { + AJAXService::checkPublicNonce(); } + + // use non ssl mode for logged in user on settings page + if ( apbct_is_user_logged_in() ) { + $this->decoded_emails_array = $this->ignoreOpenSSLMode()->decodeEmailFromPost(); + $this->response = $this->compileResponse($this->decoded_emails_array, true); + wp_send_json_success($this->response); + } + + $this->decoded_emails_array = $this->decodeEmailFromPost(); + + if ( $this->checkRequest() ) { + //has error response from cloud + if ( $this->has_connection_error ) { + $this->response = $this->compileResponse($this->decoded_emails_array, false); + wp_send_json_error($this->response); + } + //decoding is allowed by cloud + $this->response = $this->compileResponse($this->decoded_emails_array, true); + wp_send_json_success($this->response); + } + //decoding is not allowed by cloud + $this->response = $this->compileResponse($this->decoded_emails_array, false); + //important - frontend waits success true to handle response + wp_send_json_success($this->response); } /** - * Skip encoder run on hooks. + * Main logic of the decoding the encoded data. * - * 1. Applies filter "apbct_hook_skip_email_encoder_on_url_list" to get modified list of URI chunks that needs to skip. - * @return bool + * @return string[] array of decoded email */ - private static function skipEncodingOnHooks() + public function decodeEmailFromPost() { - $skip_encode = false; - $url_chunk_list = array(); - - // Apply filter "apbct_hook_skip_email_encoder_on_url_list" to get the URI chunk list. - $url_chunk_list = apply_filters('apbct_skip_email_encoder_on_uri_chunk_list', $url_chunk_list); + $encoded_emails_array = Post::get('encodedEmails') ? Post::get('encodedEmails') : false; + if ( $encoded_emails_array ) { + $encoded_emails_array = str_replace('\\', '', $encoded_emails_array); + $this->encoded_emails_array = json_decode($encoded_emails_array, true); + } - if ( !empty($url_chunk_list) && is_array($url_chunk_list) ) { - foreach ($url_chunk_list as $chunk) { - if (is_string($chunk) && strpos(TT::toString(Server::get('REQUEST_URI')), $chunk) !== false) { - $skip_encode = true; - break; - } - } + foreach ( $this->encoded_emails_array as $_key => $encoded_email) { + $this->decoded_emails_array[$encoded_email] = $this->encoder->decodeString($encoded_email); } - return $skip_encode; + return $this->decoded_emails_array; } /** - * Fluid. Ignore SSL mode for encoding/decoding on the instance. - * @return $this + * Ajax handler for the apbct_decode_email action + * + * @return bool returns json string to the JS */ - public function ignoreOpenSSLMode() + protected function checkRequest() { - $this->encoder->useSSL(false); - return $this; + return true; + } + + /** @psalm-suppress PossiblyUnusedParam */ + protected function compileResponse($decoded_emails_array, $is_allowed) + { + $result = array(); + + if ( empty($decoded_emails_array) ) { + return false; + } + + foreach ( $decoded_emails_array as $_encoded_email => $decoded_email ) { + $result[] = strip_tags($decoded_email, ''); + } + return $result; } + + /* + * =============== SERVICE =============== + */ + + /** * Register AJAX routes to run decoding * @return void @@ -1106,28 +819,96 @@ public function registerAjaxRoute() add_action('wp_ajax_nopriv_apbct_decode_email', array($this, 'ajaxDecodeEmailHandler')); } - private function registerShortcodeForEncoding() + /** + * @return void + */ + private function registerHookHandler() { - add_shortcode('apbct_encode_data', [$this, 'shortcodeCallback']); + add_filter('apbct_encode_data', [$this, 'modifyAny']); + add_filter('apbct_encode_email_data', [$this, 'modifyContent']); } - public function shortcodeCallback($_atts, $content, $_tag) + /** + * Fluid. Ignore SSL mode for encoding/decoding on the instance. + * @return $this + */ + public function ignoreOpenSSLMode() { - if ( Cookie::get('apbct_email_encoder_passed') === apbct_get_email_encoder_pass_key() ) { - return $content; - } + $this->encoder->useSSL(false); + return $this; + } - return $this->modifyAny($content); + /** + * @return string + */ + protected static function getDefaultReplacingText() + { + return 'Click to show email!'; } - private function registerHookHandler() + /** + * Drop attributes contains email from tag in the content to avoid unnecessary encoding. + * + * Example: Email + * Will be turned to Email + * + * @param string $content The content to process. + * @return string The content with attributes removed. + */ + private static function dropAttributesContainEmail($content, $tags) { - add_filter('apbct_encode_data', [$this, 'modifyAny']); - add_filter('apbct_encode_email_data', [$this, 'modifyContent']); + $attribute_content_chunk = '[\s]{0,}=[\s]{0,}[\"\']\b[_A-Za-z0-9-\.]+@[_A-Za-z0-9-\.]+\..*\b[\"\']'; + foreach ($tags as $tag => $attribute) { + // Regular expression to match the attribute without the tag + $regexp_chunk_without_tag = "/{$attribute}{$attribute_content_chunk}/"; + // Regular expression to match the attribute with the tag + $regexp_chunk_with_tag = "/<{$tag}.*{$attribute}{$attribute_content_chunk}/"; + // Find all matches of the attribute with the tag in the content + preg_match_all($regexp_chunk_with_tag, $content, $matches); + if (!empty($matches[0])) { + // Remove the attribute without the tag from the content + $content = preg_replace($regexp_chunk_without_tag, '', $content, count($matches[0])); + } + } + return $content; } - protected static function getDefaultReplacingText() + /** + * Modify content to skip aria-label cases correctly. + * @param string $content + * @param bool $reverse + * + * @return string + */ + private function handleAriaLabelContent($content, $reverse = false) { - return 'Click to show email!'; + if ( !$reverse ) { + $this->aria_matches = array(); + //save match + preg_match($this->aria_regex, $content, $this->aria_matches); + if (empty($this->aria_matches)) { + return $content; + } + //replace with temp + return preg_replace($this->aria_regex, 'ct_temp_aria', $content); + } + if ( !empty($this->aria_matches[0]) ) { + //replace temp with match + return preg_replace('/ct_temp_aria/', $this->aria_matches[0], $content); + } + return $content; + } + + /** + * @return void + */ + private function handlePrivacyPolicyHook() + { + if ( !$this->privacy_policy_hook_handled && current_action() === 'the_title' ) { + add_filter('the_privacy_policy_link', function ($link) { + return wp_specialchars_decode($link); + }); + $this->privacy_policy_hook_handled = true; + } } } diff --git a/lib/Cleantalk/Antispam/EmailEncoder/EmailEncoderHelper.php b/lib/Cleantalk/Antispam/EmailEncoder/EmailEncoderHelper.php new file mode 100644 index 000000000..ad0a77473 --- /dev/null +++ b/lib/Cleantalk/Antispam/EmailEncoder/EmailEncoderHelper.php @@ -0,0 +1,155 @@ +[attributes]. + * @var array[] + */ + private $attribute_exclusions_signs = array( + 'input' => array('placeholder', 'value'), + 'img' => array('alt', 'title'), + ); + + /** + * Checking if the string contains mailto: link + * + * @param string $string + * + * @return bool + */ + public function isMailto($string) + { + return strpos($string, 'mailto:') !== false; + } + + /** + * Checking if the string contains tel: link + * + * @param string $string + * + * @return bool + */ + public function isTelTag($string) + { + return strpos($string, 'tel:') !== false; + } + + /** + * Checking if the string contains mailto: link + * + * @param array $match + * @param string $content + * + * @return bool + */ + public function isMailtoAdditionalCopy($match, $content) + { + $position = isset($match[1]) ? (int)$match[1] : null; + + if (null === $position) { + return false; + } + + $cc_position = strrpos(substr($content, 0, $position), 'cc='); + if ( $cc_position !== false && $cc_position + 3 == $position ) { + return true; + } + + $bcc_position = strrpos(substr($content, 0, $position), 'bcc='); + if ( $bcc_position !== false && $bcc_position + 4 == $position ) { + return true; + } + + return false; + } + + /** + * Checking if email in link + * + * @param array $matches + * @param string $content + * + * @return bool + */ + public function isEmailInLink($matches, $content) + { + $email = isset($matches[0]) && is_string($matches[0]) ? $matches[0] : null; + $position = isset($matches[1]) ? (int)$matches[1] : null; + + if (null === $position || null === $email) { + return false; + } + + $href_position = strrpos(substr($content, 0, $position), 'href='); + + if ( $href_position !== false && $href_position + 6 == $position ) { + return true; + } + + return strpos($email, 'mailto:') !== false; + } + + /** + * Check if the given email is inside a script tag + * @param string $email The email to check + * @param string $content The full content + * @return bool + */ + public function isInsideScriptTag($email, $content) + { + // Find position of the email in content + $pos = strpos($content, $email); + if ($pos === false) { + return false; + } + + // Find the last script opening tag before the email + $last_script_start = strrpos(substr($content, 0, $pos), '', $last_script_start); + if ($script_end === false) { + return false; + } + + // The email is inside a script tag if its position is between the opening and closing tags + return ($pos > $last_script_start && $pos < $script_end); + } + + /** + * Check if email is placed in the tag that has attributes of exclusions. + * @param string $email_match - email + * @param string $temp_content - email + * @return bool + */ + public function hasAttributeExclusions($email_match, $temp_content) + { + $email_match = preg_quote($email_match); + foreach ( $this->attribute_exclusions_signs as $tag => $array_of_attributes ) { + foreach ( $array_of_attributes as $attribute ) { + //do not remove IDE highlighted unnecessary escape! + $pattern = '/<' + . $tag + . '+\s+[^>]*\b' + . $attribute + . '=((\\\')|")?[^"]*\b' + . $email_match + . '\b[^"]*((\\\')|")?"[^>]*>/'; + preg_match($pattern, $temp_content, $attr_match); + if ( !empty($attr_match) ) { + return true; + } + } + } + return false; + } +} diff --git a/lib/Cleantalk/Antispam/Encoder.php b/lib/Cleantalk/Antispam/EmailEncoder/Encoder.php similarity index 99% rename from lib/Cleantalk/Antispam/Encoder.php rename to lib/Cleantalk/Antispam/EmailEncoder/Encoder.php index 070bdb45f..738092859 100644 --- a/lib/Cleantalk/Antispam/Encoder.php +++ b/lib/Cleantalk/Antispam/EmailEncoder/Encoder.php @@ -1,6 +1,6 @@ byAccessKeyFail($apbct) ) { + return 'byAccessKeyFail'; + } + + return false; + } + + /** + * @return string|false + * @psalm-suppress PossiblyUnusedReturnValue + */ + public function doSkipBeforeModifyingHooksAdded() + { + global $apbct; + + if ( $this->byPluginSetting($apbct) ) { + return 'byPluginSetting'; + } + + // Excluded request + if ( $this->byServerVars() ) { + return 'byServerVars'; + } + + return false; + } + + /** + * @param $content + * + * @return string|false + * @psalm-suppress PossiblyUnusedReturnValue + * @psalm-suppress PossiblyUnusedMethod + */ + public function doReturnContentBeforeModify($content) + { + global $apbct; + + if ( !(bool)$apbct->settings['data__email_decoder_encode_email_addresses'] && !(bool)$apbct->settings['data__email_decoder_encode_phone_numbers'] ) { + return 'globallyDisabledBothEncoding'; + } + + if ( $this->byLoggedIn() ) { + return 'byLoggedIn'; + } + + //skip empty or invalid content + if ( $this->byEmptyContent($content) ) { + return 'byEmptyContent'; + } + + if ( $this->byUrlOnHooks() ) { + return 'byUrlOnHooks'; + } + + if ( $this->byContentSigns($content) ) { + return 'byContentSigns'; + } + + return false; + } + + /** + * Excluded requests + * @return bool + */ + private function byServerVars() + { + // Excluded request by alt cookie + $apbct_email_encoder_passed = Cookie::get('apbct_email_encoder_passed'); + if ( $apbct_email_encoder_passed === apbct_get_email_encoder_pass_key() ) { + return true; + } + + if ( + apbct_is_plugin_active('ultimate-member/ultimate-member.php') && + isset($_POST['um_request']) && + array_key_exists('REQUEST_METHOD', $_SERVER) && + strtoupper($_SERVER['REQUEST_METHOD']) === 'POST' && + empty(Post::get('encodedEmail')) + ) { + return true; + } + + return false; + } + + /** + * @param State $apbct + * + * @return bool + */ + private function byPluginSetting($apbct) + { + return ! $apbct->settings['data__email_decoder']; + } + + /** + * @param State $apbct + * + * @return bool + */ + private function byAccessKeyFail($apbct) + { + return !$apbct->key_is_ok || ! apbct_api_key__is_correct(); + } + + /** + * Check content if it contains exclusions from exclusion list + * @param $content - content to check + * @return bool - true if exclusions found, else - false + */ + private function byContentSigns($content) + { + if ( is_array($this->content_exclusions_signs) ) { + foreach ( array_values($this->content_exclusions_signs) as $_signs_array => $signs ) { + //process each of subarrays of signs + $signs_found_count = 0; + if ( isset($signs) && is_array($signs) ) { + //chek all the signs in the sub-array + foreach ( $signs as $sign ) { + if ( is_string($sign) ) { + if ( strpos($content, $sign) === false ) { + continue; + } else { + $signs_found_count++; + } + } + } + //if each of signs in the sub-array are found return true + if ( $signs_found_count === count($signs) ) { + if (in_array('et_pb_contact_form', $signs) && !is_admin()) { + return false; + } + return true; + } + } + } + } + //no signs found + return false; + } + + /** + * @param string $content + * + * @return bool + */ + private function byEmptyContent($content) + { + //skip empty or invalid content + return empty($content) || !is_string($content); + } + + /** + * @return bool + */ + private function byLoggedIn() + { + return apbct_is_user_logged_in() && !apbct_is_in_uri('options-general.php?page=cleantalk'); + } + + /** + * Skip encoder run on hooks. + * + * 1. Applies filter "apbct_hook_skip_email_encoder_on_url_list" to get modified list of URI chunks that needs to skip. + * @return bool + */ + private function byUrlOnHooks() + { + $skip_encode = false; + $url_chunk_list = array(); + + // Apply filter "apbct_hook_skip_email_encoder_on_url_list" to get the URI chunk list. + $url_chunk_list = apply_filters('apbct_skip_email_encoder_on_uri_chunk_list', $url_chunk_list); + + if ( !empty($url_chunk_list) && is_array($url_chunk_list) ) { + foreach ($url_chunk_list as $chunk) { + if (is_string($chunk) && strpos(TT::toString(Server::get('REQUEST_URI')), $chunk) !== false) { + $skip_encode = true; + break; + } + } + } + + return $skip_encode; + } +} diff --git a/lib/Cleantalk/Antispam/Obfuscator.php b/lib/Cleantalk/Antispam/EmailEncoder/Obfuscator.php similarity index 99% rename from lib/Cleantalk/Antispam/Obfuscator.php rename to lib/Cleantalk/Antispam/EmailEncoder/Obfuscator.php index b5cd206c9..3bca7e775 100644 --- a/lib/Cleantalk/Antispam/Obfuscator.php +++ b/lib/Cleantalk/Antispam/EmailEncoder/Obfuscator.php @@ -1,6 +1,6 @@ public_name)) { + // Process the shortcode + $content = do_shortcode($content); + } + return $content; + } + + /** + * Modifies the content before the encoder processes it. + * + * @param string $content The content to modify. + * @return string The modified content. + * @psalm-suppress PossiblyUnusedMethod + */ + protected function changeContentBeforeEncoderModify($content) + { + return $content; + } + + /** + * Modifies the content after the encoder processes it. + * + * @param string $content The content to modify. + * @return string The modified content. + * @psalm-suppress PossiblyUnusedMethod + */ + protected function changeContentAfterEncoderModify($content) + { + return $content; + } +} diff --git a/lib/Cleantalk/Antispam/EmailEncoder/Shortcodes/EncodeContentSC.php b/lib/Cleantalk/Antispam/EmailEncoder/Shortcodes/EncodeContentSC.php new file mode 100644 index 000000000..07eaaf300 --- /dev/null +++ b/lib/Cleantalk/Antispam/EmailEncoder/Shortcodes/EncodeContentSC.php @@ -0,0 +1,95 @@ +modifyAny($content); + } + + /** + * Modifies the content before the encoder processes it. + * + * Extracts shortcode content and replaces it with placeholders to protect it + * from being encoded. + * + * @param string $content The content to modify. + * @return string The modified content with placeholders for shortcodes. + * @psalm-suppress PossiblyUnusedReturnValue + * @psalm-suppress PossiblyUnusedMethod + */ + public function changeContentBeforeEncoderModify($content) + { + // skip encoding if the content is already encoded with hook + // Extract shortcode content to protect it from email encoding + $shortcode_exist_pattern = sprintf('/\[%s\](.*?)\[\/%s\]/s', $this->public_name, $this->public_name); + $shortcode_counter = 0; + $content = preg_replace_callback($shortcode_exist_pattern, function ($matches) use (&$shortcode_counter) { + $placeholder = str_replace('#COUNT#', $shortcode_counter++, $this->exclusion_wrapper); + if (isset($matches[0])) { + $this->shortcode_replacements[$placeholder] = $matches[0]; + } + return $placeholder; + }, $content); + return $content; + } + + /** + * Modifies the content after the encoder processes it. + * + * Restores the original shortcodes from placeholders and executes the callback action. + * + * @param string $content The content to modify. + * @return string The modified content with restored shortcodes. + * @psalm-suppress PossiblyUnusedReturnValue + * @psalm-suppress PossiblyUnusedMethod + */ + public function changeContentAfterEncoderModify($content) + { + // Restore shortcodes + foreach ($this->shortcode_replacements as $placeholder => $original) { + $content = str_replace($placeholder, $original, $content); + } + return $this->doCallbackAction($content); + } +} diff --git a/lib/Cleantalk/Antispam/EmailEncoder/Shortcodes/ShortCodesService.php b/lib/Cleantalk/Antispam/EmailEncoder/Shortcodes/ShortCodesService.php new file mode 100644 index 000000000..57195df42 --- /dev/null +++ b/lib/Cleantalk/Antispam/EmailEncoder/Shortcodes/ShortCodesService.php @@ -0,0 +1,42 @@ +shortcodes_registered) { + $this->encode->register(); + $this->exclude->register(); + $this->shortcodes_registered = true; + } + } + + public function __construct() + { + $this->encode = new EncodeContentSC(); + $this->exclude = new SkipContentFromEncodeSC(); + } + + public function addActionsBeforeModify($hook, $priority = 1) + { + add_filter($hook, array($this->exclude, 'changeContentBeforeEncoderModify'), $priority); + add_filter($hook, array($this->encode, 'changeContentBeforeEncoderModify'), $priority); + } + + public function addActionsAfterModify($hook, $priority = 999) + { + add_filter($hook, array($this->exclude, 'changeContentAfterEncoderModify'), $priority); + add_filter($hook, array($this->encode, 'changeContentAfterEncoderModify'), $priority); + } +} diff --git a/lib/Cleantalk/Antispam/EmailEncoder/Shortcodes/SkipContentFromEncodeSC.php b/lib/Cleantalk/Antispam/EmailEncoder/Shortcodes/SkipContentFromEncodeSC.php new file mode 100644 index 000000000..912efe32e --- /dev/null +++ b/lib/Cleantalk/Antispam/EmailEncoder/Shortcodes/SkipContentFromEncodeSC.php @@ -0,0 +1,109 @@ +exclusion_wrapper; + $content = $wrapper . $content . $wrapper; + return $content; + } + + /** + * Modifies the content before the encoder processes it. + * + * Executes the shortcode callback action on the content. + * + * @param string $content The content to modify. + * @return string The modified content. + * @psalm-suppress PossiblyUnusedReturnValue + * @psalm-suppress PossiblyUnusedMethod + */ + public function changeContentBeforeEncoderModify($content) + { + return $this->doCallbackAction($content); + } + + /** + * Modifies the content after the encoder processes it. + * + * Removes the exclusion wrapper from the content. + * + * @param string $content The content to modify. + * @return string The modified content. + * @psalm-suppress PossiblyUnusedReturnValue + * @psalm-suppress PossiblyUnusedMethod + */ + public function changeContentAfterEncoderModify($content) + { + $content = str_replace( + $this->exclusion_wrapper, + '', + $content + ); + return $content; + } + + /** + * Checks if the content is excluded from encoding. + * + * Uses a regular expression to determine if the content is wrapped + * in the exclusion wrapper. + * + * @param string $content The content to check. + * @return false|int Returns 1 if the content is excluded, 0 otherwise. + * @psalm-suppress PossiblyUnusedMethod + */ + public function isContentExcluded($content) + { + $exclusion_regex = '/' . $this->exclusion_wrapper . '.*' . $this->exclusion_wrapper . '/'; + return preg_match($exclusion_regex, $content); + } + + /** + * Clear the title if visitor is already checked. + * @param $content + * + * @return string + * @psalm-suppress PossiblyUnusedMethod + */ + public function clearTitleContentFromShortcodeConstruction($content) + { + $shortcode_pattern = sprintf('/\[%s\](.*?)\[\/%s\]/s', $this->public_name, $this->public_name); + preg_match_all($shortcode_pattern, $content, $matches); + $data_skipped = isset($matches[1][0]) ? $matches[1][0] : null; + if (is_null($data_skipped)) { + return $content; + } + $content_cleared = preg_replace($shortcode_pattern, $data_skipped, $content); + return is_string($content_cleared) ? $content_cleared : $content; + } +} diff --git a/lib/Cleantalk/ApbctWP/Antispam/EmailEncoder.php b/lib/Cleantalk/ApbctWP/Antispam/EmailEncoder.php index fde5c06f9..8dd975cee 100644 --- a/lib/Cleantalk/ApbctWP/Antispam/EmailEncoder.php +++ b/lib/Cleantalk/ApbctWP/Antispam/EmailEncoder.php @@ -2,13 +2,13 @@ namespace Cleantalk\ApbctWP\Antispam; +use Cleantalk\Antispam\Cleantalk; +use Cleantalk\Antispam\CleantalkRequest; use Cleantalk\ApbctWP\Helper; use Cleantalk\Common\TT; use Cleantalk\Variables\Post; -use Cleantalk\Antispam\CleantalkRequest; -use Cleantalk\Antispam\Cleantalk; -class EmailEncoder extends \Cleantalk\Antispam\EmailEncoder +class EmailEncoder extends \Cleantalk\Antispam\EmailEncoder\EmailEncoder { /** * @var null|string Comment from API response diff --git a/lib/Cleantalk/ApbctWP/ShortCode.php b/lib/Cleantalk/ApbctWP/ShortCode.php new file mode 100644 index 000000000..026436088 --- /dev/null +++ b/lib/Cleantalk/ApbctWP/ShortCode.php @@ -0,0 +1,46 @@ +public_name, [$this, 'callback']); + } +} diff --git a/tests/Antispam/TestEmailEncoder.php b/tests/Antispam/TestEmailEncoder.php index 8275b7f2c..bfd25500f 100644 --- a/tests/Antispam/TestEmailEncoder.php +++ b/tests/Antispam/TestEmailEncoder.php @@ -2,7 +2,7 @@ namespace Antispam; -use Cleantalk\Antispam\EmailEncoder; +use Cleantalk\Antispam\EmailEncoder\EmailEncoder; use PHPUnit\Framework\TestCase; class TestEmailEncoder extends TestCase diff --git a/tests/Antispam/testEmailEncoderShortCodeEncode.php b/tests/Antispam/testEmailEncoderShortCodeEncode.php new file mode 100644 index 000000000..96056ee51 --- /dev/null +++ b/tests/Antispam/testEmailEncoderShortCodeEncode.php @@ -0,0 +1,68 @@ +shortcode = new EncodeContentSC(); + $this->shortcode->register(); + global $apbct; + $apbct->api_key = 'tetskey'; + $apbct->data['cookies_type'] = 'native'; + $apbct->saveData(); + } + + public function testCallbackEncodesContent() + { + $_COOKIE['apbct_email_encoder_passed'] = apbct_get_email_encoder_pass_key(); + Cookie::set('apbct_email_encoder_passed', apbct_get_email_encoder_pass_key()); + $cookie = Cookie::get('apbct_email_encoder_passed'); + $content = 'Test content'; + $result = $this->shortcode->callback([], $content, 'apbct_encode_data'); + + $this->assertEquals('Test content', $result); + } + + public function testCallbackReturnsOriginalContentIfCookieSet() + { + $_COOKIE['apbct_email_encoder_passed'] = apbct_get_email_encoder_pass_key(); + $content = 'Test content'; + + $result = $this->shortcode->callback([], $content, 'apbct_encode_data'); + + $this->assertEquals('Test content', $result); + } + + public function testChangeContentBeforeEncoderModifyReplacesShortcodesWithPlaceholders() + { + $content = 'Some content with [apbct_encode_data]Test content[/apbct_encode_data]'; + $result = $this->shortcode->changeContentBeforeEncoderModify($content); + + $this->assertStringContainsString('%%APBCT_SHORT_CODE_INCLUDE_EE_0%%', $result); + $this->assertArrayHasKey('%%APBCT_SHORT_CODE_INCLUDE_EE_0%%', $this->shortcode->shortcode_replacements); + } + + public function testChangeContentAfterEncoderModifyRestoresShortcodes() + { + $this->shortcode->shortcode_replacements = [ + '%%APBCT_SHORT_CODE_INCLUDE_EE_0%%' => '[apbct_encode_data]Test content[/apbct_encode_data]' + ]; + $content = '%%APBCT_SHORT_CODE_INCLUDE_EE_0%%'; + $result = $this->shortcode->changeContentAfterEncoderModify($content); + + $this->assertEquals('Test content', $result); + } +} diff --git a/tests/Antispam/testEmailEnocderShortcodeSkip.php b/tests/Antispam/testEmailEnocderShortcodeSkip.php new file mode 100644 index 000000000..c61490871 --- /dev/null +++ b/tests/Antispam/testEmailEnocderShortcodeSkip.php @@ -0,0 +1,67 @@ +shortcode = new SkipContentFromEncodeSC(); + $this->shortcode->register(); + } + + public function testCallbackWrapsContentInExclusionWrapper() + { + $content = 'Test content'; + $result = $this->shortcode->callback([], $content, 'apbct_skip_encoding'); + + $this->assertEquals('%%APBCT_SHORT_CODE_EXCLUDE_EE%%Test content%%APBCT_SHORT_CODE_EXCLUDE_EE%%', $result); + } + + public function testChangeContentBeforeEncoderModifyExecutesCallback() + { + $content = 'Some content with [apbct_skip_encoding]Test content[/apbct_skip_encoding]'; + $result = $this->shortcode->changeContentBeforeEncoderModify($content); + + $this->assertStringContainsString('%%APBCT_SHORT_CODE_EXCLUDE_EE%%Test content%%APBCT_SHORT_CODE_EXCLUDE_EE%%', $result); + } + + public function testChangeContentAfterEncoderModifyRemovesExclusionWrapper() + { + $content = '%%APBCT_SHORT_CODE_EXCLUDE_EE%%Test content%%APBCT_SHORT_CODE_EXCLUDE_EE%%'; + $result = $this->shortcode->changeContentAfterEncoderModify($content); + + $this->assertEquals('Test content', $result); + } + + public function testIsContentExcludedReturnsTrueForExcludedContent() + { + $content = '%%APBCT_SHORT_CODE_EXCLUDE_EE%%Test content%%APBCT_SHORT_CODE_EXCLUDE_EE%%'; + $result = $this->shortcode->isContentExcluded($content); + + $this->assertEquals(1, $result); + } + + public function testIsContentExcludedReturnsFalseForNonExcludedContent() + { + $content = 'Test content'; + $result = $this->shortcode->isContentExcluded($content); + + $this->assertEquals(0, $result); + } + + public function testClearTitleContentFromShortcodeConstruction() + { + $content = 'Some content with [apbct_skip_encoding]Test content[/apbct_skip_encoding]'; + $result = $this->shortcode->clearTitleContentFromShortcodeConstruction($content); + + $this->assertEquals('Some content with Test content', $result); + } +} From de03d9687dcb2a119a9b7db3d785a109d20c9e97 Mon Sep 17 00:00:00 2001 From: alexandergull Date: Fri, 23 May 2025 12:54:31 +0500 Subject: [PATCH 2/3] Code. Merge current fix/dev to the branch. --- lib/Cleantalk/Antispam/EmailEncoder/EmailEncoder.php | 9 +++++---- .../Antispam/EmailEncoder/EmailEncoderHelper.php | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/Cleantalk/Antispam/EmailEncoder/EmailEncoder.php b/lib/Cleantalk/Antispam/EmailEncoder/EmailEncoder.php index 808a75543..18cbafe0a 100644 --- a/lib/Cleantalk/Antispam/EmailEncoder/EmailEncoder.php +++ b/lib/Cleantalk/Antispam/EmailEncoder/EmailEncoder.php @@ -131,10 +131,6 @@ protected function init() 'widget_block_content', 'render_block', ); - foreach ( $hooks_to_encode as $hook ) { - $this->shortcodes->addActionsBeforeModify($hook, 9); - add_filter($hook, array($this, 'modifyContent'), 10); - } // Search data to buffer if ($apbct->settings['data__email_decoder_buffer'] && !apbct_is_ajax() && !apbct_is_rest() && !apbct_is_post() && !is_admin()) { @@ -142,6 +138,11 @@ protected function init() add_action('shutdown', 'apbct_buffer__end', 0); add_action('shutdown', array($this, 'bufferOutput'), 2); $this->shortcodes->addActionsAfterModify('shutdown', 3); + } else { + foreach ( $hooks_to_encode as $hook ) { + $this->shortcodes->addActionsBeforeModify($hook, 9); + add_filter($hook, array($this, 'modifyContent'), 10); + } } // integration with Business Directory Plugin diff --git a/lib/Cleantalk/Antispam/EmailEncoder/EmailEncoderHelper.php b/lib/Cleantalk/Antispam/EmailEncoder/EmailEncoderHelper.php index ad0a77473..bfdf611f0 100644 --- a/lib/Cleantalk/Antispam/EmailEncoder/EmailEncoderHelper.php +++ b/lib/Cleantalk/Antispam/EmailEncoder/EmailEncoderHelper.php @@ -14,6 +14,7 @@ class EmailEncoderHelper private $attribute_exclusions_signs = array( 'input' => array('placeholder', 'value'), 'img' => array('alt', 'title'), + 'div' => array('data-et-multi-view'), ); /** From 52b7b99a25fe903ee6332aeec4788ddf4255858d Mon Sep 17 00:00:00 2001 From: alexandergull Date: Wed, 28 May 2025 14:48:48 +0500 Subject: [PATCH 3/3] Fix. After test. Ignoring other parts of title fixed. --- .../Antispam/EmailEncoder/EmailEncoder.php | 4 -- .../Shortcodes/SkipContentFromEncodeSC.php | 56 ++++++++++++------- .../testEmailEnocderShortcodeSkip.php | 46 +++++++-------- 3 files changed, 60 insertions(+), 46 deletions(-) diff --git a/lib/Cleantalk/Antispam/EmailEncoder/EmailEncoder.php b/lib/Cleantalk/Antispam/EmailEncoder/EmailEncoder.php index 18cbafe0a..f36217983 100644 --- a/lib/Cleantalk/Antispam/EmailEncoder/EmailEncoder.php +++ b/lib/Cleantalk/Antispam/EmailEncoder/EmailEncoder.php @@ -190,10 +190,6 @@ public function modifyContent($content) return $content; } - if ($this->shortcodes->exclude->isContentExcluded($content)) { - return $content; - } - // modify content to prevent aria-label replaces by hiding it $content = $this->handleAriaLabelContent($content); diff --git a/lib/Cleantalk/Antispam/EmailEncoder/Shortcodes/SkipContentFromEncodeSC.php b/lib/Cleantalk/Antispam/EmailEncoder/Shortcodes/SkipContentFromEncodeSC.php index 912efe32e..5a84b14f7 100644 --- a/lib/Cleantalk/Antispam/EmailEncoder/Shortcodes/SkipContentFromEncodeSC.php +++ b/lib/Cleantalk/Antispam/EmailEncoder/Shortcodes/SkipContentFromEncodeSC.php @@ -18,7 +18,8 @@ class SkipContentFromEncodeSC extends EmailEncoderShortCode /** * @var string The wrapper used to mark content for exclusion. */ - private $exclusion_wrapper = '%%APBCT_SHORT_CODE_EXCLUDE_EE%%'; + private $exclusion_wrapper = '##SCE_%d##'; + private $replaces = array(); /** * Callback function for the shortcode. @@ -32,9 +33,7 @@ class SkipContentFromEncodeSC extends EmailEncoderShortCode */ public function callback($_atts, $content, $_tag) { - $wrapper = $this->exclusion_wrapper; - $content = $wrapper . $content . $wrapper; - return $content; + return $this->processExclusions($content); } /** @@ -64,28 +63,47 @@ public function changeContentBeforeEncoderModify($content) */ public function changeContentAfterEncoderModify($content) { - $content = str_replace( - $this->exclusion_wrapper, - '', - $content - ); - return $content; + return $this->revertExclusions($content); } /** - * Checks if the content is excluded from encoding. + * Apply exclusions to replace modified shortcodes with service symbols. Then collect all the performed + * replacements to memory storage to being reverted after common modifying. * - * Uses a regular expression to determine if the content is wrapped - * in the exclusion wrapper. - * - * @param string $content The content to check. - * @return false|int Returns 1 if the content is excluded, 0 otherwise. + * @param string|null $content The content to check. + * @return string Returns content with handled exclusions * @psalm-suppress PossiblyUnusedMethod */ - public function isContentExcluded($content) + public function processExclusions($content) + { + if (is_null($content)) { + return (string)$content; + } + $index = count($this->replaces); + $placeholder = sprintf($this->exclusion_wrapper, $index); + $this->replaces[$index] = [ + 'origin' => $content, + 'replace' => $placeholder + ]; + $wrappedContent = $placeholder; + + return $wrappedContent; + } + + /** + * Rollback al the replaces with modified shortcodes after common encoding. + * @param string $content + * + * @return string + */ + public function revertExclusions($content) { - $exclusion_regex = '/' . $this->exclusion_wrapper . '.*' . $this->exclusion_wrapper . '/'; - return preg_match($exclusion_regex, $content); + foreach ($this->replaces as $_item => $data) { + if (isset($data['replace'], $data['origin']) && is_string($data['replace']) && is_string($data['origin'])) { + $content = str_replace($data['replace'], $data['origin'], $content); + } + } + return $content; } /** diff --git a/tests/Antispam/testEmailEnocderShortcodeSkip.php b/tests/Antispam/testEmailEnocderShortcodeSkip.php index c61490871..6e5cd21b5 100644 --- a/tests/Antispam/testEmailEnocderShortcodeSkip.php +++ b/tests/Antispam/testEmailEnocderShortcodeSkip.php @@ -22,7 +22,7 @@ public function testCallbackWrapsContentInExclusionWrapper() $content = 'Test content'; $result = $this->shortcode->callback([], $content, 'apbct_skip_encoding'); - $this->assertEquals('%%APBCT_SHORT_CODE_EXCLUDE_EE%%Test content%%APBCT_SHORT_CODE_EXCLUDE_EE%%', $result); + $this->assertEquals('##SCE_0##', $result); } public function testChangeContentBeforeEncoderModifyExecutesCallback() @@ -30,38 +30,38 @@ public function testChangeContentBeforeEncoderModifyExecutesCallback() $content = 'Some content with [apbct_skip_encoding]Test content[/apbct_skip_encoding]'; $result = $this->shortcode->changeContentBeforeEncoderModify($content); - $this->assertStringContainsString('%%APBCT_SHORT_CODE_EXCLUDE_EE%%Test content%%APBCT_SHORT_CODE_EXCLUDE_EE%%', $result); + $this->assertStringContainsString('Some content with ##SCE_0##', $result); } - public function testChangeContentAfterEncoderModifyRemovesExclusionWrapper() - { - $content = '%%APBCT_SHORT_CODE_EXCLUDE_EE%%Test content%%APBCT_SHORT_CODE_EXCLUDE_EE%%'; - $result = $this->shortcode->changeContentAfterEncoderModify($content); - - $this->assertEquals('Test content', $result); - } - - public function testIsContentExcludedReturnsTrueForExcludedContent() + public function testClearTitleContentFromShortcodeConstruction() { - $content = '%%APBCT_SHORT_CODE_EXCLUDE_EE%%Test content%%APBCT_SHORT_CODE_EXCLUDE_EE%%'; - $result = $this->shortcode->isContentExcluded($content); + $content = 'Some content with [apbct_skip_encoding]Test content[/apbct_skip_encoding]'; + $result = $this->shortcode->clearTitleContentFromShortcodeConstruction($content); - $this->assertEquals(1, $result); + $this->assertEquals('Some content with Test content', $result); } - public function testIsContentExcludedReturnsFalseForNonExcludedContent() + public function testClearTitleContentFromShortcodeConstructionSkippingEmail() { - $content = 'Test content'; - $result = $this->shortcode->isContentExcluded($content); + $content = 'Hah, there is email example@exmple.com and some content with [apbct_skip_encoding]Test content[/apbct_skip_encoding]'; + $result = $this->shortcode->clearTitleContentFromShortcodeConstruction($content); - $this->assertEquals(0, $result); + $this->assertEquals('Hah, there is email example@exmple.com and some content with Test content', $result); } - public function testClearTitleContentFromShortcodeConstruction() + public function testTitleContentWithEmailSkippedAndUnskipped() { - $content = 'Some content with [apbct_skip_encoding]Test content[/apbct_skip_encoding]'; - $result = $this->shortcode->clearTitleContentFromShortcodeConstruction($content); - - $this->assertEquals('Some content with Test content', $result); + $origin_content = 'Hah, there is email example@exmple.com and some content with [apbct_skip_encoding]Test content[/apbct_skip_encoding]'; + $content = $origin_content; + //emulate hook before + $content = $this->shortcode->changeContentBeforeEncoderModify($content); + //do common modifying + $content = \Cleantalk\ApbctWP\Antispam\EmailEncoder::getInstance()->modifyContent($content); + //emulate hook after + $content = $this->shortcode->changeContentAfterEncoderModify($content); + + $this->assertStringNotContainsString( 'example@exmple.com', $content); + $this->assertStringNotContainsString( 'apbct_skip_encoding', $content); + $this->assertStringContainsString( 'with Test content', $content); } }