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 0cc6836ab..f36217983 100644
--- a/lib/Cleantalk/Antispam/EmailEncoder.php
+++ b/lib/Cleantalk/Antispam/EmailEncoder/EmailEncoder.php
@@ -1,75 +1,54 @@
[attributes].
- * @var array[]
+ * @var ExclusionsService
*/
- private $attribute_exclusions_signs = array(
- 'input' => array('placeholder', 'value'),
- 'sc-customer-email' => array('placeholder', 'value'),
- 'img' => array('alt', 'title'),
- 'div' => array('data-et-multi-view'),
- );
-
/**
- * 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
@@ -78,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
*/
@@ -100,10 +83,6 @@ class EmailEncoder
*/
private $global_replacing_text;
- /**
- * @var Encoder
- */
- public $encoder;
/**
* @inheritDoc
@@ -112,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;
}
@@ -156,9 +137,11 @@ 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);
} else {
foreach ( $hooks_to_encode as $hook ) {
- add_filter($hook, array($this, 'modifyContent'));
+ $this->shortcodes->addActionsBeforeModify($hook, 9);
+ add_filter($hook, array($this, 'modifyContent'), 10);
}
}
@@ -166,6 +149,10 @@ protected function init()
add_filter('wpbdp_form_field_display', array($this, 'modifyFormFieldDisplay'), 10, 4);
}
+ /*
+ * =============== MODIFYING ===============
+ */
+
/**
* @param string $html
* @param object $field
@@ -185,7 +172,7 @@ public function modifyFormFieldDisplay($html, $field, $display_context, $post_id
}
/**
- * @param $content string
+ * @param string $content
*
* @return string
* @psalm-suppress PossiblyUnusedReturnValue
@@ -199,84 +186,24 @@ 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 ) {
- 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->exclusions->doReturnContentBeforeModify($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;
}
@@ -298,24 +225,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];
}
@@ -336,11 +271,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]);
}
@@ -355,7 +290,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;
@@ -376,17 +311,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];
}
}
@@ -409,7 +344,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]);
}
@@ -418,7 +353,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];
}
}
@@ -439,12 +374,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);
@@ -453,150 +394,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);
+ }
+
+ /**
+ * 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..5a84b14f7
--- /dev/null
+++ b/lib/Cleantalk/Antispam/EmailEncoder/Shortcodes/SkipContentFromEncodeSC.php
@@ -0,0 +1,127 @@
+processExclusions($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)
+ {
+ return $this->revertExclusions($content);
+ }
+
+ /**
+ * Apply exclusions to replace modified shortcodes with service symbols. Then collect all the performed
+ * replacements to memory storage to being reverted after common modifying.
+ *
+ * @param string|null $content The content to check.
+ * @return string Returns content with handled exclusions
+ * @psalm-suppress PossiblyUnusedMethod
+ */
+ 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)
+ {
+ 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;
+ }
+
+ /**
+ * 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..6e5cd21b5
--- /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('##SCE_0##', $result);
+ }
+
+ public function testChangeContentBeforeEncoderModifyExecutesCallback()
+ {
+ $content = 'Some content with [apbct_skip_encoding]Test content[/apbct_skip_encoding]';
+ $result = $this->shortcode->changeContentBeforeEncoderModify($content);
+
+ $this->assertStringContainsString('Some content with ##SCE_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);
+ }
+
+ public function testClearTitleContentFromShortcodeConstructionSkippingEmail()
+ {
+ $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('Hah, there is email example@exmple.com and some content with Test content', $result);
+ }
+
+ public function testTitleContentWithEmailSkippedAndUnskipped()
+ {
+ $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);
+ }
+}