From a3326404773545e1aa59f6f2e9aff234fa24dedc Mon Sep 17 00:00:00 2001 From: devinwalker Date: Fri, 16 Oct 2026 14:06:04 -0700 Subject: [PATCH 1/2] REST API: Only include revisioned meta in revision responses. `WP_REST_Revisions_Controller` returned every `show_in_rest` meta key registered for the parent post type, read from the revision ID. Only revisioned keys are stored on a revision, so every other key came back as its default value, or `null` when the default does not pass the key's schema. The block editor restores a revision by saving its `meta` onto the post, which reset those keys, deleted the sync status of unsynced patterns, and failed for synced patterns. The revisions controller now reads meta through a new `WP_REST_Revision_Meta_Fields` class, which limits the registered fields to the keys returned by `wp_post_revision_meta_keys()` for the parent post type. A revision's `meta` and its schema now match what is stored on the revision and what `wp_restore_post_revision()` restores. Autosave responses, which are prepared by the revisions controller, follow the same change. Follow-up to [56714]. See https://github.com/WordPress/gutenberg/issues/84809. Fixes #66329. --- .../class-wp-rest-revisions-controller.php | 5 +- .../class-wp-rest-revision-meta-fields.php | 38 ++ src/wp-settings.php | 1 + .../rest-api/rest-autosaves-controller.php | 51 +++ .../rest-api/rest-revisions-controller.php | 339 ++++++++++++++++++ tests/qunit/fixtures/wp-api-generated.js | 40 +-- 6 files changed, 443 insertions(+), 31 deletions(-) create mode 100644 src/wp-includes/rest-api/fields/class-wp-rest-revision-meta-fields.php 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 2dbba2b6d5e1d..9abced65e68b2 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 @@ -28,6 +28,7 @@ class WP_REST_Revisions_Controller extends WP_REST_Controller { * Instance of a revision meta fields object. * * @since 6.4.0 + * @since 7.2.0 Now a WP_REST_Revision_Meta_Fields instance, which only exposes revisioned meta keys. * @var WP_REST_Post_Meta_Fields */ protected $meta; @@ -68,7 +69,7 @@ public function __construct( $parent_post_type ) { $this->rest_base = 'revisions'; $this->parent_base = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name; $this->namespace = ! empty( $post_type_object->rest_namespace ) ? $post_type_object->rest_namespace : 'wp/v2'; - $this->meta = new WP_REST_Post_Meta_Fields( $parent_post_type ); + $this->meta = new WP_REST_Revision_Meta_Fields( $parent_post_type ); } /** @@ -596,6 +597,7 @@ 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. + * @since 7.2.0 The `meta` field only includes the meta keys that are revisioned for the parent post type. * * @global WP_Post|null $post Global post object. * @@ -813,6 +815,7 @@ protected function prepare_date_response( $date_gmt, $date = null ) { * Retrieves the revision's schema, conforming to JSON Schema. * * @since 4.7.0 + * @since 7.2.0 The `meta` property only describes the meta keys that are revisioned for the parent post type. * * @return array Item schema data. */ diff --git a/src/wp-includes/rest-api/fields/class-wp-rest-revision-meta-fields.php b/src/wp-includes/rest-api/fields/class-wp-rest-revision-meta-fields.php new file mode 100644 index 0000000000000..de94f7f4ce9a5 --- /dev/null +++ b/src/wp-includes/rest-api/fields/class-wp-rest-revision-meta-fields.php @@ -0,0 +1,38 @@ +post_type ) ) + ); + } +} diff --git a/src/wp-settings.php b/src/wp-settings.php index 0340582015899..29a17ab900a59 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -385,6 +385,7 @@ require ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-meta-fields.php'; require ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-comment-meta-fields.php'; require ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-post-meta-fields.php'; +require ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-revision-meta-fields.php'; require ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-term-meta-fields.php'; require ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-user-meta-fields.php'; require ABSPATH . WPINC . '/rest-api/search/class-wp-rest-search-handler.php'; diff --git a/tests/phpunit/tests/rest-api/rest-autosaves-controller.php b/tests/phpunit/tests/rest-api/rest-autosaves-controller.php index 179697f0deabc..58f162da6560d 100644 --- a/tests/phpunit/tests/rest-api/rest-autosaves-controller.php +++ b/tests/phpunit/tests/rest-api/rest-autosaves-controller.php @@ -492,6 +492,57 @@ public function test_update_item_with_meta() { $this->assertSame( 'bar', $data['meta']['foo'] ); } + /** + * The autosave's meta only includes the meta keys that are revisioned, + * since those are the only meta values stored on an autosave. + * + * @ticket 66329 + * + * @covers WP_REST_Autosaves_Controller::prepare_item_for_response + * @covers WP_REST_Revision_Meta_Fields::get_registered_fields + */ + public function test_update_item_with_meta_only_includes_revisioned_meta_keys() { + wp_set_current_user( self::$editor_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); + register_post_meta( + 'post', + 'foo', + array( + 'show_in_rest' => true, + 'revisions_enabled' => true, + 'single' => true, + ) + ); + register_post_meta( + 'post', + 'not_revisioned', + array( + 'show_in_rest' => true, + 'single' => true, + ) + ); + $params = $this->set_post_data( + array( + 'id' => self::$post_id, + 'author' => self::$contributor_id, + 'meta' => array( + 'foo' => 'bar', + 'not_revisioned' => 'baz', + ), + ) + ); + + $request->set_body_params( $params ); + $response = rest_get_server()->dispatch( $request ); + + $this->check_create_autosave_response( $response ); + + $data = $response->get_data(); + $this->assertSame( 'bar', $data['meta']['foo'] ); + $this->assertArrayNotHasKey( 'not_revisioned', $data['meta'] ); + } + public function test_update_item_with_json_meta() { $meta = '[{\"content\":\"foot 1\",\"id\":\"fa97a10d-7401-42b9-ac54-df8f4510749a\"},{\"content\":\"fdddddoot 2\\\"\",\"id\":\"2216d0aa-34b8-42b4-b441-84dedc0406e0\"}]'; wp_set_current_user( self::$editor_id ); diff --git a/tests/phpunit/tests/rest-api/rest-revisions-controller.php b/tests/phpunit/tests/rest-api/rest-revisions-controller.php index a4e89863c7a2c..2f4bfd5e3ec0d 100644 --- a/tests/phpunit/tests/rest-api/rest-revisions-controller.php +++ b/tests/phpunit/tests/rest-api/rest-revisions-controller.php @@ -1405,4 +1405,343 @@ public function test_get_revisions_pagination() { $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_revision_invalid_page_number', $response, 400 ); } + + /** + * A revision's meta only includes the meta keys that are revisioned. + * + * Meta keys without `revisions_enabled` are never stored on a revision, so + * reading them from the revision would report their default value instead. + * + * @ticket 66329 + * + * @covers WP_REST_Revisions_Controller::prepare_item_for_response + * @covers WP_REST_Revision_Meta_Fields::get_registered_fields + */ + public function test_get_item_meta_only_includes_revisioned_meta_keys() { + $this->register_revisioned_and_non_revisioned_meta(); + + $post_id = $this->create_post_with_revisioned_meta( 'revisioned value' ); + $revision_id = $this->get_oldest_revision_id( $post_id ); + + wp_set_current_user( self::$editor_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . $post_id . '/revisions/' . $revision_id ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status() ); + $this->assertArrayHasKey( 'revisioned_meta', $data['meta'], 'Revisioned meta should be included.' ); + $this->assertSame( 'revisioned value', $data['meta']['revisioned_meta'], 'Revisioned meta should be read from the revision.' ); + $this->assertArrayNotHasKey( 'non_revisioned_meta', $data['meta'], 'Meta that is not revisioned should not be included.' ); + } + + /** + * Each revision in the collection only includes the meta keys that are revisioned. + * + * @ticket 66329 + * + * @covers WP_REST_Revisions_Controller::prepare_item_for_response + * @covers WP_REST_Revision_Meta_Fields::get_registered_fields + */ + public function test_get_items_meta_only_includes_revisioned_meta_keys() { + $this->register_revisioned_and_non_revisioned_meta(); + + $post_id = $this->create_post_with_revisioned_meta( 'revisioned value' ); + + wp_set_current_user( self::$editor_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . $post_id . '/revisions' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status() ); + $this->assertCount( 2, $data ); + + foreach ( $data as $revision ) { + $this->assertArrayHasKey( 'revisioned_meta', $revision['meta'], 'Revisioned meta should be included.' ); + $this->assertArrayNotHasKey( 'non_revisioned_meta', $revision['meta'], 'Meta that is not revisioned should not be included.' ); + } + } + + /** + * The schema for a revision's meta only describes the meta keys that are revisioned. + * + * A new controller is used because the post type's controller caches its + * schema when the routes are registered, before the meta keys are. + * + * @ticket 66329 + * + * @covers WP_REST_Revisions_Controller::get_item_schema + * @covers WP_REST_Revision_Meta_Fields::get_registered_fields + */ + public function test_get_item_schema_meta_only_includes_revisioned_meta_keys() { + $this->register_revisioned_and_non_revisioned_meta(); + + $controller = new WP_REST_Revisions_Controller( 'post' ); + $properties = $controller->get_item_schema()['properties']['meta']['properties']; + + $this->assertArrayHasKey( 'revisioned_meta', $properties, 'Revisioned meta should be described.' ); + $this->assertArrayNotHasKey( 'non_revisioned_meta', $properties, 'Meta that is not revisioned should not be described.' ); + } + + /** + * Meta keys added with the `wp_post_revision_meta_keys` filter are included. + * + * @ticket 66329 + * + * @covers WP_REST_Revision_Meta_Fields::get_registered_fields + */ + public function test_get_item_meta_includes_meta_keys_from_revision_meta_keys_filter() { + $this->register_revisioned_and_non_revisioned_meta(); + + add_filter( + 'wp_post_revision_meta_keys', + static function ( $keys ) { + $keys[] = 'non_revisioned_meta'; + return $keys; + } + ); + + $post_id = self::factory()->post->create(); + update_post_meta( $post_id, 'non_revisioned_meta', 'filtered value' ); + wp_update_post( + array( + 'ID' => $post_id, + 'post_content' => 'Revised content.', + ) + ); + $revision_id = $this->get_oldest_revision_id( $post_id ); + + wp_set_current_user( self::$editor_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . $post_id . '/revisions/' . $revision_id ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status() ); + $this->assertArrayHasKey( 'non_revisioned_meta', $data['meta'] ); + $this->assertSame( 'filtered value', $data['meta']['non_revisioned_meta'] ); + } + + /** + * Revisioned meta keys with a custom REST name are included under that name. + * + * @ticket 66329 + * + * @covers WP_REST_Revision_Meta_Fields::get_registered_fields + */ + public function test_get_item_meta_uses_rest_name_of_revisioned_meta_keys() { + register_post_meta( + 'post', + 'revisioned_meta', + array( + 'show_in_rest' => array( 'name' => 'revisioned_alias' ), + 'single' => true, + 'type' => 'string', + 'revisions_enabled' => true, + ) + ); + register_post_meta( + 'post', + 'non_revisioned_meta', + array( + 'show_in_rest' => array( 'name' => 'non_revisioned_alias' ), + 'single' => true, + 'type' => 'string', + ) + ); + + $post_id = $this->create_post_with_revisioned_meta( 'revisioned value' ); + $revision_id = $this->get_oldest_revision_id( $post_id ); + + wp_set_current_user( self::$editor_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . $post_id . '/revisions/' . $revision_id ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status() ); + $this->assertArrayHasKey( 'revisioned_alias', $data['meta'] ); + $this->assertSame( 'revisioned value', $data['meta']['revisioned_alias'] ); + $this->assertArrayNotHasKey( 'revisioned_meta', $data['meta'] ); + $this->assertArrayNotHasKey( 'non_revisioned_alias', $data['meta'] ); + } + + /** + * Restoring a revision the way the block editor does restores the revisioned + * meta and leaves meta that is not revisioned alone. + * + * This matches wp_restore_post_revision(), which only restores the keys + * returned by wp_post_revision_meta_keys(). + * + * @ticket 66329 + * + * @covers WP_REST_Revisions_Controller::prepare_item_for_response + */ + public function test_restoring_revision_keeps_meta_that_is_not_revisioned() { + $this->register_revisioned_and_non_revisioned_meta(); + + $post_id = $this->create_post_with_revisioned_meta( 'old value' ); + update_post_meta( $post_id, 'revisioned_meta', 'new value' ); + update_post_meta( $post_id, 'non_revisioned_meta', 'kept value' ); + $revision_id = $this->get_oldest_revision_id( $post_id ); + + wp_set_current_user( self::$editor_id ); + $response = $this->restore_revision_like_the_block_editor( '/wp/v2/posts/' . $post_id, $revision_id ); + + $this->assertSame( 200, $response->get_status() ); + $this->assertSame( 'Revised content.', get_post( $post_id )->post_content, 'The content should be restored.' ); + $this->assertSame( 'old value', get_post_meta( $post_id, 'revisioned_meta', true ), 'Revisioned meta should be restored.' ); + $this->assertSame( 'kept value', get_post_meta( $post_id, 'non_revisioned_meta', true ), 'Meta that is not revisioned should be kept.' ); + } + + /** + * Restoring a pattern revision the way the block editor does keeps its sync status. + * + * `wp_pattern_sync_status` is not revisioned, so the revision used to report + * it as null. Saving that null deleted the status of unsynced patterns, + * turning them into synced patterns, and failed for synced patterns. + * + * @ticket 66329 + * + * @dataProvider data_pattern_sync_statuses + * + * @covers WP_REST_Revisions_Controller::prepare_item_for_response + * + * @param string $sync_status Sync status of the pattern. Empty for synced patterns. + */ + public function test_restoring_pattern_revision_keeps_sync_status( $sync_status ) { + // Registered meta keys are cleared after each test, so register the sync status again. + wp_create_initial_post_meta(); + + $pattern_id = self::factory()->post->create( + array( + 'post_type' => 'wp_block', + 'post_content' => 'Original content.', + 'meta_input' => $sync_status ? array( 'wp_pattern_sync_status' => $sync_status ) : array(), + ) + ); + wp_update_post( + array( + 'ID' => $pattern_id, + 'post_content' => 'Revised content.', + ) + ); + wp_update_post( + array( + 'ID' => $pattern_id, + 'post_content' => 'Latest content.', + ) + ); + $revision_id = $this->get_oldest_revision_id( $pattern_id ); + + wp_set_current_user( self::$editor_id ); + $response = $this->restore_revision_like_the_block_editor( '/wp/v2/blocks/' . $pattern_id, $revision_id ); + + $this->assertSame( 200, $response->get_status() ); + $this->assertSame( 'Revised content.', get_post( $pattern_id )->post_content, 'The content should be restored.' ); + $this->assertSame( $sync_status, get_post_meta( $pattern_id, 'wp_pattern_sync_status', true ), 'The sync status should be kept.' ); + } + + /** + * Data provider for test_restoring_pattern_revision_keeps_sync_status(). + * + * @return array[] + */ + public function data_pattern_sync_statuses() { + return array( + 'synced pattern' => array( '' ), + 'unsynced pattern' => array( 'unsynced' ), + 'partially synced pattern' => array( 'partial' ), + ); + } + + /** + * Registers a revisioned and a non-revisioned meta key for posts. + */ + private function register_revisioned_and_non_revisioned_meta() { + register_post_meta( + 'post', + 'revisioned_meta', + array( + 'show_in_rest' => true, + 'single' => true, + 'type' => 'string', + 'revisions_enabled' => true, + ) + ); + register_post_meta( + 'post', + 'non_revisioned_meta', + array( + 'show_in_rest' => true, + 'single' => true, + 'type' => 'string', + ) + ); + } + + /** + * Creates a post with two revisions, the older of which stores a revisioned meta value. + * + * @param string $value Value of the `revisioned_meta` key when the older revision is saved. + * @return int Post ID. + */ + private function create_post_with_revisioned_meta( $value ) { + $post_id = self::factory()->post->create( array( 'post_content' => 'Original content.' ) ); + + update_post_meta( $post_id, 'revisioned_meta', $value ); + update_post_meta( $post_id, 'non_revisioned_meta', 'non-revisioned value' ); + wp_update_post( + array( + 'ID' => $post_id, + 'post_content' => 'Revised content.', + ) + ); + wp_update_post( + array( + 'ID' => $post_id, + 'post_content' => 'Latest content.', + ) + ); + + return $post_id; + } + + /** + * Gets the ID of a post's oldest revision. + * + * @param int $post_id Post ID. + * @return int Revision ID. + */ + private function get_oldest_revision_id( $post_id ) { + $revisions = wp_get_post_revisions( $post_id ); + + return end( $revisions )->ID; + } + + /** + * Restores a revision the way the block editor's restoreRevision() action does. + * + * The revision is read with the `edit` context, then its content and meta + * are saved to the parent post. + * + * @param string $parent_route Route of the parent post, such as `/wp/v2/posts/1`. + * @param int $revision_id Revision ID. + * @return WP_REST_Response Response to saving the parent post. + */ + private function restore_revision_like_the_block_editor( $parent_route, $revision_id ) { + $request = new WP_REST_Request( 'GET', $parent_route . '/revisions/' . $revision_id ); + $request->set_param( 'context', 'edit' ); + $revision = rest_get_server()->dispatch( $request )->get_data(); + + $request = new WP_REST_Request( 'POST', $parent_route ); + $request->set_header( 'Content-Type', 'application/json' ); + $request->set_body( + wp_json_encode( + array( + 'content' => $revision['content']['raw'], + 'meta' => $revision['meta'], + ) + ) + ); + + return rest_get_server()->dispatch( $request ); + } } diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index dba0cd776b59f..53123567affe3 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -13416,9 +13416,7 @@ mockedApiResponse.postRevisions = [ "excerpt": { "rendered": "" }, - "meta": { - "meta_key": "" - }, + "meta": [], "_links": { "parent": [ { @@ -13448,9 +13446,7 @@ mockedApiResponse.postRevisions = [ "excerpt": { "rendered": "

