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
32 changes: 32 additions & 0 deletions inc/cleantalk-public.php
Original file line number Diff line number Diff line change
Expand Up @@ -409,9 +409,28 @@ function apbct_sgo_bypass_query_params($bypass_query_params)
return $bypass_query_params;
}

/**
* Nesting level of the buffer opened by apbct_buffer__start().
*
* @param int|null $set
*
* @return int
*/
function apbct_buffer__own_level($set = null)
{
static $level = 0;

if ( $set !== null ) {
$level = (int)$set;
}

return $level;
}

function apbct_buffer__start()
{
ob_start();
apbct_buffer__own_level(ob_get_level());
}

function apbct_buffer__end()
Expand All @@ -421,6 +440,19 @@ function apbct_buffer__end()
}

global $apbct;

// Buffers opened after ours (WP late-printed styles hoisting, other plugins) must be flushed,
// not discarded — otherwise their replacements never reach the output.
$own_level = apbct_buffer__own_level();
if ( $own_level > 0 ) {
while ( ob_get_level() > $own_level ) {
$previous_level = ob_get_level();
if ( ! @ob_end_flush() || ob_get_level() >= $previous_level ) {
Comment thread
AntonV1211 marked this conversation as resolved.
break;
}
}
}

$apbct->buffer = ob_get_contents();
ob_end_clean();
}
Expand Down
119 changes: 119 additions & 0 deletions tests/Inc/TestCleantalkPublicBuffer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<?php

namespace Inc;

use Cleantalk\ApbctWP\State;
use PHPUnit\Framework\TestCase;

class TestCleantalkPublicBuffer extends TestCase
{
/**
* @var int Output buffering level owned by PHPUnit, must be restored after every test.
*/
private $level_before;

protected function setUp(): void
{
parent::setUp();
global $apbct;
$apbct = new State('cleantalk', array('settings', 'data', 'errors', 'remote_calls', 'stats', 'fw_stats'));
$this->level_before = ob_get_level();
}

protected function tearDown(): void
{
while ( ob_get_level() > $this->level_before ) {
ob_end_clean();
}

global $apbct;
unset($apbct);
parent::tearDown();
}

public function testBufferEndCapturesOwnBufferAndLeavesTheOuterOneOpen()
{
global $apbct;

ob_start();
echo 'OUTER;';
$outer_level = ob_get_level();

apbct_buffer__start();
echo 'PAGE;';

apbct_buffer__end();

$this->assertSame('PAGE;', $apbct->buffer);
$this->assertSame($outer_level, ob_get_level());
$this->assertSame('OUTER;', ob_get_contents());

ob_end_clean();
}

public function testBufferStartRecordsItsOwnNestingLevel()
{
ob_start();

apbct_buffer__start();

$this->assertSame(ob_get_level(), apbct_buffer__own_level());

apbct_buffer__end();
ob_end_clean();
}

public function testBufferEndFlushesNestedBuffersAndAppliesTheirCallbacks()
{
global $apbct;

ob_start();
echo 'OUTER;';
$outer_level = ob_get_level();

apbct_buffer__start();
echo 'HEAD:PLACEHOLDER;';

// Emulates WP late-printed styles hoisting: a buffer opened after ours that replaces
// its own content only when it is flushed.
ob_start(static function ($chunk) {
return str_replace('PLACEHOLDER', 'REAL_CSS', $chunk);
});
echo 'BODY:PLACEHOLDER;';

ob_start();
echo 'FOOTER;';

apbct_buffer__end();

$this->assertSame('HEAD:PLACEHOLDER;BODY:REAL_CSS;FOOTER;', $apbct->buffer);
$this->assertSame($outer_level, ob_get_level());
$this->assertSame('OUTER;', ob_get_contents());

ob_end_clean();
}

public function testBufferEndDoesNotLeakPageContentIntoTheOuterBuffer()
{
global $apbct;

ob_start();
echo 'OUTER;';

apbct_buffer__start();
echo 'PAGE;';
ob_start();
echo 'NESTED;';

apbct_buffer__end();

$outer_content = ob_get_contents();

$this->assertSame('OUTER;', $outer_content);
$this->assertStringNotContainsString('PAGE;', $outer_content);
$this->assertStringNotContainsString('NESTED;', $outer_content);
$this->assertSame('PAGE;NESTED;', $apbct->buffer);

ob_end_clean();
}
}
Loading