diff --git a/lib/Cleantalk/ApbctWP/ContactsEncoder/ContactsEncoder.php b/lib/Cleantalk/ApbctWP/ContactsEncoder/ContactsEncoder.php index 1c97883b1..bece59f24 100644 --- a/lib/Cleantalk/ApbctWP/ContactsEncoder/ContactsEncoder.php +++ b/lib/Cleantalk/ApbctWP/ContactsEncoder/ContactsEncoder.php @@ -82,9 +82,10 @@ public function runEncoding($content = '') // Search data to buffer if ($apbct->settings['data__email_decoder_buffer'] && !apbct_is_ajax() && !apbct_is_rest() && !apbct_is_post() && !is_admin()) { 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); + add_action('shutdown', 'apbct_buffer__end', 0); // Collect $apbct->buffer + add_action('shutdown', array($this, 'modifyBuffer'), 2); // Modify $apbct->buffer by `ContactsEncoder::modifyBuffer` + $this->shortcodes->addActionsAfterModify('shutdown', 3); // Modify $apbct->buffer by `ShortCodesService::addActionsAfterModify` + add_action('shutdown', array($this, 'bufferOutput'), 999); // Output $apbct->buffer } else { foreach ( $hooks_to_encode as $hook ) { $this->shortcodes->addActionsBeforeModify($hook, 9); @@ -175,7 +176,7 @@ private function handlePrivacyPolicyHook() } } - public function bufferOutput() + public function modifyBuffer() { global $apbct; static $already_output = false; @@ -183,7 +184,13 @@ public function bufferOutput() return; } $already_output = true; - echo $this->modifyContent($apbct->buffer); + $apbct->buffer = $this->modifyContent($apbct->buffer); + } + + public function bufferOutput() + { + global $apbct; + echo $apbct->buffer; } protected function getTooltip() diff --git a/lib/Cleantalk/ApbctWP/ContactsEncoder/Shortcodes/ExcludedEncodeContentSC.php b/lib/Cleantalk/ApbctWP/ContactsEncoder/Shortcodes/ExcludedEncodeContentSC.php new file mode 100644 index 000000000..0c2766dc4 --- /dev/null +++ b/lib/Cleantalk/ApbctWP/ContactsEncoder/Shortcodes/ExcludedEncodeContentSC.php @@ -0,0 +1,82 @@ +***@***.***` + * @param $_tag string Not used here + * + * @return string Decoded content like `abc@abc.com` + */ + public function callback($_atts, $content, $_tag) + { + if ( ! $content ) { + return $content; + } + + // Pattern to get data-original-string attribute + $pattern = '/data-original-string=(["\'])(.*?)\1/'; + preg_match($pattern, $content, $matches); + + if (isset($matches[2])) { + $encoder = apbctGetContactsEncoder(); + $decoded_data = $encoder->decodeContactData([$matches[2]]); + if ( $decoded_data && is_array($decoded_data) ) { + return current($decoded_data); + } + } + + return $content; + } + + /** + * This method runs at the end of Contacts Encoder and tries to process unprocessed shortcodes + * The unprocessed shortcodes may be only in `the_title` hook + * + * @param string $content + * + * @return string Replaces $apbct->buffer by probably modified content or just return probably modified $content + */ + public function changeContentAfterEncoderModify($content) + { + global $apbct; + + if ( ! $apbct->settings['data__email_decoder_buffer'] && $this->getCurrentAction() !== 'the_title' ) { + return $content; + } + + if ( $apbct->settings['data__email_decoder_buffer'] ) { + $content = $apbct->buffer; + } + + $pattern = '/\[apbct_skip_encoding\](.*?)\[\/apbct_skip_encoding\]/s'; + $result = preg_replace_callback($pattern, function ($matches) { + // $matches[0] - all full match + if ( isset($matches[1]) ) { + // $matches[1] - only between tags group + $modifiedContent = $this->callback([], $matches[1], ''); + return $modifiedContent; // Return modified (decoded) content without tags + } + /** @psalm-suppress PossiblyUndefinedIntArrayOffset */ + return $matches[0]; // By default, return not modified match + }, $content); + + if ( $apbct->settings['data__email_decoder_buffer'] ) { + $apbct->buffer = $result; + } + + return $result; + } + + protected function getCurrentAction() + { + return function_exists('current_action') ? current_action() : null; + } +} diff --git a/lib/Cleantalk/ApbctWP/ContactsEncoder/Shortcodes/ShortCodesService.php b/lib/Cleantalk/ApbctWP/ContactsEncoder/Shortcodes/ShortCodesService.php index 30f8ac993..ad41d2c1f 100644 --- a/lib/Cleantalk/ApbctWP/ContactsEncoder/Shortcodes/ShortCodesService.php +++ b/lib/Cleantalk/ApbctWP/ContactsEncoder/Shortcodes/ShortCodesService.php @@ -11,6 +11,8 @@ class ShortCodesService { public $encode; + public $shortcode_to_exclude; + public $shortcodes_registered = false; /** @@ -18,8 +20,14 @@ class ShortCodesService */ public function registerAll() { + global $apbct; + if (!$this->shortcodes_registered) { $this->encode->register(); + if ( ! $apbct->settings['data__email_decoder_buffer'] ) { + // If buffer is active, Do not run wordpress shortcode replacement - encoder do it itself here `ExcludedEncodeContentSC::changeContentAfterEncoderModify` + $this->shortcode_to_exclude->register(); + } $this->shortcodes_registered = true; } } @@ -27,6 +35,7 @@ public function registerAll() public function __construct(Params $params) { $this->encode = new EncodeContentSC($params); + $this->shortcode_to_exclude = new ExcludedEncodeContentSC(); } public function addActionsBeforeModify($hook, $priority = 1) @@ -37,5 +46,6 @@ public function addActionsBeforeModify($hook, $priority = 1) public function addActionsAfterModify($hook, $priority = 999) { add_filter($hook, array($this->encode, 'changeContentAfterEncoderModify'), $priority); + add_filter($hook, array($this->shortcode_to_exclude, 'changeContentAfterEncoderModify'), $priority); } } diff --git a/tests/ApbctWP/ContactsEncoder/Shortcodes/ExcludedEncodeContentSCTest.php b/tests/ApbctWP/ContactsEncoder/Shortcodes/ExcludedEncodeContentSCTest.php new file mode 100644 index 000000000..7127dd3c4 --- /dev/null +++ b/tests/ApbctWP/ContactsEncoder/Shortcodes/ExcludedEncodeContentSCTest.php @@ -0,0 +1,183 @@ +api_key = 'testapikey'; + $this->contacts_encoder = apbctGetContactsEncoder(); + + $this->exclude_content_sc = new ExcludedEncodeContentSC(); + + // Create a partial mock of the tested class for isolation + $this->shortcode = $this->getMockBuilder(ExcludedEncodeContentSC::class) + ->setMethods(null) // In PHPUnit 8, use setMethods(null) for no mocking + ->getMock(); + } + + /** + * Test callback with empty content + */ + public function testCallbackWithEmptyContent(): void + { + $result = $this->exclude_content_sc->callback([], null, ''); + $this->assertNull($result); + + $result = $this->exclude_content_sc->callback([], '', ''); + $this->assertEquals('', $result); + } + + /** + * Test callback with valid content containing data-original-string + */ + public function testCallbackWithValidContent(): void + { + // Prepare test data + $originalString = 'test@example.com'; + $encodedString = $this->contacts_encoder->modifyContent($originalString); + + $result = $this->exclude_content_sc->callback([], $encodedString, ''); + + $this->assertEquals($originalString, $result); + } + + /** + * Test callback with content without data-original-string + */ + public function testCallbackWithContentWithoutDataAttribute(): void + { + $content = 'Some text without data attribute'; + + $result = $this->exclude_content_sc->callback([], $content, ''); + + $this->assertEquals($content, $result); + } + + /** + * Test changeContentAfterEncoderModify when buffer is off and not in the_title + */ + public function testChangeContentAfterEncoderModifyWithBufferOn(): void + { + global $apbct; + + $content = 'original buffer content test@example.com'; + + $apbct->settings['data__email_decoder_buffer'] = true; + $apbct->buffer = $content; + $apbct->saveSettings(); + $this->contacts_encoder->dropInstance(); // Need to rebuild the object after the settings changed + $this->contacts_encoder = apbctGetContactsEncoder(); + + $result = $this->exclude_content_sc->changeContentAfterEncoderModify(''); + + $this->assertEquals($content, $result); + } + + /** + * Test changeContentAfterEncoderModify when buffer is off but in the_title hook + */ + public function testChangeContentAfterEncoderModifyInTheTitleHook(): void + { + global $apbct; + + $content = 'title with [apbct_skip_encoding]test@example.com[/apbct_skip_encoding]'; + + $apbct->settings['data__email_decoder_buffer'] = false; + $apbct->saveSettings(); + $this->contacts_encoder->dropInstance(); // Need to rebuild the object after the settings changed + $this->contacts_encoder = apbctGetContactsEncoder(); + + // Create a partial mock that overrides getCurrentAction + $shortcodeMock = $this->getMockBuilder(ExcludedEncodeContentSC::class) + ->setMethods(['getCurrentAction']) + ->getMock(); + + // Mock getCurrentAction to return 'the_title' + $shortcodeMock->expects($this->once()) + ->method('getCurrentAction') + ->willReturn('the_title'); + + $encoded_content = $this->contacts_encoder->modifyContent($content); + + $result = $shortcodeMock->changeContentAfterEncoderModify($encoded_content); + + $this->assertEquals('title with test@example.com', $result); + } + + /** + * Test changeContentAfterEncoderModify with multiple shortcodes in content + */ + public function testChangeContentAfterEncoderModifyWithMultipleShortcodes(): void + { + global $apbct; + + $apbct->settings['data__email_decoder_buffer'] = true; + $apbct->saveSettings(); + $this->contacts_encoder->dropInstance(); // Need to rebuild the object after the settings changed + $this->contacts_encoder = apbctGetContactsEncoder(); + $apbct->buffer = 'buffer [apbct_skip_encoding]first[/apbct_skip_encoding] and [apbct_skip_encoding]second[/apbct_skip_encoding]'; + + $shortcodeMock = $this->getMockBuilder(ExcludedEncodeContentSC::class) + ->setMethods(['callback']) + ->getMock(); + + // Expect two calls to callback + $matcher = $this->exactly(2); + $shortcodeMock->expects($matcher) + ->method('callback') + ->willReturnCallback(function ($atts, $content, $tag) use ($matcher) { + switch ($matcher->getInvocationCount()) { + case 1: + $this->assertEquals('first', $content); + return 'decoded first'; + case 2: + $this->assertEquals('second', $content); + return 'decoded second'; + } + return ''; + }); + + $result = $shortcodeMock->changeContentAfterEncoderModify('input'); + + $this->assertEquals('buffer decoded first and decoded second', $result); + } + + /** + * Test changeContentAfterEncoderModify when callback returns unmodified content + */ + public function testChangeContentAfterEncoderModifyWhenCallbackReturnsUnmodified(): void + { + global $apbct; + + $apbct->settings['data__email_decoder_buffer'] = true; + $apbct->saveSettings(); + $this->contacts_encoder->dropInstance(); // Need to rebuild the object after the settings changed + $this->contacts_encoder = apbctGetContactsEncoder(); + $apbct->buffer = 'buffer [apbct_skip_encoding]content[/apbct_skip_encoding]'; + + $shortcodeMock = $this->getMockBuilder(ExcludedEncodeContentSC::class) + ->setMethods(['callback']) + ->getMock(); + + $shortcodeMock->expects($this->once()) + ->method('callback') + ->willReturn('content'); // Return unmodified content + + $result = $shortcodeMock->changeContentAfterEncoderModify('input'); + + // Expect content not to change, but tags are removed + $this->assertEquals('buffer content', $result); + } +} diff --git a/tests/ApbctWP/ContactsEncoder/TestContactsEncoder.php b/tests/ApbctWP/ContactsEncoder/TestContactsEncoder.php index c2f3fb6e2..adf8fa147 100644 --- a/tests/ApbctWP/ContactsEncoder/TestContactsEncoder.php +++ b/tests/ApbctWP/ContactsEncoder/TestContactsEncoder.php @@ -251,4 +251,32 @@ public function testGetPhonesEncodingLongDescription() $this->assertStringEndsWith('>', $description); } + public function testModifyBuffer() + { + global $apbct; + $test_string = 'test string with email test@example.com'; + $apbct->buffer = $test_string; + + $this->contacts_encoder->modifyBuffer(); + + $this->assertNotEquals($apbct->buffer, $test_string); + } + + public function testBufferOutput() + { + global $apbct; + ob_start(); + $apbct->buffer = $this->plain_text; + + $this->contacts_encoder->bufferOutput(); + $output = ob_get_clean(); + + $this->assertEquals($this->plain_text, $output); + } + + public function tearDown() : void + { + global $apbct; + $apbct->buffer = ''; + } }