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.' + ); } /**