diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php index ebdcb60a79..5fdf9530cb 100644 --- a/src/wp-includes/rest-api.php +++ b/src/wp-includes/rest-api.php @@ -985,6 +985,50 @@ function rest_cookie_collect_status() { $wp_rest_auth_cookie = true; } +/** + * Retrieves the avatar urls in various sizes. + * + * @since 4.7.0 + * + * @see get_avatar_url() + * + * @param mixed $id_or_email The Gravatar to retrieve a URL for. Accepts a user_id, gravatar md5 hash, + * user email, WP_User object, WP_Post object, or WP_Comment object. + * @return array Avatar URLs keyed by size. Each value can be a URL string or boolean false. + */ +function rest_get_avatar_urls( $id_or_email ) { + $avatar_sizes = rest_get_avatar_sizes(); + + $urls = array(); + foreach ( $avatar_sizes as $size ) { + $urls[ $size ] = get_avatar_url( $id_or_email, array( 'size' => $size ) ); + } + + return $urls; +} + +/** + * Retrieves the pixel sizes for avatars. + * + * @since 4.7.0 + * + * @return int[] List of pixel sizes for avatars. Default `[ 24, 48, 96 ]`. + */ +function rest_get_avatar_sizes() { + /** + * Filters the REST avatar sizes. + * + * Use this filter to adjust the array of sizes returned by the + * `rest_get_avatar_sizes` function. + * + * @since 4.4.0 + * + * @param int[] $sizes An array of int values that are the pixel sizes for avatars. + * Default `[ 24, 48, 96 ]`. + */ + return apply_filters( 'rest_avatar_sizes', array( 24, 48, 96 ) ); +} + /** * Parses an RFC3339 time into a Unix timestamp. * @@ -1116,7 +1160,7 @@ function rest_sanitize_request_arg( $value, $request, $param ) { } $args = $attributes['args'][ $param ]; - return rest_sanitize_value_from_schema( $value, $args ); + return rest_sanitize_value_from_schema( $value, $args, $param ); } /** @@ -1219,47 +1263,179 @@ function rest_is_boolean( $maybe_bool ) { } /** - * Retrieves the avatar urls in various sizes. + * Determines if a given value is integer-like. * - * @since 4.7.0 + * @since 5.5.0 * - * @see get_avatar_url() - * - * @param mixed $id_or_email The Gravatar to retrieve a URL for. Accepts a user_id, gravatar md5 hash, - * user email, WP_User object, WP_Post object, or WP_Comment object. - * @return array Avatar URLs keyed by size. Each value can be a URL string or boolean false. + * @param mixed $maybe_integer The value being evaluated. + * @return bool True if an integer, otherwise false. */ -function rest_get_avatar_urls( $id_or_email ) { - $avatar_sizes = rest_get_avatar_sizes(); - - $urls = array(); - foreach ( $avatar_sizes as $size ) { - $urls[ $size ] = get_avatar_url( $id_or_email, array( 'size' => $size ) ); - } - - return $urls; +function rest_is_integer( $maybe_integer ) { + return round( floatval( $maybe_integer ) ) === floatval( $maybe_integer ); } /** - * Retrieves the pixel sizes for avatars. + * Determines if a given value is array-like. * - * @since 4.7.0 + * @since 5.5.0 * - * @return int[] List of pixel sizes for avatars. Default `[ 24, 48, 96 ]`. + * @param mixed $maybe_array The value being evaluated. + * @return bool */ -function rest_get_avatar_sizes() { - /** - * Filters the REST avatar sizes. - * - * Use this filter to adjust the array of sizes returned by the - * `rest_get_avatar_sizes` function. - * - * @since 4.4.0 - * - * @param int[] $sizes An array of int values that are the pixel sizes for avatars. - * Default `[ 24, 48, 96 ]`. - */ - return apply_filters( 'rest_avatar_sizes', array( 24, 48, 96 ) ); +function rest_is_array( $maybe_array ) { + if ( is_scalar( $maybe_array ) ) { + $maybe_array = wp_parse_list( $maybe_array ); + } + + return wp_is_numeric_array( $maybe_array ); +} + +/** + * Converts an array-like value to an array. + * + * @since 5.5.0 + * + * @param mixed $maybe_array The value being evaluated. + * @return array Returns the array extracted from the value. + */ +function rest_sanitize_array( $maybe_array ) { + if ( is_scalar( $maybe_array ) ) { + return wp_parse_list( $maybe_array ); + } + + if ( ! is_array( $maybe_array ) ) { + return array(); + } + + // Normalize to numeric array so nothing unexpected is in the keys. + return array_values( $maybe_array ); +} + +/** + * Determines if a given value is object-like. + * + * @since 5.5.0 + * + * @param mixed $maybe_object The value being evaluated. + * @return bool True if object like, otherwise false. + */ +function rest_is_object( $maybe_object ) { + if ( '' === $maybe_object ) { + return true; + } + + if ( $maybe_object instanceof stdClass ) { + return true; + } + + if ( $maybe_object instanceof JsonSerializable ) { + $maybe_object = $maybe_object->jsonSerialize(); + } + + return is_array( $maybe_object ); +} + +/** + * Converts an object-like value to an object. + * + * @since 5.5.0 + * + * @param mixed $maybe_object The value being evaluated. + * @return array Returns the object extracted from the value. + */ +function rest_sanitize_object( $maybe_object ) { + if ( '' === $maybe_object ) { + return array(); + } + + if ( $maybe_object instanceof stdClass ) { + return (array) $maybe_object; + } + + if ( $maybe_object instanceof JsonSerializable ) { + $maybe_object = $maybe_object->jsonSerialize(); + } + + if ( ! is_array( $maybe_object ) ) { + return array(); + } + + return $maybe_object; +} + +/** + * Gets the best type for a value. + * + * @since 5.5.0 + * + * @param mixed $value The value to check. + * @param array $types The list of possible types. + * @return string The best matching type, an empty string if no types match. + */ +function rest_get_best_type_for_value( $value, $types ) { + static $checks = array( + 'array' => 'rest_is_array', + 'object' => 'rest_is_object', + 'integer' => 'rest_is_integer', + 'number' => 'is_numeric', + 'boolean' => 'rest_is_boolean', + 'string' => 'is_string', + 'null' => 'is_null', + ); + + // Both arrays and objects allow empty strings to be converted to their types. + // But the best answer for this type is a string. + if ( '' === $value && in_array( 'string', $types, true ) ) { + return 'string'; + } + + foreach ( $types as $type ) { + if ( isset( $checks[ $type ] ) && $checks[ $type ]( $value ) ) { + return $type; + } + } + + return ''; +} + +/** + * Handles getting the best type for a multi-type schema. + * + * This is a wrapper for {@see rest_get_best_type_for_value()} that handles + * backward compatibility for schemas that use invalid types. + * + * @since 5.5.0 + * + * @param mixed $value The value to check. + * @param array $args The schema array to use. + * @param string $param The parameter name, used in error messages. + * @return string + */ +function rest_handle_multi_type_schema( $value, $args, $param = '' ) { + $allowed_types = array( 'array', 'object', 'string', 'number', 'integer', 'boolean', 'null' ); + $invalid_types = array_diff( $args['type'], $allowed_types ); + + if ( $invalid_types ) { + _doing_it_wrong( + __FUNCTION__, + /* translators: 1. Parameter. 2. List of allowed types. */ + wp_sprintf( __( 'The "type" schema keyword for %1$s can only contain the built-in types: %2$l.' ), $param, $allowed_types ), + '5.5.0' + ); + } + + $best_type = rest_get_best_type_for_value( $value, $args['type'] ); + + if ( ! $best_type ) { + if ( ! $invalid_types ) { + return ''; + } + + // Backward compatibility for previous behavior which allowed the value if there was an invalid type used. + $best_type = reset( $invalid_types ); + } + + return $best_type; } /** @@ -1284,42 +1460,38 @@ function rest_validate_value_from_schema( $value, $args, $param = '' ) { $allowed_types = array( 'array', 'object', 'string', 'number', 'integer', 'boolean', 'null' ); if ( ! isset( $args['type'] ) ) { - _doing_it_wrong( __FUNCTION__, __( 'The "type" schema keyword is required.' ), '5.5.0' ); + /* translators: 1. Parameter */ + _doing_it_wrong( __FUNCTION__, sprintf( __( 'The "type" schema keyword for %s is required.' ), $param ), '5.5.0' ); } if ( is_array( $args['type'] ) ) { - foreach ( $args['type'] as $type ) { - $type_args = $args; - $type_args['type'] = $type; + $best_type = rest_handle_multi_type_schema( $value, $args, $param ); - if ( true === rest_validate_value_from_schema( $value, $type_args, $param ) ) { - return true; - } + if ( ! $best_type ) { + /* translators: 1: Parameter, 2: List of types. */ + return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, implode( ',', $args['type'] ) ) ); } - /* translators: 1: Parameter, 2: List of types. */ - return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, implode( ',', $args['type'] ) ) ); + $args['type'] = $best_type; } if ( ! in_array( $args['type'], $allowed_types, true ) ) { _doing_it_wrong( __FUNCTION__, - /* translators: 1. The list of allowed types. */ - wp_sprintf( __( 'The "type" schema keyword can only be on of the built-in types: %l.' ), $allowed_types ), + /* translators: 1. Parameter 2. The list of allowed types. */ + wp_sprintf( __( 'The "type" schema keyword for %1$s can only be on of the built-in types: %2$l.' ), $param, $allowed_types ), '5.5.0' ); } if ( 'array' === $args['type'] ) { - if ( ! is_null( $value ) ) { - $value = wp_parse_list( $value ); - } - - if ( ! wp_is_numeric_array( $value ) ) { + if ( ! rest_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, 'array' ) ); } + $value = rest_sanitize_array( $value ); + foreach ( $value as $index => $v ) { $is_valid = rest_validate_value_from_schema( $v, $args['items'], $param . '[' . $index . ']' ); if ( is_wp_error( $is_valid ) ) { @@ -1339,23 +1511,13 @@ function rest_validate_value_from_schema( $value, $args, $param = '' ) { } if ( 'object' === $args['type'] ) { - if ( '' === $value ) { - $value = array(); - } - - if ( $value instanceof stdClass ) { - $value = (array) $value; - } - - if ( $value instanceof JsonSerializable ) { - $value = $value->jsonSerialize(); - } - - if ( ! is_array( $value ) ) { + if ( ! rest_is_object( $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' ) ); } + $value = rest_sanitize_object( $value ); + if ( isset( $args['required'] ) && is_array( $args['required'] ) ) { // schema version 4 foreach ( $args['required'] as $name ) { if ( ! array_key_exists( $name, $value ) ) { @@ -1415,7 +1577,7 @@ function rest_validate_value_from_schema( $value, $args, $param = '' ) { return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, $args['type'] ) ); } - if ( 'integer' === $args['type'] && round( floatval( $value ) ) !== floatval( $value ) ) { + if ( 'integer' === $args['type'] && ! rest_is_integer( $value ) ) { /* translators: 1: Parameter, 2: Type name. */ return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'integer' ) ); } @@ -1551,85 +1713,65 @@ function rest_validate_value_from_schema( $value, $args, $param = '' ) { * Sanitize a value based on a schema. * * @since 4.7.0 + * @since 5.5.0 Added the `$param` parameter. * - * @param mixed $value The value to sanitize. - * @param array $args Schema array to use for sanitization. + * @param mixed $value The value to sanitize. + * @param array $args Schema array to use for sanitization. + * @param string $param The parameter name, used in error messages. * @return true|WP_Error */ -function rest_sanitize_value_from_schema( $value, $args ) { +function rest_sanitize_value_from_schema( $value, $args, $param = '' ) { $allowed_types = array( 'array', 'object', 'string', 'number', 'integer', 'boolean', 'null' ); if ( ! isset( $args['type'] ) ) { - _doing_it_wrong( __FUNCTION__, __( 'The "type" schema keyword is required.' ), '5.5.0' ); + /* translators: 1. Parameter */ + _doing_it_wrong( __FUNCTION__, sprintf( __( 'The "type" schema keyword for %s is required.' ), $param ), '5.5.0' ); } if ( is_array( $args['type'] ) ) { - // Determine which type the value was validated against, - // and use that type when performing sanitization. - $validated_type = ''; + $best_type = rest_handle_multi_type_schema( $value, $args, $param ); - foreach ( $args['type'] as $type ) { - $type_args = $args; - $type_args['type'] = $type; - - if ( ! is_wp_error( rest_validate_value_from_schema( $value, $type_args ) ) ) { - $validated_type = $type; - break; - } - } - - if ( ! $validated_type ) { + if ( ! $best_type ) { return null; } - $args['type'] = $validated_type; + $args['type'] = $best_type; } if ( ! in_array( $args['type'], $allowed_types, true ) ) { _doing_it_wrong( __FUNCTION__, - /* translators: 1. The list of allowed types. */ - wp_sprintf( __( 'The "type" schema keyword can only be on of the built-in types: %l.' ), $allowed_types ), + /* translators: 1. Parameter. 2. The list of allowed types. */ + wp_sprintf( __( 'The "type" schema keyword for %1$s can only be on of the built-in types: %2$l.' ), $param, $allowed_types ), '5.5.0' ); } if ( 'array' === $args['type'] ) { + $value = rest_sanitize_array( $value ); + if ( empty( $args['items'] ) ) { - return (array) $value; + return $value; } - $value = wp_parse_list( $value ); foreach ( $value as $index => $v ) { - $value[ $index ] = rest_sanitize_value_from_schema( $v, $args['items'] ); + $value[ $index ] = rest_sanitize_value_from_schema( $v, $args['items'], $param . '[' . $index . ']' ); } - // Normalize to numeric array so nothing unexpected is in the keys. - $value = array_values( $value ); return $value; } if ( 'object' === $args['type'] ) { - if ( $value instanceof stdClass ) { - $value = (array) $value; - } - - if ( $value instanceof JsonSerializable ) { - $value = $value->jsonSerialize(); - } - - if ( ! is_array( $value ) ) { - return array(); - } + $value = rest_sanitize_object( $value ); foreach ( $value as $property => $v ) { if ( isset( $args['properties'][ $property ] ) ) { - $value[ $property ] = rest_sanitize_value_from_schema( $v, $args['properties'][ $property ] ); + $value[ $property ] = rest_sanitize_value_from_schema( $v, $args['properties'][ $property ], $param . '[' . $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'] ); + $value[ $property ] = rest_sanitize_value_from_schema( $v, $args['additionalProperties'], $param . '[' . $property . ']' ); } } } diff --git a/tests/phpunit/tests/rest-api.php b/tests/phpunit/tests/rest-api.php index 275727a5a8..7a27471b2f 100644 --- a/tests/phpunit/tests/rest-api.php +++ b/tests/phpunit/tests/rest-api.php @@ -8,6 +8,7 @@ require_once ABSPATH . 'wp-admin/includes/admin.php'; require_once ABSPATH . WPINC . '/rest-api.php'; +require_once __DIR__ . '/../includes/class-jsonserializable-object.php'; /** * @group restapi @@ -1457,4 +1458,359 @@ class Tests_REST_API extends WP_UnitTestCase { $term = self::factory()->term->create_and_get(); $this->assertEquals( '/wp/v2/tags/' . $term->term_id, rest_get_route_for_term( $term->term_id ) ); } + + /** + * @dataProvider _dp_rest_is_object + * @ticket 50300 + * @param bool $expected Expected result of the check. + * @param mixed $value The value to check. + */ + public function test_rest_is_object( $expected, $value ) { + $is_object = rest_is_object( $value ); + + if ( $expected ) { + $this->assertTrue( $is_object ); + } else { + $this->assertFalse( $is_object ); + } + } + + public function _dp_rest_is_object() { + return array( + array( + true, + '', + ), + array( + true, + new stdClass(), + ), + array( + true, + new JsonSerializable_Object( array( 'hi' => 'there' ) ), + ), + array( + true, + array( 'hi' => 'there' ), + ), + array( + true, + array(), + ), + array( + true, + array( 'a', 'b' ), + ), + array( + false, + new Basic_Object(), + ), + array( + false, + new JsonSerializable_Object( 'str' ), + ), + array( + false, + 'str', + ), + array( + false, + 5, + ), + ); + } + + /** + * @dataProvider _dp_rest_sanitize_object + * @ticket 50300 + * @param array $expected Expected sanitized version. + * @param mixed $value The value to sanitize. + */ + public function test_rest_sanitize_object( $expected, $value ) { + $sanitized = rest_sanitize_object( $value ); + $this->assertEquals( $expected, $sanitized ); + } + + public function _dp_rest_sanitize_object() { + return array( + array( + array(), + '', + ), + array( + array( 'a' => '1' ), + (object) array( 'a' => '1' ), + ), + array( + array( 'hi' => 'there' ), + new JsonSerializable_Object( array( 'hi' => 'there' ) ), + ), + array( + array( 'hi' => 'there' ), + array( 'hi' => 'there' ), + ), + array( + array(), + array(), + ), + array( + array( 'a', 'b' ), + array( 'a', 'b' ), + ), + array( + array(), + new Basic_Object(), + ), + array( + array(), + new JsonSerializable_Object( 'str' ), + ), + array( + array(), + 'str', + ), + array( + array(), + 5, + ), + ); + } + + /** + * @dataProvider _dp_rest_is_array + * @ticket 50300 + * @param bool $expected Expected result of the check. + * @param mixed $value The value to check. + */ + public function test_rest_is_array( $expected, $value ) { + $is_array = rest_is_array( $value ); + + if ( $expected ) { + $this->assertTrue( $is_array ); + } else { + $this->assertFalse( $is_array ); + } + } + + public function _dp_rest_is_array() { + return array( + array( + true, + '', + ), + array( + true, + array( 'a', 'b' ), + ), + array( + true, + array(), + ), + array( + true, + 'a,b,c', + ), + array( + true, + 'a', + ), + array( + true, + 5, + ), + array( + false, + new stdClass(), + ), + array( + false, + new JsonSerializable_Object( array( 'hi' => 'there' ) ), + ), + array( + false, + array( 'hi' => 'there' ), + ), + array( + false, + new Basic_Object(), + ), + array( + false, + new JsonSerializable_Object( 'str' ), + ), + array( + false, + null, + ), + ); + } + + /** + * @dataProvider _dp_rest_sanitize_array + * @ticket 50300 + * @param array $expected Expected sanitized version. + * @param mixed $value The value to sanitize. + */ + public function test_rest_sanitize_array( $expected, $value ) { + $sanitized = rest_sanitize_array( $value ); + $this->assertEquals( $expected, $sanitized ); + } + + public function _dp_rest_sanitize_array() { + return array( + array( + array(), + '', + ), + array( + array( 'a', 'b' ), + array( 'a', 'b' ), + ), + array( + array(), + array(), + ), + array( + array( 'a', 'b', 'c' ), + 'a,b,c', + ), + array( + array( 'a' ), + 'a', + ), + array( + array( 'a', 'b' ), + 'a,b,', + ), + array( + array( '5' ), + 5, + ), + array( + array(), + new stdClass(), + ), + array( + array(), + new JsonSerializable_Object( array( 'hi' => 'there' ) ), + ), + array( + array( 'there' ), + array( 'hi' => 'there' ), + ), + array( + array(), + new Basic_Object(), + ), + array( + array(), + new JsonSerializable_Object( 'str' ), + ), + array( + array(), + null, + ), + ); + } + + /** + * @dataProvider _dp_get_best_type_for_value + * @ticket 50300 + * @param string $expected The expected best type. + * @param mixed $value The value to test. + * @param array $types The list of available types. + */ + public function test_get_best_type_for_value( $expected, $value, $types ) { + $this->assertEquals( $expected, rest_get_best_type_for_value( $value, $types ) ); + } + + public function _dp_get_best_type_for_value() { + return array( + array( + 'array', + array( 'hi' ), + array( 'array' ), + ), + array( + 'object', + array( 'hi' => 'there' ), + array( 'object' ), + ), + array( + 'integer', + 5, + array( 'integer' ), + ), + array( + 'number', + 4.0, + array( 'number' ), + ), + array( + 'boolean', + true, + array( 'boolean' ), + ), + array( + 'string', + 'str', + array( 'string' ), + ), + array( + 'null', + null, + array( 'null' ), + ), + array( + 'string', + '', + array( 'array', 'string' ), + ), + array( + 'string', + '', + array( 'object', 'string' ), + ), + array( + 'string', + 'Hello', + array( 'object', 'string' ), + ), + array( + 'object', + array( 'hello' => 'world' ), + array( 'object', 'string' ), + ), + array( + 'number', + '5.0', + array( 'number', 'string' ), + ), + array( + 'string', + '5.0', + array( 'string', 'number' ), + ), + array( + 'boolean', + 'false', + array( 'boolean', 'string' ), + ), + array( + 'string', + 'false', + array( 'string', 'boolean' ), + ), + array( + 'string', + 'a,b', + array( 'string', 'array' ), + ), + array( + 'array', + 'a,b', + array( 'array', 'string' ), + ), + ); + } } diff --git a/tests/phpunit/tests/rest-api/rest-schema-sanitization.php b/tests/phpunit/tests/rest-api/rest-schema-sanitization.php index aa212e8d9d..da76aaaa41 100644 --- a/tests/phpunit/tests/rest-api/rest-schema-sanitization.php +++ b/tests/phpunit/tests/rest-api/rest-schema-sanitization.php @@ -341,7 +341,7 @@ class WP_Test_REST_Schema_Sanitization extends WP_UnitTestCase { $this->assertNull( rest_sanitize_value_from_schema( null, $schema ) ); $this->assertEquals( '2019-09-19T18:00:00', rest_sanitize_value_from_schema( '2019-09-19T18:00:00', $schema ) ); - $this->assertNull( rest_sanitize_value_from_schema( 'lalala', $schema ) ); + $this->assertEquals( 'lalala', rest_sanitize_value_from_schema( 'lalala', $schema ) ); } /** @@ -394,7 +394,7 @@ class WP_Test_REST_Schema_Sanitization extends WP_UnitTestCase { $this->assertEquals( 'My Value', rest_sanitize_value_from_schema( 'My Value', $schema ) ); $this->assertEquals( array( 'raw' => 'My Value' ), rest_sanitize_value_from_schema( array( 'raw' => 'My Value' ), $schema ) ); - $this->assertNull( rest_sanitize_value_from_schema( array( 'raw' => 1 ), $schema ) ); + $this->assertEquals( array( 'raw' => '1' ), rest_sanitize_value_from_schema( array( 'raw' => 1 ), $schema ) ); } public function test_object_or_bool() { @@ -423,6 +423,45 @@ class WP_Test_REST_Schema_Sanitization extends WP_UnitTestCase { $this->assertEquals( array( 'raw' => false ), rest_sanitize_value_from_schema( array( 'raw' => '0' ), $schema ) ); $this->assertEquals( array( 'raw' => false ), rest_sanitize_value_from_schema( array( 'raw' => 0 ), $schema ) ); - $this->assertNull( rest_sanitize_value_from_schema( array( 'raw' => 'something non boolean' ), $schema ) ); + $this->assertEquals( array( 'raw' => true ), rest_sanitize_value_from_schema( array( 'raw' => 'something non boolean' ), $schema ) ); + } + + /** + * @ticket 50300 + */ + public function test_multi_type_with_no_known_types() { + $this->setExpectedIncorrectUsage( 'rest_handle_multi_type_schema' ); + $this->setExpectedIncorrectUsage( 'rest_sanitize_value_from_schema' ); + + $schema = array( + 'type' => array( 'invalid', 'type' ), + ); + + $this->assertEquals( 'My Value', rest_sanitize_value_from_schema( 'My Value', $schema ) ); + } + + /** + * @ticket 50300 + */ + public function test_multi_type_with_some_unknown_types() { + $this->setExpectedIncorrectUsage( 'rest_handle_multi_type_schema' ); + $this->setExpectedIncorrectUsage( 'rest_sanitize_value_from_schema' ); + + $schema = array( + 'type' => array( 'object', 'type' ), + ); + + $this->assertEquals( 'My Value', rest_sanitize_value_from_schema( 'My Value', $schema ) ); + } + + /** + * @ticket 50300 + */ + public function test_multi_type_returns_null_if_no_valid_type() { + $schema = array( + 'type' => array( 'number', 'string' ), + ); + + $this->assertNull( rest_sanitize_value_from_schema( array( 'Hello!' ), $schema ) ); } } diff --git a/tests/phpunit/tests/rest-api/rest-schema-validation.php b/tests/phpunit/tests/rest-api/rest-schema-validation.php index 6052b666a9..4e6ea523c7 100644 --- a/tests/phpunit/tests/rest-api/rest-schema-validation.php +++ b/tests/phpunit/tests/rest-api/rest-schema-validation.php @@ -387,7 +387,10 @@ class WP_Test_REST_Schema_Validation extends WP_UnitTestCase { $this->assertTrue( rest_validate_value_from_schema( null, $schema ) ); $this->assertTrue( rest_validate_value_from_schema( '2019-09-19T18:00:00', $schema ) ); - $this->assertWPError( rest_validate_value_from_schema( 'some random string', $schema ) ); + + $error = rest_validate_value_from_schema( 'some random string', $schema ); + $this->assertWPError( $error ); + $this->assertEquals( 'Invalid date.', $error->get_error_message() ); } public function test_object_or_string() { @@ -402,7 +405,57 @@ class WP_Test_REST_Schema_Validation extends WP_UnitTestCase { $this->assertTrue( rest_validate_value_from_schema( 'My Value', $schema ) ); $this->assertTrue( rest_validate_value_from_schema( array( 'raw' => 'My Value' ), $schema ) ); - $this->assertWPError( rest_validate_value_from_schema( array( 'raw' => array( 'a list' ) ), $schema ) ); + + $error = rest_validate_value_from_schema( array( 'raw' => array( 'a list' ) ), $schema ); + $this->assertWPError( $error ); + $this->assertEquals( '[raw] is not of type string.', $error->get_error_message() ); + } + + /** + * @ticket 50300 + */ + public function test_null_or_integer() { + $schema = array( + 'type' => array( 'null', 'integer' ), + 'minimum' => 10, + 'maximum' => 20, + ); + + $this->assertTrue( rest_validate_value_from_schema( null, $schema ) ); + $this->assertTrue( rest_validate_value_from_schema( 15, $schema ) ); + $this->assertTrue( rest_validate_value_from_schema( '15', $schema ) ); + + $error = rest_validate_value_from_schema( 30, $schema, 'param' ); + $this->assertWPError( $error ); + $this->assertEquals( 'param must be between 10 (inclusive) and 20 (inclusive)', $error->get_error_message() ); + } + + /** + * @ticket 50300 + */ + public function test_multi_type_with_no_known_types() { + $this->setExpectedIncorrectUsage( 'rest_handle_multi_type_schema' ); + $this->setExpectedIncorrectUsage( 'rest_validate_value_from_schema' ); + + $schema = array( + 'type' => array( 'invalid', 'type' ), + ); + + $this->assertTrue( rest_validate_value_from_schema( 'My Value', $schema ) ); + } + + /** + * @ticket 50300 + */ + public function test_multi_type_with_some_unknown_types() { + $this->setExpectedIncorrectUsage( 'rest_handle_multi_type_schema' ); + $this->setExpectedIncorrectUsage( 'rest_validate_value_from_schema' ); + + $schema = array( + 'type' => array( 'object', 'type' ), + ); + + $this->assertTrue( rest_validate_value_from_schema( 'My Value', $schema ) ); } /**