Skip to content

REST API: Only include revisioned meta in revision responses - #14359

Closed
DevinWalker wants to merge 4 commits into
WordPress:trunkfrom
DevinWalker:66329-revisions-only-revisioned-meta
Closed

DevinWalker wants to merge 4 commits into
WordPress:trunkfrom
DevinWalker:66329-revisions-only-revisioned-meta

Conversation

@DevinWalker

@DevinWalker DevinWalker commented Oct 16, 2026 •

Copy link
Copy Markdown

Visual Revisions in the block editor restores a revision by reading it from the REST API and saving its meta onto the post. The revisions endpoint returns every show_in_rest meta 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, or null when 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_status comes back null. 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-revisioned show_in_rest key is quietly reset.

Reported in Gutenberg as WordPress/gutenberg#84809, where @davidsword traced it to this endpoint.

What changed

  • New WP_REST_Revision_Meta_Fields, a small WP_REST_Post_Meta_Fields subclass whose get_registered_fields() only keeps the keys from wp_post_revision_meta_keys() for the parent post type. That method is where meta keys get mapped to their REST names and schemas, so get_value() and get_field_schema() both follow it, including keys registered with a custom show_in_rest name.
  • WP_REST_Revisions_Controller uses it for $this->meta, so a revision's meta and the meta schema only include revisioned keys.
  • When an author autosaves their own draft, the autosaves controller updates the post and returns the post itself instead of an autosave. WP_REST_Revision_Meta_Fields::get_value() reads all of that post's meta, since it isn't a revision.
  • The keys from wp_post_revision_meta_keys() are cast to an array and limited to strings, so a filter that returns something else, like false, doesn't throw.

This lines up with wp_restore_post_revision(), which only restores those same keys, and the editor doesn't need a change. restoreRevision copies whatever meta the revision has, so it now only sends revisioned keys back.

Back compat

  • The response shape doesn't change. meta is still there (an object, or [] when nothing is revisioned, same as before), it just stops including keys that were never stored on the revision.
  • Keys added with the wp_post_revision_meta_keys filter are still included.
  • WP_REST_Autosaves_Controller prepares its responses and schema through the revisions controller, so autosave responses follow too. Autosaves only store revisioned keys already in create_post_autosave(), and autosave requests are still validated against the parent post type's schema, so what clients can send doesn't change.
  • Because of that, the autosaves meta schema now only lists revisioned keys while autosave requests still accept all of the parent post type's meta, and those other keys are saved when an author autosaves their own draft. Worth a mention in the dev note.
  • The $meta property is still documented as WP_REST_Post_Meta_Fields, so subclasses that use it keep working.
  • Template revisions build their response with the templates controller, and global styles revisions remove meta from their schema, so neither is affected.
  • A plugin that writes meta onto revisions itself, without revisions_enabled or the filter, would stop seeing those keys in revision responses. Adding them to wp_post_revision_meta_keys brings them back, though core then also copies those keys when a revision is saved and restores them from the classic revisions screen.

Testing

