diff --git a/src/wp-includes/rest-api/fields/class-wp-rest-meta-fields.php b/src/wp-includes/rest-api/fields/class-wp-rest-meta-fields.php index 7b46905a7aa4f..5f3b55843e23a 100644 --- a/src/wp-includes/rest-api/fields/class-wp-rest-meta-fields.php +++ b/src/wp-includes/rest-api/fields/class-wp-rest-meta-fields.php @@ -268,6 +268,7 @@ protected function delete_meta_value( $object_id, $meta_key, $name ) { * Alters the list of values in the database to match the list of provided values. * * @since 4.7.0 + * @since 6.7.0 Stores values into DB even if provided registered default value. * * @param int $object_id Object ID to update. * @param string $meta_key Key for the custom field. @@ -290,7 +291,7 @@ protected function update_multi_meta_value( $object_id, $meta_key, $name, $value ); } - $current_values = get_metadata( $meta_type, $object_id, $meta_key, false ); + $current_values = get_metadata_raw( $meta_type, $object_id, $meta_key, false ); $subtype = get_object_subtype( $meta_type, $object_id ); if ( ! is_array( $current_values ) ) { @@ -367,6 +368,7 @@ function ( $stored_value ) use ( $meta_key, $subtype, $value ) { * Updates a meta value for an object. * * @since 4.7.0 + * @since 6.7.0 Stores values into DB even if provided registered default value. * * @param int $object_id Object ID to update. * @param string $meta_key Key for the custom field. @@ -378,7 +380,7 @@ protected function update_meta_value( $object_id, $meta_key, $name, $value ) { $meta_type = $this->get_meta_type(); // Do the exact same check for a duplicate value as in update_metadata() to avoid update_metadata() returning false. - $old_value = get_metadata( $meta_type, $object_id, $meta_key ); + $old_value = get_metadata_raw( $meta_type, $object_id, $meta_key ); $subtype = get_object_subtype( $meta_type, $object_id ); if ( is_array( $old_value ) && 1 === count( $old_value ) diff --git a/tests/phpunit/tests/rest-api/rest-post-meta-fields.php b/tests/phpunit/tests/rest-api/rest-post-meta-fields.php index 1418db19dbce2..786396df2e215 100644 --- a/tests/phpunit/tests/rest-api/rest-post-meta-fields.php +++ b/tests/phpunit/tests/rest-api/rest-post-meta-fields.php @@ -3095,6 +3095,464 @@ public function test_default_is_added_to_schema() { $this->assertSame( 'Goodnight Moon', $schema['default'] ); } + /** + * Ensures that REST API calls with post meta containing the default value for the + * registered meta field stores the default value into the database. + * + * When the default value isn't persisted in the database, a read of the post meta + * at some point in the future might return a different value if the code setting the + * default changed. This ensures that once a value is intentionally saved into the + * database that it will remain durably in future reads. + * + * @ticket 55600 + * + * @dataProvider data_scalar_default_values + * + * @param string $type Scalar type of default value: one of `boolean`, `integer`, `number`, or `string`. + * @param mixed $default_value Appropriate default value for given type. + * @param mixed $alternative_value Ignored in this test. + */ + public function test_scalar_singular_default_is_saved_to_db( $type, $default_value, $alternative_value ) { + $this->grant_write_permission(); + + $meta_key_single = "with_{$type}_default"; + + register_post_meta( + 'post', + $meta_key_single, + array( + 'type' => $type, + 'single' => true, + 'show_in_rest' => true, + 'default' => $default_value, + ) + ); + + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + $meta_key_single => $default_value, + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( + 200, + $response->get_status(), + "API call should have returned successfully but didn't: check test setup." + ); + + $this->assertSame( + array( (string) $default_value ), + get_metadata_raw( 'post', self::$post_id, $meta_key_single, false ), + 'Should have stored a single meta value with string-cast version of default value.' + ); + } + + /** + * Ensures that REST API calls with multi post meta values (containing the default) + * for the registered meta field stores the default value into the database. + * + * When the default value isn't persisted in the database, a read of the post meta + * at some point in the future might return a different value if the code setting the + * default changed. This ensures that once a value is intentionally saved into the + * database that it will remain durably in future reads. + * + * Further, the total count of stored values may be wrong if the default value + * is culled from the results of a "multi" read. + * + * @ticket 55600 + * + * @dataProvider data_scalar_default_values + * + * @param string $type Scalar type of default value: one of `boolean`, `integer`, `number`, or `string`. + * @param mixed $default_value Appropriate default value for given type. + * @param mixed $alternative_value Appropriate value for given type that doesn't match the default value. + */ + public function test_scalar_multi_default_is_saved_to_db( $type, $default_value, $alternative_value ) { + $this->grant_write_permission(); + + $meta_key_multiple = "with_multi_{$type}_default"; + + // Register non-singular post meta for type. + register_post_meta( + 'post', + $meta_key_multiple, + array( + 'type' => $type, + 'single' => false, + 'show_in_rest' => true, + 'default' => $default_value, + ) + ); + + // Write the default value as the sole value. + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + $meta_key_multiple => array( $default_value ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( + 200, + $response->get_status(), + "API call should have returned successfully but didn't: check test setup." + ); + + $this->assertSame( + array( (string) $default_value ), + get_metadata_raw( 'post', self::$post_id, $meta_key_multiple, false ), + 'Should have stored a single meta value with string-cast version of default value.' + ); + + // Write multiple values, including the default, to ensure it remains. + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + $meta_key_multiple => array( + $default_value, + $alternative_value, + ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( + 200, + $response->get_status(), + "API call should have returned successfully but didn't: check test setup." + ); + + $this->assertSame( + array( (string) $default_value, (string) $alternative_value ), + get_metadata_raw( 'post', self::$post_id, $meta_key_multiple, false ), + 'Should have stored both the default and non-default string-cast values.' + ); + } + + /** + * Ensures that REST API calls with post meta containing an object as the default + * value for the registered meta field stores the default value into the database. + * + * When the default value isn't persisted in the database, a read of the post meta + * at some point in the future might return a different value if the code setting the + * default changed. This ensures that once a value is intentionally saved into the + * database that it will remain durably in future reads. + * + * @ticket 55600 + * + * @dataProvider data_scalar_default_values + * + * @param string $type Scalar type of default value: one of `boolean`, `integer`, `number`, or `string`. + * @param mixed $default_value Appropriate default value for given type. + * @param mixed $alternative_value Ignored in this test. + */ + public function test_object_singular_default_is_saved_to_db( $type, $default_value, $alternative_value ) { + $this->grant_write_permission(); + + $meta_key_single = "with_{$type}_default"; + + // Register singular post meta for type. + register_post_meta( + 'post', + $meta_key_single, + array( + 'type' => 'object', + 'single' => true, + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'object', + 'properties' => array( + $type => array( 'type' => $type ), + ), + ), + ), + 'default' => (object) array( $type => $default_value ), + ) + ); + + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + $meta_key_single => (object) array( $type => $default_value ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( + 200, + $response->get_status(), + "API call should have returned successfully but didn't: check test setup." + ); + + // Objects stored into the database are read back as arrays. + $this->assertSame( + array( array( $type => $default_value ) ), + get_metadata_raw( 'post', self::$post_id, $meta_key_single, false ), + 'Should have stored a single meta value with an object representing the default value.' + ); + } + + /** + * Ensures that REST API calls with multi post meta values (containing an object as + * the default) for the registered meta field stores the default value into the database. + * + * When the default value isn't persisted in the database, a read of the post meta + * at some point in the future might return a different value if the code setting the + * default changed. This ensures that once a value is intentionally saved into the + * database that it will remain durably in future reads. + * + * Further, the total count of stored values may be wrong if the default value + * is culled from the results of a "multi" read. + * + * @ticket 55600 + * + * @dataProvider data_scalar_default_values + * + * @param string $type Scalar type of default value: one of `boolean`, `integer`, `number`, or `string`. + * @param mixed $default_value Appropriate default value for given type. + * @param mixed $alternative_value Appropriate value for given type that doesn't match the default value. + */ + public function test_object_multi_default_is_saved_to_db( $type, $default_value, $alternative_value ) { + $this->grant_write_permission(); + + $meta_key_multiple = "with_multi_{$type}_default"; + + // Register non-singular post meta for type. + register_post_meta( + 'post', + $meta_key_multiple, + array( + 'type' => 'object', + 'single' => false, + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'object', + 'properties' => array( + $type => array( 'type' => $type ), + ), + ), + ), + 'default' => (object) array( $type => $default_value ), + ) + ); + + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + $meta_key_multiple => array( (object) array( $type => $default_value ) ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( + 200, + $response->get_status(), + "API call should have returned successfully but didn't: check test setup." + ); + + // Objects stored into the database are read back as arrays. + $this->assertSame( + array( array( $type => $default_value ) ), + get_metadata_raw( 'post', self::$post_id, $meta_key_multiple, false ), + 'Should have stored a single meta value with an object representing the default value.' + ); + + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + $meta_key_multiple => array( + (object) array( $type => $default_value ), + (object) array( $type => $alternative_value ), + ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( + 200, + $response->get_status(), + "API call should have returned successfully but didn't: check test setup." + ); + + // Objects stored into the database are read back as arrays. + $this->assertSame( + array( array( $type => $default_value ), array( $type => $alternative_value ) ), + get_metadata_raw( 'post', self::$post_id, $meta_key_multiple, false ), + 'Should have stored a single meta value with an object representing the default value.' + ); + } + + /** + * Ensures that REST API calls with post meta containing a list array as the default + * value for the registered meta field stores the default value into the database. + * + * When the default value isn't persisted in the database, a read of the post meta + * at some point in the future might return a different value if the code setting the + * default changed. This ensures that once a value is intentionally saved into the + * database that it will remain durably in future reads. + * + * @ticket 55600 + * + * @dataProvider data_scalar_default_values + * + * @param string $type Scalar type of default value: one of `boolean`, `integer`, `number`, or `string`. + * @param mixed $default_value Appropriate default value for given type. + * @param mixed $alternative_value Ignored in this test. + */ + public function test_array_singular_default_is_saved_to_db( $type, $default_value, $alternative_value ) { + $this->grant_write_permission(); + + $meta_key_single = "with_{$type}_default"; + + // Register singular post meta for type. + register_post_meta( + 'post', + $meta_key_single, + array( + 'type' => 'array', + 'single' => true, + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'array', + 'items' => array( + 'type' => $type, + ), + ), + ), + 'default' => $default_value, + ) + ); + + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + $meta_key_single => array( $default_value ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( + 200, + $response->get_status(), + "API call should have returned successfully but didn't: check test setup." + ); + + $this->assertSame( + array( array( $default_value ) ), + get_metadata_raw( 'post', self::$post_id, $meta_key_single, false ), + 'Should have stored a single meta value with an array containing only the default value.' + ); + } + + /** + * Ensures that REST API calls with multi post meta values (containing a list array as + * the default) for the registered meta field stores the default value into the database. + * + * When the default value isn't persisted in the database, a read of the post meta + * at some point in the future might return a different value if the code setting the + * default changed. This ensures that once a value is intentionally saved into the + * database that it will remain durably in future reads. + * + * Further, the total count of stored values may be wrong if the default value + * is culled from the results of a "multi" read. + * + * @ticket 55600 + * + * @dataProvider data_scalar_default_values + * + * @param string $type Scalar type of default value: one of `boolean`, `integer`, `number`, or `string`. + * @param mixed $default_value Appropriate default value for given type. + * @param mixed $alternative_value Appropriate value for given type that doesn't match the default value. + */ + public function test_array_multi_default_is_saved_to_db( $type, $default_value, $alternative_value ) { + $this->grant_write_permission(); + + $meta_key_multiple = "with_multi_{$type}_default"; + + // Register non-singular post meta for type. + register_post_meta( + 'post', + $meta_key_multiple, + array( + 'type' => 'array', + 'single' => false, + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'array', + 'items' => array( + 'type' => $type, + ), + ), + ), + 'default' => $default_value, + ) + ); + + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + $meta_key_multiple => array( array( $default_value ) ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( + 200, + $response->get_status(), + "API call should have returned successfully but didn't: check test setup." + ); + + $this->assertSame( + array( array( $default_value ) ), + get_metadata_raw( 'post', self::$post_id, $meta_key_multiple, false ), + 'Should have stored a single meta value with an object representing the default value.' + ); + + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + $meta_key_multiple => array( + array( $default_value ), + array( $alternative_value ), + ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( + 200, + $response->get_status(), + "API call should have returned successfully but didn't: check test setup." + ); + + $this->assertSame( + array( array( $default_value ), array( $alternative_value ) ), + get_metadata_raw( 'post', self::$post_id, $meta_key_multiple, false ), + 'Should have stored a single meta value with an object representing the default value.' + ); + } + /** * @ticket 48823 */ @@ -3516,4 +3974,21 @@ public function data_revisioned_single_post_meta_with_posts_endpoint_page_and_cp ), ); } + + /** + * Data provider. + * + * Provides example default values of scalar types; + * in contrast to arrays, objects, etc... + * + * @return array[] + */ + public static function data_scalar_default_values() { + return array( + 'boolean default' => array( 'boolean', true, false ), + 'integer default' => array( 'integer', 42, 43 ), + 'number default' => array( 'number', 42.99, 43.99 ), + 'string default' => array( 'string', 'string', 'string2' ), + ); + } }