REST API Client Fixture: Post

\n" }, - "meta": { - "meta_key": "" - }, + "meta": [], "_links": { "parent": [ { @@ -13482,9 +13478,7 @@ mockedApiResponse.revision = { "excerpt": { "rendered": "

REST API Client Fixture: Post

\n" }, - "meta": { - "meta_key": "" - } + "meta": [] }; mockedApiResponse.postAutosaves = [ @@ -13509,9 +13503,7 @@ mockedApiResponse.postAutosaves = [ "excerpt": { "rendered": "" }, - "meta": { - "meta_key": "" - }, + "meta": [], "_links": { "parent": [ { @@ -13543,9 +13535,7 @@ mockedApiResponse.autosave = { "excerpt": { "rendered": "" }, - "meta": { - "meta_key": "" - } + "meta": [] }; mockedApiResponse.PagesCollection = [ @@ -13714,9 +13704,7 @@ mockedApiResponse.pageRevisions = [ "excerpt": { "rendered": "" }, - "meta": { - "meta_key": "" - }, + "meta": [], "_links": { "parent": [ { @@ -13746,9 +13734,7 @@ mockedApiResponse.pageRevisions = [ "excerpt": { "rendered": "

REST API Client Fixture: Page

\n" }, - "meta": { - "meta_key": "" - }, + "meta": [], "_links": { "parent": [ { @@ -13780,9 +13766,7 @@ mockedApiResponse.pageRevision = { "excerpt": { "rendered": "

