| @@ -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. |