From c92ef7c441c7049b92e6613d20221ad1a99b0740 Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Thu, 15 Aug 2019 17:16:21 +0000 Subject: [PATCH] REST API: Support 'object' and 'array' types in register_meta() schemas. Extends meta registration to support complex schema values, mirroring the functionality in the settings controller. Error when trying to modify a meta key containing schema-nonconformant data. Props @TimothyBlynJacobs, @birgire, @mnelson4, @flixos90. Fixes #43392. git-svn-id: https://develop.svn.wordpress.org/trunk@45807 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/rest-api.php | 31 +- .../fields/class-wp-rest-meta-fields.php | 137 ++- tests/phpunit/includes/class-basic-object.php | 2 - tests/phpunit/includes/functions.php | 1 + .../tests/rest-api/rest-post-meta-fields.php | 779 +++++++++++++++++- 5 files changed, 911 insertions(+), 39 deletions(-) diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php index 5ec557547c..3054d61ea4 100644 --- a/src/wp-includes/rest-api.php +++ b/src/wp-includes/rest-api.php @@ -1159,6 +1159,11 @@ function rest_validate_value_from_schema( $value, $args, $param = '' ) { if ( $value instanceof stdClass ) { $value = (array) $value; } + + if ( $value instanceof JsonSerializable ) { + $value = $value->jsonSerialize(); + } + if ( ! is_array( $value ) ) { /* translators: 1: parameter, 2: type name */ return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'object' ) ); @@ -1170,8 +1175,17 @@ function rest_validate_value_from_schema( $value, $args, $param = '' ) { if ( is_wp_error( $is_valid ) ) { return $is_valid; } - } elseif ( isset( $args['additionalProperties'] ) && false === $args['additionalProperties'] ) { - return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not a valid property of Object.' ), $property ) ); + } elseif ( isset( $args['additionalProperties'] ) ) { + if ( false === $args['additionalProperties'] ) { + return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not a valid property of Object.' ), $property ) ); + } + + if ( is_array( $args['additionalProperties'] ) ) { + $is_valid = rest_validate_value_from_schema( $v, $args['additionalProperties'], $param . '[' . $property . ']' ); + if ( is_wp_error( $is_valid ) ) { + return $is_valid; + } + } } } } @@ -1298,6 +1312,11 @@ function rest_sanitize_value_from_schema( $value, $args ) { if ( $value instanceof stdClass ) { $value = (array) $value; } + + if ( $value instanceof JsonSerializable ) { + $value = $value->jsonSerialize(); + } + if ( ! is_array( $value ) ) { return array(); } @@ -1305,8 +1324,12 @@ function rest_sanitize_value_from_schema( $value, $args ) { foreach ( $value as $property => $v ) { if ( isset( $args['properties'][ $property ] ) ) { $value[ $property ] = rest_sanitize_value_from_schema( $v, $args['properties'][ $property ] ); - } elseif ( isset( $args['additionalProperties'] ) && false === $args['additionalProperties'] ) { - unset( $value[ $property ] ); + } elseif ( isset( $args['additionalProperties'] ) ) { + if ( false === $args['additionalProperties'] ) { + unset( $value[ $property ] ); + } elseif ( is_array( $args['additionalProperties'] ) ) { + $value[ $property ] = rest_sanitize_value_from_schema( $v, $args['additionalProperties'] ); + } } } 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 9cfee2ee71..5b7240ddc6 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 @@ -143,6 +143,21 @@ abstract class WP_REST_Meta_Fields { * from the database and then relying on the default value. */ if ( is_null( $meta[ $name ] ) ) { + $args = $this->get_registered_fields()[ $meta_key ]; + + if ( $args['single'] ) { + $current = get_metadata( $this->get_meta_type(), $object_id, $meta_key, true ); + + if ( is_wp_error( rest_validate_value_from_schema( $current, $args['schema'] ) ) ) { + return new WP_Error( + 'rest_invalid_stored_value', + /* translators: %s: custom field key */ + sprintf( __( 'The %s property has an invalid stored value, and cannot be updated to null.' ), $name ), + array( 'status' => 500 ) + ); + } + } + $result = $this->delete_meta_value( $object_id, $meta_key, $name ); if ( is_wp_error( $result ) ) { return $result; @@ -150,13 +165,24 @@ abstract class WP_REST_Meta_Fields { continue; } - $is_valid = rest_validate_value_from_schema( $meta[ $name ], $args['schema'], 'meta.' . $name ); + $value = $meta[ $name ]; + + if ( ! $args['single'] && is_array( $value ) && count( array_filter( $value, 'is_null' ) ) ) { + return new WP_Error( + 'rest_invalid_stored_value', + /* translators: %s: custom field key */ + sprintf( __( 'The %s property has an invalid stored value, and cannot be updated to null.' ), $name ), + array( 'status' => 500 ) + ); + } + + $is_valid = rest_validate_value_from_schema( $value, $args['schema'], 'meta.' . $name ); if ( is_wp_error( $is_valid ) ) { $is_valid->add_data( array( 'status' => 400 ) ); return $is_valid; } - $value = rest_sanitize_value_from_schema( $meta[ $name ], $args['schema'] ); + $value = rest_sanitize_value_from_schema( $value, $args['schema'] ); if ( $args['single'] ) { $result = $this->update_meta_value( $object_id, $meta_key, $name, $value ); @@ -260,8 +286,10 @@ abstract class WP_REST_Meta_Fields { unset( $to_add[ $add_key ] ); } - // `delete_metadata` removes _all_ instances of the value, so only call once. - $to_remove = array_unique( $to_remove ); + // `delete_metadata` removes _all_ instances of the value, so only call once. Otherwise, + // `delete_metadata` will return false for subsequent calls of the same value. + // Use serialization to produce a predictable string that can be used by array_unique. + $to_remove = array_map( 'maybe_unserialize', array_unique( array_map( 'maybe_serialize', $to_remove ) ) ); foreach ( $to_remove as $value ) { if ( ! delete_metadata( $meta_type, $object_id, wp_slash( $meta_key ), wp_slash( $value ) ) ) { @@ -393,15 +421,21 @@ abstract class WP_REST_Meta_Fields { $type = ! empty( $rest_args['type'] ) ? $rest_args['type'] : null; $type = ! empty( $rest_args['schema']['type'] ) ? $rest_args['schema']['type'] : $type; - if ( ! in_array( $type, array( 'string', 'boolean', 'integer', 'number' ) ) ) { + if ( null === $rest_args['schema']['default'] ) { + $rest_args['schema']['default'] = $this->get_default_for_type( $type ); + } + + $rest_args['schema'] = $this->default_additional_properties_to_false( $rest_args['schema'] ); + + if ( ! in_array( $type, array( 'string', 'boolean', 'integer', 'number', 'array', 'object' ) ) ) { continue; } if ( empty( $rest_args['single'] ) ) { - $rest_args['schema']['items'] = array( - 'type' => $rest_args['type'], + $rest_args['schema'] = array( + 'type' => 'array', + 'items' => $rest_args['schema'], ); - $rest_args['schema']['type'] = 'array'; } $registered[ $name ] = $rest_args; @@ -452,34 +486,18 @@ abstract class WP_REST_Meta_Fields { * @return mixed Value prepared for output. If a non-JsonSerializable object, null. */ public static function prepare_value( $value, $request, $args ) { - $type = $args['schema']['type']; - // For multi-value fields, check the item type instead. - if ( 'array' === $type && ! empty( $args['schema']['items']['type'] ) ) { - $type = $args['schema']['items']['type']; + if ( $args['single'] ) { + $schema = $args['schema']; + } else { + $schema = $args['schema']['items']; } - switch ( $type ) { - case 'string': - $value = (string) $value; - break; - case 'integer': - $value = (int) $value; - break; - case 'number': - $value = (float) $value; - break; - case 'boolean': - $value = (bool) $value; - break; - } - - // Don't allow objects to be output. - if ( is_object( $value ) && ! ( $value instanceof JsonSerializable ) ) { + if ( is_wp_error( rest_validate_value_from_schema( $value, $schema ) ) ) { return null; } - return $value; + return rest_sanitize_value_from_schema( $value, $schema ); } /** @@ -499,4 +517,63 @@ abstract class WP_REST_Meta_Fields { return $value; } + + /** + * Recursively add additionalProperties = false to all objects in a schema if no additionalProperties setting + * is specified. + * + * This is needed to restrict properties of objects in meta values to only + * registered items, as the REST API will allow additional properties by + * default. + * + * @since 5.3.0 + * + * @param array $schema The schema array. + * @return array + */ + protected function default_additional_properties_to_false( $schema ) { + switch ( $schema['type'] ) { + case 'object': + foreach ( $schema['properties'] as $key => $child_schema ) { + $schema['properties'][ $key ] = $this->default_additional_properties_to_false( $child_schema ); + } + + if ( ! isset( $schema['additionalProperties'] ) ) { + $schema['additionalProperties'] = false; + } + break; + case 'array': + $schema['items'] = $this->default_additional_properties_to_false( $schema['items'] ); + break; + } + + return $schema; + } + + /** + * Gets the default value for a schema type. + * + * @since 5.3.0 + * + * @param string $type + * @return mixed + */ + protected function get_default_for_type( $type ) { + switch ( $type ) { + case 'string': + return ''; + case 'boolean': + return false; + case 'integer': + return 0; + case 'number': + return 0.0; + case 'array': + return array(); + case 'object': + return array(); + default: + return null; + } + } } diff --git a/tests/phpunit/includes/class-basic-object.php b/tests/phpunit/includes/class-basic-object.php index 66ac53703e..6165344bde 100644 --- a/tests/phpunit/includes/class-basic-object.php +++ b/tests/phpunit/includes/class-basic-object.php @@ -7,8 +7,6 @@ * @since 4.7.0 */ -trigger_error( __FILE__ . ' is deprecated since version 5.0.0 with no alternative available.' ); - /** * Class used to test accessing methods and properties * diff --git a/tests/phpunit/includes/functions.php b/tests/phpunit/includes/functions.php index d21aa25235..670d89c44b 100644 --- a/tests/phpunit/includes/functions.php +++ b/tests/phpunit/includes/functions.php @@ -1,4 +1,5 @@ assertEquals( 403, $response->get_status() ); $this->assertEmpty( get_post_meta( $post_id, $meta_key, $single ) ); + return; } @@ -1298,7 +1299,7 @@ class WP_Test_REST_Post_Meta_Fields extends WP_Test_REST_TestCase { } /** - * @ticket 42069 + * @ticket 42069 * @dataProvider data_update_value_return_success_with_same_value */ public function test_update_value_return_success_with_same_value( $meta_key, $meta_value ) { @@ -1347,6 +1348,778 @@ class WP_Test_REST_Post_Meta_Fields extends WP_Test_REST_TestCase { $this->assertEquals( 'Hello', $data['meta']['test\'slashed\'key'] ); } + /** + * @ticket 43392 + */ + public function test_object_single() { + $this->grant_write_permission(); + + register_post_meta( + 'post', + 'object', + array( + 'single' => true, + 'type' => 'object', + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'object', + 'properties' => array( + 'project' => array( + 'type' => 'string', + ), + ), + ), + ), + ) + ); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + 'object' => array( + 'project' => 'WordPress', + ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertArrayHasKey( 'object', $data['meta'] ); + $this->assertArrayHasKey( 'project', $data['meta']['object'] ); + $this->assertEquals( 'WordPress', $data['meta']['object']['project'] ); + + $meta = get_post_meta( self::$post_id, 'object', true ); + $this->assertArrayHasKey( 'project', $meta ); + $this->assertEquals( 'WordPress', $meta['project'] ); + } + + /** + * @ticket 43392 + */ + public function test_object_multiple() { + $this->grant_write_permission(); + + register_post_meta( + 'post', + 'object', + array( + 'single' => false, + 'type' => 'object', + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'object', + 'properties' => array( + 'project' => array( + 'type' => 'string', + ), + ), + ), + ), + ) + ); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + 'object' => array( + array( + 'project' => 'WordPress', + ), + array( + 'project' => 'bbPress', + ), + ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertArrayHasKey( 'object', $data['meta'] ); + $this->assertCount( 2, $data['meta']['object'] ); + + $this->assertArrayHasKey( 'project', $data['meta']['object'][0] ); + $this->assertEquals( 'WordPress', $data['meta']['object'][0]['project'] ); + + $this->assertArrayHasKey( 'project', $data['meta']['object'][1] ); + $this->assertEquals( 'bbPress', $data['meta']['object'][1]['project'] ); + + $meta = get_post_meta( self::$post_id, 'object' ); + + $this->assertCount( 2, $meta ); + + $this->assertArrayHasKey( 'project', $meta[0] ); + $this->assertEquals( 'WordPress', $meta[0]['project'] ); + + $this->assertArrayHasKey( 'project', $meta[1] ); + $this->assertEquals( 'bbPress', $meta[1]['project'] ); + } + + /** + * @ticket 43392 + */ + public function test_array_single() { + $this->grant_write_permission(); + + register_post_meta( + 'post', + 'list', + array( + 'single' => true, + 'type' => 'array', + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + ), + ), + ) + ); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + 'list' => array( 'WordPress', 'bbPress' ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertArrayHasKey( 'list', $data['meta'] ); + $this->assertEquals( array( 'WordPress', 'bbPress' ), $data['meta']['list'] ); + + $meta = get_post_meta( self::$post_id, 'list', true ); + $this->assertEquals( array( 'WordPress', 'bbPress' ), $meta ); + } + + /** + * @ticket 43392 + */ + public function test_array_of_objects_multiple() { + $this->grant_write_permission(); + + register_post_meta( + 'post', + 'list_of_objects', + array( + 'single' => false, + 'type' => 'array', + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'version' => array( + 'type' => 'string', + ), + 'artist' => array( + 'type' => 'string', + ), + ), + ), + ), + ), + ) + ); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + 'list_of_objects' => array( + // Meta 1 + array( + array( + 'version' => '5.2', + 'artist' => 'Jaco', + ), + array( + 'version' => '5.1', + 'artist' => 'Betty', + ), + ), + // Meta 2 + array( + array( + 'version' => '4.9', + 'artist' => 'Tipton', + ), + ), + ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertArrayHasKey( 'list_of_objects', $data['meta'] ); + $this->assertCount( 2, $data['meta']['list_of_objects'] ); + + $this->assertEquals( + array( + array( + 'version' => '5.2', + 'artist' => 'Jaco', + ), + array( + 'version' => '5.1', + 'artist' => 'Betty', + ), + ), + $data['meta']['list_of_objects'][0] + ); + + $this->assertEquals( + array( + array( + 'version' => '4.9', + 'artist' => 'Tipton', + ), + ), + $data['meta']['list_of_objects'][1] + ); + + $meta = get_post_meta( self::$post_id, 'list_of_objects' ); + + $this->assertCount( 2, $meta ); + + $this->assertEquals( + array( + array( + 'version' => '5.2', + 'artist' => 'Jaco', + ), + array( + 'version' => '5.1', + 'artist' => 'Betty', + ), + ), + $meta[0] + ); + + $this->assertEquals( + array( + array( + 'version' => '4.9', + 'artist' => 'Tipton', + ), + ), + $meta[1] + ); + } + + /** + * @ticket 43392 + */ + public function test_php_objects_returned_as_null() { + register_post_meta( + 'post', + 'object', + array( + 'single' => true, + 'type' => 'object', + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'object', + 'properties' => array( + 'project' => array( + 'type' => 'string', + ), + ), + ), + ), + ) + ); + + $basic = new Basic_Object(); + $basic->project = 'WordPress'; + update_post_meta( self::$post_id, 'object', $basic ); + + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertArrayHasKey( 'object', $data['meta'] ); + $this->assertNull( $data['meta']['object'] ); + } + + /** + * @ticket 43392 + */ + public function test_php_objects_returned_as_null_multiple() { + register_post_meta( + 'post', + 'object', + array( + 'single' => false, + 'type' => 'object', + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'object', + 'properties' => array( + 'project' => array( + 'type' => 'string', + ), + ), + ), + ), + ) + ); + + $basic = new Basic_Object(); + $basic->project = 'WordPress'; + add_post_meta( self::$post_id, 'object', array( 'project' => 'bbPress' ) ); + add_post_meta( self::$post_id, 'object', $basic ); + + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertArrayHasKey( 'object', $data['meta'] ); + $this->assertCount( 2, $data['meta']['object'] ); + $this->assertEquals( array( 'project' => 'bbPress' ), $data['meta']['object'][0] ); + $this->assertNull( $data['meta']['object'][1] ); + } + + /** + * @ticket 43392 + */ + public function test_php_jsonserializable_object_returns_value() { + require_once __DIR__ . '/../../includes/class-jsonserializable-object.php'; + + register_post_meta( + 'post', + 'object', + array( + 'single' => true, + 'type' => 'object', + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'object', + 'properties' => array( + 'project' => array( + 'type' => 'string', + ), + ), + ), + ), + ) + ); + + update_post_meta( self::$post_id, 'object', new JsonSerializable_Object( array( 'project' => 'WordPress' ) ) ); + + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertArrayHasKey( 'object', $data['meta'] ); + $this->assertEquals( array( 'project' => 'WordPress' ), $data['meta']['object'] ); + } + + /** + * @ticket 43392 + */ + public function test_updating_meta_to_null_for_key_with_existing_php_object_does_not_delete_meta_value() { + $this->grant_write_permission(); + + register_post_meta( + 'post', + 'object', + array( + 'single' => true, + 'type' => 'object', + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'object', + 'properties' => array( + 'project' => array( + 'type' => 'string', + ), + ), + ), + ), + ) + ); + + $basic = new Basic_Object(); + $basic->project = 'WordPress'; + update_post_meta( self::$post_id, 'object', $basic ); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + 'object' => null, + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertEquals( 500, $response->get_status() ); + } + + /** + * @ticket 43392 + */ + public function test_updating_non_single_meta_to_null_for_key_with_existing_php_object_does_not_set_meta_value_to_null() { + $this->grant_write_permission(); + + register_post_meta( + 'post', + 'object', + array( + 'single' => false, + 'type' => 'object', + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'object', + 'properties' => array( + 'project' => array( + 'type' => 'string', + ), + ), + ), + ), + ) + ); + + $basic = new Basic_Object(); + $basic->project = 'WordPress'; + add_post_meta( self::$post_id, 'object', array( 'project' => 'bbPress' ) ); + add_post_meta( self::$post_id, 'object', $basic ); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + 'object' => array( + array( 'project' => 'BuddyPress' ), + null, + ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertEquals( 500, $response->get_status() ); + } + + /** + * @ticket 43392 + */ + public function test_object_rejects_additional_properties_by_default() { + $this->grant_write_permission(); + + register_post_meta( + 'post', + 'object', + array( + 'single' => true, + 'type' => 'object', + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'object', + 'properties' => array( + 'project' => array( + 'type' => 'string', + ), + ), + ), + ), + ) + ); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + 'object' => array( + 'project' => 'BuddyPress', + 'awesomeness' => 100, + ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertEquals( 400, $response->get_status() ); + } + + /** + * @ticket 43392 + */ + public function test_object_allows_additional_properties_if_explicitly_set() { + $this->grant_write_permission(); + + $value = array( + 'project' => 'BuddyPress', + 'awesomeness' => 100, + ); + + register_post_meta( + 'post', + 'object', + array( + 'single' => true, + 'type' => 'object', + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'object', + 'additionalProperties' => true, + 'properties' => array( + 'project' => array( + 'type' => 'string', + ), + ), + ), + ), + ) + ); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + 'object' => $value, + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( $value, $response->get_data()['meta']['object'] ); + + $this->assertEquals( $value, get_post_meta( self::$post_id, 'object', true ) ); + } + + /** + * @ticket 43392 + */ + public function test_object_allows_additional_properties_and_uses_its_schema() { + $this->grant_write_permission(); + + $value = array( + 'project' => 'BuddyPress', + 'awesomeness' => 'fabulous', + ); + + register_post_meta( + 'post', + 'object', + array( + 'single' => true, + 'type' => 'object', + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'object', + 'additionalProperties' => array( + 'type' => 'number', + ), + 'properties' => array( + 'project' => array( + 'type' => 'string', + ), + ), + ), + ), + ) + ); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + 'object' => $value, + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertEquals( 400, $response->get_status() ); + } + + /** + * @ticket 43392 + */ + public function test_invalid_meta_value_are_set_to_null_in_response() { + register_post_meta( + 'post', + 'email', + array( + 'single' => true, + 'type' => 'string', + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'string', + 'format' => 'email', + ), + ), + ) + ); + + update_post_meta( self::$post_id, 'email', 'invalid_meta_value' ); + + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertNull( $response->get_data()['meta']['email'] ); + } + + /** + * @ticket 43392 + */ + public function test_meta_values_are_not_set_to_null_in_response_if_type_safely_serializable() { + register_post_meta( + 'post', + 'boolean', + array( + 'single' => true, + 'show_in_rest' => true, + 'type' => 'boolean', + ) + ); + + update_post_meta( self::$post_id, 'boolean', 'true' ); + + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertTrue( $response->get_data()['meta']['boolean'] ); + } + + /** + * @ticket 43392 + */ + public function test_update_multi_meta_value_object() { + register_post_meta( + 'post', + 'object', + array( + 'single' => false, + 'type' => 'object', + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'object', + 'properties' => array( + 'project' => array( + 'type' => 'string', + ), + ), + ), + ), + ) + ); + + add_post_meta( + self::$post_id, + 'object', + array( + 'project' => 'WordPress', + ) + ); + add_post_meta( + self::$post_id, + 'object', + array( + 'project' => 'bbPress', + ) + ); + + $this->grant_write_permission(); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + 'object' => array( + array( 'project' => 'WordPress' ), + array( 'project' => 'BuddyPress' ), + ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + $this->assertArrayHasKey( 'object', $data['meta'] ); + + $this->assertCount( 2, $data['meta']['object'] ); + $this->assertEquals( array( 'project' => 'WordPress' ), $data['meta']['object'][0] ); + $this->assertEquals( array( 'project' => 'BuddyPress' ), $data['meta']['object'][1] ); + + $meta = get_post_meta( self::$post_id, 'object' ); + $this->assertCount( 2, $meta ); + $this->assertEquals( array( 'project' => 'WordPress' ), $meta[0] ); + $this->assertEquals( array( 'project' => 'BuddyPress' ), $meta[1] ); + } + + /** + * @ticket 43392 + */ + public function test_update_multi_meta_value_array() { + register_post_meta( + 'post', + 'list', + array( + 'type' => 'array', + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + ), + ), + ) + ); + + add_post_meta( self::$post_id, 'list', array( 'WordPress', 'bbPress' ) ); + add_post_meta( self::$post_id, 'list', array( 'WordCamp' ) ); + + $this->grant_write_permission(); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + 'list' => array( + array( 'WordPress', 'bbPress' ), + array( 'BuddyPress' ), + ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + $this->assertArrayHasKey( 'list', $data['meta'] ); + + $this->assertCount( 2, $data['meta']['list'] ); + $this->assertEquals( array( 'WordPress', 'bbPress' ), $data['meta']['list'][0] ); + $this->assertEquals( array( 'BuddyPress' ), $data['meta']['list'][1] ); + + $meta = get_post_meta( self::$post_id, 'list' ); + $this->assertCount( 2, $meta ); + $this->assertEquals( array( 'WordPress', 'bbPress' ), $meta[0] ); + $this->assertEquals( array( 'BuddyPress' ), $meta[1] ); + } + /** * Internal function used to disable an insert query which * will trigger a wpdb error for testing purposes.