From 6b533ba2b0925bede798d5a1612ef270bc4d00fb Mon Sep 17 00:00:00 2001 From: Joe Hoyle Date: Wed, 4 Oct 2017 08:26:44 +0000 Subject: [PATCH] REST API: Support for objects in schema validation and sanitization. When registering routes developers can now define their complex objects in the schema and benefit from the automatic validation and sanitization in the REST API. This also paves the way for support for complex object registration via register_meta and register_setting. See #38583. Props TimothyBlynJacobs5. git-svn-id: https://develop.svn.wordpress.org/trunk@41727 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/rest-api.php | 42 ++++++++++ .../class-wp-rest-attachments-controller.php | 2 + .../class-wp-rest-comments-controller.php | 1 + .../class-wp-rest-posts-controller.php | 3 + .../rest-api/rest-schema-sanitization.php | 78 +++++++++++++++++++ .../tests/rest-api/rest-schema-validation.php | 53 +++++++++++++ 6 files changed, 179 insertions(+) diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php index 06bb9f9f80..34e1c591f6 100644 --- a/src/wp-includes/rest-api.php +++ b/src/wp-includes/rest-api.php @@ -1051,6 +1051,28 @@ function rest_validate_value_from_schema( $value, $args, $param = '' ) { } } } + + if ( 'object' === $args['type'] ) { + if ( $value instanceof stdClass ) { + $value = (array) $value; + } + 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' ) ); + } + + foreach ( $value as $property => $v ) { + if ( ! isset( $args['properties'][ $property ] ) ) { + continue; + } + $is_valid = rest_validate_value_from_schema( $v, $args['properties'][ $property ], $param . '[' . $property . ']' ); + + if ( is_wp_error( $is_valid ) ) { + return $is_valid; + } + } + } + if ( ! empty( $args['enum'] ) ) { if ( ! in_array( $value, $args['enum'], true ) ) { /* translators: 1: parameter, 2: list of valid values */ @@ -1170,6 +1192,26 @@ function rest_sanitize_value_from_schema( $value, $args ) { $value = array_values( $value ); return $value; } + + if ( 'object' === $args['type'] ) { + if ( $value instanceof stdClass ) { + $value = (array) $value; + } + if ( ! is_array( $value ) ) { + return array(); + } + + foreach ( $value as $property => $v ) { + if ( ! isset( $args['properties'][ $property ] ) ) { + unset( $value[ $property ] ); + continue; + } + $value[ $property ] = rest_sanitize_value_from_schema( $v, $args['properties'][ $property ] ); + } + + return $value; + } + if ( 'integer' === $args['type'] ) { return (int) $value; } diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php index 76802fea76..ed101815a1 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php @@ -391,6 +391,7 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller { 'context' => array( 'view', 'edit', 'embed' ), 'arg_options' => array( 'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database() + 'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database() ), 'properties' => array( 'raw' => array( @@ -413,6 +414,7 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller { 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database() + 'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database() ), 'properties' => array( 'raw' => array( diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php index 810a3fb867..415ba8e9bc 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php @@ -1200,6 +1200,7 @@ class WP_REST_Comments_Controller extends WP_REST_Controller { 'context' => array( 'view', 'edit', 'embed' ), 'arg_options' => array( 'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database() + 'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database() ), 'properties' => array( 'raw' => array( diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php index 018b66ec2d..80027cfea8 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php @@ -1846,6 +1846,7 @@ class WP_REST_Posts_Controller extends WP_REST_Controller { 'context' => array( 'view', 'edit', 'embed' ), 'arg_options' => array( 'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database() + 'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database() ), 'properties' => array( 'raw' => array( @@ -1870,6 +1871,7 @@ class WP_REST_Posts_Controller extends WP_REST_Controller { 'context' => array( 'view', 'edit' ), 'arg_options' => array( 'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database() + 'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database() ), 'properties' => array( 'raw' => array( @@ -1908,6 +1910,7 @@ class WP_REST_Posts_Controller extends WP_REST_Controller { 'context' => array( 'view', 'edit', 'embed' ), 'arg_options' => array( 'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database() + 'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database() ), 'properties' => array( 'raw' => array( diff --git a/tests/phpunit/tests/rest-api/rest-schema-sanitization.php b/tests/phpunit/tests/rest-api/rest-schema-sanitization.php index e1ae29d3fa..cb224b349b 100644 --- a/tests/phpunit/tests/rest-api/rest-schema-sanitization.php +++ b/tests/phpunit/tests/rest-api/rest-schema-sanitization.php @@ -146,6 +146,84 @@ class WP_Test_REST_Schema_Sanitization extends WP_UnitTestCase { $this->assertEquals( array( '1', '2' ), rest_sanitize_value_from_schema( array( 'first' => '1', 'second' => '2' ), $schema ) ); } + public function test_type_object() { + $schema = array( + 'type' => 'object', + 'properties' => array( + 'a' => array( + 'type' => 'number' + ), + ), + ); + $this->assertEquals( array( 'a' => 1 ), rest_sanitize_value_from_schema( array( 'a' => 1 ), $schema ) ); + $this->assertEquals( array( 'a' => 1 ), rest_sanitize_value_from_schema( array( 'a' => '1' ), $schema ) ); + } + + public function test_type_object_nested() { + $schema = array( + 'type' => 'object', + 'properties' => array( + 'a' => array( + 'type' => 'object', + 'properties' => array( + 'b' => array( 'type' => 'number' ), + 'c' => array( 'type' => 'number' ), + ) + ) + ), + ); + + $this->assertEquals( + array( + 'a' => array( + 'b' => 1, + 'c' => 3, + ), + ), + rest_sanitize_value_from_schema( + array( + 'a' => array( + 'b' => '1', + 'c' => '3', + ), + ), + $schema + ) + ); + $this->assertEquals( + array( + 'a' => array( + 'b' => 1, + 'c' => 3, + ), + ), + rest_sanitize_value_from_schema( + array( + 'a' => array( + 'b' => '1', + 'c' => '3', + 'd' => '1', + ), + 'b' => 1, + ), + $schema + ) + ); + $this->assertEquals( array( 'a' => array() ), rest_sanitize_value_from_schema( array( 'a' => null ), $schema ) ); + } + + public function test_type_object_stdclass() { + $schema = array( + 'type' => 'object', + 'properties' => array( + 'a' => array( + 'type' => 'number' + ), + ), + ); + $this->assertEquals( array( 'a' => 1 ), rest_sanitize_value_from_schema( (object) array( 'a' => '1' ), $schema ) ); + } + public function test_type_unknown() { $schema = array( 'type' => 'lalala', diff --git a/tests/phpunit/tests/rest-api/rest-schema-validation.php b/tests/phpunit/tests/rest-api/rest-schema-validation.php index 82f58e58d8..9932538420 100644 --- a/tests/phpunit/tests/rest-api/rest-schema-validation.php +++ b/tests/phpunit/tests/rest-api/rest-schema-validation.php @@ -181,6 +181,59 @@ class WP_Test_REST_Schema_Validation extends WP_UnitTestCase { $this->assertWPError( rest_validate_value_from_schema( array( 'first' => '1', 'second' => '2' ), $schema ) ); } + public function test_type_object() { + $schema = array( + 'type' => 'object', + 'properties' => array( + 'a' => array( + 'type' => 'number' + ), + ), + ); + $this->assertTrue( rest_validate_value_from_schema( array( 'a' => 1 ), $schema ) ); + $this->assertWPError( rest_validate_value_from_schema( array( 'a' => 'invalid' ), $schema ) ); + } + + public function test_type_object_nested() { + $schema = array( + 'type' => 'object', + 'properties' => array( + 'a' => array( + 'type' => 'object', + 'properties' => array( + 'b' => array( 'type' => 'number' ), + 'c' => array( 'type' => 'number' ), + ) + ) + ), + ); + $this->assertTrue( + rest_validate_value_from_schema( + array( + 'a' => array( + 'b' => '1', + 'c' => 3, + ), + ), + $schema + ) + ); + $this->assertWPError( rest_validate_value_from_schema( array( 'a' => array( 'b' => 1, 'c' => 'invalid' ) ), $schema ) ); + $this->assertWPError( rest_validate_value_from_schema( array( 'a' => 1 ), $schema ) ); + } + + public function test_type_object_stdclass() { + $schema = array( + 'type' => 'object', + 'properties' => array( + 'a' => array( + 'type' => 'number' + ), + ), + ); + $this->assertTrue( rest_validate_value_from_schema( (object) array( 'a' => 1 ), $schema ) ); + } + public function test_type_unknown() { $schema = array( 'type' => 'lalala',