From 6c1e98d1fbe9758357fd476a7fc7e6d482a3f379 Mon Sep 17 00:00:00 2001 From: Gary Pendergast Date: Mon, 31 Oct 2016 01:48:41 +0000 Subject: [PATCH] REST API: Add support for arrays in schema validation and sanitization. By allowing more fine-grained validation and sanitisation of endpoint args, we can ensure the correct data is being passed to endpoints. This can easily be extended to support new data types, such as CSV fields or objects. Props joehoyle, rachelbaker, pento. Fixes #38531. Built from https://develop.svn.wordpress.org/trunk@39046 git-svn-id: http://core.svn.wordpress.org/trunk@38988 1a063a9b-81f0-0310-95a4-ce76da25c4cd --- wp-includes/rest-api.php | 255 +++++++++++------- .../endpoints/class-wp-rest-controller.php | 2 +- .../class-wp-rest-posts-controller.php | 3 + .../class-wp-rest-settings-controller.php | 22 ++ .../class-wp-rest-users-controller.php | 3 + wp-includes/version.php | 2 +- 6 files changed, 183 insertions(+), 104 deletions(-) diff --git a/wp-includes/rest-api.php b/wp-includes/rest-api.php index 91b7343f56..dc359aec50 100644 --- a/wp-includes/rest-api.php +++ b/wp-includes/rest-api.php @@ -820,80 +820,7 @@ function rest_validate_request_arg( $value, $request, $param ) { } $args = $attributes['args'][ $param ]; - if ( ! empty( $args['enum'] ) ) { - if ( ! in_array( $value, $args['enum'], true ) ) { - return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: list of valid values */ __( '%1$s is not one of %2$s.' ), $param, implode( ', ', $args['enum'] ) ) ); - } - } - - if ( 'integer' === $args['type'] && ! is_numeric( $value ) ) { - return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: type name */ __( '%1$s is not of type %2$s.' ), $param, 'integer' ) ); - } - - if ( 'boolean' === $args['type'] && ! rest_is_boolean( $value ) ) { - return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: type name */ __( '%1$s is not of type %2$s.' ), $value, 'boolean' ) ); - } - - if ( 'string' === $args['type'] && ! is_string( $value ) ) { - return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: type name */ __( '%1$s is not of type %2$s.' ), $param, 'string' ) ); - } - - if ( isset( $args['format'] ) ) { - switch ( $args['format'] ) { - case 'date-time' : - if ( ! rest_parse_date( $value ) ) { - return new WP_Error( 'rest_invalid_date', __( 'The date you provided is invalid.' ) ); - } - break; - - case 'email' : - if ( ! is_email( $value ) ) { - return new WP_Error( 'rest_invalid_email', __( 'The email address you provided is invalid.' ) ); - } - break; - case 'ipv4' : - if ( ! rest_is_ip_address( $value ) ) { - return new WP_Error( 'rest_invalid_param', sprintf( __( '%s is not a valid IP address.' ), $value ) ); - } - break; - } - } - - if ( in_array( $args['type'], array( 'numeric', 'integer' ), true ) && ( isset( $args['minimum'] ) || isset( $args['maximum'] ) ) ) { - if ( isset( $args['minimum'] ) && ! isset( $args['maximum'] ) ) { - if ( ! empty( $args['exclusiveMinimum'] ) && $value <= $args['minimum'] ) { - return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be greater than %2$d (exclusive)' ), $param, $args['minimum'] ) ); - } elseif ( empty( $args['exclusiveMinimum'] ) && $value < $args['minimum'] ) { - return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be greater than %2$d (inclusive)' ), $param, $args['minimum'] ) ); - } - } elseif ( isset( $args['maximum'] ) && ! isset( $args['minimum'] ) ) { - if ( ! empty( $args['exclusiveMaximum'] ) && $value >= $args['maximum'] ) { - return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be less than %2$d (exclusive)' ), $param, $args['maximum'] ) ); - } elseif ( empty( $args['exclusiveMaximum'] ) && $value > $args['maximum'] ) { - return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be less than %2$d (inclusive)' ), $param, $args['maximum'] ) ); - } - } elseif ( isset( $args['maximum'] ) && isset( $args['minimum'] ) ) { - if ( ! empty( $args['exclusiveMinimum'] ) && ! empty( $args['exclusiveMaximum'] ) ) { - if ( $value >= $args['maximum'] || $value <= $args['minimum'] ) { - return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: minimum number, 3: maximum number */ __( '%1$s must be between %2$d (exclusive) and %3$d (exclusive)' ), $param, $args['minimum'], $args['maximum'] ) ); - } - } elseif ( empty( $args['exclusiveMinimum'] ) && ! empty( $args['exclusiveMaximum'] ) ) { - if ( $value >= $args['maximum'] || $value < $args['minimum'] ) { - return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: minimum number, 3: maximum number */ __( '%1$s must be between %2$d (inclusive) and %3$d (exclusive)' ), $param, $args['minimum'], $args['maximum'] ) ); - } - } elseif ( ! empty( $args['exclusiveMinimum'] ) && empty( $args['exclusiveMaximum'] ) ) { - if ( $value > $args['maximum'] || $value <= $args['minimum'] ) { - return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: minimum number, 3: maximum number */ __( '%1$s must be between %2$d (exclusive) and %3$d (inclusive)' ), $param, $args['minimum'], $args['maximum'] ) ); - } - } elseif ( empty( $args['exclusiveMinimum'] ) && empty( $args['exclusiveMaximum'] ) ) { - if ( $value > $args['maximum'] || $value < $args['minimum'] ) { - return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: minimum number, 3: maximum number */ __( '%1$s must be between %2$d (inclusive) and %3$d (inclusive)' ), $param, $args['minimum'], $args['maximum'] ) ); - } - } - } - } - - return true; + return rest_validate_value_from_schema( $value, $args, $param ); } /** @@ -913,34 +840,7 @@ function rest_sanitize_request_arg( $value, $request, $param ) { } $args = $attributes['args'][ $param ]; - if ( 'integer' === $args['type'] ) { - return (int) $value; - } - - if ( 'boolean' === $args['type'] ) { - return rest_sanitize_boolean( $value ); - } - - if ( isset( $args['format'] ) ) { - switch ( $args['format'] ) { - case 'date-time' : - return sanitize_text_field( $value ); - - case 'email' : - /* - * sanitize_email() validates, which would be unexpected - */ - return sanitize_text_field( $value ); - - case 'uri' : - return esc_url_raw( $value ); - - case 'ipv4' : - return sanitize_text_field( $value ); - } - } - - return $value; + return rest_sanitize_value_from_schema( $value, $args, $param ); } /** @@ -1084,3 +984,154 @@ function rest_get_avatar_sizes() { */ return apply_filters( 'rest_avatar_sizes', array( 24, 48, 96 ) ); } + +/** + * Validate a value based on a schema. + * + * @param mixed $value The value to validate. + * @param array $args Schema array to use for validation. + * @param string $param The parameter name, used in error messages. + * @return true|WP_Error + */ +function rest_validate_value_from_schema( $value, $args, $param = '' ) { + if ( 'array' === $args['type'] ) { + if ( ! is_array( $value ) ) { + return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: type name */ __( '%1$s is not of type %2$s.' ), $param, 'array' ) ); + } + 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 ( ! empty( $args['enum'] ) ) { + if ( ! in_array( $value, $args['enum'], true ) ) { + return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: list of valid values */ __( '%1$s is not one of %2$s.' ), $param, implode( ', ', $args['enum'] ) ) ); + } + } + + if ( in_array( $args['type'], array( 'integer', 'number' ) ) && ! is_numeric( $value ) ) { + return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: type name */ __( '%1$s is not of type %2$s.' ), $param, $args['type'] ) ); + } + + if ( 'integer' === $args['type'] && round( floatval( $value ) ) !== floatval( $value ) ) { + return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: type name */ __( '%1$s is not of type %2$s.' ), $param, 'integer' ) ); + } + + if ( 'boolean' === $args['type'] && ! rest_is_boolean( $value ) ) { + return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: type name */ __( '%1$s is not of type %2$s.' ), $value, 'boolean' ) ); + } + + if ( 'string' === $args['type'] && ! is_string( $value ) ) { + return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: type name */ __( '%1$s is not of type %2$s.' ), $param, 'string' ) ); + } + + if ( isset( $args['format'] ) ) { + switch ( $args['format'] ) { + case 'date-time' : + if ( ! rest_parse_date( $value ) ) { + return new WP_Error( 'rest_invalid_date', __( 'The date you provided is invalid.' ) ); + } + break; + + case 'email' : + if ( ! is_email( $value ) ) { + return new WP_Error( 'rest_invalid_email', __( 'The email address you provided is invalid.' ) ); + } + break; + case 'ipv4' : + if ( ! rest_is_ip_address( $value ) ) { + return new WP_Error( 'rest_invalid_param', sprintf( __( '%s is not a valid IP address.' ), $value ) ); + } + break; + } + } + + if ( in_array( $args['type'], array( 'number', 'integer' ), true ) && ( isset( $args['minimum'] ) || isset( $args['maximum'] ) ) ) { + if ( isset( $args['minimum'] ) && ! isset( $args['maximum'] ) ) { + if ( ! empty( $args['exclusiveMinimum'] ) && $value <= $args['minimum'] ) { + return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be greater than %2$d (exclusive)' ), $param, $args['minimum'] ) ); + } elseif ( empty( $args['exclusiveMinimum'] ) && $value < $args['minimum'] ) { + return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be greater than %2$d (inclusive)' ), $param, $args['minimum'] ) ); + } + } elseif ( isset( $args['maximum'] ) && ! isset( $args['minimum'] ) ) { + if ( ! empty( $args['exclusiveMaximum'] ) && $value >= $args['maximum'] ) { + return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be less than %2$d (exclusive)' ), $param, $args['maximum'] ) ); + } elseif ( empty( $args['exclusiveMaximum'] ) && $value > $args['maximum'] ) { + return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be less than %2$d (inclusive)' ), $param, $args['maximum'] ) ); + } + } elseif ( isset( $args['maximum'] ) && isset( $args['minimum'] ) ) { + if ( ! empty( $args['exclusiveMinimum'] ) && ! empty( $args['exclusiveMaximum'] ) ) { + if ( $value >= $args['maximum'] || $value <= $args['minimum'] ) { + return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: minimum number, 3: maximum number */ __( '%1$s must be between %2$d (exclusive) and %3$d (exclusive)' ), $param, $args['minimum'], $args['maximum'] ) ); + } + } elseif ( empty( $args['exclusiveMinimum'] ) && ! empty( $args['exclusiveMaximum'] ) ) { + if ( $value >= $args['maximum'] || $value < $args['minimum'] ) { + return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: minimum number, 3: maximum number */ __( '%1$s must be between %2$d (inclusive) and %3$d (exclusive)' ), $param, $args['minimum'], $args['maximum'] ) ); + } + } elseif ( ! empty( $args['exclusiveMinimum'] ) && empty( $args['exclusiveMaximum'] ) ) { + if ( $value > $args['maximum'] || $value <= $args['minimum'] ) { + return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: minimum number, 3: maximum number */ __( '%1$s must be between %2$d (exclusive) and %3$d (inclusive)' ), $param, $args['minimum'], $args['maximum'] ) ); + } + } elseif ( empty( $args['exclusiveMinimum'] ) && empty( $args['exclusiveMaximum'] ) ) { + if ( $value > $args['maximum'] || $value < $args['minimum'] ) { + return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: minimum number, 3: maximum number */ __( '%1$s must be between %2$d (inclusive) and %3$d (inclusive)' ), $param, $args['minimum'], $args['maximum'] ) ); + } + } + } + } + + return true; +} + +/** + * Sanitize a value based on a schema. + * + * @param mixed $value The value to sanitize. + * @param array $args Schema array to use for sanitization. + * @return true|WP_Error + */ +function rest_sanitize_value_from_schema( $value, $args ) { + if ( 'array' === $args['type'] ) { + if ( empty( $args['items'] ) ) { + return (array) $value; + } + foreach ( $value as $index => $v ) { + $value[ $index ] = rest_sanitize_value_from_schema( $v, $args['items'] ); + } + return $value; + } + if ( 'integer' === $args['type'] ) { + return (int) $value; + } + + if ( 'number' === $args['type'] ) { + return (float) $value; + } + + if ( 'boolean' === $args['type'] ) { + return rest_sanitize_boolean( $value ); + } + + if ( isset( $args['format'] ) ) { + switch ( $args['format'] ) { + case 'date-time' : + return sanitize_text_field( $value ); + + case 'email' : + /* + * sanitize_email() validates, which would be unexpected. + */ + return sanitize_text_field( $value ); + + case 'uri' : + return esc_url_raw( $value ); + + case 'ipv4' : + return sanitize_text_field( $value ); + } + } + + return $value; +} diff --git a/wp-includes/rest-api/endpoints/class-wp-rest-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-controller.php index b4d9f65517..9bf2624fd9 100644 --- a/wp-includes/rest-api/endpoints/class-wp-rest-controller.php +++ b/wp-includes/rest-api/endpoints/class-wp-rest-controller.php @@ -559,7 +559,7 @@ abstract class WP_REST_Controller { $endpoint_args[ $field_id ]['required'] = true; } - foreach ( array( 'type', 'format', 'enum' ) as $schema_prop ) { + foreach ( array( 'type', 'format', 'enum', 'items' ) as $schema_prop ) { if ( isset( $params[ $schema_prop ] ) ) { $endpoint_args[ $field_id ][ $schema_prop ] = $params[ $schema_prop ]; } diff --git a/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php index a9d8f3c557..49d030bad7 100644 --- a/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php +++ b/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php @@ -1971,6 +1971,9 @@ class WP_REST_Posts_Controller extends WP_REST_Controller { $schema['properties'][ $base ] = array( 'description' => sprintf( __( 'The terms assigned to the object in the %s taxonomy.' ), $taxonomy->name ), 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), 'context' => array( 'view', 'edit' ), ); $schema['properties'][ $base . '_exclude' ] = array( diff --git a/wp-includes/rest-api/endpoints/class-wp-rest-settings-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-settings-controller.php index 66110efe8c..0f1ead65f2 100644 --- a/wp-includes/rest-api/endpoints/class-wp-rest-settings-controller.php +++ b/wp-includes/rest-api/endpoints/class-wp-rest-settings-controller.php @@ -288,8 +288,30 @@ class WP_REST_Settings_Controller extends WP_REST_Controller { foreach ( $options as $option_name => $option ) { $schema['properties'][ $option_name ] = $option['schema']; + $schema['properties'][ $option_name ]['arg_options'] = array( + 'sanitize_callback' => array( $this, 'sanitize_callback' ), + ); } return $this->add_additional_fields_schema( $schema ); } + + /** + * Custom sanitize callback used for all options to allow the use of 'null'. + * + * By default, the schema of settings will throw an error if a value is set to + * `null` as it's not a valid value for something like "type => string". We + * provide a wrapper sanitizer to whitelist the use of `null`. + * + * @param mixed $value The value for the setting. + * @param WP_REST_Request $request The request object. + * @param string $param The parameter name. + * @return mixed|WP_Error + */ + public function sanitize_callback( $value, $request, $param ) { + if ( is_null( $value ) ) { + return $value; + } + return rest_parse_request_arg( $value, $request, $param ); + } } diff --git a/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php index 48d649a10b..d366c70371 100644 --- a/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php +++ b/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php @@ -1006,6 +1006,9 @@ class WP_REST_Users_Controller extends WP_REST_Controller { 'roles' => array( 'description' => __( 'Roles assigned to the resource.' ), 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), 'context' => array( 'edit' ), ), 'password' => array( diff --git a/wp-includes/version.php b/wp-includes/version.php index 7a8e3b2bc3..6fa2803807 100644 --- a/wp-includes/version.php +++ b/wp-includes/version.php @@ -4,7 +4,7 @@ * * @global string $wp_version */ -$wp_version = '4.7-beta1-39045'; +$wp_version = '4.7-beta1-39046'; /** * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.