PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.12.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.12.0
2.12.0 2.11.0 2.10.0 2.9.0 2.8.0 2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk All 53 releases
← All changes | includes/seo/class-builder-content.php +87 -4 2.10.0 → 2.12.0 View file →
@@ -303,8 +303,22 @@
303 303 */
304 304 private const ALT_KEYS = ['alt', 'alt_text', 'image_alt', 'title'];
305 305
306 306 /**
307 + * The global post and every global `setup_postdata()` writes.
308 + *
309 + * Rendering points them at the post being analyzed, then puts each one
310 + * back exactly as it was, unset included (#860).
311 + *
312 + * @since 2.12.0
313 + * @var string[]
314 + */
315 + private const POSTDATA_GLOBALS = [
316 + 'post', 'id', 'authordata', 'currentday', 'currentmonth',
317 + 'page', 'pages', 'multipage', 'more', 'numpages',
318 + ];
319 +
320 + /**
307 321 * Resolve the content worth analyzing for a post.
308 322 *
309 323 * @param \WP_Post $post Post being analyzed.
310 324 * @return string HTML/text to analyze.
@@ -554,9 +568,9 @@
554 568 * @param \WP_Post $post Post the markup belongs to.
555 569 * @return string Content to analyze.
556 570 */
557 571 public static function resolve_markup(string $raw, \WP_Post $post): string {
558 - $content = self::render_post_content($raw);
572 + $content = self::render_post_content($raw, $post);
559 573
560 574 // Block markup that renders to nothing usually means the builder that
561 575 // owns those blocks did not register them in this context — Divi 5
562 576 // loads its module library lazily per-request, so in CLI, REST, admin
@@ -604,19 +618,45 @@
604 618 *
605 619 * Best-effort: a third-party block that fatals must not take the whole
606 620 * score down with it.
607 621 *
608 - * @param string $raw Raw post content.
622 + * Runs as the post's own context, as it would on the front end. Admin and
623 + * REST requests have no current post, so a shortcode reading
624 + * `get_the_ID()` got nothing, and one looping a related-posts query left
625 + * the global post on the last of them: its `wp_reset_postdata()` goes back
626 + * to the main query's post, and there is none. On the Classic Editor this
627 + * runs after the form prints its hidden `post_ID` and before the title and
628 + * editor, which then showed the related post, and Update saved it over the
629 + * original (#860).
630 + *
631 + * @param string $raw Raw post content.
632 + * @param \WP_Post $post Post the content belongs to.
609 633 * @return string Rendered content.
610 634 */
611 - private static function render_post_content(string $raw): string {
635 + private static function render_post_content(string $raw, \WP_Post $post): string {
612 636 if ('' === trim($raw)) {
613 637 return '';
614 638 }
615 639
616 - $content = $raw;
640 + $content = $raw;
641 + $previous = self::snapshot_post_globals();
617 642
618 643 try {
644 + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Made current for the render, restored in finally.
645 + $GLOBALS['post'] = $post;
646 +
647 + // Fires `the_post`, which these paths never fired before: admin,
648 + // REST and cron analysis had no current post at all. That is the
649 + // same signal the front-end loop sends and it is what makes
650 + // `get_the_ID()` work inside a shortcode, but it is a new call on a
651 + // path that runs in bulk — the word-count index resolves every post
652 + // it visits — so a theme that counts views on `the_post` will count
653 + // them during indexing. Accepted deliberately: without it a
654 + // shortcode cannot resolve its own post, which is the bug (#860).
655 + if (function_exists('setup_postdata')) {
656 + setup_postdata($post);
657 + }
658 +
619 659 if (function_exists('has_blocks') && function_exists('do_blocks') && has_blocks($raw)) {
620 660 $content = do_blocks($raw);
621 661 }
622 662
@@ -625,11 +665,54 @@
625 665 $content = do_shortcode($content);
626 666 }
627 667 } catch (\Throwable $e) {
628 668 return $raw;
669 + } finally {
670 + self::restore_post_globals($previous);
629 671 }
630 672
631 673 return self::is_blank($content) ? $raw : $content;
674 + }
675 +
676 + /**
677 + * The post globals as they are now; a global that is unset has no key.
678 + *
679 + * @since 2.12.0
680 + *
681 + * @return array<string,mixed>
682 + */
683 + private static function snapshot_post_globals(): array {
684 + $snapshot = [];
685 +
686 + foreach (self::POSTDATA_GLOBALS as $name) {
687 + if (array_key_exists($name, $GLOBALS)) {
688 + $snapshot[$name] = $GLOBALS[$name];
689 + }
690 + }
691 +
692 + return $snapshot;
693 + }
694 +
695 + /**
696 + * Put the post globals back as snapshot_post_globals() found them.
697 + *
698 + * Assigned directly rather than through `setup_postdata()`: there may have
699 + * been no post to set up, and re-running it would fire `the_post` again.
700 + *
701 + * @since 2.12.0
702 + *
703 + * @param array<string,mixed> $snapshot From snapshot_post_globals().
704 + * @return void
705 + */
706 + private static function restore_post_globals(array $snapshot): void {
707 + foreach (self::POSTDATA_GLOBALS as $name) {
708 + if (array_key_exists($name, $snapshot)) {
709 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- Core's own globals, put back as they were.
710 + $GLOBALS[$name] = $snapshot[$name];
711 + } else {
712 + unset($GLOBALS[$name]);
713 + }
714 + }
632 715 }
633 716
634 717 /**
635 718 * Extract text from the attributes of parsed blocks.