fix: bound render-summary strings, restore postdata globals, drain buffers
Review fix round 1 for lit/page-render-summary: extract shared Text::shorten() (reused by NodeSummary), replace data: URIs with a short marker, prefer data-src/data-lazy-src over placeholder src, dedup nested p/li/blockquote text and ignore script/style/noscript content, restore every setup_postdata global (not just $post), drain output buffers robustly around the_content, and disclose that rendering can trigger oEmbed/network requests. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
@@ -42,6 +42,81 @@ final class PageRenderSummaryAbilityTest extends TestCase
|
||||
self::assertCount(2, RenderSummary::fromHtml(str_repeat('<p>x</p>', 5), 2)['texts']);
|
||||
}
|
||||
|
||||
public function testTruncatesLongExtractedStrings(): void
|
||||
{
|
||||
$longHeading = str_repeat('Ü', 250);
|
||||
$longText = str_repeat('a', 400);
|
||||
$longLinkText = str_repeat('b', 200);
|
||||
$longHref = 'https://k.test/' . str_repeat('c', 400);
|
||||
$longAlt = str_repeat('d', 300);
|
||||
$html = "<h2>{$longHeading}</h2><p>{$longText}</p>"
|
||||
. "<a href=\"{$longHref}\">{$longLinkText}</a>"
|
||||
. "<img src=\"https://k.test/x.jpg\" alt=\"{$longAlt}\">";
|
||||
|
||||
$summary = RenderSummary::fromHtml($html);
|
||||
|
||||
self::assertSame(200, mb_strlen($summary['headings'][0]['text']));
|
||||
self::assertStringEndsWith('…', $summary['headings'][0]['text']);
|
||||
self::assertSame(300, mb_strlen($summary['texts'][0]));
|
||||
self::assertStringEndsWith('…', $summary['texts'][0]);
|
||||
self::assertSame(150, mb_strlen($summary['links'][0]['text']));
|
||||
self::assertStringEndsWith('…', $summary['links'][0]['text']);
|
||||
self::assertSame(300, mb_strlen($summary['links'][0]['href']));
|
||||
self::assertStringEndsWith('…', $summary['links'][0]['href']);
|
||||
self::assertSame(200, mb_strlen($summary['images'][0]['alt']));
|
||||
self::assertStringEndsWith('…', $summary['images'][0]['alt']);
|
||||
}
|
||||
|
||||
public function testRepresentsDataUrisAsAShortMarkerInsteadOfTruncatingThem(): void
|
||||
{
|
||||
$dataUri = 'data:image/png;base64,' . base64_encode(str_repeat('x', 5000));
|
||||
$html = '<img src="' . $dataUri . '"><a href="' . $dataUri . '">Anhang</a>';
|
||||
|
||||
$summary = RenderSummary::fromHtml($html);
|
||||
|
||||
$marker = 'data:image/png;… (' . strlen($dataUri) . ' Bytes)';
|
||||
self::assertSame($marker, $summary['images'][0]['src']);
|
||||
self::assertSame($marker, $summary['links'][0]['href']);
|
||||
}
|
||||
|
||||
public function testPrefersDataSrcOrDataLazySrcOverAPlaceholderSrc(): void
|
||||
{
|
||||
$html = '<img src="data:image/gif;base64,R0lGODlhAQABAAAAACw=" data-src="https://k.test/real.jpg" alt="Team">'
|
||||
. '<img src="placeholder.gif" data-lazy-src="https://k.test/lazy.jpg">';
|
||||
|
||||
$summary = RenderSummary::fromHtml($html);
|
||||
|
||||
self::assertSame('https://k.test/real.jpg', $summary['images'][0]['src']);
|
||||
self::assertSame('https://k.test/lazy.jpg', $summary['images'][1]['src']);
|
||||
}
|
||||
|
||||
public function testWarnsAboutMultipleH1Headings(): void
|
||||
{
|
||||
$summary = RenderSummary::fromHtml('<h1>Erste</h1><h1>Zweite</h1>');
|
||||
|
||||
self::assertContains('2 Überschriften der Ebene h1 im Inhalt.', $summary['warnings']);
|
||||
}
|
||||
|
||||
public function testDoesNotDuplicateTextFromNestedParagraphsListItemsOrBlockquotes(): void
|
||||
{
|
||||
$html = '<ul><li>Punkt <p>eingebettet</p></li></ul>'
|
||||
. '<blockquote>Zitat <blockquote>innen</blockquote></blockquote>';
|
||||
|
||||
$summary = RenderSummary::fromHtml($html);
|
||||
|
||||
self::assertSame(['Punkt eingebettet', 'Zitat innen'], $summary['texts']);
|
||||
}
|
||||
|
||||
public function testIgnoresScriptStyleAndNoscriptText(): void
|
||||
{
|
||||
$html = '<p>Sichtbar<script>var x = 1;</script><style>.a{color:red}</style>'
|
||||
. '<noscript>Fallback</noscript> Ende</p>';
|
||||
|
||||
$summary = RenderSummary::fromHtml($html);
|
||||
|
||||
self::assertSame(['Sichtbar Ende'], $summary['texts']);
|
||||
}
|
||||
|
||||
public function testRendersTheEntryAndAddsNotes(): void
|
||||
{
|
||||
$post = new WP_Post(['ID' => 7, 'post_type' => 'page', 'post_password' => 'geheim']);
|
||||
@@ -60,7 +135,7 @@ final class PageRenderSummaryAbilityTest extends TestCase
|
||||
self::assertSame(7, $result['post_id']);
|
||||
self::assertSame('https://k.test/start/', $result['url']);
|
||||
self::assertCount(2, $result['headings']);
|
||||
self::assertCount(2, $result['notes']);
|
||||
self::assertCount(3, $result['notes']);
|
||||
self::assertTrue($ability->isDirect());
|
||||
self::assertTrue($ability->annotations()['readonly']);
|
||||
}
|
||||
|
||||
@@ -13,20 +13,40 @@ use WP_Post;
|
||||
|
||||
final class PageRendererTest extends TestCase
|
||||
{
|
||||
private const POSTDATA_GLOBALS = ['id', 'authordata', 'pages', 'page', 'more', 'numpages', 'multipage'];
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
unset($GLOBALS['post']);
|
||||
foreach (self::POSTDATA_GLOBALS as $name) {
|
||||
unset($GLOBALS[$name]);
|
||||
}
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function testRendersTheContentFilterAndRestoresTheGlobalPost(): void
|
||||
public function testRendersTheContentFilterAndRestoresGlobalPostAndPostdata(): void
|
||||
{
|
||||
$previous = new WP_Post(['ID' => 1]);
|
||||
$GLOBALS['post'] = $previous;
|
||||
$previousPost = new WP_Post(['ID' => 1]);
|
||||
$GLOBALS['post'] = $previousPost;
|
||||
$GLOBALS['id'] = 1;
|
||||
$GLOBALS['authordata'] = 'previous-author';
|
||||
$GLOBALS['pages'] = ['previous'];
|
||||
$GLOBALS['page'] = 1;
|
||||
$GLOBALS['more'] = 1;
|
||||
$GLOBALS['numpages'] = 1;
|
||||
$GLOBALS['multipage'] = 0;
|
||||
|
||||
$post = new WP_Post(['ID' => 7, 'post_content' => '<!-- wp:divi/section /-->']);
|
||||
$setup = [];
|
||||
Functions\when('setup_postdata')->alias(static function (WP_Post $p) use (&$setup): bool {
|
||||
$setup[] = $p->ID;
|
||||
$GLOBALS['id'] = $p->ID;
|
||||
$GLOBALS['authordata'] = 'author-of-' . $p->ID;
|
||||
$GLOBALS['pages'] = ['page-of-' . $p->ID];
|
||||
$GLOBALS['page'] = 2;
|
||||
$GLOBALS['more'] = 0;
|
||||
$GLOBALS['numpages'] = 3;
|
||||
$GLOBALS['multipage'] = 1;
|
||||
|
||||
return true;
|
||||
});
|
||||
@@ -41,14 +61,20 @@ final class PageRendererTest extends TestCase
|
||||
$html = (new PageRenderer())->render($post);
|
||||
|
||||
self::assertSame('<p>echoed</p><div class="et_pb_section"></div>', $html);
|
||||
self::assertSame($previous, $GLOBALS['post']);
|
||||
self::assertSame([7, 1], $setup);
|
||||
self::assertSame([7], $setup);
|
||||
self::assertSame($previousPost, $GLOBALS['post']);
|
||||
self::assertSame(1, $GLOBALS['id']);
|
||||
self::assertSame('previous-author', $GLOBALS['authordata']);
|
||||
self::assertSame(['previous'], $GLOBALS['pages']);
|
||||
self::assertSame(1, $GLOBALS['page']);
|
||||
self::assertSame(1, $GLOBALS['more']);
|
||||
self::assertSame(1, $GLOBALS['numpages']);
|
||||
self::assertSame(0, $GLOBALS['multipage']);
|
||||
}
|
||||
|
||||
public function testRestoresEvenWhenRenderingThrows(): void
|
||||
{
|
||||
Functions\when('setup_postdata')->justReturn(true);
|
||||
Functions\expect('wp_reset_postdata')->once();
|
||||
Filters\expectApplied('the_content')->once()->andReturnUsing(static function (): string {
|
||||
throw new RuntimeException('module crashed');
|
||||
});
|
||||
@@ -59,6 +85,47 @@ final class PageRendererTest extends TestCase
|
||||
} catch (RuntimeException) {
|
||||
self::assertArrayHasKey('post', $GLOBALS);
|
||||
self::assertNull($GLOBALS['post']);
|
||||
foreach (self::POSTDATA_GLOBALS as $name) {
|
||||
self::assertArrayHasKey($name, $GLOBALS);
|
||||
self::assertNull($GLOBALS[$name]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testDrainsAnExtraOutputBufferAShortcodeLeavesOpen(): void
|
||||
{
|
||||
Functions\when('setup_postdata')->justReturn(true);
|
||||
Filters\expectApplied('the_content')->once()->andReturnUsing(static function (): string {
|
||||
echo 'before-';
|
||||
ob_start(); // a buggy shortcode opens a buffer and never closes it
|
||||
echo 'leaked';
|
||||
|
||||
return 'html';
|
||||
});
|
||||
$levelBefore = ob_get_level();
|
||||
|
||||
$html = (new PageRenderer())->render(new WP_Post(['ID' => 7]));
|
||||
|
||||
self::assertSame('before-leakedhtml', $html);
|
||||
self::assertSame($levelBefore, ob_get_level());
|
||||
}
|
||||
|
||||
public function testToleratesAShortcodeThatClosesOneOutputBufferTooMany(): void
|
||||
{
|
||||
Functions\when('setup_postdata')->justReturn(true);
|
||||
Filters\expectApplied('the_content')->once()->andReturnUsing(static function (): string {
|
||||
ob_start(); // shortcode's own nested buffer
|
||||
echo 'own';
|
||||
ob_end_clean(); // correct: closes its own buffer
|
||||
ob_end_clean(); // bug: also closes PageRenderer's buffer
|
||||
|
||||
return 'html';
|
||||
});
|
||||
$levelBefore = ob_get_level();
|
||||
|
||||
$html = (new PageRenderer())->render(new WP_Post(['ID' => 7]));
|
||||
|
||||
self::assertSame('html', $html);
|
||||
self::assertSame($levelBefore, ob_get_level());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user