From b307c16bc91e066e71a6631664c6fce638e531e6 Mon Sep 17 00:00:00 2001 From: svfcode Date: Thu, 20 Aug 2026 16:10:25 +0500 Subject: [PATCH 1/2] Fix. ContactEncoder. Improve shortcode statement. --- .../Integrations/CEIntegrationCommentList.php | 184 ++++++++++++++++++ .../Shortcodes/ShortCodesService.php | 13 +- .../TestCEIntegrationCommentList.php | 51 +++++ .../TestContactsEncoderShortCodeEncode.php | 33 ++++ 4 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 lib/Cleantalk/ApbctWP/ContactsEncoder/Integrations/CEIntegrationCommentList.php create mode 100644 tests/ApbctWP/ContactsEncoder/Integrations/TestCEIntegrationCommentList.php diff --git a/lib/Cleantalk/ApbctWP/ContactsEncoder/Integrations/CEIntegrationCommentList.php b/lib/Cleantalk/ApbctWP/ContactsEncoder/Integrations/CEIntegrationCommentList.php new file mode 100644 index 000000000..9ede9092c --- /dev/null +++ b/lib/Cleantalk/ApbctWP/ContactsEncoder/Integrations/CEIntegrationCommentList.php @@ -0,0 +1,184 @@ + + */ + private $region_placeholders = array(); + + /** + * @param string $content + * + * @return string + */ + public function protect($content) + { + if ( ! is_string($content) || $content === '' ) { + return $content; + } + + if ( + stripos($content, 'comment-list') === false + && stripos($content, 'id="comments"') === false + && stripos($content, "id='comments'") === false + ) { + return $content; + } + + $this->region_placeholders = array(); + + $content = $this->protectElementsById($content, 'comments'); + $content = $this->protectElementsByClass($content, 'comment-list', 'ol'); + $content = $this->protectElementsByClass($content, 'comment-list', 'ul'); + + return $content; + } + + /** + * @param string $content + * + * @return string + */ + public function restore($content) + { + if ( ! is_string($content) || $this->region_placeholders === array() ) { + return $content; + } + + return strtr($content, $this->region_placeholders); + } + + /** + * @param string $content + * @param string $id + * + * @return string + */ + private function protectElementsById($content, $id) + { + $pattern = '/]*\bid=(["\'])' . preg_quote($id, '/') . '\1)/i'; + + return $this->protectByOpeningTagPattern($content, $pattern, 'div'); + } + + /** + * @param string $content + * @param string $class + * @param string $tag_name + * + * @return string + */ + private function protectElementsByClass($content, $class, $tag_name) + { + $pattern = '/<' . $tag_name . '\b(?=[^>]*\bclass=(["\'])[^"\']*\b' + . preg_quote($class, '/') + . '\b[^"\']*\1)/i'; + + return $this->protectByOpeningTagPattern($content, $pattern, $tag_name); + } + + /** + * @param string $content + * @param string $open_pattern + * @param string $tag_name + * + * @return string + */ + private function protectByOpeningTagPattern($content, $open_pattern, $tag_name) + { + $offset = 0; + + while ( preg_match($open_pattern, $content, $matches, PREG_OFFSET_CAPTURE, $offset) ) { + $start = $matches[0][1] ?? null; + + if ( $start === null ) { + break; + } + + $element = $this->extractBalancedElement($content, $start, $tag_name); + + if ( $element === null ) { + $offset = $start + 1; + continue; + } + + $placeholder = '%%APBCT_COMMENT_REGION_' . count($this->region_placeholders) . '%%'; + $this->region_placeholders[$placeholder] = $element; + + $content = substr($content, 0, $start) . $placeholder . substr($content, $start + strlen($element)); + $offset = $start + strlen($placeholder); + } + + return $content; + } + + /** + * @param string $content + * @param int $start + * @param string $tag_name + * + * @return string|null + */ + private function extractBalancedElement($content, $start, $tag_name) + { + $open_end = strpos($content, '>', $start); + + if ( $open_end === false ) { + return null; + } + + $depth = 1; + $pos = $open_end + 1; + $length = strlen($content); + $open_needle = '<' . $tag_name; + $close_needle = ' 0 ) { + $next_open = stripos($content, $open_needle, $pos); + $next_close = stripos($content, $close_needle, $pos); + + if ( $next_close === false ) { + return null; + } + + if ( $next_open !== false && $next_open < $next_close && $this->isTagOpenAt($content, $next_open, $tag_name) ) { + $depth++; + $pos = $next_open + 1; + continue; + } + + $depth--; + $close_end = strpos($content, '>', $next_close); + + if ( $close_end === false ) { + return null; + } + + $pos = $close_end + 1; + + if ( $depth === 0 ) { + return substr($content, $start, $pos - $start); + } + } + + return null; + } + + /** + * @param string $content + * @param int $offset + * @param string $tag_name + * + * @return bool + */ + private function isTagOpenAt($content, $offset, $tag_name) + { + return (bool) preg_match('/^<' . preg_quote($tag_name, '/') . '\b/i', substr($content, $offset, strlen($tag_name) + 2)); + } +} diff --git a/lib/Cleantalk/ApbctWP/ContactsEncoder/Shortcodes/ShortCodesService.php b/lib/Cleantalk/ApbctWP/ContactsEncoder/Shortcodes/ShortCodesService.php index 7dbdd82c5..7b716ae2a 100644 --- a/lib/Cleantalk/ApbctWP/ContactsEncoder/Shortcodes/ShortCodesService.php +++ b/lib/Cleantalk/ApbctWP/ContactsEncoder/Shortcodes/ShortCodesService.php @@ -2,6 +2,7 @@ namespace Cleantalk\ApbctWP\ContactsEncoder\Shortcodes; +use Cleantalk\ApbctWP\ContactsEncoder\Integrations\CEIntegrationCommentList; use Cleantalk\Common\ContactsEncoder\Dto\Params; /** @@ -15,6 +16,11 @@ class ShortCodesService public $shortcodes_registered = false; + /** + * @var CEIntegrationCommentList + */ + private $comment_list_integration; + /** * @return void */ @@ -65,6 +71,7 @@ public function __construct(Params $params) { $this->encode = new EncodeContentSC($params); $this->shortcode_to_exclude = new ExcludedEncodeContentSC(); + $this->comment_list_integration = new CEIntegrationCommentList(); } public function addActionsBeforeModify($hook, $priority = 1) @@ -100,6 +107,7 @@ public function modifyBufferBefore($buffer) { $this->encode->resetShortcodeReplacements(); $this->shortcode_to_exclude->resetShortcodeReplacements(); + $buffer = $this->comment_list_integration->protect($buffer); $buffer = $this->shortcode_to_exclude->changeContentBeforeEncoderModify($buffer); return $this->encode->changeContentBeforeEncoderModify($buffer); @@ -120,11 +128,14 @@ public function modifyBufferAfter($buffer) if ( $apbct->settings['data__email_decoder_buffer'] ) { $buffer = $this->shortcode_to_exclude->finalizeBufferAfterEncoding($buffer); + $buffer = $this->comment_list_integration->restore($buffer); $apbct->buffer = $buffer; return $buffer; } - return $this->shortcode_to_exclude->changeContentAfterEncoderModify($buffer); + $buffer = $this->shortcode_to_exclude->changeContentAfterEncoderModify($buffer); + + return $this->comment_list_integration->restore($buffer); } } diff --git a/tests/ApbctWP/ContactsEncoder/Integrations/TestCEIntegrationCommentList.php b/tests/ApbctWP/ContactsEncoder/Integrations/TestCEIntegrationCommentList.php new file mode 100644 index 000000000..3f569b5bc --- /dev/null +++ b/tests/ApbctWP/ContactsEncoder/Integrations/TestCEIntegrationCommentList.php @@ -0,0 +1,51 @@ +' + . '
    ' + . '
  1. [apbct_encode_data]
  2. ' + . '
  3. Second comment text
  4. ' + . '
  5. [apbct_encode_data]z[/apbct_encode_data][/apbct_encode_data]
  6. ' + . '
' + . ''; + + $integration = new CEIntegrationCommentList(); + $protected = $integration->protect($html); + $restored = $integration->restore($protected); + + $this->assertStringNotContainsString('[apbct_encode_data]', $protected); + $this->assertStringContainsString('%%APBCT_COMMENT_REGION_0%%', $protected); + $this->assertSame($html, $restored); + } + + public function testProtectStandaloneCommentListWithoutCommentsWrapper() + { + $html = '
    ' + . '
  1. one
  2. ' + . '
  3. two
  4. ' + . '
'; + + $integration = new CEIntegrationCommentList(); + $protected = $integration->protect($html); + + $this->assertStringContainsString('%%APBCT_COMMENT_REGION_0%%', $protected); + $this->assertSame($html, $integration->restore($protected)); + } + + public function testProtectLeavesContentWithoutCommentListUntouched() + { + $html = '
[apbct_encode_data]post@example.com[/apbct_encode_data]
'; + + $integration = new CEIntegrationCommentList(); + + $this->assertSame($html, $integration->protect($html)); + } +} diff --git a/tests/ApbctWP/ContactsEncoder/TestContactsEncoderShortCodeEncode.php b/tests/ApbctWP/ContactsEncoder/TestContactsEncoderShortCodeEncode.php index 63474bc2f..dfa5a44f5 100644 --- a/tests/ApbctWP/ContactsEncoder/TestContactsEncoderShortCodeEncode.php +++ b/tests/ApbctWP/ContactsEncoder/TestContactsEncoderShortCodeEncode.php @@ -3,6 +3,7 @@ namespace ApbctWP\ContactsEncoder; use Cleantalk\ApbctWP\ContactsEncoder\Shortcodes\EncodeContentSC; +use Cleantalk\ApbctWP\ContactsEncoder\Shortcodes\ShortCodesService; use Cleantalk\ApbctWP\Variables\Cookie; use Cleantalk\Common\ContactsEncoder\Dto\Params; use PHPUnit\Framework\TestCase; @@ -14,6 +15,11 @@ class testEmailEncoderShortCodeEncode extends TestCase */ private $shortcode; + /** + * @var ShortCodesService + */ + private $shortcodes_service; + protected function setUp(): void { /** @@ -27,6 +33,7 @@ protected function setUp(): void $params = new Params(); $params->api_key = $apbct->api_key; $this->shortcode = new EncodeContentSC($params); + $this->shortcodes_service = new ShortCodesService($params); $this->shortcode->register(); $this->clearDecoderPassedCookie(); } @@ -345,4 +352,30 @@ public function testAdjacentShortcodesOutsideEncodeDataAreNotExecuted() } } + public function testBufferModeDoesNotAbsorbCommentsFromEncodeDataTags() + { + global $apbct; + + $previous_buffer_setting = $apbct->settings['data__email_decoder_buffer']; + $apbct->settings['data__email_decoder_buffer'] = true; + + $html = '
    ' + . '
  1. [apbct_encode_data]
  2. ' + . '
  3. Second comment text
  4. ' + . '
  5. [apbct_encode_data]z[/apbct_encode_data][/apbct_encode_data]
  6. ' + . '
'; + + try { + $buffer = $this->shortcodes_service->modifyBufferBefore($html); + $buffer = apbctGetContactsEncoder()->modifyContent($buffer); + $buffer = $this->shortcodes_service->modifyBufferAfter($buffer); + + $this->assertStringContainsString('Second comment text', $buffer); + $this->assertStringContainsString('[apbct_encode_data]', $buffer); + $this->assertStringNotContainsString('apbct-email-encoder', $buffer); + } finally { + $apbct->settings['data__email_decoder_buffer'] = $previous_buffer_setting; + } + } + } From c3f525240424d8799613fd89668f2935f0b1a9a8 Mon Sep 17 00:00:00 2001 From: svfcode Date: Thu, 20 Aug 2026 16:20:36 +0500 Subject: [PATCH 2/2] fix cp --- .../Integrations/CEIntegrationCommentList.php | 5 ++++- .../Integrations/TestCEIntegrationCommentList.php | 12 ++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/Cleantalk/ApbctWP/ContactsEncoder/Integrations/CEIntegrationCommentList.php b/lib/Cleantalk/ApbctWP/ContactsEncoder/Integrations/CEIntegrationCommentList.php index 9ede9092c..9b7d12942 100644 --- a/lib/Cleantalk/ApbctWP/ContactsEncoder/Integrations/CEIntegrationCommentList.php +++ b/lib/Cleantalk/ApbctWP/ContactsEncoder/Integrations/CEIntegrationCommentList.php @@ -51,7 +51,10 @@ public function restore($content) return $content; } - return strtr($content, $this->region_placeholders); + $content = strtr($content, $this->region_placeholders); + $this->region_placeholders = array(); + + return $content; } /** diff --git a/tests/ApbctWP/ContactsEncoder/Integrations/TestCEIntegrationCommentList.php b/tests/ApbctWP/ContactsEncoder/Integrations/TestCEIntegrationCommentList.php index 3f569b5bc..8f950da90 100644 --- a/tests/ApbctWP/ContactsEncoder/Integrations/TestCEIntegrationCommentList.php +++ b/tests/ApbctWP/ContactsEncoder/Integrations/TestCEIntegrationCommentList.php @@ -48,4 +48,16 @@ public function testProtectLeavesContentWithoutCommentListUntouched() $this->assertSame($html, $integration->protect($html)); } + + public function testRestoreClearsPlaceholdersForSubsequentCalls() + { + $html = '
  1. one
'; + + $integration = new CEIntegrationCommentList(); + $protected = $integration->protect($html); + $restored = $integration->restore($protected); + + $this->assertSame($html, $restored); + $this->assertSame($protected, $integration->restore($protected)); + } }