From a3308673bf984d8856bda29ab91871d692ff1ca3 Mon Sep 17 00:00:00 2001 From: Michael Etokakpan Date: Sun, 21 Jun 2026 01:04:21 +0100 Subject: [PATCH] REST API: Restore the global post after preparing a revision. WP_REST_Revisions_Controller::prepare_item_for_response() set the global $post and called setup_postdata() without restoring them, so the change leaked for the rest of the request. The autosaves controller delegates here, so preloading that endpoint in the block editor could leave the global $post pointing at an autosave and initialize the editor with the wrong post. Capture the previous global post and restore it on every return path. Update the existing "sets up postdata" tests to confirm rendered fields still reflect the revision while the global post no longer leaks. Fixes #65495. --- .../class-wp-rest-revisions-controller.php | 34 ++++++ .../rest-api/rest-autosaves-controller.php | 32 ++++-- .../rest-api/rest-revisions-controller.php | 102 ++++++++++++++++-- 3 files changed, 154 insertions(+), 14 deletions(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-revisions-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-revisions-controller.php index 73a888d6eac48..f0f13b97f100f 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-revisions-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-revisions-controller.php @@ -606,12 +606,23 @@ public function prepare_item_for_response( $item, $request ) { // Restores the more descriptive, specific name for use within this method. $post = $item; + /* + * Save the previous global post so it can be restored before returning. + * Preparing the revision sets up the global post and post data, which + * must not leak into the rest of the request (e.g. the autosaves endpoint + * is preloaded in the block editor, where a leaked global post can cause + * the editor to be initialized with the wrong post). + */ + $previous_post = isset( $GLOBALS['post'] ) ? $GLOBALS['post'] : null; + $GLOBALS['post'] = $post; setup_postdata( $post ); // Don't prepare the response body for HEAD requests. if ( $request->is_method( 'HEAD' ) ) { + $this->reset_post_data( $previous_post ); + /** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-revisions-controller.php */ return apply_filters( 'rest_prepare_revision', new WP_REST_Response( array() ), $post, $request ); } @@ -706,6 +717,8 @@ public function prepare_item_for_response( $item, $request ) { $response->add_link( 'parent', rest_url( rest_get_route_for_post( $data['parent'] ) ) ); } + $this->reset_post_data( $previous_post ); + /** * Filters a revision returned from the REST API. * @@ -720,6 +733,27 @@ public function prepare_item_for_response( $item, $request ) { return apply_filters( 'rest_prepare_revision', $response, $post, $request ); } + /** + * Restores the global post to its previous value after preparing a revision. + * + * Preparing a revision overwrites the global post and post data via + * setup_postdata(). This restores the global post that was in place + * beforehand so the change does not leak into the rest of the request. + * + * @since 7.0.1 + * + * @param WP_Post|null $previous_post The global post to restore, or null if there was none. + */ + private function reset_post_data( $previous_post ) { + if ( $previous_post instanceof WP_Post ) { + $GLOBALS['post'] = $previous_post; + setup_postdata( $previous_post ); + } else { + unset( $GLOBALS['post'] ); + wp_reset_postdata(); + } + } + /** * Checks the post_date_gmt or modified_gmt and prepare any post or * modified date for single post output. diff --git a/tests/phpunit/tests/rest-api/rest-autosaves-controller.php b/tests/phpunit/tests/rest-api/rest-autosaves-controller.php index 7815f8ced23c9..11b294283c96d 100644 --- a/tests/phpunit/tests/rest-api/rest-autosaves-controller.php +++ b/tests/phpunit/tests/rest-api/rest-autosaves-controller.php @@ -731,16 +731,34 @@ protected function check_get_autosave_response( $response, $autosave ) { $this->assertSame( rest_url( '/wp/v2/' . $parent_base . '/' . $autosave->post_parent ), $links['parent'][0]['href'] ); } - public function test_get_item_sets_up_postdata() { + /** + * The autosave's postdata should be set up while preparing the response, + * so rendered fields reflect the autosave, without leaking into the global + * post after the request completes. + * + * @ticket 65495 + */ + public function test_get_item_sets_up_postdata_without_leaking_global_post() { wp_set_current_user( self::$editor_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves/' . self::$autosave_post_id ); - rest_get_server()->dispatch( $request ); - $post = get_post(); - $parent_post_id = wp_is_post_revision( $post->ID ); + $GLOBALS['post'] = get_post( self::$post_id ); + setup_postdata( $GLOBALS['post'] ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves/' . self::$autosave_post_id ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $autosave = get_post( self::$autosave_post_id ); - $this->assertSame( $post->ID, self::$autosave_post_id ); - $this->assertSame( $parent_post_id, self::$post_id ); + // The rendered title reflects the autosave, proving postdata was set up during preparation. + $this->assertSame( get_the_title( $autosave->ID ), $data['title']['rendered'] ); + + // The global post is restored to the post that was set before the request. + $this->assertSame( + self::$post_id, + $GLOBALS['post']->ID, + 'The global post should not leak the autosave after the request.' + ); } public function test_update_item_draft_page_with_parent() { diff --git a/tests/phpunit/tests/rest-api/rest-revisions-controller.php b/tests/phpunit/tests/rest-api/rest-revisions-controller.php index 52011afcb9318..160617b0fbd24 100644 --- a/tests/phpunit/tests/rest-api/rest-revisions-controller.php +++ b/tests/phpunit/tests/rest-api/rest-revisions-controller.php @@ -266,6 +266,78 @@ public function test_get_item() { $this->assertSame( self::$editor_id, $data['author'] ); } + /** + * Preparing a revision must not leak the revision into the global post. + * + * @ticket 65495 + * + * @covers WP_REST_Revisions_Controller::prepare_item_for_response + */ + public function test_get_items_restores_global_post() { + wp_set_current_user( self::$editor_id ); + + $GLOBALS['post'] = get_post( self::$post_id ); + setup_postdata( $GLOBALS['post'] ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions' ); + $request->set_param( 'context', 'edit' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + $this->assertInstanceOf( 'WP_Post', $GLOBALS['post'], 'The global post should still be set after the request.' ); + $this->assertSame( + self::$post_id, + $GLOBALS['post']->ID, + 'The global post should be restored to the post that was set before the request.' + ); + } + + /** + * Preparing a revision for a HEAD request must also restore the global post. + * + * @ticket 65495 + * + * @covers WP_REST_Revisions_Controller::prepare_item_for_response + */ + public function test_get_items_head_request_restores_global_post() { + wp_set_current_user( self::$editor_id ); + + $GLOBALS['post'] = get_post( self::$post_id ); + setup_postdata( $GLOBALS['post'] ); + + $request = new WP_REST_Request( 'HEAD', '/wp/v2/posts/' . self::$post_id . '/revisions' ); + $request->set_param( 'context', 'edit' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + $this->assertInstanceOf( 'WP_Post', $GLOBALS['post'], 'The global post should still be set after a HEAD request.' ); + $this->assertSame( + self::$post_id, + $GLOBALS['post']->ID, + 'The global post should be restored after a HEAD request.' + ); + } + + /** + * When there is no global post before the request, none should be set afterwards. + * + * @ticket 65495 + * + * @covers WP_REST_Revisions_Controller::prepare_item_for_response + */ + public function test_get_items_without_global_post_leaves_it_unset() { + wp_set_current_user( self::$editor_id ); + + unset( $GLOBALS['post'] ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions' ); + $request->set_param( 'context', 'edit' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + $this->assertArrayNotHasKey( 'post', $GLOBALS, 'The global post should not be set when there was none before the request.' ); + } + /** * @dataProvider data_readable_http_methods * @ticket 56481 @@ -639,16 +711,32 @@ protected function check_get_revision_response( $response, $revision ) { $this->assertSame( rest_url( '/wp/v2/' . $parent_base . '/' . $revision->post_parent ), $links['parent'][0]['href'] ); } - public function test_get_item_sets_up_postdata() { + /** + * The revision's postdata should be set up while preparing the response, + * so rendered fields reflect the revision, without leaking into the global + * post after the request completes. + * + * @ticket 65495 + */ + public function test_get_item_sets_up_postdata_without_leaking_global_post() { wp_set_current_user( self::$editor_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions/' . $this->revision_id1 ); - rest_get_server()->dispatch( $request ); - $post = get_post(); - $parent_post_id = wp_is_post_revision( $post->ID ); + $GLOBALS['post'] = get_post( self::$post_id ); + setup_postdata( $GLOBALS['post'] ); - $this->assertSame( $post->ID, $this->revision_id1 ); - $this->assertSame( $parent_post_id, self::$post_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions/' . $this->revision_id1 ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + // The rendered title reflects the revision, proving postdata was set up during preparation. + $this->assertSame( get_the_title( $this->revision_id1 ), $data['title']['rendered'] ); + + // The global post is restored to the post that was set before the request. + $this->assertSame( + self::$post_id, + $GLOBALS['post']->ID, + 'The global post should not leak the revision after the request.' + ); } /**