REST API: Support type coercion when validating the enum JSON Schema keyword.

Previously, the `enum` keyword was validated by perform a strict equality check. For `string` types this is generally ok, but it prevented using alternative types like `number` when rich type support isn't available.

Now the same level of type coercion/sanitization is applied when validating `enum` as all other validation checks. This means that a value of `"1"` will be accepted for an `enum` of `[ 0, 1 ]`. Additionally, `object` types now properly ignore key order when checking for equality.

Props yakimun.
Fixes #51911.


git-svn-id: https://develop.svn.wordpress.org/trunk@50010 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
Timothy Jacobs
2021-01-24 16:50:39 +00:00
parent e4e24c84de
commit 0ceee0f871
2 changed files with 580 additions and 7 deletions

View File

@@ -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'] )

View File

@@ -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',