diff --git a/lib/Cleantalk/ApbctWP/ContactsEncoder/Integrations/CEIntegrationCommentList.php b/lib/Cleantalk/ApbctWP/ContactsEncoder/Integrations/CEIntegrationCommentList.php
new file mode 100644
index 000000000..9b7d12942
--- /dev/null
+++ b/lib/Cleantalk/ApbctWP/ContactsEncoder/Integrations/CEIntegrationCommentList.php
@@ -0,0 +1,187 @@
+
+ */
+ 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;
+ }
+
+ $content = strtr($content, $this->region_placeholders);
+ $this->region_placeholders = array();
+
+ return $content;
+ }
+
+ /**
+ * @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 = '' . $tag_name;
+
+ while ( $pos < $length && $depth > 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..8f950da90
--- /dev/null
+++ b/tests/ApbctWP/ContactsEncoder/Integrations/TestCEIntegrationCommentList.php
@@ -0,0 +1,63 @@
+'
+ . ''
+ . '
';
+
+ $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 = '';
+
+ $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));
+ }
+
+ public function testRestoreClearsPlaceholdersForSubsequentCalls()
+ {
+ $html = '';
+
+ $integration = new CEIntegrationCommentList();
+ $protected = $integration->protect($html);
+ $restored = $integration->restore($protected);
+
+ $this->assertSame($html, $restored);
+ $this->assertSame($protected, $integration->restore($protected));
+ }
+}
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 = '';
+
+ 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;
+ }
+ }
+
}