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 f4c5cb483d105..0121298105ebf 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 @@ -595,14 +595,29 @@ protected function prepare_items_query( $prepared_args = array(), $request = nul * * @since 4.7.0 * @since 5.9.0 Renamed `$post` to `$item` to match parent class for PHP 8 named parameter support. + * @since 7.1.0 The global post is now restored to its previous value before returning. * - * @global WP_Post $post Global post object. + * @global WP_Post|null $post Global post object. * * @param WP_Post $item Post revision object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response Response object. */ public function prepare_item_for_response( $item, $request ) { + /* + * 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). + * + * Note that $post is intentionally not declared as a global here. It must + * remain local to this method so that a filter which reassigns the global + * post while the response is being prepared (for example on 'the_content') + * cannot change which post the remaining fields are read from. + */ + $previous_post = isset( $GLOBALS['post'] ) && $GLOBALS['post'] instanceof WP_Post ? $GLOBALS['post'] : null; + // Restores the more descriptive, specific name for use within this method. $post = $item; @@ -613,7 +628,11 @@ public function prepare_item_for_response( $item, $request ) { // Don't prepare the response body for HEAD requests. if ( $request->is_method( 'HEAD' ) ) { /** 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 ); + $response = apply_filters( 'rest_prepare_revision', new WP_REST_Response( array() ), $post, $request ); + + $this->restore_post_data( $previous_post ); + + return $response; } $fields = $this->get_fields_for_response( $request ); @@ -717,7 +736,55 @@ public function prepare_item_for_response( $item, $request ) { * @param WP_Post $post The original revision object. * @param WP_REST_Request $request Request used to generate the response. */ - return apply_filters( 'rest_prepare_revision', $response, $post, $request ); + $response = apply_filters( 'rest_prepare_revision', $response, $post, $request ); + + $this->restore_post_data( $previous_post ); + + return $response; + } + + /** + * 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. + * + * Only the global post is guaranteed to be restored. When there was no + * previous global post and the main query has no post either, which is the + * usual state during a REST request, wp_reset_postdata() has nothing to + * restore from, so the remaining globals set by setup_postdata() (such as + * $id, $authordata and $pages) are left describing the revision. Clearing + * those would mean unsetting each one by hand, which is beyond what is + * needed to keep the global post from leaking. + * + * @since 7.1.0 + * + * @param WP_Post|null $previous_post The global post to restore, or null if there was none. + */ + private function restore_post_data( ?WP_Post $previous_post ): void { + if ( $previous_post ) { + $GLOBALS['post'] = $previous_post; + setup_postdata( $previous_post ); + return; + } + + /* + * There was no global post to restore, so clear the revision's post data. + * This runs before clearing the global post because wp_reset_postdata() + * repopulates it from the main query whenever that query has a post. Note + * that it is a no-op when the main query has no post, in which case only + * the global post below is cleared. + */ + wp_reset_postdata(); + + /* + * Assigned rather than unset so that any `global $post` binding made before + * this request keeps pointing at the global. Unsetting removes the entry from + * the symbol table, which detaches those bindings, and a later write through + * one of them would no longer be visible to get_post(). + */ + $GLOBALS['post'] = null; } /** diff --git a/tests/phpunit/tests/rest-api/rest-autosaves-controller.php b/tests/phpunit/tests/rest-api/rest-autosaves-controller.php index 7815f8ced23c9..179ce60e047df 100644 --- a/tests/phpunit/tests/rest-api/rest-autosaves-controller.php +++ b/tests/phpunit/tests/rest-api/rest-autosaves-controller.php @@ -9,21 +9,21 @@ * @group restapi */ class WP_Test_REST_Autosaves_Controller extends WP_Test_REST_Post_Type_Controller_Testcase { - protected static $post_id; - protected static $page_id; - protected static $draft_page_id; + protected static int $post_id; + protected static int $page_id; + protected static int $draft_page_id; - protected static $autosave_post_id; - protected static $autosave_page_id; + protected static int $autosave_post_id; + protected static int $autosave_page_id; - protected static $editor_id; - protected static $contributor_id; + protected static int $editor_id; + protected static int $contributor_id; - protected static $parent_page_id; - protected static $child_page_id; - protected static $child_draft_page_id; + protected static int $parent_page_id; + protected static int $child_page_id; + protected static int $child_draft_page_id; - private $post_autosave; + private WP_Post $post_autosave; protected function set_post_data( $args = array() ) { $defaults = array( @@ -731,16 +731,46 @@ 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 + * + * @global int|null $id ID from the set up global post data. + * + * @covers WP_REST_Autosaves_Controller::prepare_item_for_response + */ + public function test_get_item_sets_up_postdata_without_leaking_global_post() { + global $id; + + // Populate the global $wp_query with the post and set it up. 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 ); + query_posts( array( 'p' => self::$post_id ) ); + the_post(); - $post = get_post(); - $parent_post_id = wp_is_post_revision( $post->ID ); + // Assert initial state. + $post = get_post(); + $this->assertInstanceOf( WP_Post::class, $post, 'The global post should be set up before the request.' ); + $this->assertSame( self::$post_id, $post->ID, 'The global post should be the parent post before the request.' ); + $this->assertSame( self::$post_id, $id, 'The global $id should be the parent post ID before the request.' ); - $this->assertSame( $post->ID, self::$autosave_post_id ); - $this->assertSame( $parent_post_id, self::$post_id ); + // Make the request to get the autosave. + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves/' . self::$autosave_post_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data(); + $this->assertIsArray( $data ); + $this->assertArrayHasKey( 'title', $data ); + $this->assertIsArray( $data['title'] ); + $this->assertSame( get_the_title( self::$autosave_post_id ), $data['title']['rendered'], 'Expected the rendered title to reflect the autosave, proving postdata was set up during preparation.' ); + + // The global post is restored to the post that was set before the request. + $post = get_post(); + $this->assertInstanceOf( WP_Post::class, $post, 'The global post should still be set after the request.' ); + $this->assertSame( self::$post_id, $post->ID, 'The global post should be restored to the post that was set before the request.' ); + $this->assertSame( self::$post_id, $id, 'The global $id should be restored to the post that was set before 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..71b333fa39fd4 100644 --- a/tests/phpunit/tests/rest-api/rest-revisions-controller.php +++ b/tests/phpunit/tests/rest-api/rest-revisions-controller.php @@ -8,22 +8,24 @@ * @group restapi */ class WP_Test_REST_Revisions_Controller extends WP_Test_REST_Controller_Testcase { - protected static $post_id; - protected static $post_id_2; - protected static $page_id; + protected static int $post_id; + protected static int $post_id_2; + protected static int $page_id; - protected static $editor_id; - protected static $contributor_id; + protected static int $editor_id; + protected static int $contributor_id; + + private int $total_revisions; - private $total_revisions; private $revisions; - private $revision_1; - private $revision_id1; - private $revision_2; - private $revision_id2; - private $revision_3; - private $revision_id3; - private $revision_2_1_id; + + private WP_Post $revision_1; + private int $revision_id1; + private WP_Post $revision_2; + private int $revision_id2; + private WP_Post $revision_3; + private int $revision_id3; + private int $revision_2_1_id; public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { self::$post_id = $factory->post->create(); @@ -266,6 +268,266 @@ 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 + * + * @global int|null $id ID from the set up global post data. + * + * @covers WP_REST_Revisions_Controller::prepare_item_for_response + */ + public function test_get_items_restores_global_post() { + global $id; + + // Populate the global $wp_query with the post and set it up. + wp_set_current_user( self::$editor_id ); + query_posts( array( 'p' => self::$post_id ) ); + the_post(); + + // Assert initial state. + $post = get_post(); + $this->assertInstanceOf( WP_Post::class, $post, 'The global post should be set up before the request.' ); + $this->assertSame( self::$post_id, $post->ID, 'The global post should be the parent post before the request.' ); + $this->assertSame( self::$post_id, $id, 'The global $id should be the parent post ID before the request.' ); + + // Capture the arguments the rest_prepare_revision filter receives. + $mock = new MockAction(); + add_filter( 'rest_prepare_revision', array( $mock, 'filter' ), 10, 3 ); + + // Make the request to get revisions. + $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() ); + + // The filter is passed each revision, not the global post restored afterwards. + $this->check_rest_prepare_revision_filter_args( + $mock, + array( $this->revision_id3, $this->revision_id2, $this->revision_id1 ), + $request + ); + + // The global post is restored to the post that was set before the request. + $post = get_post(); + $this->assertInstanceOf( WP_Post::class, $post, 'The global post should still be set after the request.' ); + $this->assertSame( self::$post_id, $post->ID, 'The global post should be restored to the post that was set before the request.' ); + $this->assertSame( self::$post_id, $id, 'The global $id 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. + * + * The collection endpoint short-circuits before preparing items for HEAD + * requests, so the single revision endpoint is used to reach the HEAD + * branch of prepare_item_for_response(). + * + * @ticket 65495 + * + * @global int|null $id ID from the set up global post data. + * + * @covers WP_REST_Revisions_Controller::prepare_item_for_response + */ + public function test_get_item_head_request_restores_global_post() { + global $id; + + // Populate the global $wp_query with the post and set it up. + wp_set_current_user( self::$editor_id ); + query_posts( array( 'p' => self::$post_id ) ); + the_post(); + + // Assert initial state. + $post = get_post(); + $this->assertInstanceOf( WP_Post::class, $post, 'The global post should be set up before the request.' ); + $this->assertSame( self::$post_id, $post->ID, 'The global post should be the parent post before the request.' ); + $this->assertSame( self::$post_id, $id, 'The global $id should be the parent post ID before the request.' ); + + // Capture the arguments the rest_prepare_revision filter receives. + $mock = new MockAction(); + add_filter( 'rest_prepare_revision', array( $mock, 'filter' ), 10, 3 ); + + // Make the HEAD request to get a revision. + $request = new WP_REST_Request( 'HEAD', '/wp/v2/posts/' . self::$post_id . '/revisions/' . $this->revision_id1 ); + $request->set_param( 'context', 'edit' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + + // The filter is passed the revision, not the global post restored afterwards. + $this->check_rest_prepare_revision_filter_args( $mock, array( $this->revision_id1 ), $request ); + + // The global post is restored to the post that was set before the request. + $post = get_post(); + $this->assertInstanceOf( WP_Post::class, $post, 'The global post should still be set after the request.' ); + $this->assertSame( self::$post_id, $post->ID, 'The global post should be restored to the post that was set before the request.' ); + $this->assertSame( self::$post_id, $id, 'The global $id should be restored to the post that was set before the request.' ); + } + + /** + * When there is no global post before the request, none should be set afterwards. + * + * @ticket 65495 + * + * @global WP_Query|null $wp_query The global WP_Query. + * + * @covers WP_REST_Revisions_Controller::prepare_item_for_response + */ + public function test_get_items_without_global_post_leaves_it_unset() { + global $wp_query; + + // Leave the global $wp_query without a post, so there is no post data to restore. + wp_set_current_user( self::$editor_id ); + $this->assertInstanceOf( WP_Query::class, $wp_query, 'The WP_Query global must be set for wp_reset_postdata() to have anything to restore from.' ); + $this->assertNull( get_post(), 'The global post should not have been initially set.' ); + + // Capture the arguments the rest_prepare_revision filter receives. + $mock = new MockAction(); + add_filter( 'rest_prepare_revision', array( $mock, 'filter' ), 10, 3 ); + + // Make the request to get a revision. + $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() ); + + // The filter is passed each revision, even though there is no global post to restore. + $this->check_rest_prepare_revision_filter_args( + $mock, + array( $this->revision_id3, $this->revision_id2, $this->revision_id1 ), + $request + ); + + /* + * Note: At this point, the global $id is still populated because there was no $wp_query->post to begin with, + * so WP_Query::reset_postdata() has nothing to set it to. It does not null out any globals when there is no post. + */ + $this->assertNull( get_post(), 'The global post should not be set when there was none before the request.' ); + } + + /** + * A main query with a post must not cause a global post to be set where there was none. + * + * The restore calls wp_reset_postdata() to clear the revision's post data, and that + * repopulates the global post from the main query. The global post must be unset + * afterwards so the request does not introduce one that was not there before. + * + * @ticket 65495 + * + * @global WP_Query|null $wp_query The global WP_Query. + * @global int|null $id ID from the set up global post data. + * + * @covers WP_REST_Revisions_Controller::prepare_item_for_response + */ + public function test_get_items_without_global_post_leaves_it_unset_when_main_query_has_post() { + global $wp_query, $id; + + /* + * Populate the main query with the post, but do not run the loop, so the main + * query has a post to restore from while the global post remains unset. + */ + wp_set_current_user( self::$editor_id ); + query_posts( array( 'p' => self::$post_id ) ); + + // Assert initial state. + $this->assertInstanceOf( WP_Query::class, $wp_query, 'The WP_Query global must be set for wp_reset_postdata() to have anything to restore from.' ); + $this->assertInstanceOf( WP_Post::class, $wp_query->post, 'The main query should have a post for wp_reset_postdata() to restore from.' ); + $this->assertSame( self::$post_id, $wp_query->post->ID, 'The main query should have the parent post before the request.' ); + $this->assertNull( get_post(), 'The global post should not have been initially set.' ); + + // Make the request to get revisions. + $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->assertNull( get_post(), 'The global post should not be set when there was none before the request, even though the main query has a post.' ); + $this->assertSame( self::$post_id, $id, 'The remaining post data should be reset to the main query post rather than left on the revision.' ); + } + + /** + * A filter that reassigns the global post must not change the revision being prepared. + * + * The revision is held in a method-local variable rather than the global post, so a + * filter which swaps the global out mid-preparation (as a plugin running a secondary + * loop on 'the_content' may do) cannot retarget the fields prepared after it. + * + * @ticket 65495 + * + * @covers WP_REST_Revisions_Controller::prepare_item_for_response + */ + public function test_prepare_item_for_response_is_unaffected_by_a_filter_reassigning_the_global_post() { + wp_set_current_user( self::$editor_id ); + + $decoy_post = get_post( self::$post_id_2 ); + $this->assertInstanceOf( WP_Post::class, $decoy_post ); + $this->assertNotSame( $this->revision_1->post_excerpt, $decoy_post->post_excerpt, 'The decoy post must have a different excerpt for this test to be meaningful.' ); + + // Simulate a plugin that leaves a different post in the global while filtering the content. + add_filter( + 'the_content', + static function ( $content ) use ( $decoy_post ) { + $GLOBALS['post'] = $decoy_post; + return $content; + } + ); + + // Capture the post the rest_prepare_revision filter receives. + $mock = new MockAction(); + add_filter( 'rest_prepare_revision', array( $mock, 'filter' ), 10, 3 ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions/' . $this->revision_id1 ); + $request->set_param( 'context', 'edit' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + + // Fields prepared after 'the_content' still come from the revision. + $data = $response->get_data(); + $this->assertIsArray( $data ); + $this->assertArrayHasKey( 'excerpt', $data ); + $this->assertIsArray( $data['excerpt'] ); + $this->assertSame( $this->revision_id1, $data['id'], 'The prepared id should be the revision, not the post left in the global by the filter.' ); + $this->assertSame( $this->revision_1->post_excerpt, $data['excerpt']['raw'], 'The prepared excerpt should come from the revision, not the post left in the global by the filter.' ); + + // The filter is still passed the revision. + $this->check_rest_prepare_revision_filter_args( $mock, array( $this->revision_id1 ), $request ); + } + + /** + * Clearing the global post must not detach an existing `global $post` binding. + * + * `global $post` binds a caller to the global symbol table entry. Unsetting that + * entry detaches the binding, so a caller which sets the post after the request + * would be writing somewhere get_post() can no longer see. Note that the caller's + * own `global $post` is what creates the entry as null, which is why there is no + * previous global post to restore here. + * + * @ticket 65495 + * + * @global WP_Post|null $post Global post object. + * + * @covers WP_REST_Revisions_Controller::prepare_item_for_response + */ + public function test_prepare_item_for_response_does_not_detach_an_existing_global_post_binding() { + // Bind to the global post the way a caller does before dispatching a request. + global $post; + + wp_set_current_user( self::$editor_id ); + $this->assertNull( $post, 'The global post should not be set before the request.' ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions/' . $this->revision_id1 ); + $request->set_param( 'context', 'edit' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + + // The caller sets the global post, expecting template tags to pick it up. + $post = get_post( self::$post_id ); + + $global_post = get_post(); + $this->assertInstanceOf( WP_Post::class, $global_post, 'get_post() should see the post set through the binding after the request.' ); + $this->assertSame( self::$post_id, $global_post->ID, 'get_post() should return the post the caller set, not a stale or detached value.' ); + } + /** * @dataProvider data_readable_http_methods * @ticket 56481 @@ -601,6 +863,33 @@ public function additional_field_update_callback( $value, $post, $field_name ) { update_post_meta( $post->ID, $field_name, $value ); } + /** + * Checks the arguments the rest_prepare_revision filter received. + * + * The filter must be passed the revision that was prepared. Restoring the + * global post afterwards must not replace it with the previous global post. + * + * @param MockAction $mock Mock registered on the rest_prepare_revision filter. + * @param int[] $revision_ids Expected revision IDs, in the order the filter is expected to fire. + * @param WP_REST_Request $request Request the revisions were prepared for. + */ + private function check_rest_prepare_revision_filter_args( MockAction $mock, array $revision_ids, WP_REST_Request $request ): void { + $filter_args = $mock->get_args(); + + $this->assertCount( count( $revision_ids ), $filter_args, 'The rest_prepare_revision filter should fire once per prepared revision.' ); + + foreach ( $filter_args as $index => $args ) { + $call = 'Filter call ' . $index . ': '; + + $this->assertCount( 3, $args, $call . 'the filter should receive three arguments.' ); + $this->assertInstanceOf( WP_REST_Response::class, $args[0], $call . 'the first argument should be the response.' ); + $this->assertInstanceOf( WP_Post::class, $args[1], $call . 'the second argument should be a post object.' ); + $this->assertSame( 'revision', $args[1]->post_type, $call . 'the second argument should be a revision, not the restored global post.' ); + $this->assertSame( $revision_ids[ $index ], $args[1]->ID, $call . 'the second argument should be the revision that was prepared.' ); + $this->assertSame( $request, $args[2], $call . 'the third argument should be the request.' ); + } + } + protected function check_get_revision_response( $response, $revision ) { if ( $response instanceof WP_REST_Response ) { $links = $response->get_links(); @@ -639,16 +928,54 @@ 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 + * + * @global int|null $id ID from the set up global post data. + * + * @covers WP_REST_Revisions_Controller::prepare_item_for_response + */ + public function test_get_item_sets_up_postdata_without_leaking_global_post() { + global $id; + + // Populate the global $wp_query with the post and set it up. 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 ); + query_posts( array( 'p' => self::$post_id ) ); + the_post(); + + // Assert initial state. + $post = get_post(); + $this->assertInstanceOf( WP_Post::class, $post, 'The global post should be set up before the request.' ); + $this->assertSame( self::$post_id, $post->ID, 'The global post should be the parent post before the request.' ); + $this->assertSame( self::$post_id, $id, 'The global $id should be the parent post ID before the request.' ); + + // Capture the arguments the rest_prepare_revision filter receives. + $mock = new MockAction(); + add_filter( 'rest_prepare_revision', array( $mock, 'filter' ), 10, 3 ); + + // Make the request to get a revision. + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions/' . $this->revision_id1 ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); - $post = get_post(); - $parent_post_id = wp_is_post_revision( $post->ID ); + // The filter is passed the revision, not the global post restored afterwards. + $this->check_rest_prepare_revision_filter_args( $mock, array( $this->revision_id1 ), $request ); - $this->assertSame( $post->ID, $this->revision_id1 ); - $this->assertSame( $parent_post_id, self::$post_id ); + $data = $response->get_data(); + $this->assertIsArray( $data ); + $this->assertArrayHasKey( 'title', $data ); + $this->assertIsArray( $data['title'] ); + $this->assertSame( get_the_title( $this->revision_id1 ), $data['title']['rendered'], 'Expected the rendered title to reflect the revision, proving postdata was set up during preparation.' ); + + // The global post is restored to the post that was set before the request. + $post = get_post(); + $this->assertInstanceOf( WP_Post::class, $post, 'The global post should still be set after the request.' ); + $this->assertSame( self::$post_id, $post->ID, 'The global post should be restored to the post that was set before the request.' ); + $this->assertSame( self::$post_id, $id, 'The global $id should be restored to the post that was set before the request.' ); } /**