Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions lib/Cleantalk/ApbctWP/ContactsEncoder/ContactsEncoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -175,15 +176,21 @@ private function handlePrivacyPolicyHook()
}
}

public function bufferOutput()
public function modifyBuffer()
{
global $apbct;
static $already_output = false;
if ($already_output) {
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()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

namespace Cleantalk\ApbctWP\ContactsEncoder\Shortcodes;

class ExcludedEncodeContentSC extends EmailEncoderShortCode
{
protected $public_name = 'apbct_skip_encoding';

/**
* Callback for the shortcode
*
* @param $_atts array Not used here
* @param $content string|null Encoded content like `<span data-original-string='abc'>***@***.***</span>`
* @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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,31 @@ class ShortCodesService
{
public $encode;

public $shortcode_to_exclude;

public $shortcodes_registered = false;

/**
* @return void
*/
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;
}
}

public function __construct(Params $params)
{
$this->encode = new EncodeContentSC($params);
$this->shortcode_to_exclude = new ExcludedEncodeContentSC();
}

public function addActionsBeforeModify($hook, $priority = 1)
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
<?php

namespace ApbctWP\ContactsEncoder\Shortcodes;

use Cleantalk\ApbctWP\ContactsEncoder\Shortcodes\ExcludedEncodeContentSC;
use PHPUnit\Framework\TestCase;

class ExcludedEncodeContentSCTest extends TestCase
{
private $shortcode;

private $exclude_content_sc;

protected function setUp(): void
{
parent::setUp();

global $apbct;
$apbct->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 = '<span>Some text without data attribute</span>';

$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);
}
}
28 changes: 28 additions & 0 deletions tests/ApbctWP/ContactsEncoder/TestContactsEncoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '';
}
}
Loading