diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php index d59f570c02..8e48f892f7 100644 --- a/src/wp-includes/rest-api.php +++ b/src/wp-includes/rest-api.php @@ -998,6 +998,9 @@ function rest_validate_value_from_schema( $value, $args, $param = '' ) { if ( ! is_array( $value ) ) { $value = preg_split( '/[\s,]+/', $value ); } + if ( ! wp_is_numeric_array( $value ) ) { + return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: type name */ __( '%1$s is not of type %2$s.' ), $param, 'array' ) ); + } foreach ( $value as $index => $v ) { $is_valid = rest_validate_value_from_schema( $v, $args['items'], $param . '[' . $index . ']' ); if ( is_wp_error( $is_valid ) ) { @@ -1107,6 +1110,9 @@ function rest_sanitize_value_from_schema( $value, $args ) { foreach ( $value as $index => $v ) { $value[ $index ] = rest_sanitize_value_from_schema( $v, $args['items'] ); } + // Normalize to numeric array so nothing unexpected + // is in the keys. + $value = array_values( $value ); return $value; } if ( 'integer' === $args['type'] ) { @@ -1140,5 +1146,9 @@ function rest_sanitize_value_from_schema( $value, $args ) { } } + if ( 'string' === $args['type'] ) { + return strval( $value ); + } + return $value; } 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 e48a307a51..c4200a68d2 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 @@ -84,7 +84,7 @@ abstract class WP_REST_Meta_Fields { $response[ $name ] = $value; } - return (object) $response; + return $response; } /** @@ -133,10 +133,24 @@ abstract class WP_REST_Meta_Fields { */ if ( is_null( $request[ $name ] ) ) { $result = $this->delete_meta_value( $object_id, $name ); - } elseif ( $args['single'] ) { - $result = $this->update_meta_value( $object_id, $name, $request[ $name ] ); + if ( is_wp_error( $result ) ) { + return $result; + } + continue; + } + + $is_valid = rest_validate_value_from_schema( $request[ $name ], $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( $request[ $name ], $args['schema'] ); + + if ( $args['single'] ) { + $result = $this->update_meta_value( $object_id, $name, $value ); } else { - $result = $this->update_multi_meta_value( $object_id, $name, $request[ $name ] ); + $result = $this->update_multi_meta_value( $object_id, $name, $value ); } if ( is_wp_error( $result ) ) { @@ -319,12 +333,13 @@ abstract class WP_REST_Meta_Fields { $default_args = array( 'name' => $name, 'single' => $args['single'], + 'type' => ! empty( $args['type'] ) ? $args['type'] : null, 'schema' => array(), 'prepare_callback' => array( $this, 'prepare_value' ), ); $default_schema = array( - 'type' => null, + 'type' => $default_args['type'], 'description' => empty( $args['description'] ) ? '' : $args['description'], 'default' => isset( $args['default'] ) ? $args['default'] : null, ); @@ -332,20 +347,18 @@ abstract class WP_REST_Meta_Fields { $rest_args = array_merge( $default_args, $rest_args ); $rest_args['schema'] = array_merge( $default_schema, $rest_args['schema'] ); - if ( empty( $rest_args['schema']['type'] ) ) { - // Skip over meta fields that don't have a defined type. - if ( empty( $args['type'] ) ) { - continue; - } + $type = ! empty( $rest_args['type'] ) ? $rest_args['type'] : null; + $type = ! empty( $rest_args['schema']['type'] ) ? $rest_args['schema']['type'] : $type; - if ( $rest_args['single'] ) { - $rest_args['schema']['type'] = $args['type']; - } else { - $rest_args['schema']['type'] = 'array'; - $rest_args['schema']['items'] = array( - 'type' => $args['type'], - ); - } + if ( ! in_array( $type, array( 'string', 'boolean', 'integer', 'number' ) ) ) { + continue; + } + + if ( empty( $rest_args['single'] ) ) { + $rest_args['schema']['items'] = array( + 'type' => $rest_args['type'], + ); + $rest_args['schema']['type'] = 'array'; } $registered[ $rest_args['name'] ] = $rest_args; 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 79f907c7ab..98a95e9783 100644 --- a/tests/phpunit/tests/rest-api/rest-post-meta-fields.php +++ b/tests/phpunit/tests/rest-api/rest-post-meta-fields.php @@ -26,24 +26,29 @@ class WP_Test_REST_Post_Meta_Fields extends WP_Test_REST_TestCase { register_meta( 'post', 'test_single', array( 'show_in_rest' => true, 'single' => true, + 'type' => 'string', )); register_meta( 'post', 'test_multi', array( 'show_in_rest' => true, 'single' => false, + 'type' => 'string', )); register_meta( 'post', 'test_bad_auth', array( 'show_in_rest' => true, 'single' => true, 'auth_callback' => '__return_false', + 'type' => 'string', )); register_meta( 'post', 'test_bad_auth_multi', array( 'show_in_rest' => true, 'single' => false, 'auth_callback' => '__return_false', + 'type' => 'string', )); register_meta( 'post', 'test_no_rest', array() ); register_meta( 'post', 'test_rest_disabled', array( 'show_in_rest' => false, + 'type' => 'string', )); register_meta( 'post', 'test_custom_schema', array( 'single' => true, @@ -54,9 +59,23 @@ class WP_Test_REST_Post_Meta_Fields extends WP_Test_REST_TestCase { ), ), )); + register_meta( 'post', 'test_custom_schema_multi', array( + 'single' => false, + 'type' => 'integer', + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'number', + ), + ), + )); register_meta( 'post', 'test_invalid_type', array( 'single' => true, - 'type' => false, + 'type' => 'lalala', + 'show_in_rest' => true, + )); + register_meta( 'post', 'test_no_type', array( + 'single' => true, + 'type' => null, 'show_in_rest' => true, )); @@ -341,6 +360,24 @@ class WP_Test_REST_Post_Meta_Fields extends WP_Test_REST_TestCase { $wpdb->show_errors = true; } + public function test_set_value_invalid_type() { + $values = get_post_meta( self::$post_id, 'test_invalid_type', false ); + $this->assertEmpty( $values ); + + $this->grant_write_permission(); + + $data = array( + 'meta' => array( + 'test_invalid_type' => 'test_value', + ), + ); + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( $data ); + + $response = $this->server->dispatch( $request ); + $this->assertEmpty( get_post_meta( self::$post_id, 'test_invalid_type', false ) ); + } + public function test_set_value_multiple() { // Ensure no data exists currently. $values = get_post_meta( self::$post_id, 'test_multi', false ); @@ -435,6 +472,92 @@ class WP_Test_REST_Post_Meta_Fields extends WP_Test_REST_TestCase { $this->assertEmpty( $meta ); } + public function test_set_value_invalid_value() { + register_meta( 'post', 'my_meta_key', array( + 'show_in_rest' => true, + 'single' => true, + 'type' => 'string', + )); + + $this->grant_write_permission(); + + $data = array( + 'meta' => array( + 'my_meta_key' => array( 'c', 'n' ), + ), + ); + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( $data ); + + $response = $this->server->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + } + + public function test_set_value_invalid_value_multiple() { + register_meta( 'post', 'my_meta_key', array( + 'show_in_rest' => true, + 'single' => false, + 'type' => 'string', + )); + + $this->grant_write_permission(); + + $data = array( + 'meta' => array( + 'my_meta_key' => array( array( 'a' ) ), + ), + ); + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( $data ); + + $response = $this->server->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + } + + public function test_set_value_sanitized() { + register_meta( 'post', 'my_meta_key', array( + 'show_in_rest' => true, + 'single' => true, + 'type' => 'integer', + )); + + $this->grant_write_permission(); + + $data = array( + 'meta' => array( + 'my_meta_key' => '1', // Set to a string. + ), + ); + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( $data ); + + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( 1, $data['meta']['my_meta_key'] ); + } + + public function test_set_value_csv() { + register_meta( 'post', 'my_meta_key', array( + 'show_in_rest' => true, + 'single' => false, + 'type' => 'integer', + )); + + $this->grant_write_permission(); + + $data = array( + 'meta' => array( + 'my_meta_key' => '1,2,3', // Set to a string. + ), + ); + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( $data ); + + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( array( 1, 2, 3 ), $data['meta']['my_meta_key'] ); + } + /** * @depends test_set_value_multiple */ @@ -485,6 +608,79 @@ class WP_Test_REST_Post_Meta_Fields extends WP_Test_REST_TestCase { $this->assertErrorResponse( 'rest_meta_database_error', $response, 500 ); } + /** + * @depends test_get_value + */ + public function test_set_value_single_custom_schema() { + // Ensure no data exists currently. + $values = get_post_meta( self::$post_id, 'test_custom_schema', false ); + $this->assertEmpty( $values ); + + $this->grant_write_permission(); + + $data = array( + 'meta' => array( + 'test_custom_schema' => 3, + ), + ); + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( $data ); + + $response = $this->server->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + + $meta = get_post_meta( self::$post_id, 'test_custom_schema', false ); + $this->assertNotEmpty( $meta ); + $this->assertCount( 1, $meta ); + $this->assertEquals( 3, $meta[0] ); + + $data = $response->get_data(); + $meta = (array) $data['meta']; + $this->assertArrayHasKey( 'test_custom_schema', $meta ); + $this->assertEquals( 3, $meta['test_custom_schema'] ); + } + + public function test_set_value_multiple_custom_schema() { + // Ensure no data exists currently. + $values = get_post_meta( self::$post_id, 'test_custom_schema_multi', false ); + $this->assertEmpty( $values ); + + $this->grant_write_permission(); + + $data = array( + 'meta' => array( + 'test_custom_schema_multi' => array( 2 ), + ), + ); + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( $data ); + + $response = $this->server->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + + $meta = get_post_meta( self::$post_id, 'test_custom_schema_multi', false ); + $this->assertNotEmpty( $meta ); + $this->assertCount( 1, $meta ); + $this->assertEquals( 2, $meta[0] ); + + // Add another value. + $data = array( + 'meta' => array( + 'test_custom_schema_multi' => array( 2, 8 ), + ), + ); + $request->set_body_params( $data ); + + $response = $this->server->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + + $meta = get_post_meta( self::$post_id, 'test_custom_schema_multi', false ); + $this->assertNotEmpty( $meta ); + $this->assertCount( 2, $meta ); + $this->assertContains( 2, $meta ); + $this->assertContains( 8, $meta ); + } + public function test_remove_multi_value_db_error() { add_post_meta( self::$post_id, 'test_multi', 'val1' ); $values = get_post_meta( self::$post_id, 'test_multi', false ); @@ -515,6 +711,7 @@ class WP_Test_REST_Post_Meta_Fields extends WP_Test_REST_TestCase { $this->assertErrorResponse( 'rest_meta_database_error', $response, 500 ); } + public function test_delete_value() { add_post_meta( self::$post_id, 'test_single', 'val1' ); $current = get_post_meta( self::$post_id, 'test_single', true ); @@ -618,6 +815,7 @@ class WP_Test_REST_Post_Meta_Fields extends WP_Test_REST_TestCase { $this->assertArrayNotHasKey( 'test_no_rest', $meta_schema ); $this->assertArrayNotHasKey( 'test_rest_disabled', $meta_schema ); $this->assertArrayNotHasKey( 'test_invalid_type', $meta_schema ); + $this->assertArrayNotHasKey( 'test_no_type', $meta_schema ); } /** diff --git a/tests/phpunit/tests/rest-api/rest-schema-sanitization.php b/tests/phpunit/tests/rest-api/rest-schema-sanitization.php index 77b60d615f..4c523df1e3 100644 --- a/tests/phpunit/tests/rest-api/rest-schema-sanitization.php +++ b/tests/phpunit/tests/rest-api/rest-schema-sanitization.php @@ -76,6 +76,20 @@ class WP_Test_REST_Schema_Sanitization extends WP_UnitTestCase { $this->assertEquals( array( 1 ), rest_sanitize_value_from_schema( array( '1' ), $schema ) ); } + public function test_type_array_nested() { + $schema = array( + 'type' => 'array', + 'items' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'number', + ), + ), + ); + $this->assertEquals( array( array( 1 ), array( 2 ) ), rest_sanitize_value_from_schema( array( array( 1 ), array( 2 ) ), $schema ) ); + $this->assertEquals( array( array( 1 ), array( 2 ) ), rest_sanitize_value_from_schema( array( array( '1' ), array( '2' ) ), $schema ) ); + } + public function test_type_array_as_csv() { $schema = array( 'type' => 'array', @@ -110,4 +124,32 @@ class WP_Test_REST_Schema_Sanitization extends WP_UnitTestCase { $this->assertEquals( array( 'ribs', 'chicken' ), rest_sanitize_value_from_schema( 'ribs,chicken', $schema ) ); $this->assertEquals( array( 'chicken', 'coleslaw' ), rest_sanitize_value_from_schema( 'chicken,coleslaw', $schema ) ); } + + public function test_type_array_is_associative() { + $schema = array( + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + ); + $this->assertEquals( array( '1', '2' ), rest_sanitize_value_from_schema( array( 'first' => '1', 'second' => '2' ), $schema ) ); + } + + public function test_type_unknown() { + $schema = array( + 'type' => 'lalala', + ); + $this->assertEquals( 'Best lyrics', rest_sanitize_value_from_schema( 'Best lyrics', $schema ) ); + $this->assertEquals( 1.10, rest_sanitize_value_from_schema( 1.10, $schema ) ); + $this->assertEquals( 1, rest_sanitize_value_from_schema( 1, $schema ) ); + } + + public function test_no_type() { + $schema = array( + 'type' => null, + ); + $this->assertEquals( 'Nothing', rest_sanitize_value_from_schema( 'Nothing', $schema ) ); + $this->assertEquals( 1.10, rest_sanitize_value_from_schema( 1.10, $schema ) ); + $this->assertEquals( 1, rest_sanitize_value_from_schema( 1, $schema ) ); + } } diff --git a/tests/phpunit/tests/rest-api/rest-schema-validation.php b/tests/phpunit/tests/rest-api/rest-schema-validation.php index fa3f327b13..4e4a111018 100644 --- a/tests/phpunit/tests/rest-api/rest-schema-validation.php +++ b/tests/phpunit/tests/rest-api/rest-schema-validation.php @@ -104,6 +104,19 @@ class WP_Test_REST_Schema_Validation extends WP_UnitTestCase { $this->assertWPError( rest_validate_value_from_schema( array( true ), $schema ) ); } + public function test_type_array_nested() { + $schema = array( + 'type' => 'array', + 'items' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'number', + ), + ), + ); + $this->assertTrue( rest_validate_value_from_schema( array( array( 1 ), array( 2 ) ), $schema ) ); + } + public function test_type_array_as_csv() { $schema = array( 'type' => 'array', @@ -139,4 +152,23 @@ class WP_Test_REST_Schema_Validation extends WP_UnitTestCase { $this->assertTrue( rest_validate_value_from_schema( 'ribs,chicken', $schema ) ); $this->assertWPError( rest_validate_value_from_schema( 'chicken,coleslaw', $schema ) ); } + + public function test_type_array_is_associative() { + $schema = array( + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + ); + $this->assertWPError( rest_validate_value_from_schema( array( 'first' => '1', 'second' => '2' ), $schema ) ); + } + + public function test_type_unknown() { + $schema = array( + 'type' => 'lalala', + ); + $this->assertTrue( rest_validate_value_from_schema( 'Best lyrics', $schema ) ); + $this->assertTrue( rest_validate_value_from_schema( 1, $schema ) ); + $this->assertTrue( rest_validate_value_from_schema( array(), $schema ) ); + } }