diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php index 1dd54b27d6..946c3cdf33 100644 --- a/src/wp-includes/rest-api.php +++ b/src/wp-includes/rest-api.php @@ -1874,6 +1874,73 @@ function rest_find_one_matching_schema( $value, $args, $param, $stop_after_first return $matching_schemas[0]['schema_object']; } +/** + * Checks the equality of two values, following JSON Schema semantics. + * + * Property order is ignored for objects. + * + * Values must have been previously sanitized/coerced to their native types. + * + * @since 5.7.0 + * + * @param mixed $value1 The first value to check. + * @param mixed $value2 The second value to check. + * @return bool True if the values are equal or false otherwise. + */ +function rest_are_values_equal( $value1, $value2 ) { + if ( is_array( $value1 ) && is_array( $value2 ) ) { + if ( count( $value1 ) !== count( $value2 ) ) { + return false; + } + + foreach ( $value1 as $index => $value ) { + if ( ! array_key_exists( $index, $value2 ) || ! rest_are_values_equal( $value, $value2[ $index ] ) ) { + return false; + } + } + + return true; + } + + return $value1 === $value2; +} + +/** + * Validates that the given value is a member of the JSON Schema "enum". + * + * @since 5.7.0 + * + * @param mixed $value The value to validate. + * @param array $args The schema array to use. + * @param string $param The parameter name, used in error messages. + * @return true|WP_Error True if the "enum" contains the value or a WP_Error instance otherwise. + */ +function rest_validate_enum( $value, $args, $param ) { + $sanitized_value = rest_sanitize_value_from_schema( $value, $args, $param ); + if ( is_wp_error( $sanitized_value ) ) { + return $sanitized_value; + } + + foreach ( $args['enum'] as $enum_value ) { + if ( rest_are_values_equal( $sanitized_value, $enum_value ) ) { + return true; + } + } + + $encoded_enum_values = array(); + foreach ( $args['enum'] as $enum_value ) { + $encoded_enum_values[] = is_scalar( $enum_value ) ? $enum_value : wp_json_encode( $enum_value ); + } + + if ( count( $encoded_enum_values ) === 1 ) { + /* translators: 1: Parameter, 2: Valid values. */ + return new WP_Error( 'rest_not_in_enum', wp_sprintf( __( '%1$s is not %2$s.' ), $param, $encoded_enum_values[0] ) ); + } + + /* translators: 1: Parameter, 2: List of valid values. */ + return new WP_Error( 'rest_not_in_enum', wp_sprintf( __( '%1$s is not one of %2$l.' ), $param, $encoded_enum_values ) ); +} + /** * Get all valid JSON schema properties. * @@ -2153,13 +2220,6 @@ function rest_validate_value_from_schema( $value, $args, $param = '' ) { return true; } - if ( ! empty( $args['enum'] ) ) { - if ( ! in_array( $value, $args['enum'], true ) ) { - /* translators: 1: Parameter, 2: List of valid values. */ - return new WP_Error( 'rest_not_in_enum', sprintf( __( '%1$s is not one of %2$s.' ), $param, implode( ', ', $args['enum'] ) ) ); - } - } - if ( in_array( $args['type'], array( 'integer', 'number' ), true ) ) { if ( ! is_numeric( $value ) ) { return new WP_Error( @@ -2234,6 +2294,13 @@ function rest_validate_value_from_schema( $value, $args, $param = '' ) { } } + if ( ! empty( $args['enum'] ) ) { + $enum_contains_value = rest_validate_enum( $value, $args, $param ); + if ( is_wp_error( $enum_contains_value ) ) { + return $enum_contains_value; + } + } + // The "format" keyword should only be applied to strings. However, for backward compatibility, // we allow the "format" keyword if the type keyword was not specified, or was set to an invalid value. if ( isset( $args['format'] ) diff --git a/tests/phpunit/tests/rest-api/rest-schema-validation.php b/tests/phpunit/tests/rest-api/rest-schema-validation.php index f33990ae59..6eb6c423d3 100644 --- a/tests/phpunit/tests/rest-api/rest-schema-validation.php +++ b/tests/phpunit/tests/rest-api/rest-schema-validation.php @@ -248,6 +248,512 @@ class WP_Test_REST_Schema_Validation extends WP_UnitTestCase { $this->assertTrue( rest_validate_value_from_schema( '', $schema ) ); } + /** + * @ticket 51911 + * + * @dataProvider data_different_types_of_value_and_enum_elements + * + * @param mixed $value + * @param array $args + * @param bool $expected + */ + public function test_different_types_of_value_and_enum_elements( $value, $args, $expected ) { + $result = rest_validate_value_from_schema( $value, $args ); + if ( $expected ) { + $this->assertTrue( $result ); + } else { + $this->assertWPError( $result ); + } + } + + /** + * @return array + */ + public function data_different_types_of_value_and_enum_elements() { + return array( + // enum with integers + array( + 0, + array( + 'type' => 'integer', + 'enum' => array( 0, 1 ), + ), + true, + ), + array( + 0.0, + array( + 'type' => 'integer', + 'enum' => array( 0, 1 ), + ), + true, + ), + array( + '0', + array( + 'type' => 'integer', + 'enum' => array( 0, 1 ), + ), + true, + ), + array( + 1, + array( + 'type' => 'integer', + 'enum' => array( 0, 1 ), + ), + true, + ), + array( + 1.0, + array( + 'type' => 'integer', + 'enum' => array( 0, 1 ), + ), + true, + ), + array( + '1', + array( + 'type' => 'integer', + 'enum' => array( 0, 1 ), + ), + true, + ), + array( + 2, + array( + 'type' => 'integer', + 'enum' => array( 0, 1 ), + ), + false, + ), + array( + 2.0, + array( + 'type' => 'integer', + 'enum' => array( 0, 1 ), + ), + false, + ), + array( + '2', + array( + 'type' => 'integer', + 'enum' => array( 0, 1 ), + ), + false, + ), + + // enum with floats + array( + 0, + array( + 'type' => 'number', + 'enum' => array( 0.0, 1.0 ), + ), + true, + ), + array( + 0.0, + array( + 'type' => 'number', + 'enum' => array( 0.0, 1.0 ), + ), + true, + ), + array( + '0', + array( + 'type' => 'number', + 'enum' => array( 0.0, 1.0 ), + ), + true, + ), + array( + 1, + array( + 'type' => 'number', + 'enum' => array( 0.0, 1.0 ), + ), + true, + ), + array( + 1.0, + array( + 'type' => 'number', + 'enum' => array( 0.0, 1.0 ), + ), + true, + ), + array( + '1', + array( + 'type' => 'number', + 'enum' => array( 0.0, 1.0 ), + ), + true, + ), + array( + 2, + array( + 'type' => 'number', + 'enum' => array( 0.0, 1.0 ), + ), + false, + ), + array( + 2.0, + array( + 'type' => 'number', + 'enum' => array( 0.0, 1.0 ), + ), + false, + ), + array( + '2', + array( + 'type' => 'number', + 'enum' => array( 0.0, 1.0 ), + ), + false, + ), + + // enum with booleans + array( + true, + array( + 'type' => 'boolean', + 'enum' => array( true ), + ), + true, + ), + array( + 1, + array( + 'type' => 'boolean', + 'enum' => array( true ), + ), + true, + ), + array( + 'true', + array( + 'type' => 'boolean', + 'enum' => array( true ), + ), + true, + ), + array( + false, + array( + 'type' => 'boolean', + 'enum' => array( true ), + ), + false, + ), + array( + 0, + array( + 'type' => 'boolean', + 'enum' => array( true ), + ), + false, + ), + array( + 'false', + array( + 'type' => 'boolean', + 'enum' => array( true ), + ), + false, + ), + array( + false, + array( + 'type' => 'boolean', + 'enum' => array( false ), + ), + true, + ), + array( + 0, + array( + 'type' => 'boolean', + 'enum' => array( false ), + ), + true, + ), + array( + 'false', + array( + 'type' => 'boolean', + 'enum' => array( false ), + ), + true, + ), + array( + true, + array( + 'type' => 'boolean', + 'enum' => array( false ), + ), + false, + ), + array( + 1, + array( + 'type' => 'boolean', + 'enum' => array( false ), + ), + false, + ), + array( + 'true', + array( + 'type' => 'boolean', + 'enum' => array( false ), + ), + false, + ), + + // enum with arrays + array( + array( 0, 1 ), + array( + 'type' => 'array', + 'items' => array( 'type' => 'integer' ), + 'enum' => array( array( 0, 1 ), array( 1, 2 ) ), + ), + true, + ), + array( + array( '0', 1 ), + array( + 'type' => 'array', + 'items' => array( 'type' => 'integer' ), + 'enum' => array( array( 0, 1 ), array( 1, 2 ) ), + ), + true, + ), + array( + array( 0, '1' ), + array( + 'type' => 'array', + 'items' => array( 'type' => 'integer' ), + 'enum' => array( array( 0, 1 ), array( 1, 2 ) ), + ), + true, + ), + array( + array( '0', '1' ), + array( + 'type' => 'array', + 'items' => array( 'type' => 'integer' ), + 'enum' => array( array( 0, 1 ), array( 1, 2 ) ), + ), + true, + ), + array( + array( 1, 2 ), + array( + 'type' => 'array', + 'items' => array( 'type' => 'integer' ), + 'enum' => array( array( 0, 1 ), array( 1, 2 ) ), + ), + true, + ), + array( + array( 2, 3 ), + array( + 'type' => 'array', + 'items' => array( 'type' => 'integer' ), + 'enum' => array( array( 0, 1 ), array( 1, 2 ) ), + ), + false, + ), + array( + array( 1, 0 ), + array( + 'type' => 'array', + 'items' => array( 'type' => 'integer' ), + 'enum' => array( array( 0, 1 ), array( 1, 2 ) ), + ), + false, + ), + + // enum with objects + array( + array( + 'a' => 1, + 'b' => 2, + ), + array( + 'type' => 'object', + 'additionalProperties' => array( 'type' => 'integer' ), + 'enum' => array( + array( + 'a' => 1, + 'b' => 2, + ), + array( + 'b' => 2, + 'c' => 3, + ), + ), + ), + true, + ), + array( + array( + 'a' => '1', + 'b' => 2, + ), + array( + 'type' => 'object', + 'additionalProperties' => array( 'type' => 'integer' ), + 'enum' => array( + array( + 'a' => 1, + 'b' => 2, + ), + array( + 'b' => 2, + 'c' => 3, + ), + ), + ), + true, + ), + array( + array( + 'a' => 1, + 'b' => '2', + ), + array( + 'type' => 'object', + 'additionalProperties' => array( 'type' => 'integer' ), + 'enum' => array( + array( + 'a' => 1, + 'b' => 2, + ), + array( + 'b' => 2, + 'c' => 3, + ), + ), + ), + true, + ), + array( + array( + 'a' => '1', + 'b' => '2', + ), + array( + 'type' => 'object', + 'additionalProperties' => array( 'type' => 'integer' ), + 'enum' => array( + array( + 'a' => 1, + 'b' => 2, + ), + array( + 'b' => 2, + 'c' => 3, + ), + ), + ), + true, + ), + array( + array( + 'b' => 2, + 'a' => 1, + ), + array( + 'type' => 'object', + 'additionalProperties' => array( 'type' => 'integer' ), + 'enum' => array( + array( + 'a' => 1, + 'b' => 2, + ), + array( + 'b' => 2, + 'c' => 3, + ), + ), + ), + true, + ), + array( + array( + 'b' => 2, + 'c' => 3, + ), + array( + 'type' => 'object', + 'additionalProperties' => array( 'type' => 'integer' ), + 'enum' => array( + array( + 'a' => 1, + 'b' => 2, + ), + array( + 'b' => 2, + 'c' => 3, + ), + ), + ), + true, + ), + array( + array( + 'a' => 1, + 'b' => 3, + ), + array( + 'type' => 'object', + 'additionalProperties' => array( 'type' => 'integer' ), + 'enum' => array( + array( + 'a' => 1, + 'b' => 2, + ), + array( + 'b' => 2, + 'c' => 3, + ), + ), + ), + false, + ), + array( + array( + 'c' => 3, + 'd' => 4, + ), + array( + 'type' => 'object', + 'additionalProperties' => array( 'type' => 'integer' ), + 'enum' => array( + array( + 'a' => 1, + 'b' => 2, + ), + array( + 'b' => 2, + 'c' => 3, + ), + ), + ), + false, + ), + ); + } + public function test_type_array_is_associative() { $schema = array( 'type' => 'array',