diff -r 34716fd837a4 -r be944660c56a wp/wp-includes/rest-api.php --- a/wp/wp-includes/rest-api.php Tue Dec 15 15:52:01 2020 +0100 +++ b/wp/wp-includes/rest-api.php Wed Sep 21 18:19:35 2022 +0200 @@ -94,7 +94,7 @@ _doing_it_wrong( __FUNCTION__, sprintf( - /* translators: 1. The REST API route being registered. 2. The argument name. 3. The suggested function name. */ + /* translators: 1: The REST API route being registered, 2: The argument name, 3: The suggested function name. */ __( 'The REST API route definition for %1$s is missing the required %2$s argument. For REST API routes that are intended to be public, use %3$s as the permission callback.' ), '' . $clean_namespace . '/' . trim( $route, '/' ) . '', 'permission_callback', @@ -130,7 +130,7 @@ * @type callable|null $update_callback Optional. The callback function used to set and update the field value. Default * is 'null', the value cannot be set or updated. The function will be passed * the model object, like WP_Post. - * @type array|null $schema Optional. The callback function used to create the schema for this field. + * @type array|null $schema Optional. The schema for this field. * Default is 'null', no schema entry will be returned. * } */ @@ -153,7 +153,7 @@ } /** - * Registers rewrite rules for the API. + * Registers rewrite rules for the REST API. * * @since 4.4.0 * @@ -209,6 +209,7 @@ add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 ); add_filter( 'rest_pre_dispatch', 'rest_handle_options_request', 10, 3 ); + add_filter( 'rest_index', 'rest_add_application_passwords_to_index' ); } /** @@ -264,10 +265,20 @@ $controller = new WP_REST_Users_Controller; $controller->register_routes(); + // Application Passwords + $controller = new WP_REST_Application_Passwords_Controller(); + $controller->register_routes(); + // Comments. $controller = new WP_REST_Comments_Controller; $controller->register_routes(); + $search_handlers = array( + new WP_REST_Post_Search_Handler(), + new WP_REST_Term_Search_Handler(), + new WP_REST_Post_Format_Search_Handler(), + ); + /** * Filters the search handlers to use in the REST search controller. * @@ -277,7 +288,7 @@ * handler instance must extend the `WP_REST_Search_Handler` class. * Default is only a handler for posts. */ - $search_handlers = apply_filters( 'wp_rest_search_handlers', array( new WP_REST_Post_Search_Handler() ) ); + $search_handlers = apply_filters( 'wp_rest_search_handlers', $search_handlers ); $controller = new WP_REST_Search_Controller( $search_handlers ); $controller->register_routes(); @@ -302,10 +313,30 @@ $controller = new WP_REST_Plugins_Controller(); $controller->register_routes(); + // Sidebars. + $controller = new WP_REST_Sidebars_Controller(); + $controller->register_routes(); + + // Widget Types. + $controller = new WP_REST_Widget_Types_Controller(); + $controller->register_routes(); + + // Widgets. + $controller = new WP_REST_Widgets_Controller(); + $controller->register_routes(); + // Block Directory. $controller = new WP_REST_Block_Directory_Controller(); $controller->register_routes(); + // Pattern Directory. + $controller = new WP_REST_Pattern_Directory_Controller(); + $controller->register_routes(); + + // Site Health. + $site_health = WP_Site_Health::get_instance(); + $controller = new WP_REST_Site_Health_Controller( $site_health ); + $controller->register_routes(); } /** @@ -370,9 +401,9 @@ * @todo Check if this is even necessary * @global WP_Rewrite $wp_rewrite WordPress rewrite component. * - * @param int $blog_id Optional. Blog ID. Default of null returns URL for current blog. - * @param string $path Optional. REST route. Default '/'. - * @param string $scheme Optional. Sanitization scheme. Default 'rest'. + * @param int|null $blog_id Optional. Blog ID. Default of null returns URL for current blog. + * @param string $path Optional. REST route. Default '/'. + * @param string $scheme Optional. Sanitization scheme. Default 'rest'. * @return string Full URL to the endpoint. */ function get_rest_url( $blog_id = null, $path = '/', $scheme = 'rest' ) { @@ -426,10 +457,10 @@ * * @since 4.4.0 * - * @param string $url REST URL. - * @param string $path REST route. - * @param int $blog_id Blog ID. - * @param string $scheme Sanitization scheme. + * @param string $url REST URL. + * @param string $path REST route. + * @param int|null $blog_id Blog ID. + * @param string $scheme Sanitization scheme. */ return apply_filters( 'rest_url', $url, $path, $blog_id, $scheme ); } @@ -483,7 +514,7 @@ /** * Filters the REST Server Class. * - * This filter allows you to adjust the server class used by the API, using a + * This filter allows you to adjust the server class used by the REST API, using a * different class to handle requests. * * @since 4.4.0 @@ -494,7 +525,7 @@ $wp_rest_server = new $wp_rest_server_class; /** - * Fires when preparing to serve an API request. + * Fires when preparing to serve a REST API request. * * Endpoint objects should be created and register their hooks on this action rather * than another action to ensure they're only loaded when needed. @@ -787,7 +818,7 @@ } /** - * Filter the API response to include only a white-listed set of response object fields. + * Filters the REST API response to include only a white-listed set of response object fields. * * @since 4.8.0 * @@ -997,7 +1028,7 @@ $result = wp_verify_nonce( $nonce, 'wp_rest' ); if ( ! $result ) { - return new WP_Error( 'rest_cookie_invalid_nonce', __( 'Cookie nonce is invalid' ), array( 'status' => 403 ) ); + return new WP_Error( 'rest_cookie_invalid_nonce', __( 'Cookie check failed' ), array( 'status' => 403 ) ); } // Send a refreshed nonce in header. @@ -1030,6 +1061,104 @@ } /** + * Collects the status of authenticating with an application password. + * + * @since 5.6.0 + * @since 5.7.0 Added the `$app_password` parameter. + * + * @global WP_User|WP_Error|null $wp_rest_application_password_status + * @global string|null $wp_rest_application_password_uuid + * + * @param WP_Error $user_or_error The authenticated user or error instance. + * @param array $app_password The Application Password used to authenticate. + */ +function rest_application_password_collect_status( $user_or_error, $app_password = array() ) { + global $wp_rest_application_password_status, $wp_rest_application_password_uuid; + + $wp_rest_application_password_status = $user_or_error; + + if ( empty( $app_password['uuid'] ) ) { + $wp_rest_application_password_uuid = null; + } else { + $wp_rest_application_password_uuid = $app_password['uuid']; + } +} + +/** + * Gets the Application Password used for authenticating the request. + * + * @since 5.7.0 + * + * @global string|null $wp_rest_application_password_uuid + * + * @return string|null The App Password UUID, or null if Application Passwords was not used. + */ +function rest_get_authenticated_app_password() { + global $wp_rest_application_password_uuid; + + return $wp_rest_application_password_uuid; +} + +/** + * Checks for errors when using application password-based authentication. + * + * @since 5.6.0 + * + * @global WP_User|WP_Error|null $wp_rest_application_password_status + * + * @param WP_Error|null|true $result Error from another authentication handler, + * null if we should handle it, or another value if not. + * @return WP_Error|null|true WP_Error if the application password is invalid, the $result, otherwise true. + */ +function rest_application_password_check_errors( $result ) { + global $wp_rest_application_password_status; + + if ( ! empty( $result ) ) { + return $result; + } + + if ( is_wp_error( $wp_rest_application_password_status ) ) { + $data = $wp_rest_application_password_status->get_error_data(); + + if ( ! isset( $data['status'] ) ) { + $data['status'] = 401; + } + + $wp_rest_application_password_status->add_data( $data ); + + return $wp_rest_application_password_status; + } + + if ( $wp_rest_application_password_status instanceof WP_User ) { + return true; + } + + return $result; +} + +/** + * Adds Application Passwords info to the REST API index. + * + * @since 5.6.0 + * + * @param WP_REST_Response $response The index response object. + * @return WP_REST_Response + */ +function rest_add_application_passwords_to_index( $response ) { + if ( ! wp_is_application_passwords_available() ) { + return $response; + } + + $response->data['authentication']['application-passwords'] = array( + 'endpoints' => array( + 'authorization' => admin_url( 'authorize-application.php' ), + ), + ); + + return $response; +} + +/** * Retrieves the avatar urls in various sizes. * * @since 4.7.0 @@ -1161,7 +1290,7 @@ * * @since 4.7.0 * - * @return integer 401 if the user is not logged in, 403 if the user is logged in. + * @return int 401 if the user is not logged in, 403 if the user is logged in. */ function rest_authorization_required_code() { return is_user_logged_in() ? 403 : 401; @@ -1258,7 +1387,7 @@ * @since 4.7.0 * * @param bool|string|int $value The value being evaluated. - * @return boolean Returns the proper associated boolean value. + * @return bool Returns the proper associated boolean value. */ function rest_sanitize_boolean( $value ) { // String values are translated to `true`; make sure 'false' is false. @@ -1279,7 +1408,7 @@ * @since 4.7.0 * * @param bool|string $maybe_bool The value being evaluated. - * @return boolean True if a boolean, otherwise false. + * @return bool True if a boolean, otherwise false. */ function rest_is_boolean( $maybe_bool ) { if ( is_bool( $maybe_bool ) ) { @@ -1315,7 +1444,7 @@ * @return bool True if an integer, otherwise false. */ function rest_is_integer( $maybe_integer ) { - return is_numeric( $maybe_integer ) && round( floatval( $maybe_integer ) ) === floatval( $maybe_integer ); + return is_numeric( $maybe_integer ) && round( (float) $maybe_integer ) === (float) $maybe_integer; } /** @@ -1462,7 +1591,7 @@ if ( $invalid_types ) { _doing_it_wrong( __FUNCTION__, - /* translators: 1. Parameter. 2. List of allowed types. */ + /* 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' ); @@ -1540,6 +1669,362 @@ } /** + * Validates if the JSON Schema pattern matches a value. + * + * @since 5.6.0 + * + * @param string $pattern The pattern to match against. + * @param string $value The value to check. + * @return bool True if the pattern matches the given value, false otherwise. + */ +function rest_validate_json_schema_pattern( $pattern, $value ) { + $escaped_pattern = str_replace( '#', '\\#', $pattern ); + + return 1 === preg_match( '#' . $escaped_pattern . '#u', $value ); +} + +/** + * Finds the schema for a property using the patternProperties keyword. + * + * @since 5.6.0 + * + * @param string $property The property name to check. + * @param array $args The schema array to use. + * @return array|null The schema of matching pattern property, or null if no patterns match. + */ +function rest_find_matching_pattern_property_schema( $property, $args ) { + if ( isset( $args['patternProperties'] ) ) { + foreach ( $args['patternProperties'] as $pattern => $child_schema ) { + if ( rest_validate_json_schema_pattern( $pattern, $property ) ) { + return $child_schema; + } + } + } + + return null; +} + +/** + * Formats a combining operation error into a WP_Error object. + * + * @since 5.6.0 + * + * @param string $param The parameter name. + * @param array $error The error details. + * @return WP_Error + */ +function rest_format_combining_operation_error( $param, $error ) { + $position = $error['index']; + $reason = $error['error_object']->get_error_message(); + + if ( isset( $error['schema']['title'] ) ) { + $title = $error['schema']['title']; + + return new WP_Error( + 'rest_no_matching_schema', + /* translators: 1: Parameter, 2: Schema title, 3: Reason. */ + sprintf( __( '%1$s is not a valid %2$s. Reason: %3$s' ), $param, $title, $reason ), + array( 'position' => $position ) + ); + } + + return new WP_Error( + 'rest_no_matching_schema', + /* translators: 1: Parameter, 2: Reason. */ + sprintf( __( '%1$s does not match the expected format. Reason: %2$s' ), $param, $reason ), + array( 'position' => $position ) + ); +} + +/** + * Gets the error of combining operation. + * + * @since 5.6.0 + * + * @param array $value The value to validate. + * @param string $param The parameter name, used in error messages. + * @param array $errors The errors array, to search for possible error. + * @return WP_Error The combining operation error. + */ +function rest_get_combining_operation_error( $value, $param, $errors ) { + // If there is only one error, simply return it. + if ( 1 === count( $errors ) ) { + return rest_format_combining_operation_error( $param, $errors[0] ); + } + + // Filter out all errors related to type validation. + $filtered_errors = array(); + foreach ( $errors as $error ) { + $error_code = $error['error_object']->get_error_code(); + $error_data = $error['error_object']->get_error_data(); + + if ( 'rest_invalid_type' !== $error_code || ( isset( $error_data['param'] ) && $param !== $error_data['param'] ) ) { + $filtered_errors[] = $error; + } + } + + // If there is only one error left, simply return it. + if ( 1 === count( $filtered_errors ) ) { + return rest_format_combining_operation_error( $param, $filtered_errors[0] ); + } + + // If there are only errors related to object validation, try choosing the most appropriate one. + if ( count( $filtered_errors ) > 1 && 'object' === $filtered_errors[0]['schema']['type'] ) { + $result = null; + $number = 0; + + foreach ( $filtered_errors as $error ) { + if ( isset( $error['schema']['properties'] ) ) { + $n = count( array_intersect_key( $error['schema']['properties'], $value ) ); + if ( $n > $number ) { + $result = $error; + $number = $n; + } + } + } + + if ( null !== $result ) { + return rest_format_combining_operation_error( $param, $result ); + } + } + + // If each schema has a title, include those titles in the error message. + $schema_titles = array(); + foreach ( $errors as $error ) { + if ( isset( $error['schema']['title'] ) ) { + $schema_titles[] = $error['schema']['title']; + } + } + + if ( count( $schema_titles ) === count( $errors ) ) { + /* translators: 1: Parameter, 2: Schema titles. */ + return new WP_Error( 'rest_no_matching_schema', wp_sprintf( __( '%1$s is not a valid %2$l.' ), $param, $schema_titles ) ); + } + + /* translators: %s: Parameter. */ + return new WP_Error( 'rest_no_matching_schema', sprintf( __( '%s does not match any of the expected formats.' ), $param ) ); +} + +/** + * Finds the matching schema among the "anyOf" schemas. + * + * @since 5.6.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 array|WP_Error The matching schema or WP_Error instance if all schemas do not match. + */ +function rest_find_any_matching_schema( $value, $args, $param ) { + $errors = array(); + + foreach ( $args['anyOf'] as $index => $schema ) { + if ( ! isset( $schema['type'] ) && isset( $args['type'] ) ) { + $schema['type'] = $args['type']; + } + + $is_valid = rest_validate_value_from_schema( $value, $schema, $param ); + if ( ! is_wp_error( $is_valid ) ) { + return $schema; + } + + $errors[] = array( + 'error_object' => $is_valid, + 'schema' => $schema, + 'index' => $index, + ); + } + + return rest_get_combining_operation_error( $value, $param, $errors ); +} + +/** + * Finds the matching schema among the "oneOf" schemas. + * + * @since 5.6.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. + * @param bool $stop_after_first_match Optional. Whether the process should stop after the first successful match. + * @return array|WP_Error The matching schema or WP_Error instance if the number of matching schemas is not equal to one. + */ +function rest_find_one_matching_schema( $value, $args, $param, $stop_after_first_match = false ) { + $matching_schemas = array(); + $errors = array(); + + foreach ( $args['oneOf'] as $index => $schema ) { + if ( ! isset( $schema['type'] ) && isset( $args['type'] ) ) { + $schema['type'] = $args['type']; + } + + $is_valid = rest_validate_value_from_schema( $value, $schema, $param ); + if ( ! is_wp_error( $is_valid ) ) { + if ( $stop_after_first_match ) { + return $schema; + } + + $matching_schemas[] = array( + 'schema_object' => $schema, + 'index' => $index, + ); + } else { + $errors[] = array( + 'error_object' => $is_valid, + 'schema' => $schema, + 'index' => $index, + ); + } + } + + if ( ! $matching_schemas ) { + return rest_get_combining_operation_error( $value, $param, $errors ); + } + + if ( count( $matching_schemas ) > 1 ) { + $schema_positions = array(); + $schema_titles = array(); + + foreach ( $matching_schemas as $schema ) { + $schema_positions[] = $schema['index']; + + if ( isset( $schema['schema_object']['title'] ) ) { + $schema_titles[] = $schema['schema_object']['title']; + } + } + + // If each schema has a title, include those titles in the error message. + if ( count( $schema_titles ) === count( $matching_schemas ) ) { + return new WP_Error( + 'rest_one_of_multiple_matches', + /* translators: 1: Parameter, 2: Schema titles. */ + wp_sprintf( __( '%1$s matches %2$l, but should match only one.' ), $param, $schema_titles ), + array( 'positions' => $schema_positions ) + ); + } + + return new WP_Error( + 'rest_one_of_multiple_matches', + /* translators: %s: Parameter. */ + sprintf( __( '%s matches more than one of the expected formats.' ), $param ), + array( 'positions' => $schema_positions ) + ); + } + + 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; + } + + if ( is_int( $value1 ) && is_float( $value2 ) + || is_float( $value1 ) && is_int( $value2 ) + ) { + return (float) $value1 === (float) $value2; + } + + 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. + * + * @since 5.6.0 + * + * @return string[] All valid JSON schema properties. + */ +function rest_get_allowed_schema_keywords() { + return array( + 'title', + 'description', + 'default', + 'type', + 'format', + 'enum', + 'items', + 'properties', + 'additionalProperties', + 'patternProperties', + 'minProperties', + 'maxProperties', + 'minimum', + 'maximum', + 'exclusiveMinimum', + 'exclusiveMaximum', + 'multipleOf', + 'minLength', + 'maxLength', + 'pattern', + 'minItems', + 'maxItems', + 'uniqueItems', + 'anyOf', + 'oneOf', + ); +} + +/** * Validate a value based on a schema. * * @since 4.7.0 @@ -1551,6 +2036,10 @@ * Support the "minLength", "maxLength" and "pattern" keywords for strings. * Support the "minItems", "maxItems" and "uniqueItems" keywords for arrays. * Validate required properties. + * @since 5.6.0 Support the "minProperties" and "maxProperties" keywords for objects. + * Support the "multipleOf" keyword for numbers and integers. + * Support the "patternProperties" keyword for objects. + * Support the "anyOf" and "oneOf" keywords. * * @param mixed $value The value to validate. * @param array $args Schema array to use for validation. @@ -1558,10 +2047,32 @@ * @return true|WP_Error */ function rest_validate_value_from_schema( $value, $args, $param = '' ) { + if ( isset( $args['anyOf'] ) ) { + $matching_schema = rest_find_any_matching_schema( $value, $args, $param ); + if ( is_wp_error( $matching_schema ) ) { + return $matching_schema; + } + + if ( ! isset( $args['type'] ) && isset( $matching_schema['type'] ) ) { + $args['type'] = $matching_schema['type']; + } + } + + if ( isset( $args['oneOf'] ) ) { + $matching_schema = rest_find_one_matching_schema( $value, $args, $param ); + if ( is_wp_error( $matching_schema ) ) { + return $matching_schema; + } + + if ( ! isset( $args['type'] ) && isset( $matching_schema['type'] ) ) { + $args['type'] = $matching_schema['type']; + } + } + $allowed_types = array( 'array', 'object', 'string', 'number', 'integer', 'boolean', 'null' ); if ( ! isset( $args['type'] ) ) { - /* translators: 1. Parameter */ + /* translators: %s: Parameter. */ _doing_it_wrong( __FUNCTION__, sprintf( __( 'The "type" schema keyword for %s is required.' ), $param ), '5.5.0' ); } @@ -1569,8 +2080,12 @@ $best_type = rest_handle_multi_type_schema( $value, $args, $param ); 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'] ) ) ); + return new WP_Error( + 'rest_invalid_type', + /* translators: 1: Parameter, 2: List of types. */ + sprintf( __( '%1$s is not of type %2$s.' ), $param, implode( ',', $args['type'] ) ), + array( 'param' => $param ) + ); } $args['type'] = $best_type; @@ -1579,158 +2094,47 @@ if ( ! in_array( $args['type'], $allowed_types, true ) ) { _doing_it_wrong( __FUNCTION__, - /* translators: 1. Parameter 2. The list of allowed types. */ + /* translators: 1: Parameter, 2: The list of allowed types. */ wp_sprintf( __( 'The "type" schema keyword for %1$s can only be one of the built-in types: %2$l.' ), $param, $allowed_types ), '5.5.0' ); } - if ( 'array' === $args['type'] ) { - 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 ); - - 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; - } - } - } - - if ( isset( $args['minItems'] ) && count( $value ) < $args['minItems'] ) { - /* translators: 1: Parameter, 2: Number. */ - return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must contain at least %2$s items.' ), $param, number_format_i18n( $args['minItems'] ) ) ); - } - - if ( isset( $args['maxItems'] ) && count( $value ) > $args['maxItems'] ) { - /* 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 ) ); - } + switch ( $args['type'] ) { + case 'null': + $is_valid = rest_validate_null_value_from_schema( $value, $param ); + break; + case 'boolean': + $is_valid = rest_validate_boolean_value_from_schema( $value, $param ); + break; + case 'object': + $is_valid = rest_validate_object_value_from_schema( $value, $args, $param ); + break; + case 'array': + $is_valid = rest_validate_array_value_from_schema( $value, $args, $param ); + break; + case 'number': + $is_valid = rest_validate_number_value_from_schema( $value, $args, $param ); + break; + case 'string': + $is_valid = rest_validate_string_value_from_schema( $value, $args, $param ); + break; + case 'integer': + $is_valid = rest_validate_integer_value_from_schema( $value, $args, $param ); + break; + default: + $is_valid = true; + break; } - if ( 'object' === $args['type'] ) { - 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 ) ) { - /* translators: 1: Property of an object, 2: Parameter. */ - return new WP_Error( 'rest_property_required', sprintf( __( '%1$s is a required property of %2$s.' ), $name, $param ) ); - } - } - } elseif ( isset( $args['properties'] ) ) { // schema version 3 - foreach ( $args['properties'] as $name => $property ) { - if ( isset( $property['required'] ) && true === $property['required'] && ! array_key_exists( $name, $value ) ) { - /* translators: 1: Property of an object, 2: Parameter. */ - return new WP_Error( 'rest_property_required', sprintf( __( '%1$s is a required property of %2$s.' ), $name, $param ) ); - } - } - } - - foreach ( $value as $property => $v ) { - if ( isset( $args['properties'][ $property ] ) ) { - $is_valid = rest_validate_value_from_schema( $v, $args['properties'][ $property ], $param . '[' . $property . ']' ); - if ( is_wp_error( $is_valid ) ) { - return $is_valid; - } - } elseif ( isset( $args['additionalProperties'] ) ) { - if ( false === $args['additionalProperties'] ) { - /* translators: %s: Property of an object. */ - return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not a valid property of Object.' ), $property ) ); - } - - if ( is_array( $args['additionalProperties'] ) ) { - $is_valid = rest_validate_value_from_schema( $v, $args['additionalProperties'], $param . '[' . $property . ']' ); - if ( is_wp_error( $is_valid ) ) { - return $is_valid; - } - } - } - } - } - - if ( 'null' === $args['type'] ) { - if ( null !== $value ) { - /* translators: 1: Parameter, 2: Type name. */ - return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'null' ) ); - } - - return true; + if ( is_wp_error( $is_valid ) ) { + return $is_valid; } if ( ! empty( $args['enum'] ) ) { - if ( ! in_array( $value, $args['enum'], true ) ) { - /* translators: 1: Parameter, 2: List of valid values. */ - return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not one of %2$s.' ), $param, implode( ', ', $args['enum'] ) ) ); - } - } - - if ( in_array( $args['type'], array( 'integer', 'number' ), true ) && ! is_numeric( $value ) ) { - /* translators: 1: Parameter, 2: Type name. */ - return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, $args['type'] ) ); - } - - 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' ) ); - } - - if ( 'boolean' === $args['type'] && ! rest_is_boolean( $value ) ) { - /* translators: 1: Parameter, 2: Type name. */ - return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'boolean' ) ); - } - - if ( 'string' === $args['type'] ) { - if ( ! is_string( $value ) ) { - /* translators: 1: Parameter, 2: Type name. */ - return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'string' ) ); - } - - if ( isset( $args['minLength'] ) && mb_strlen( $value ) < $args['minLength'] ) { - return new WP_Error( - 'rest_invalid_param', - sprintf( - /* translators: 1: Parameter, 2: Number of characters. */ - _n( '%1$s must be at least %2$s character long.', '%1$s must be at least %2$s characters long.', $args['minLength'] ), - $param, - number_format_i18n( $args['minLength'] ) - ) - ); - } - - if ( isset( $args['maxLength'] ) && mb_strlen( $value ) > $args['maxLength'] ) { - return new WP_Error( - 'rest_invalid_param', - sprintf( - /* translators: 1: Parameter, 2: Number of characters. */ - _n( '%1$s must be at most %2$s character long.', '%1$s must be at most %2$s characters long.', $args['maxLength'] ), - $param, - number_format_i18n( $args['maxLength'] ) - ) - ); - } - - if ( isset( $args['pattern'] ) ) { - $pattern = str_replace( '#', '\\#', $args['pattern'] ); - if ( ! preg_match( '#' . $pattern . '#u', $value ) ) { - /* translators: 1: Parameter, 2: Pattern. */ - return new WP_Error( 'rest_invalid_pattern', sprintf( __( '%1$s does not match pattern %2$s.' ), $param, $args['pattern'] ) ); - } + $enum_contains_value = rest_validate_enum( $value, $args, $param ); + if ( is_wp_error( $enum_contains_value ) ) { + return $enum_contains_value; } } @@ -1760,60 +2164,468 @@ case 'ip': if ( ! rest_is_ip_address( $value ) ) { /* translators: %s: IP address. */ - return new WP_Error( 'rest_invalid_param', sprintf( __( '%s is not a valid IP address.' ), $param ) ); + return new WP_Error( 'rest_invalid_ip', sprintf( __( '%s is not a valid IP address.' ), $param ) ); } break; case 'uuid': if ( ! wp_is_uuid( $value ) ) { - /* translators: %s is the name of a JSON field expecting a valid uuid. */ + /* translators: %s: The name of a JSON field expecting a valid UUID. */ return new WP_Error( 'rest_invalid_uuid', sprintf( __( '%s is not a valid UUID.' ), $param ) ); } 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'] ) { - /* translators: 1: Parameter, 2: Minimum number. */ - return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be greater than %2$d' ), $param, $args['minimum'] ) ); - } elseif ( empty( $args['exclusiveMinimum'] ) && $value < $args['minimum'] ) { - /* translators: 1: Parameter, 2: Minimum number. */ - return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be greater than or equal to %2$d' ), $param, $args['minimum'] ) ); - } - } elseif ( isset( $args['maximum'] ) && ! isset( $args['minimum'] ) ) { - if ( ! empty( $args['exclusiveMaximum'] ) && $value >= $args['maximum'] ) { - /* translators: 1: Parameter, 2: Maximum number. */ - return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be less than %2$d' ), $param, $args['maximum'] ) ); - } elseif ( empty( $args['exclusiveMaximum'] ) && $value > $args['maximum'] ) { - /* translators: 1: Parameter, 2: Maximum number. */ - return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be less than or equal to %2$d' ), $param, $args['maximum'] ) ); + return true; +} + +/** + * Validates a null value based on a schema. + * + * @since 5.7.0 + * + * @param mixed $value The value to validate. + * @param string $param The parameter name, used in error messages. + * @return true|WP_Error + */ +function rest_validate_null_value_from_schema( $value, $param ) { + if ( null !== $value ) { + return new WP_Error( + 'rest_invalid_type', + /* translators: 1: Parameter, 2: Type name. */ + sprintf( __( '%1$s is not of type %2$s.' ), $param, 'null' ), + array( 'param' => $param ) + ); + } + + return true; +} + +/** + * Validates a boolean value based on a schema. + * + * @since 5.7.0 + * + * @param mixed $value The value to validate. + * @param string $param The parameter name, used in error messages. + * @return true|WP_Error + */ +function rest_validate_boolean_value_from_schema( $value, $param ) { + if ( ! rest_is_boolean( $value ) ) { + return new WP_Error( + 'rest_invalid_type', + /* translators: 1: Parameter, 2: Type name. */ + sprintf( __( '%1$s is not of type %2$s.' ), $param, 'boolean' ), + array( 'param' => $param ) + ); + } + + return true; +} + +/** + * Validates an object value based on a schema. + * + * @since 5.7.0 + * + * @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_object_value_from_schema( $value, $args, $param ) { + if ( ! rest_is_object( $value ) ) { + return new WP_Error( + 'rest_invalid_type', + /* translators: 1: Parameter, 2: Type name. */ + sprintf( __( '%1$s is not of type %2$s.' ), $param, 'object' ), + array( 'param' => $param ) + ); + } + + $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 ) ) { + return new WP_Error( + 'rest_property_required', + /* translators: 1: Property of an object, 2: Parameter. */ + sprintf( __( '%1$s is a required property of %2$s.' ), $name, $param ) + ); } - } elseif ( isset( $args['maximum'] ) && isset( $args['minimum'] ) ) { - if ( ! empty( $args['exclusiveMinimum'] ) && ! empty( $args['exclusiveMaximum'] ) ) { - if ( $value >= $args['maximum'] || $value <= $args['minimum'] ) { - /* translators: 1: Parameter, 2: Minimum number, 3: Maximum number. */ - return new WP_Error( 'rest_invalid_param', sprintf( __( '%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'] ) { - /* translators: 1: Parameter, 2: Minimum number, 3: Maximum number. */ - return new WP_Error( 'rest_invalid_param', sprintf( __( '%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'] ) { - /* translators: 1: Parameter, 2: Minimum number, 3: Maximum number. */ - return new WP_Error( 'rest_invalid_param', sprintf( __( '%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'] ) { - /* translators: 1: Parameter, 2: Minimum number, 3: Maximum number. */ - return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be between %2$d (inclusive) and %3$d (inclusive)' ), $param, $args['minimum'], $args['maximum'] ) ); + } + } elseif ( isset( $args['properties'] ) ) { // schema version 3 + foreach ( $args['properties'] as $name => $property ) { + if ( isset( $property['required'] ) && true === $property['required'] && ! array_key_exists( $name, $value ) ) { + return new WP_Error( + 'rest_property_required', + /* translators: 1: Property of an object, 2: Parameter. */ + sprintf( __( '%1$s is a required property of %2$s.' ), $name, $param ) + ); + } + } + } + + foreach ( $value as $property => $v ) { + if ( isset( $args['properties'][ $property ] ) ) { + $is_valid = rest_validate_value_from_schema( $v, $args['properties'][ $property ], $param . '[' . $property . ']' ); + if ( is_wp_error( $is_valid ) ) { + return $is_valid; + } + continue; + } + + $pattern_property_schema = rest_find_matching_pattern_property_schema( $property, $args ); + if ( null !== $pattern_property_schema ) { + $is_valid = rest_validate_value_from_schema( $v, $pattern_property_schema, $param . '[' . $property . ']' ); + if ( is_wp_error( $is_valid ) ) { + return $is_valid; + } + continue; + } + + if ( isset( $args['additionalProperties'] ) ) { + if ( false === $args['additionalProperties'] ) { + return new WP_Error( + 'rest_additional_properties_forbidden', + /* translators: %s: Property of an object. */ + sprintf( __( '%1$s is not a valid property of Object.' ), $property ) + ); + } + + if ( is_array( $args['additionalProperties'] ) ) { + $is_valid = rest_validate_value_from_schema( $v, $args['additionalProperties'], $param . '[' . $property . ']' ); + if ( is_wp_error( $is_valid ) ) { + return $is_valid; } } } } + if ( isset( $args['minProperties'] ) && count( $value ) < $args['minProperties'] ) { + return new WP_Error( + 'rest_too_few_properties', + sprintf( + /* translators: 1: Parameter, 2: Number. */ + _n( + '%1$s must contain at least %2$s property.', + '%1$s must contain at least %2$s properties.', + $args['minProperties'] + ), + $param, + number_format_i18n( $args['minProperties'] ) + ) + ); + } + + if ( isset( $args['maxProperties'] ) && count( $value ) > $args['maxProperties'] ) { + return new WP_Error( + 'rest_too_many_properties', + sprintf( + /* translators: 1: Parameter, 2: Number. */ + _n( + '%1$s must contain at most %2$s property.', + '%1$s must contain at most %2$s properties.', + $args['maxProperties'] + ), + $param, + number_format_i18n( $args['maxProperties'] ) + ) + ); + } + + return true; +} + +/** + * Validates an array value based on a schema. + * + * @since 5.7.0 + * + * @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_array_value_from_schema( $value, $args, $param ) { + if ( ! rest_is_array( $value ) ) { + return new WP_Error( + 'rest_invalid_type', + /* translators: 1: Parameter, 2: Type name. */ + sprintf( __( '%1$s is not of type %2$s.' ), $param, 'array' ), + array( 'param' => $param ) + ); + } + + $value = rest_sanitize_array( $value ); + + 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; + } + } + } + + if ( isset( $args['minItems'] ) && count( $value ) < $args['minItems'] ) { + return new WP_Error( + 'rest_too_few_items', + sprintf( + /* translators: 1: Parameter, 2: Number. */ + _n( + '%1$s must contain at least %2$s item.', + '%1$s must contain at least %2$s items.', + $args['minItems'] + ), + $param, + number_format_i18n( $args['minItems'] ) + ) + ); + } + + if ( isset( $args['maxItems'] ) && count( $value ) > $args['maxItems'] ) { + return new WP_Error( + 'rest_too_many_items', + sprintf( + /* translators: 1: Parameter, 2: Number. */ + _n( + '%1$s must contain at most %2$s item.', + '%1$s must contain at most %2$s items.', + $args['maxItems'] + ), + $param, + number_format_i18n( $args['maxItems'] ) + ) + ); + } + + if ( ! empty( $args['uniqueItems'] ) && ! rest_validate_array_contains_unique_items( $value ) ) { + /* translators: %s: Parameter. */ + return new WP_Error( 'rest_duplicate_items', sprintf( __( '%s has duplicate items.' ), $param ) ); + } + + return true; +} + +/** + * Validates a number value based on a schema. + * + * @since 5.7.0 + * + * @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_number_value_from_schema( $value, $args, $param ) { + if ( ! is_numeric( $value ) ) { + return new WP_Error( + 'rest_invalid_type', + /* translators: 1: Parameter, 2: Type name. */ + sprintf( __( '%1$s is not of type %2$s.' ), $param, $args['type'] ), + array( 'param' => $param ) + ); + } + + if ( isset( $args['multipleOf'] ) && fmod( $value, $args['multipleOf'] ) !== 0.0 ) { + return new WP_Error( + 'rest_invalid_multiple', + /* translators: 1: Parameter, 2: Multiplier. */ + sprintf( __( '%1$s must be a multiple of %2$s.' ), $param, $args['multipleOf'] ) + ); + } + + if ( isset( $args['minimum'] ) && ! isset( $args['maximum'] ) ) { + if ( ! empty( $args['exclusiveMinimum'] ) && $value <= $args['minimum'] ) { + return new WP_Error( + 'rest_out_of_bounds', + /* translators: 1: Parameter, 2: Minimum number. */ + sprintf( __( '%1$s must be greater than %2$d' ), $param, $args['minimum'] ) + ); + } + + if ( empty( $args['exclusiveMinimum'] ) && $value < $args['minimum'] ) { + return new WP_Error( + 'rest_out_of_bounds', + /* translators: 1: Parameter, 2: Minimum number. */ + sprintf( __( '%1$s must be greater than or equal to %2$d' ), $param, $args['minimum'] ) + ); + } + } + + if ( isset( $args['maximum'] ) && ! isset( $args['minimum'] ) ) { + if ( ! empty( $args['exclusiveMaximum'] ) && $value >= $args['maximum'] ) { + return new WP_Error( + 'rest_out_of_bounds', + /* translators: 1: Parameter, 2: Maximum number. */ + sprintf( __( '%1$s must be less than %2$d' ), $param, $args['maximum'] ) + ); + } + + if ( empty( $args['exclusiveMaximum'] ) && $value > $args['maximum'] ) { + return new WP_Error( + 'rest_out_of_bounds', + /* translators: 1: Parameter, 2: Maximum number. */ + sprintf( __( '%1$s must be less than or equal to %2$d' ), $param, $args['maximum'] ) + ); + } + } + + if ( isset( $args['minimum'], $args['maximum'] ) ) { + if ( ! empty( $args['exclusiveMinimum'] ) && ! empty( $args['exclusiveMaximum'] ) ) { + if ( $value >= $args['maximum'] || $value <= $args['minimum'] ) { + return new WP_Error( + 'rest_out_of_bounds', + 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'] + ) + ); + } + } + + if ( ! empty( $args['exclusiveMinimum'] ) && empty( $args['exclusiveMaximum'] ) ) { + if ( $value > $args['maximum'] || $value <= $args['minimum'] ) { + return new WP_Error( + 'rest_out_of_bounds', + 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'] + ) + ); + } + } + + if ( ! empty( $args['exclusiveMaximum'] ) && empty( $args['exclusiveMinimum'] ) ) { + if ( $value >= $args['maximum'] || $value < $args['minimum'] ) { + return new WP_Error( + 'rest_out_of_bounds', + 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'] + ) + ); + } + } + + if ( empty( $args['exclusiveMinimum'] ) && empty( $args['exclusiveMaximum'] ) ) { + if ( $value > $args['maximum'] || $value < $args['minimum'] ) { + return new WP_Error( + 'rest_out_of_bounds', + 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; +} + +/** + * Validates a string value based on a schema. + * + * @since 5.7.0 + * + * @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_string_value_from_schema( $value, $args, $param ) { + if ( ! is_string( $value ) ) { + return new WP_Error( + 'rest_invalid_type', + /* translators: 1: Parameter, 2: Type name. */ + sprintf( __( '%1$s is not of type %2$s.' ), $param, 'string' ), + array( 'param' => $param ) + ); + } + + if ( isset( $args['minLength'] ) && mb_strlen( $value ) < $args['minLength'] ) { + return new WP_Error( + 'rest_too_short', + sprintf( + /* translators: 1: Parameter, 2: Number of characters. */ + _n( + '%1$s must be at least %2$s character long.', + '%1$s must be at least %2$s characters long.', + $args['minLength'] + ), + $param, + number_format_i18n( $args['minLength'] ) + ) + ); + } + + if ( isset( $args['maxLength'] ) && mb_strlen( $value ) > $args['maxLength'] ) { + return new WP_Error( + 'rest_too_long', + sprintf( + /* translators: 1: Parameter, 2: Number of characters. */ + _n( + '%1$s must be at most %2$s character long.', + '%1$s must be at most %2$s characters long.', + $args['maxLength'] + ), + $param, + number_format_i18n( $args['maxLength'] ) + ) + ); + } + + if ( isset( $args['pattern'] ) && ! rest_validate_json_schema_pattern( $args['pattern'], $value ) ) { + return new WP_Error( + 'rest_invalid_pattern', + /* translators: 1: Parameter, 2: Pattern. */ + sprintf( __( '%1$s does not match pattern %2$s.' ), $param, $args['pattern'] ) + ); + } + + return true; +} + +/** + * Validates an integer value based on a schema. + * + * @since 5.7.0 + * + * @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_integer_value_from_schema( $value, $args, $param ) { + $is_valid_number = rest_validate_number_value_from_schema( $value, $args, $param ); + if ( is_wp_error( $is_valid_number ) ) { + return $is_valid_number; + } + + if ( ! rest_is_integer( $value ) ) { + return new WP_Error( + 'rest_invalid_type', + /* translators: 1: Parameter, 2: Type name. */ + sprintf( __( '%1$s is not of type %2$s.' ), $param, 'integer' ), + array( 'param' => $param ) + ); + } + return true; } @@ -1822,6 +2634,7 @@ * * @since 4.7.0 * @since 5.5.0 Added the `$param` parameter. + * @since 5.6.0 Support the "anyOf" and "oneOf" keywords. * * @param mixed $value The value to sanitize. * @param array $args Schema array to use for sanitization. @@ -1829,10 +2642,36 @@ * @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 = '' ) { + if ( isset( $args['anyOf'] ) ) { + $matching_schema = rest_find_any_matching_schema( $value, $args, $param ); + if ( is_wp_error( $matching_schema ) ) { + return $matching_schema; + } + + if ( ! isset( $args['type'] ) ) { + $args['type'] = $matching_schema['type']; + } + + $value = rest_sanitize_value_from_schema( $value, $matching_schema, $param ); + } + + if ( isset( $args['oneOf'] ) ) { + $matching_schema = rest_find_one_matching_schema( $value, $args, $param ); + if ( is_wp_error( $matching_schema ) ) { + return $matching_schema; + } + + if ( ! isset( $args['type'] ) ) { + $args['type'] = $matching_schema['type']; + } + + $value = rest_sanitize_value_from_schema( $value, $matching_schema, $param ); + } + $allowed_types = array( 'array', 'object', 'string', 'number', 'integer', 'boolean', 'null' ); if ( ! isset( $args['type'] ) ) { - /* translators: 1. Parameter */ + /* translators: %s: Parameter. */ _doing_it_wrong( __FUNCTION__, sprintf( __( 'The "type" schema keyword for %s is required.' ), $param ), '5.5.0' ); } @@ -1849,7 +2688,7 @@ if ( ! in_array( $args['type'], $allowed_types, true ) ) { _doing_it_wrong( __FUNCTION__, - /* translators: 1. Parameter. 2. The list of allowed types. */ + /* translators: 1: Parameter, 2: The list of allowed types. */ wp_sprintf( __( 'The "type" schema keyword for %1$s can only be one of the built-in types: %2$l.' ), $param, $allowed_types ), '5.5.0' ); @@ -1865,8 +2704,8 @@ } 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 ) ); + /* translators: %s: Parameter. */ + return new WP_Error( 'rest_duplicate_items', sprintf( __( '%s has duplicate items.' ), $param ) ); } return $value; @@ -1878,7 +2717,16 @@ foreach ( $value as $property => $v ) { if ( isset( $args['properties'][ $property ] ) ) { $value[ $property ] = rest_sanitize_value_from_schema( $v, $args['properties'][ $property ], $param . '[' . $property . ']' ); - } elseif ( isset( $args['additionalProperties'] ) ) { + continue; + } + + $pattern_property_schema = rest_find_matching_pattern_property_schema( $property, $args ); + if ( null !== $pattern_property_schema ) { + $value[ $property ] = rest_sanitize_value_from_schema( $v, $pattern_property_schema, $param . '[' . $property . ']' ); + continue; + } + + if ( isset( $args['additionalProperties'] ) ) { if ( false === $args['additionalProperties'] ) { unset( $value[ $property ] ); } elseif ( is_array( $args['additionalProperties'] ) ) { @@ -1933,7 +2781,7 @@ } if ( 'string' === $args['type'] ) { - return strval( $value ); + return (string) $value; } return $value; @@ -1984,11 +2832,8 @@ $response = rest_do_request( $request ); if ( 200 === $response->status ) { $server = rest_get_server(); - $data = (array) $response->get_data(); - $links = $server::get_compact_response_links( $response ); - if ( ! empty( $links ) ) { - $data['_links'] = $links; - } + $embed = $request->has_param( '_embed' ) ? rest_parse_embed_param( $request['_embed'] ) : false; + $data = (array) $server->response_to_data( $response, $embed ); if ( 'OPTIONS' === $method ) { $response = rest_send_allow_header( $response, $server, $request ); @@ -2034,6 +2879,8 @@ * Filters the response to remove any fields not available in the given context. * * @since 5.5.0 + * @since 5.6.0 Support the "patternProperties" keyword for objects. + * Support the "anyOf" and "oneOf" keywords. * * @param array|object $data The response data to modify. * @param array $schema The schema for the endpoint used to filter the response. @@ -2041,6 +2888,28 @@ * @return array|object The filtered response data. */ function rest_filter_response_by_context( $data, $schema, $context ) { + if ( isset( $schema['anyOf'] ) ) { + $matching_schema = rest_find_any_matching_schema( $data, $schema, '' ); + if ( ! is_wp_error( $matching_schema ) ) { + if ( ! isset( $schema['type'] ) ) { + $schema['type'] = $matching_schema['type']; + } + + $data = rest_filter_response_by_context( $data, $matching_schema, $context ); + } + } + + if ( isset( $schema['oneOf'] ) ) { + $matching_schema = rest_find_one_matching_schema( $data, $schema, '', true ); + if ( ! is_wp_error( $matching_schema ) ) { + if ( ! isset( $schema['type'] ) ) { + $schema['type'] = $matching_schema['type']; + } + + $data = rest_filter_response_by_context( $data, $matching_schema, $context ); + } + } + if ( ! is_array( $data ) && ! is_object( $data ) ) { return $data; } @@ -2074,8 +2943,13 @@ } elseif ( $is_object_type ) { if ( isset( $schema['properties'][ $key ] ) ) { $check = $schema['properties'][ $key ]; - } elseif ( $has_additional_properties ) { - $check = $schema['additionalProperties']; + } else { + $pattern_property_schema = rest_find_matching_pattern_property_schema( $key, $schema ); + if ( null !== $pattern_property_schema ) { + $check = $pattern_property_schema; + } elseif ( $has_additional_properties ) { + $check = $schema['additionalProperties']; + } } } @@ -2113,6 +2987,7 @@ * Sets the "additionalProperties" to false by default for all object definitions in the schema. * * @since 5.5.0 + * @since 5.6.0 Support the "patternProperties" keyword. * * @param array $schema The schema to modify. * @return array The modified schema. @@ -2127,6 +3002,12 @@ } } + if ( isset( $schema['patternProperties'] ) ) { + foreach ( $schema['patternProperties'] as $key => $child_schema ) { + $schema['patternProperties'][ $key ] = rest_default_additional_properties_to_false( $child_schema ); + } + } + if ( ! isset( $schema['additionalProperties'] ) ) { $schema['additionalProperties'] = false; } @@ -2214,7 +3095,7 @@ $route = ''; // The only controller that works is the Terms controller. - if ( 'WP_REST_Terms_Controller' === get_class( $controller ) ) { + if ( $controller instanceof WP_REST_Terms_Controller ) { $namespace = 'wp/v2'; $rest_base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; $route = sprintf( '/%s/%s/%d', $namespace, $rest_base, $term->term_id ); @@ -2258,3 +3139,122 @@ */ return apply_filters( 'rest_queried_resource_route', $route ); } + +/** + * Retrieves an array of endpoint arguments from the item schema and endpoint method. + * + * @since 5.6.0 + * + * @param array $schema The full JSON schema for the endpoint. + * @param string $method Optional. HTTP method of the endpoint. The arguments for `CREATABLE` endpoints are + * checked for required values and may fall-back to a given default, this is not done + * on `EDITABLE` endpoints. Default WP_REST_Server::CREATABLE. + * @return array The endpoint arguments. + */ +function rest_get_endpoint_args_for_schema( $schema, $method = WP_REST_Server::CREATABLE ) { + + $schema_properties = ! empty( $schema['properties'] ) ? $schema['properties'] : array(); + $endpoint_args = array(); + $valid_schema_properties = rest_get_allowed_schema_keywords(); + $valid_schema_properties = array_diff( $valid_schema_properties, array( 'default', 'required' ) ); + + foreach ( $schema_properties as $field_id => $params ) { + + // Arguments specified as `readonly` are not allowed to be set. + if ( ! empty( $params['readonly'] ) ) { + continue; + } + + $endpoint_args[ $field_id ] = array( + 'validate_callback' => 'rest_validate_request_arg', + 'sanitize_callback' => 'rest_sanitize_request_arg', + ); + + if ( WP_REST_Server::CREATABLE === $method && isset( $params['default'] ) ) { + $endpoint_args[ $field_id ]['default'] = $params['default']; + } + + if ( WP_REST_Server::CREATABLE === $method && ! empty( $params['required'] ) ) { + $endpoint_args[ $field_id ]['required'] = true; + } + + foreach ( $valid_schema_properties as $schema_prop ) { + if ( isset( $params[ $schema_prop ] ) ) { + $endpoint_args[ $field_id ][ $schema_prop ] = $params[ $schema_prop ]; + } + } + + // Merge in any options provided by the schema property. + if ( isset( $params['arg_options'] ) ) { + + // Only use required / default from arg_options on CREATABLE endpoints. + if ( WP_REST_Server::CREATABLE !== $method ) { + $params['arg_options'] = array_diff_key( + $params['arg_options'], + array( + 'required' => '', + 'default' => '', + ) + ); + } + + $endpoint_args[ $field_id ] = array_merge( $endpoint_args[ $field_id ], $params['arg_options'] ); + } + } + + return $endpoint_args; +} + + +/** + * Converts an error to a response object. + * + * This iterates over all error codes and messages to change it into a flat + * array. This enables simpler client behaviour, as it is represented as a + * list in JSON rather than an object/map. + * + * @since 5.7.0 + * + * @param WP_Error $error WP_Error instance. + * + * @return WP_REST_Response List of associative arrays with code and message keys. + */ +function rest_convert_error_to_response( $error ) { + $status = array_reduce( + $error->get_all_error_data(), + function ( $status, $error_data ) { + return is_array( $error_data ) && isset( $error_data['status'] ) ? $error_data['status'] : $status; + }, + 500 + ); + + $errors = array(); + + foreach ( (array) $error->errors as $code => $messages ) { + $all_data = $error->get_all_error_data( $code ); + $last_data = array_pop( $all_data ); + + foreach ( (array) $messages as $message ) { + $formatted = array( + 'code' => $code, + 'message' => $message, + 'data' => $last_data, + ); + + if ( $all_data ) { + $formatted['additional_data'] = $all_data; + } + + $errors[] = $formatted; + } + } + + $data = $errors[0]; + if ( count( $errors ) > 1 ) { + // Remove the primary error. + array_shift( $errors ); + $data['additional_errors'] = $errors; + } + + return new WP_REST_Response( $data, $status ); +}