New tests in tests/phpunit/tests/rest-api/rest-revisions-controller.php (10 cases) and tests/phpunit/tests/rest-api/rest-autosaves-controller.php (2 cases), all in --group 66329. They cover single revisions, the collection, the schema, autosaves (including an author autosaving their own draft), the filter (including one that doesn't return an array), custom REST names, and a restore done the same way restoreRevision does it for a post and for synced, unsynced and partially synced patterns. The filter test and the own-draft autosave test pass on trunk too, since they check that keys aren't dropped.

trunk this PR
--group 66329 10 of 12 fail 12/12 pass
Revisions and autosaves controller tests 120/120 pass
--group restapi 3649 tests, no failures
--group revision 45/45 pass

The restapi run has 4 warnings (test_get_items_wdotorg_unavailable, test_create_item_wdotorg_unreachable and the two test_format_validation_is_applied_if_missing_type tests). They're PHPUnit deprecation notices that show up on trunk too and aren't related to this change.

tests/qunit/fixtures/wp-api-generated.js is regenerated as part of the run. The fixture setup registers a meta_key that 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 (GET the revision with context=edit, then save its content and meta to the post) against a local trunk install:

trunk this PR
Post with a non-revisioned kept_meta set to hello kept_meta is '' after restoring stays hello
Unsynced pattern wp_pattern_sync_status deleted, pattern is synced stays unsynced
Synced pattern 500 rest_invalid_stored_value, content already restored 200, restored

Manual testing

  1. Add this as an mu-plugin:
    <?php
    add_action( 'init', function () {
    	register_post_meta( 'post', 'kept_meta', array(
    		'show_in_rest' => true,
    		'single'       => true,
    		'type'         => 'string',
    	) );
    } );
  2. Create a post and update it a couple of times so it has revisions, then run wp post meta update <ID> kept_meta hello.
  3. With "Custom fields" turned off in the editor Preferences, open Revisions, move to an earlier revision, and click "Restore".
    • Before: kept_meta is empty.
    • After: kept_meta is still hello.
  4. For patterns, create an unsynced pattern, save a few edits, and restore an earlier revision from the Pattern tab.
    • Before: the pattern becomes synced. On a synced pattern, the restore shows an error.
    • After: the pattern keeps its sync status and the restore works.

Trac ticket: https://core.trac.wordpress.org/ticket/66329

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.


This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

Commit message

Drafted with Claude from the PR, the Trac ticket and the linked Gutenberg issue, then edited by Adam.

REST API: only include revisioned meta in revision responses.

Fix an issue where restoring a revision in the editor overwrote meta values that weren't associated with the revision.

Visual Revisions in the block editor, introduced in 7.0, restores a revision by saving the `meta` returned from the revisions endpoint onto the post. That endpoint returned every `show_in_rest` meta key for the parent post type, but only revisioned keys are stored on a revision, so the rest came back as their default (or `null`) and the restore overwrote the post's real values. Unsynced patterns silently became synced, synced patterns failed to restore with a `rest_invalid_stored_value` error, and any other non-revisioned `show_in_rest` key was reset.

Introduce `WP_REST_Revision_Meta_Fields`, used by `WP_REST_Revisions_Controller`, which limits a revision's `meta` and its schema to the keys from `wp_post_revision_meta_keys()`. This matches what `wp_restore_post_revision()` restores from the classic revisions screen, and requires no editor change. Autosave responses follow the same rules, since they are prepared by the revisions controller; autosave requests still accept all of the parent post type's meta. Plugins that write meta onto revisions directly should add those keys with the `wp_post_revision_meta_keys` filter to keep them in responses.

Also fixes https://github.com/WordPress/gutenberg/issues/84809.

Props dlocc, davidsword, talldanwp, rachelbaker, prasadkarmalkar, umeshnevase.
Fixes #66329.

`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 WordPress/gutenberg#84809.
Fixes #66329.
@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs.

Unlinked Accounts

The following contributors have not linked their GitHub and WordPress.org accounts: @rachelbaker.

Contributors, please read how to link your accounts to ensure your work is properly credited in WordPress releases.

Core Committers: Use this line as a base for the props when committing in SVN:

Props dlocc, adamsilverstein, talldanwp.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.


Props Bot commands

You can trigger Props Bot actions by commenting on this PR:

  • @propsbot add <username> to give props to a GitHub user. Requires write access.
  • @propsbot remove <username> to leave a user out of the props. Requires write access.
  • @propsbot refresh to refresh this list. Adding the props-bot label works too.

@github-actions

Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@talldan

talldan commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

I've added some extra reviewers (REST API component maintainers and contributors to https://core.trac.wordpress.org/ticket/20564)

@annezazu annezazu added the bug label Oct 20, 2026
@adamsilverstein

Copy link
Copy Markdown
Member

A plugin that writes meta onto revisions itself, without revisions_enabled or the filter, would stop seeing those keys in revision responses.

It might be worth seeing if there are any plugins expecting this behavior or trying anyway, https://veloria.dev/ is a good resource for this.

Adding them to wp_post_revision_meta_keys brings them back.

That would also have other consequences though. Might be fine?

@adamsilverstein

adamsilverstein commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

Thanks for tracking this down @DevinWalker, the restore approach makes sense to me and lines up nicely with wp_restore_post_revision_meta(). A couple of things look worth looking at from a review session with Claude:

Claude reviewed the PR and verified each finding with throwaway PHPUnit tests:

  1. Autosaving your own draft now drops meta from the response. When the author autosaves their own draft, create_item() updates the parent post with wp_update_post() instead of creating an autosave, then returns get_post( $post_id ) through the revisions controller. The object is the live post, but WP_REST_Revision_Meta_Fields still filters its meta, so non-revisioned keys disappear from the response.

A test with a draft by the current user, a non-revisioned kept_meta set to hello, and a POST /wp/v2/posts/<id>/autosaves: the response id is the post ID and kept_meta is in meta on the base commit, but missing with this PR. The value is still stored, only the response changes.

This matters for the editor: in saveEntityRecord() (packages/core-data/src/actions.js), when persistedRecord.id === updatedRecord.id it replaces the stored record's meta with the response's meta. So after every draft autosave, the editor's copy of the post loses its non-revisioned meta. The new autosave test uses the editor-autosaving-someone-else's-post path, so it doesn't catch this.

One way to fix it is to only filter when the object really is a revision:

public function get_value( $object_id, $request ) {
	if ( 'revision' !== get_post_type( $object_id ) ) {
		$meta = new WP_REST_Post_Meta_Fields( $this->post_type );
		return $meta->get_value( $object_id, $request );
	}

	return parent::get_value( $object_id, $request );
}

With that in place the draft autosave test passes, along with all of --group 66329 and the full revisions and autosaves controller suites (118 tests).

  1. array_flip() fatals if the filter returns a non-array. wp_post_revision_meta_keys is filterable, and a callback returning false (eg. to turn meta revisions off) now throws TypeError: array_flip(): Argument #1 ($array) must be of type array, false given on PHP 8, so every revision and autosave GET returns a 500. On the base commit the same request returns 200. Something like array_flip( array_filter( (array) wp_post_revision_meta_keys( $this->post_type ), 'is_string' ) ) guards it. Worth noting that _wp_preview_meta_filter() already has the same weakness with in_array(), so this is more about not widening it.

  2. Minor: the autosave schema no longer matches what the endpoint accepts. The autosaves meta schema now lists only revisioned keys (footnotes,rev_meta in testing), while the POST args still come from the posts controller (footnotes,kept_meta,rev_meta), and on the draft path those extra keys really are saved. On the base commit the two matched. Probably fine to leave, but maybe worth a mention in the dev note alongside the plugin back compat item already in the description.

  3. This only fixes 7.2 and later, but the bug goes back to 7.0, when Visual Revisions shipped. Gutenberg supports the last two WordPress releases and doesn't override the revisions controller, so plugin users on 7.0 and 7.1 would still hit it. Gutenberg could ship the same fix in lib/compat/wordpress-7.2/, for example a rest_prepare_revision filter that drops the meta keys not in wp_post_revision_meta_keys(). It would need the same revision post type check as point 1, since the draft autosave path runs through that filter too. Patterns: Restoring revisions is broken gutenberg#84809 should probably stay open until that lands.

Also checked and not issues: older revisions returning the default for a revisioned key that was never stored matches what wp_restore_post_revision_meta() already does (delete, then copy), and calling wp_create_initial_post_meta() in the pattern test is safe since the test case unregisters all meta keys between tests.

What do you think about scoping the filter to revisions like that? Happy to help test once it's updated.

Manual testing with Playground: https://playground.wordpress.net/wordpress.html?pr=14359

…tered 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.
@DevinWalker

Copy link
Copy Markdown
Author

Thanks for the thorough review, @adamsilverstein… and nice catch on the draft autosave path.

  1. Autosaving your own draft now drops meta from the response.

Fixed with your suggestion. WP_REST_Revision_Meta_Fields::get_value() now reads the post's own meta when the object isn't a revision, and there's a new test for an author autosaving their own draft.

  1. array_flip() fatals if the filter returns a non-array.

Fixed too, with the cast and is_string filter you suggested, plus a test that returns false from wp_post_revision_meta_keys.

  1. Minor: the autosave schema no longer matches what the endpoint accepts.

I added it to the back compat notes in the description so it makes it into the dev note.

  1. This only fixes 7.2 and later, but the bug goes back to 7.0, when Visual Revisions shipped.

Agreed. Happy to put up the Gutenberg compat PR in lib/compat/wordpress-7.2/ with the same revision check, and we can keep WordPress/gutenberg#84809 open until it lands.

On plugins that write meta onto revisions, I searched Veloria and found 21 that do it directly. The three biggest (MetForm, YayMail and Visual Composer) read those values back in their own code and, from what I can tell, don't register them for REST, so they shouldn't be affected. You're right that adding keys to wp_post_revision_meta_keys has other effects, since core then copies them on save and restores them from the classic screen. I think that's fine for anyone who wants them in the response, and I noted it in the description.

All of --group 66329, the revisions and autosaves controller tests, and --group restapi pass. Ready for another look when you have time.

@rachelbaker rachelbaker left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm aligned with the overall approach here.

@adamsilverstein adamsilverstein left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good one.

DevinWalker added a commit to DevinWalker/gutenberg that referenced this pull request Oct 24, 2026
The revisions endpoint returns every `show_in_rest` meta key of the parent post type, but only revisioned keys are stored on a revision. The others come back as their default, and restoring the revision in the editor saves those defaults over the post's values. Restoring an unsynced pattern makes it synced, and restoring a synced pattern fails.

Filter `rest_prepare_revision` to only keep the keys from `wp_post_revision_meta_keys()`, so WordPress 7.0 and 7.1 behave like the Core fix in WordPress/wordpress-develop#14359. Objects that aren't revisions are left alone, since an author autosaving their own draft gets the post itself back.
@DevinWalker

Copy link
Copy Markdown
Author

The Gutenberg compat PR for 7.0 and 7.1 is up: WordPress/gutenberg#85509

@github-actions

Copy link
Copy Markdown

A commit was made that fixes the Trac ticket referenced in the description of this pull request.

SVN changeset: 64731
GitHub commit: 0c8d8da

This PR will be closed, but please confirm the accuracy of this and reopen if there is more work to be done.

@adamsilverstein

adamsilverstein commented Oct 26, 2026 •

Copy link
Copy Markdown
Member

Draft dev note section for Trac #66329, to go in the 7.2 REST API roundup. The main action item is for plugins that write meta onto revisions.

Claude read through the changeset and the review thread and drafted this:

REST API: revision and autosave responses only include revisioned meta

Visual Revisions in the block editor (new in 7.0) restores a revision by reading it from the REST API and saving its meta onto the post. Before 7.2, 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 ever stored on a revision, so every other key came back as its registered default, or null when the default did not pass the key's schema. The restore then wrote those values over the post's real meta.

The visible effects were:

  • Restoring an unsynced pattern sent null for wp_pattern_sync_status, which deleted the status and made the pattern synced.
  • Restoring a synced pattern failed with a rest_invalid_stored_value error after the content had already been saved.
  • On other post types, non-revisioned show_in_rest keys were silently reset.

The classic revisions screen was never affected, because wp_restore_post_revision() only restores the keys from wp_post_revision_meta_keys(). The editor cannot tell which keys are revisioned, since revisions_enabled is not exposed over REST, so the fix had to happen in the endpoint (#66329).

What changed in 7.2

The new WP_REST_Revision_Meta_Fields class, a subclass of WP_REST_Post_Meta_Fields, limits a revision's meta response and the meta property of the schema to the keys returned by wp_post_revision_meta_keys() for the parent post type. WP_REST_Revisions_Controller uses it for its $meta property, so no editor change is needed. Keys registered with a custom show_in_rest name are handled the same way.

  • Autosave responses follow the same rules, because the revisions controller prepares them. Autosave requests still accept all of the parent post type's meta.
  • When an author autosaves their own draft, the autosaves controller updates the post itself and returns it instead of an autosave. That response still includes all of the post's meta.
  • Anything returned by the wp_post_revision_meta_keys filter that is not a string is ignored.

What plugin developers should check

A meta key is revisioned when it is registered with revisions_enabled set to true, or when it is added with the wp_post_revision_meta_keys filter.

register_post_meta(
	'post',
	'my_meta',
	array(
		'show_in_rest'      => true,
		'single'            => true,
		'type'              => 'string',
		'revisions_enabled' => true,
	)
);

For meta that is registered elsewhere, or by code that cannot be changed, use the filter:

add_filter(
	'wp_post_revision_meta_keys',
	static function ( array $keys ): array {
		$keys[] = 'my_meta';
		return $keys;
	}
);
  • Plugins that write meta onto revisions directly, without either of the above, will no longer see those keys in REST revision and autosave responses. Adding the key to wp_post_revision_meta_keys brings it back. Note that this also makes the key part of what the classic screen restores, so it is worth confirming that is wanted.
  • Clients that read revision meta over REST should expect non-revisioned keys to be absent, rather than present with a default value. The meta schema for revisions shrinks to match.
  • Code that subclasses or inspects the controller's $meta property now finds a WP_REST_Revision_Meta_Fields instance. It still extends WP_REST_Post_Meta_Fields.

For sites on the Gutenberg plugin

A compat change in Gutenberg (#85509, merged) applies the same filtering for sites running the plugin on WordPress 7.0 and 7.1.

Related: r64731, #14359, Gutenberg #84809.

Props to dlocc, davidsword, talldanwp, rachelbaker, prasadkarmalkar, umeshnevase and adamsilverstein.

@adamsilverstein

adamsilverstein commented Oct 26, 2026 •

Copy link
Copy Markdown
Member

Second part of the REST API dev note for 7.2, added here so the whole note lives on one PR. The revision meta section (#66329) is in the comment above, and the sections for #63982 and #46992 follow.

Claude read both changesets and the ticket history, and drafted these sections:

REST API: the comments endpoint accepts multiple statuses

The comments collection endpoint (/wp/v2/comments) now accepts several statuses in one request. WP_Comment_Query has always supported an array of statuses, but the endpoint only accepted a single string, so clients that needed comments in more than one status, such as approved and pending notes, had to make a request for each (#63982).

/wp/v2/comments?status=approve,hold
/wp/v2/comments?status[]=approve&status[]=hold

Single status requests work as before, and so does the all status. The permission check is unchanged: unauthenticated requests may only use the default approve status, and anything else, including a list that contains approve, is rejected with rest_forbidden_param unless the user can edit_posts.

What changed in 7.2

  • The status collection parameter is now an array of strings, with a default of array( 'approve' ), and the route's argument schema says so.
  • The new WP_REST_Comments_Controller::sanitize_comment_statuses() accepts a string, a comma-separated list or an array. It sanitizes each value with sanitize_key, removes duplicates and drops empty values. A literal 0 is kept, because held comments are stored with a comment_approved value of 0.
  • No enum of allowed values was added. WP_Comment_Query supports custom comment statuses, so an enum limited to the registered statuses would have broken existing requests.

What plugin developers should check

After sanitization, $request['status'] is always an array, and so is status in the arguments passed to the rest_comment_query filter. Code that compared it to a string will no longer match:

// Before: only matches in 7.1 and earlier.
if ( 'hold' === $request['status'] ) {
	// ...
}

// After: works in 7.1 and in 7.2.
if ( in_array( 'hold', (array) $request['status'], true ) ) {
	// ...
}

Clients and tools that read the route's argument schema will see status as an array rather than a string.

Related: r64494, #9870, Gutenberg #71271.

Props to adamsilverstein, westonruter, swissspidy, timothyblynjacobs, mukesh27 and wildworks.

REST API: new rest_allowed_cors_methods filter

WordPress 7.2 adds a rest_allowed_cors_methods filter for the Access-Control-Allow-Methods header that the REST API sends with cross-origin (CORS) requests. It also stops discarding an Access-Control-Allow-Methods value set on a REST response (#46992).

The problem

When a request carries an Origin header, rest_send_cors_headers() sent a hardcoded list of methods: OPTIONS, GET, POST, PUT, PATCH, DELETE. There was no way to change that list short of unhooking rest_send_cors_headers() from rest_pre_serve_request and re-implementing it, or hooking in at a later priority and calling header() directly. Both are fragile, and two plugins doing it will conflict.

Setting the header on the response through rest_post_dispatch, the usual advice for changing REST response headers, did not work either. rest_send_cors_headers() runs after the response's own headers have been sent, and replaced whatever value the response set.

The two sibling CORS headers have had filters since 5.5: rest_exposed_cors_headers for Access-Control-Expose-Headers and rest_allowed_cors_headers for Access-Control-Allow-Headers. This filter fills in the third.

The new filter

rest_allowed_cors_methods follows the same shape as its siblings. It receives the list of methods as an array, along with the request in context, and the result is joined into the header.

/**
 * @param string[]             $allow_methods The list of HTTP methods to allow.
 * @param WP_REST_Request|null $request       The request in context, if available.
 */
apply_filters( 'rest_allowed_cors_methods', $allow_methods, $request );

Since it is an array, plugins can add to or narrow the list without restating it, and several plugins can do so without stepping on each other.

The filter is documented as string[], and callbacks should return an array. Core also accepts a comma-separated string, such as 'OPTIONS, GET'. Filter callbacks run in a chain, so a callback can receive a string returned by an earlier one. For that reason, avoid an array type hint on $methods, which would throw a TypeError in that case, and normalize the value with wp_parse_list() before changing it.

Advertise an extra method, for example for a route that accepts PROPFIND:

add_filter(
	'rest_allowed_cors_methods',
	function ( $methods ) {
		$methods   = wp_parse_list( $methods );
		$methods[] = 'PROPFIND';
		return $methods;
	}
);

Narrow the list for cross-origin requests to a read-only namespace:

add_filter(
	'rest_allowed_cors_methods',
	function ( $methods, ?WP_REST_Request $request ) {
		if ( $request && str_starts_with( $request->get_route(), '/my-plugin/v1/public' ) ) {
			return array( 'OPTIONS', 'GET' );
		}
		return $methods;
	},
	10,
	2
);

This is not a permission check. Access-Control-Allow-Methods only affects the preflight requests browsers send for some cross-origin requests. Simple cross-origin requests, such as a POST with a form content type, are not preflighted, and non-browser clients ignore the header. Use permission_callback on the route to restrict who can do what.

Returning an empty array omits the Access-Control-Allow-Methods header entirely. Entries that are not valid HTTP method tokens are dropped, and a callback that returns something other than an array or a string is ignored in favor of the unfiltered list.

Response values are no longer discarded

If a response sets its own Access-Control-Allow-Methods header, for example from rest_post_dispatch, that value is now used as the starting list passed to the filter instead of being overwritten by the default:

add_filter(
	'rest_post_dispatch',
	function ( WP_HTTP_Response $response ): WP_HTTP_Response {
		$response->header( 'Access-Control-Allow-Methods', 'OPTIONS, GET' );
		return $response;
	}
);

The filter still runs after that, so a site always has the last word: a callback can keep the response's value, extend it, or replace it. If the response value names no valid methods, the default list is used.

Backward compatibility

The default output is unchanged. A site that neither sets the header on a response nor uses the filter sends the same OPTIONS, GET, POST, PUT, PATCH, DELETE list as before.

rest_send_cors_headers() gains three optional parameters, $result, $request and $server, and is now hooked to rest_pre_serve_request with four accepted arguments. Existing calls with a single argument keep working, and remove_filter( 'rest_pre_serve_request', 'rest_send_cors_headers' ) still removes it, since the priority is unchanged at 10. When serving a REST request, the CORS headers are now sent through WP_REST_Server::send_header() rather than a direct header() call.

Edge case: ?_envelope=1

With _envelope set, the response's headers are moved into the response body before rest_send_cors_headers() runs, so a value set on the response is not seen and the filtered default is sent. The filter itself still applies.

Related: r64730, #13151, #57752 (the sibling filters gained the $request parameter in 6.3).

Props to moonmeister, sudar, timothyblynjacobs, aaemnnosttv and westonruter.

@DevinWalker

Copy link
Copy Markdown
Author

Thanks for getting this committed, @adamsilverstein, and for wrapping up the last review items on WordPress/gutenberg#85509 as well - I appreciate it.

The dev note looks accurate to me. One small addition for the "What plugin developers should check" list: when a post type has no revisioned meta, a revision's meta is now an empty array ([]) instead of an object with each key's default.

@adamsilverstein

Copy link
Copy Markdown
Member

The dev note looks accurate to me. One small addition for the "What plugin developers should check" list: when a post type has no revisioned meta, a revision's meta is now an empty array ([]) instead of an object with each key's default.

Thanks, I'll make sure to include that change in the final version.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants