From 281e45008e29c81e06f95ea7aa00cbcc1bc4a106 Mon Sep 17 00:00:00 2001 From: Jignesh Bhavani Date: Fri, 31 Jul 2026 16:31:18 +0530 Subject: [PATCH] REST API: Correctly store multiple value meta on autosaves. `WP_REST_Autosaves_Controller::create_post_autosave()` wrote every revisioned meta key with `update_metadata()`. Without a `$prev_value` that updates every row sharing the meta key, so a meta key registered with `single => false` ended up with each of its rows holding the entire array of values instead of one value each. Values are now stored the way `_wp_copy_post_meta()` stores them, by replacing the existing rows and adding one row per value. Whether a key is treated as single is taken from its registration rather than from the type of the value, since a key registered with `single => true` may legitimately hold an array. The change detection above it hardcoded `$single` to `true` when reading the stored value, so a multiple value key was always compared as a single string against an array and never matched, marking every autosave as different. Props jigneshbhavani, wongjn. Fixes #65754. --- .../class-wp-rest-autosaves-controller.php | 41 ++++++++++++- .../rest-api/rest-autosaves-controller.php | 58 +++++++++++++++++++ 2 files changed, 96 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php index c0d58160467b2..3368dbd7d97fb 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php @@ -398,10 +398,30 @@ public function create_post_autosave( $post_data, array $meta = array() ) { // Check if meta values have changed. if ( ! empty( $meta ) ) { $revisioned_meta_keys = wp_post_revision_meta_keys( $post->post_type ); + $registered_meta = array_merge( + get_registered_meta_keys( 'post' ), + get_registered_meta_keys( 'post', $post->post_type ) + ); + + /* + * Meta keys registered with 'single' => false store each of their values in a + * separate row, so they must be read, compared, and stored as a list of values. + * Keys without a registration are treated as single, matching previous behavior. + */ + $is_single_meta = array(); + + foreach ( $revisioned_meta_keys as $meta_key ) { + $is_single_meta[ $meta_key ] = ! isset( $registered_meta[ $meta_key ]['single'] ) || $registered_meta[ $meta_key ]['single']; + } + foreach ( $revisioned_meta_keys as $meta_key ) { // get_metadata_raw is used to avoid retrieving the default value. - $old_meta = get_metadata_raw( 'post', $post_id, $meta_key, true ); - $new_meta = $meta[ $meta_key ] ?? ''; + $old_meta = get_metadata_raw( 'post', $post_id, $meta_key, $is_single_meta[ $meta_key ] ); + $new_meta = $meta[ $meta_key ] ?? ( $is_single_meta[ $meta_key ] ? '' : array() ); + + if ( ! $is_single_meta[ $meta_key ] && null === $old_meta ) { + $old_meta = array(); + } if ( $new_meta !== $old_meta ) { $autosave_is_different = true; @@ -441,8 +461,23 @@ public function create_post_autosave( $post_data, array $meta = array() ) { // Attached any passed meta values that have revisions enabled. if ( ! empty( $meta ) ) { foreach ( $revisioned_meta_keys as $meta_key ) { - if ( isset( $meta[ $meta_key ] ) ) { + if ( ! isset( $meta[ $meta_key ] ) ) { + continue; + } + + if ( $is_single_meta[ $meta_key ] ) { update_metadata( 'post', $revision_id, $meta_key, wp_slash( $meta[ $meta_key ] ) ); + continue; + } + + /* + * update_metadata() would overwrite every row sharing the meta key with the + * same value, so replace the stored values instead, as _wp_copy_post_meta() does. + */ + delete_metadata( 'post', $revision_id, $meta_key ); + + foreach ( (array) $meta[ $meta_key ] as $meta_value ) { + add_metadata( 'post', $revision_id, $meta_key, wp_slash( $meta_value ) ); } } } diff --git a/tests/phpunit/tests/rest-api/rest-autosaves-controller.php b/tests/phpunit/tests/rest-api/rest-autosaves-controller.php index 179ce60e047df..ba64edbf65b7e 100644 --- a/tests/phpunit/tests/rest-api/rest-autosaves-controller.php +++ b/tests/phpunit/tests/rest-api/rest-autosaves-controller.php @@ -516,6 +516,64 @@ public function test_update_item_with_json_meta() { $this->assertNotNull( $values ); } + /** + * @ticket 65754 + */ + public function test_update_item_with_multiple_value_meta() { + wp_set_current_user( self::$editor_id ); + + register_post_meta( + 'post', + 'foo_multi', + array( + 'show_in_rest' => true, + 'revisions_enabled' => true, + 'single' => false, + 'type' => 'string', + ) + ); + + $post_id = self::factory()->post->create( + array( + 'post_content' => 'Original content.', + 'post_author' => self::$editor_id, + ) + ); + + add_post_meta( $post_id, 'foo_multi', 'bar' ); + add_post_meta( $post_id, 'foo_multi', 'baz' ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/posts/' . $post_id . '/autosaves' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); + $request->set_body_params( + $this->set_post_data( + array( + 'id' => $post_id, + 'content' => 'Autosaved content.', + 'meta' => array( + 'foo_multi' => array( 'bar', 'qux' ), + ), + ) + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->check_create_autosave_response( $response ); + + $data = $response->get_data(); + + $this->assertSame( + array( 'bar', 'qux' ), + get_post_meta( $data['id'], 'foo_multi' ), + 'The autosave revision should store each meta value in its own row.' + ); + $this->assertSame( + array( 'bar', 'qux' ), + $data['meta']['foo_multi'], + 'The response should return the autosaved meta values.' + ); + } + public function test_update_item_nopriv() { wp_set_current_user( self::$contributor_id );