REST API: Make multi-typed schemas more robust.

A multi-type schema is a schema where the `type` keyword is an array of possible types instead of a single type. For instance, `[ 'object', 'string' ]` would allow objects or string values.

In [46249] basic support for these schemas was introduced. The validator would loop over each schema type trying to find a version that matched. This worked for valid values, but for invalid values it provided unhelpful error messages. The sanitizer also had its utility restricted.

In this commit, the validators and sanitizers will first determine the best type of the passed value and then apply the schema with that set type. In the case that a value could match multiple types, the schema of the first matching type will be used.

To maintain backward compatibility, if unsupported schema types are used, the value will always pass validation. A doing it wrong notice is issued in this case.

Fixes #50300.
Props pentatonicfunk, dlh, TimothyBlynJacobs.

Built from https://develop.svn.wordpress.org/trunk@48306


git-svn-id: http://core.svn.wordpress.org/trunk@48075 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
TimothyBlynJacobs 2020-07-05 00:15:05 +00:00
parent 0ba0b61ed8
commit 77b474c905
2 changed files with 245 additions and 103 deletions

View File

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

View File

@ -13,7 +13,7 @@
*
* @global string $wp_version
*/
$wp_version = '5.5-alpha-48305';
$wp_version = '5.5-alpha-48306';
/**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.