diff --git a/wp-includes/rest-api.php b/wp-includes/rest-api.php index c8435a1919..e0387500f0 100644 --- a/wp-includes/rest-api.php +++ b/wp-includes/rest-api.php @@ -1438,6 +1438,63 @@ function rest_handle_multi_type_schema( $value, $args, $param = '' ) { return $best_type; } +/** + * Checks if an array is made up of unique items. + * + * @since 5.5.0 + * + * @param array $array The array to check. + * @return bool True if the array contains unique items, false otherwise. + */ +function rest_validate_array_contains_unique_items( $array ) { + $seen = array(); + + foreach ( $array as $item ) { + $stabilized = rest_stabilize_value( $item ); + $key = serialize( $stabilized ); + + if ( ! isset( $seen[ $key ] ) ) { + $seen[ $key ] = true; + + continue; + } + + return false; + } + + return true; +} + +/** + * Stabilizes a value following JSON Schema semantics. + * + * For lists, order is preserved. For objects, properties are reordered alphabetically. + * + * @since 5.5.0 + * + * @param mixed $value The value to stabilize. Must already be sanitized. Objects should have been converted to arrays. + * @return mixed The stabilized value. + */ +function rest_stabilize_value( $value ) { + if ( is_scalar( $value ) || is_null( $value ) ) { + return $value; + } + + if ( is_object( $value ) ) { + _doing_it_wrong( __FUNCTION__, __( 'Cannot stabilize objects. Convert the object to an array first.' ), '5.5.0' ); + + return $value; + } + + ksort( $value ); + + foreach ( $value as $k => $v ) { + $value[ $k ] = rest_stabilize_value( $v ); + } + + return $value; +} + /** * Validate a value based on a schema. * @@ -1448,8 +1505,8 @@ function rest_handle_multi_type_schema( $value, $args, $param = '' ) { * @since 5.4.0 Convert an empty string to an empty object. * @since 5.5.0 Add the "uuid" and "hex-color" formats. * Support the "minLength", "maxLength" and "pattern" keywords for strings. + * Support the "minItems", "maxItems" and "uniqueItems" keywords for arrays. * Validate required properties. - * Support the "minItems" and "maxItems" keywords for arrays. * * @param mixed $value The value to validate. * @param array $args Schema array to use for validation. @@ -1492,10 +1549,12 @@ function rest_validate_value_from_schema( $value, $args, $param = '' ) { $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 ) ) { - return $is_valid; + if ( isset( $args['items'] ) ) { + foreach ( $value as $index => $v ) { + $is_valid = rest_validate_value_from_schema( $v, $args['items'], $param . '[' . $index . ']' ); + if ( is_wp_error( $is_valid ) ) { + return $is_valid; + } } } @@ -1508,6 +1567,11 @@ function rest_validate_value_from_schema( $value, $args, $param = '' ) { /* translators: 1: Parameter, 2: Number. */ return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must contain at most %2$s items.' ), $param, number_format_i18n( $args['maxItems'] ) ) ); } + + if ( ! empty( $args['uniqueItems'] ) && ! rest_validate_array_contains_unique_items( $value ) ) { + /* translators: 1: Parameter */ + return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s has duplicate items.' ), $param ) ); + } } if ( 'object' === $args['type'] ) { @@ -1718,7 +1782,7 @@ function rest_validate_value_from_schema( $value, $args, $param = '' ) { * @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 mixed The sanitized value. + * @return mixed|WP_Error The sanitized value or a WP_Error instance if the value cannot be safely sanitized. */ function rest_sanitize_value_from_schema( $value, $args, $param = '' ) { $allowed_types = array( 'array', 'object', 'string', 'number', 'integer', 'boolean', 'null' ); @@ -1750,12 +1814,15 @@ function rest_sanitize_value_from_schema( $value, $args, $param = '' ) { if ( 'array' === $args['type'] ) { $value = rest_sanitize_array( $value ); - if ( empty( $args['items'] ) ) { - return $value; + if ( ! empty( $args['items'] ) ) { + foreach ( $value as $index => $v ) { + $value[ $index ] = rest_sanitize_value_from_schema( $v, $args['items'], $param . '[' . $index . ']' ); + } } - foreach ( $value as $index => $v ) { - $value[ $index ] = rest_sanitize_value_from_schema( $v, $args['items'], $param . '[' . $index . ']' ); + if ( ! empty( $args['uniqueItems'] ) && ! rest_validate_array_contains_unique_items( $value ) ) { + /* translators: 1: Parameter */ + return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s has duplicate items.' ), $param ) ); } return $value; diff --git a/wp-includes/version.php b/wp-includes/version.php index f5c0bf685b..835b694f75 100644 --- a/wp-includes/version.php +++ b/wp-includes/version.php @@ -13,7 +13,7 @@ * * @global string $wp_version */ -$wp_version = '5.5-alpha-48356'; +$wp_version = '5.5-alpha-48357'; /** * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.