REST API Client Fixture: Page

\n" }, - "meta": { - "meta_key": "" - } + "meta": [] }; mockedApiResponse.pageAutosaves = [ @@ -13807,9 +13791,7 @@ mockedApiResponse.pageAutosaves = [ "excerpt": { "rendered": "" }, - "meta": { - "meta_key": "" - }, + "meta": [], "_links": { "parent": [ { @@ -13841,9 +13823,7 @@ mockedApiResponse.pageAutosave = { "excerpt": { "rendered": "" }, - "meta": { - "meta_key": "" - } + "meta": [] }; mockedApiResponse.MediaCollection = [ From d370fb0e0f4baeb7fb941aae7a9d9f22ed765281 Mon Sep 17 00:00:00 2001 From: devinwalker Date: Tue, 20 Oct 2026 12:49:37 -0700 Subject: [PATCH 2/2] REST API: Keep all meta in own-draft autosave responses and guard filtered revision meta keys. When an author autosaves their own draft, the autosaves controller updates the post itself and returns it through the revisions controller. Its meta is now read as regular post meta, so keys that aren't revisioned stay in the response and the editor doesn't drop them from its copy of the post. Also ignore anything that isn't a meta key when the `wp_post_revision_meta_keys` filter returns something other than an array of strings, instead of throwing a TypeError on every revision and autosave request. See #66329. --- .../class-wp-rest-revision-meta-fields.php | 27 +++++++++++- .../rest-api/rest-autosaves-controller.php | 41 +++++++++++++++++++ .../rest-api/rest-revisions-controller.php | 23 +++++++++++ 3 files changed, 90 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/rest-api/fields/class-wp-rest-revision-meta-fields.php b/src/wp-includes/rest-api/fields/class-wp-rest-revision-meta-fields.php index de94f7f4ce9a5..3db7c0a6dc581 100644 --- a/src/wp-includes/rest-api/fields/class-wp-rest-revision-meta-fields.php +++ b/src/wp-includes/rest-api/fields/class-wp-rest-revision-meta-fields.php @@ -22,6 +22,28 @@ */ class WP_REST_Revision_Meta_Fields extends WP_REST_Post_Meta_Fields { + /** + * Retrieves the meta field values for a revision. + * + * When an author autosaves their own draft, the autosaves controller updates + * the post itself and returns it instead of an autosave. That post stores all + * of its meta, so its values are read as regular post meta. + * + * @since 7.2.0 + * + * @param int $object_id Object ID to fetch meta for. + * @param WP_REST_Request $request Full details about the request. + * @return array Array containing the meta values keyed by name. + */ + public function get_value( $object_id, $request ) { + if ( 'revision' !== get_post_type( $object_id ) ) { + $post_meta = new WP_REST_Post_Meta_Fields( $this->post_type ); + return $post_meta->get_value( $object_id, $request ); + } + + return parent::get_value( $object_id, $request ); + } + /** * Retrieves the registered meta fields that are revisioned for the parent post type. * @@ -30,9 +52,12 @@ class WP_REST_Revision_Meta_Fields extends WP_REST_Post_Meta_Fields { * @return array Registered fields, limited to the revisioned meta keys. */ protected function get_registered_fields() { + // The keys are filterable, so ignore anything that isn't a meta key. + $revisioned_keys = array_filter( (array) wp_post_revision_meta_keys( $this->post_type ), 'is_string' ); + return array_intersect_key( parent::get_registered_fields(), - array_flip( wp_post_revision_meta_keys( $this->post_type ) ) + array_flip( $revisioned_keys ) ); } } diff --git a/tests/phpunit/tests/rest-api/rest-autosaves-controller.php b/tests/phpunit/tests/rest-api/rest-autosaves-controller.php index 58f162da6560d..b1907c9d7ace2 100644 --- a/tests/phpunit/tests/rest-api/rest-autosaves-controller.php +++ b/tests/phpunit/tests/rest-api/rest-autosaves-controller.php @@ -543,6 +543,47 @@ public function test_update_item_with_meta_only_includes_revisioned_meta_keys() $this->assertArrayNotHasKey( 'not_revisioned', $data['meta'] ); } + /** + * Autosaving your own draft updates the post itself instead of creating an + * autosave, so the response includes all of the post's meta. + * + * @ticket 66329 + * + * @covers WP_REST_Autosaves_Controller::create_item + * @covers WP_REST_Revision_Meta_Fields::get_value + */ + public function test_create_item_for_own_draft_includes_meta_that_is_not_revisioned() { + register_post_meta( + 'post', + 'not_revisioned', + array( + 'show_in_rest' => true, + 'single' => true, + 'type' => 'string', + ) + ); + + $draft_id = self::factory()->post->create( + array( + 'post_status' => 'draft', + 'post_author' => self::$editor_id, + ) + ); + update_post_meta( $draft_id, 'not_revisioned', 'kept value' ); + + wp_set_current_user( self::$editor_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/posts/' . $draft_id . '/autosaves' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); + $request->set_body_params( array( 'content' => 'Autosaved content.' ) ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status() ); + $this->assertSame( $draft_id, $data['id'], 'Autosaving your own draft should update the post itself.' ); + $this->assertArrayHasKey( 'not_revisioned', $data['meta'], 'The post itself should include meta that is not revisioned.' ); + $this->assertSame( 'kept value', $data['meta']['not_revisioned'] ); + } + public function test_update_item_with_json_meta() { $meta = '[{\"content\":\"foot 1\",\"id\":\"fa97a10d-7401-42b9-ac54-df8f4510749a\"},{\"content\":\"fdddddoot 2\\\"\",\"id\":\"2216d0aa-34b8-42b4-b441-84dedc0406e0\"}]'; wp_set_current_user( self::$editor_id ); diff --git a/tests/phpunit/tests/rest-api/rest-revisions-controller.php b/tests/phpunit/tests/rest-api/rest-revisions-controller.php index 2f4bfd5e3ec0d..7a6bc226b9e8c 100644 --- a/tests/phpunit/tests/rest-api/rest-revisions-controller.php +++ b/tests/phpunit/tests/rest-api/rest-revisions-controller.php @@ -1652,6 +1652,29 @@ public function data_pattern_sync_statuses() { ); } + /** + * A `wp_post_revision_meta_keys` filter that doesn't return an array doesn't break revision responses. + * + * @ticket 66329 + * + * @covers WP_REST_Revision_Meta_Fields::get_registered_fields + */ + public function test_get_item_with_non_array_revision_meta_keys_filter() { + $this->register_revisioned_and_non_revisioned_meta(); + + $post_id = $this->create_post_with_revisioned_meta( 'revisioned value' ); + $revision_id = $this->get_oldest_revision_id( $post_id ); + + add_filter( 'wp_post_revision_meta_keys', '__return_false' ); + + wp_set_current_user( self::$editor_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . $post_id . '/revisions/' . $revision_id ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + $this->assertSame( array(), $response->get_data()['meta'] ); + } + /** * Registers a revisioned and a non-revisioned meta key for posts. */