Files
WP-LIT-MCP/src/Integrations/WordPress/Render/PageRenderer.php
T
janniclas.loosenandClaude Opus 5.5 b3cd0eee62 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
2026-09-26 12:31:20 +02:00

69 lines
2.5 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\WordPress\Render;
use WP_Post;
/**
* Renders the content area of an entry in-process, like the front end does for the_content
* (Divi 5 renders its blocks there – verified in DDEV). Header, footer and Theme Builder
* layouts are not part of it. Rendering runs every plugin's/theme's content filters and
* shortcodes exactly as the front end would, which can trigger network requests (e.g. oEmbed
* lookups) that get cached by WordPress or other plugins as a side effect. Not final so
* abilities can be unit-tested with a mock.
*/
class PageRenderer
{
/**
* Globals setup_postdata() populates from the post loop (WP_Query::the_post()); all of
* them are restored to their pre-render value afterwards, not just $post itself.
*/
private const POSTDATA_GLOBALS = ['id', 'authordata', 'pages', 'page', 'more', 'numpages', 'multipage'];
public function render(WP_Post $post): string
{
$previousPost = $GLOBALS['post'] ?? null;
$previousGlobals = [];
foreach (self::POSTDATA_GLOBALS as $name) {
$previousGlobals[$name] = $GLOBALS[$name] ?? null;
}
$GLOBALS['post'] = $post;
$level = ob_get_level();
ob_start();
try {
setup_postdata($post);
$html = (string) apply_filters('the_content', (string) $post->post_content);
} finally {
$echoed = self::drain($level);
$GLOBALS['post'] = $previousPost;
foreach ($previousGlobals as $name => $value) {
$GLOBALS[$name] = $value;
}
}
return $echoed . $html;
}
/**
* Pops every output buffer opened while rendering, including one a rogue shortcode/filter
* left open (drained here instead of leaking into the caller) – but never one it already
* closed for us: the loop only pops down to the level recorded before our own ob_start(),
* so a filter that closes one buffer too many simply loses whatever was already echoed
* (nothing left to drain) rather than us popping a buffer that belongs to the caller.
* Buffers are combined in the order their content was written (innermost popped first,
* then prepended) so a leaked nested buffer's output stays after what came before it.
*/
private static function drain(int $level): string
{
$output = '';
while (ob_get_level() > $level) {
$output = (string) ob_get_clean() . $output;
}
return $output;
}
}