Opened 20 hours ago
Last modified 20 hours ago
#66329 new defect (bug)
Revisions endpoint returns meta that isn't revisioned, so restoring a revision in the editor overwrites it
| Reported by: | dlocc | Owned by: | |
|---|---|---|---|
| Priority: | normal | Milestone: | Awaiting Review |
| Component: | REST API | Version: | 7.0 |
| Severity: | normal | Keywords: | has-patch has-unit-tests |
| Cc: | Focuses: |
Description
Hey there! I'm report that the block editor's Visual Revisions, new in 7.0, restores a revision by reading it from the REST API and saving its title, excerpt, content and meta onto the post. The meta part is where it goes wrong.
WP_REST_Revisions_Controller returns every show_in_rest meta key registered for the parent post type, read from the revision ID. Only keys with revisions_enabled, or added with the wp_post_revision_meta_keys filter, are ever stored on a revision, so the rest come back as their default, or null when the default doesn't pass the key's schema. The editor then saves those over the post's real values.
Patterns hit this right away, since wp_pattern_sync_status isn't revisioned and only allows partial or unsynced:
- Restoring an unsynced pattern sends
null, which deletes the status, so the pattern quietly becomes synced. - Restoring a synced pattern fails with a 500: "The wp_pattern_sync_status property has an invalid stored value, and cannot be updated to null." The content is already saved by then, so the restore half works and the user sees an error.
On other post types it's silent. Restoring resets every non-revisioned show_in_rest key: strings become '', keys with a registered default get that default, and multi keys are deleted.
The classic revisions screen is fine, because wp_restore_post_revision() only restores the keys from wp_post_revision_meta_keys().
This was reported in Gutenberg as #84809, and @davidsword traced it to this endpoint. The editor can't tell which keys are revisioned, since revisions_enabled isn't exposed over REST, so the fix needs to happen here.
Steps to reproduce
- Register a meta key that isn't revisioned, for example in an mu-plugin:
<?php add_action( 'init', function () { register_post_meta( 'post', 'kept_meta', array( 'show_in_rest' => true, 'single' => true, 'type' => 'string', ) ); } );
- Create a post and update it a couple of times so it has revisions.
- Give it a value with
wp post meta update <ID> kept_meta hello. - With "Custom fields" turned off in the editor Preferences, so the editor uses Visual Revisions, open Revisions, move to an earlier revision, and click "Restore".
kept_metais now empty.
GET /wp/v2/posts/<ID>/revisions/<revision ID>?context=edit shows "kept_meta": "" even though nothing is stored on the revision. The steps for patterns are in the Gutenberg issue.
Proposed fix
Have the revisions endpoint only return, and only describe in its schema, the meta keys from wp_post_revision_meta_keys() for the parent post type. That matches what's stored on a revision and what the classic restore does, and the editor doesn't need a change. Autosave responses are built by the revisions controller, so they'd follow too, and autosaves only store revisioned keys anyway.
I have a patch with tests and will link the PR here.
Change History (1)
This ticket was mentioned in PR #14359 on WordPress/wordpress-develop by @dlocc.
20 hours ago
#1
- Keywords has-patch has-unit-tests added
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)
Visual Revisions in the block editor restores a revision by reading it from the REST API and saving its
metaonto the post. The revisions endpoint returns everyshow_in_restmeta key for the parent post type, read from the revision ID, but only revisioned keys are ever stored on a revision. Everything else comes back as its default, ornullwhen the default doesn't pass the key's schema, and the restore writes that over the post's real values.For patterns,
wp_pattern_sync_statuscomes backnull. Restoring an unsynced pattern deletes the status, so it becomes synced, and restoring a synced pattern fails with "The wp_pattern_sync_status property has an invalid stored value, and cannot be updated to null." after the content has already been saved. On other post types, any non-revisionedshow_in_restkey is quietly reset.Reported in Gutenberg as https://github.com/WordPress/gutenberg/issues/84809, where @davidsword traced it to this endpoint.
What changed
WP_REST_Revision_Meta_Fields, a smallWP_REST_Post_Meta_Fieldssubclass whoseget_registered_fields()only keeps the keys fromwp_post_revision_meta_keys()for the parent post type. That method is where meta keys get mapped to their REST names and schemas, soget_value()andget_field_schema()both follow it, including keys registered with a customshow_in_restname.WP_REST_Revisions_Controlleruses it for$this->meta, so a revision'smetaand themetaschema only include revisioned keys.This lines up with
wp_restore_post_revision(), which only restores those same keys, and the editor doesn't need a change.restoreRevisioncopies whatevermetathe revision has, so it now only sends revisioned keys back.Back compat
metais still there (an object, or[]when nothing is revisioned, same as before), it just stops including keys that were never stored on the revision.wp_post_revision_meta_keysfilter are still included.WP_REST_Autosaves_Controllerprepares its responses and schema through the revisions controller, so autosave responses follow too. Autosaves only store revisioned keys already increate_post_autosave(), and autosave requests are still validated against the parent post type's schema, so what clients can send doesn't change.$metaproperty is still documented asWP_REST_Post_Meta_Fields, so subclasses that use it keep working.metafrom their schema, so neither is affected.revisions_enabledor the filter, would stop seeing those keys in revision responses. Adding them towp_post_revision_meta_keysbrings them back.Testing
New tests in
tests/phpunit/tests/rest-api/rest-revisions-controller.php(9 cases) andtests/phpunit/tests/rest-api/rest-autosaves-controller.php(1 case), all in--group 66329. They cover single revisions, the collection, the schema, autosaves, the filter, custom REST names, and a restore done the same wayrestoreRevisiondoes it for a post and for synced, unsynced and partially synced patterns. The filter test passes on trunk too, since it checks that filtered keys aren't dropped.--group 66329--group restapi--group revisionThe
restapirun has 4 warnings (test_get_items_wdotorg_unavailable,test_create_item_wdotorg_unreachableand the twotest_format_validation_is_applied_if_missing_typetests). They're PHPUnit deprecation notices that show up on trunk too and aren't related to this change.tests/qunit/fixtures/wp-api-generated.jsis regenerated as part of the run. The fixture setup registers ameta_keythat isn't revisioned, so the revision and autosave fixtures now show"meta": []instead of{ "meta_key": "" }.PHPCS and PHPStan are clean on the changed files.
I also ran the restore the way the editor does it (
GETthe revision withcontext=edit, then save its content andmetato the post) against a local trunk install:kept_metaset tohellokept_metais''after restoringhellowp_pattern_sync_statusdeleted, pattern is syncedunsyncedrest_invalid_stored_value, content already restoredManual testing
wp post meta update <ID> kept_meta hello.kept_metais empty.kept_metais stillhello.Use of AI Tools
AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 5.5
Used for: Reproducing the bug, writing the fix and the PHPUnit tests, and drafting this description. I reviewed the code and the test results before opening this PR.