From 58169b04fadd2b6ef53a28ddd4f67a7d8483b7b2 Mon Sep 17 00:00:00 2001 From: Joe Hoyle Date: Mon, 14 Nov 2016 16:35:35 +0000 Subject: [PATCH] REST API: Validate and Sanitize registered meta based off the schema. With the addition of Array support in our schema validation functions, it's now possible to use these in the meta validation and sanitization steps. Also, this increases the test coverage of using registered via meta the API significantly. Fixes #38531. Props rachelbaker, tharsheblows. git-svn-id: https://develop.svn.wordpress.org/trunk@39222 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/rest-api.php | 10 + .../fields/class-wp-rest-meta-fields.php | 49 +++-- .../tests/rest-api/rest-post-meta-fields.php | 200 +++++++++++++++++- .../rest-api/rest-schema-sanitization.php | 42 ++++ .../tests/rest-api/rest-schema-validation.php | 32 +++ 5 files changed, 314 insertions(+), 19 deletions(-) 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 ) ); + } }