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:
@@ -9,28 +9,60 @@ 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. Not final so abilities can be unit-tested with a mock.
|
||||
* 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
|
||||
{
|
||||
$previous = $GLOBALS['post'] ?? null;
|
||||
$previousPost = $GLOBALS['post'] ?? null;
|
||||
$previousGlobals = [];
|
||||
foreach (self::POSTDATA_GLOBALS as $name) {
|
||||
$previousGlobals[$name] = $GLOBALS[$name] ?? null;
|
||||
}
|
||||
|
||||
$GLOBALS['post'] = $post;
|
||||
setup_postdata($post);
|
||||
$level = ob_get_level();
|
||||
ob_start();
|
||||
try {
|
||||
setup_postdata($post);
|
||||
$html = (string) apply_filters('the_content', (string) $post->post_content);
|
||||
} finally {
|
||||
$echoed = (string) ob_get_clean();
|
||||
$GLOBALS['post'] = $previous;
|
||||
if ($previous instanceof WP_Post) {
|
||||
setup_postdata($previous);
|
||||
} else {
|
||||
wp_reset_postdata();
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user