Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
}
Expand Down Expand Up @@ -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.
*
Expand All @@ -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.
Expand Down
32 changes: 25 additions & 7 deletions tests/phpunit/tests/rest-api/rest-autosaves-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
102 changes: 95 additions & 7 deletions tests/phpunit/tests/rest-api/rest-revisions-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.'
);
}

/**
Expand Down
Loading