diff -r 3d4e9c994f10 -r a86126ab1dd4 wp/wp-includes/rest-api.php --- a/wp/wp-includes/rest-api.php Tue Oct 22 16:11:46 2019 +0200 +++ b/wp/wp-includes/rest-api.php Tue Dec 15 13:49:49 2020 +0100 @@ -20,7 +20,8 @@ * Note: Do not use before the {@see 'rest_api_init'} hook. * * @since 4.4.0 - * @since 5.1.0 Added a _doing_it_wrong() notice when not called on or after the rest_api_init hook. + * @since 5.1.0 Added a `_doing_it_wrong()` notice when not called on or after the `rest_api_init` hook. + * @since 5.5.0 Added a `_doing_it_wrong()` notice when the required `permission_callback` argument is not set. * * @param string $namespace The first URL segment after core prefix. Should be unique to your package/plugin. * @param string $route The base URL for route you are adding. @@ -44,6 +45,12 @@ return false; } + $clean_namespace = trim( $namespace, '/' ); + + if ( $clean_namespace !== $namespace ) { + _doing_it_wrong( __FUNCTION__, __( 'Namespace must not start or end with a slash.' ), '5.4.2' ); + } + if ( ! did_action( 'rest_api_init' ) ) { _doing_it_wrong( 'register_rest_route', @@ -73,6 +80,7 @@ 'callback' => null, 'args' => array(), ); + foreach ( $args as $key => &$arg_group ) { if ( ! is_numeric( $key ) ) { // Route option, skip here. @@ -81,10 +89,24 @@ $arg_group = array_merge( $defaults, $arg_group ); $arg_group['args'] = array_merge( $common_args, $arg_group['args'] ); + + if ( ! isset( $arg_group['permission_callback'] ) ) { + _doing_it_wrong( + __FUNCTION__, + sprintf( + /* 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', + '__return_true' + ), + '5.5.0' + ); + } } - $full_route = '/' . trim( $namespace, '/' ) . '/' . trim( $route, '/' ); - rest_get_server()->register_route( $namespace, $full_route, $args, $override ); + $full_route = '/' . $clean_namespace . '/' . trim( $route, '/' ); + rest_get_server()->register_route( $clean_namespace, $full_route, $args, $override ); return true; } @@ -98,18 +120,18 @@ * * @param string|array $object_type Object(s) the field is being registered * to, "post"|"term"|"comment" etc. - * @param string $attribute The attribute name. - * @param array $args { + * @param string $attribute The attribute name. + * @param array $args { * Optional. An array of arguments used to handle the registered field. * - * @type string|array|null $get_callback Optional. The callback function used to retrieve the field - * value. Default is 'null', the field will not be returned in - * the response. - * @type string|array|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. - * @type string|array|null $schema Optional. The callback function used to create the schema for - * this field. Default is 'null', no schema entry will be returned. + * @type callable|null $get_callback Optional. The callback function used to retrieve the field value. Default is + * 'null', the field will not be returned in the response. The function will + * be passed the prepared object data. + * @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. + * Default is 'null', no schema entry will be returned. * } */ function register_rest_field( $object_type, $attribute, $args = array() ) { @@ -151,7 +173,7 @@ * @since 4.4.0 * * @see add_rewrite_rule() - * @global WP_Rewrite $wp_rewrite + * @global WP_Rewrite $wp_rewrite WordPress rewrite component. */ function rest_api_register_rewrites() { global $wp_rewrite; @@ -171,11 +193,15 @@ * @since 4.4.0 */ function rest_api_default_filters() { - // Deprecated reporting. - add_action( 'deprecated_function_run', 'rest_handle_deprecated_function', 10, 3 ); - add_filter( 'deprecated_function_trigger_error', '__return_false' ); - add_action( 'deprecated_argument_run', 'rest_handle_deprecated_argument', 10, 3 ); - add_filter( 'deprecated_argument_trigger_error', '__return_false' ); + if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { + // Deprecated reporting. + add_action( 'deprecated_function_run', 'rest_handle_deprecated_function', 10, 3 ); + add_filter( 'deprecated_function_trigger_error', '__return_false' ); + add_action( 'deprecated_argument_run', 'rest_handle_deprecated_argument', 10, 3 ); + add_filter( 'deprecated_argument_trigger_error', '__return_false' ); + add_action( 'doing_it_wrong_run', 'rest_handle_doing_it_wrong', 10, 3 ); + add_filter( 'doing_it_wrong_trigger_error', '__return_false' ); + } // Default serving. add_filter( 'rest_pre_serve_request', 'rest_send_cors_headers' ); @@ -192,13 +218,9 @@ */ function create_initial_rest_routes() { foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) { - $class = ! empty( $post_type->rest_controller_class ) ? $post_type->rest_controller_class : 'WP_REST_Posts_Controller'; + $controller = $post_type->get_rest_controller(); - if ( ! class_exists( $class ) ) { - continue; - } - $controller = new $class( $post_type->name ); - if ( ! is_subclass_of( $controller, 'WP_REST_Controller' ) ) { + if ( ! $controller ) { continue; } @@ -229,13 +251,9 @@ // Terms. foreach ( get_taxonomies( array( 'show_in_rest' => true ), 'object' ) as $taxonomy ) { - $class = ! empty( $taxonomy->rest_controller_class ) ? $taxonomy->rest_controller_class : 'WP_REST_Terms_Controller'; + $controller = $taxonomy->get_rest_controller(); - if ( ! class_exists( $class ) ) { - continue; - } - $controller = new $class( $taxonomy->name ); - if ( ! is_subclass_of( $controller, 'WP_REST_Controller' ) ) { + if ( ! $controller ) { continue; } @@ -268,6 +286,10 @@ $controller = new WP_REST_Block_Renderer_Controller; $controller->register_routes(); + // Block Types. + $controller = new WP_REST_Block_Types_Controller(); + $controller->register_routes(); + // Settings. $controller = new WP_REST_Settings_Controller; $controller->register_routes(); @@ -276,6 +298,14 @@ $controller = new WP_REST_Themes_Controller; $controller->register_routes(); + // Plugins. + $controller = new WP_REST_Plugins_Controller(); + $controller->register_routes(); + + // Block Directory. + $controller = new WP_REST_Block_Directory_Controller(); + $controller->register_routes(); + } /** @@ -283,7 +313,7 @@ * * @since 4.4.0 * - * @global WP $wp Current WordPress environment instance. + * @global WP $wp Current WordPress environment instance. */ function rest_api_loaded() { if ( empty( $GLOBALS['wp']->query_vars['rest_route'] ) ) { @@ -338,7 +368,7 @@ * @since 4.4.0 * * @todo Check if this is even necessary - * @global WP_Rewrite $wp_rewrite + * @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 '/'. @@ -364,7 +394,7 @@ $url .= $path; } else { $url = trailingslashit( get_home_url( $blog_id, '', $scheme ) ); - // nginx only allows HTTP/1.0 methods when redirecting from / to /index.php + // nginx only allows HTTP/1.0 methods when redirecting from / to /index.php. // To work around this, we manually add index.php to the URL, avoiding the redirect. if ( 'index.php' !== substr( $url, 9 ) ) { $url .= 'index.php'; @@ -373,18 +403,19 @@ $url = add_query_arg( 'rest_route', $path, $url ); } - if ( is_ssl() ) { + if ( is_ssl() && isset( $_SERVER['SERVER_NAME'] ) ) { // If the current host is the same as the REST URL host, force the REST URL scheme to HTTPS. - if ( $_SERVER['SERVER_NAME'] === parse_url( get_home_url( $blog_id ), PHP_URL_HOST ) ) { + if ( parse_url( get_home_url( $blog_id ), PHP_URL_HOST ) === $_SERVER['SERVER_NAME'] ) { $url = set_url_scheme( $url, 'https' ); } } if ( is_admin() && force_ssl_admin() ) { - // In this situation the home URL may be http:, and `is_ssl()` may be - // false, but the admin is served over https: (one way or another), so - // REST API usage will be blocked by browsers unless it is also served - // over HTTPS. + /* + * In this situation the home URL may be http:, and `is_ssl()` may be false, + * but the admin is served over https: (one way or another), so REST API usage + * will be blocked by browsers unless it is also served over HTTPS. + */ $url = set_url_scheme( $url, 'https' ); } @@ -411,10 +442,10 @@ * @since 4.4.0 * * @param string $path Optional. REST route. Default empty. - * @param string $scheme Optional. Sanitization scheme. Default 'json'. + * @param string $scheme Optional. Sanitization scheme. Default 'rest'. * @return string Full URL to the endpoint. */ -function rest_url( $path = '', $scheme = 'json' ) { +function rest_url( $path = '', $scheme = 'rest' ) { return get_rest_url( null, $path, $scheme ); } @@ -482,8 +513,9 @@ * Ensures request arguments are a request object (for consistency). * * @since 4.4.0 + * @since 5.3.0 Accept string argument for the request path. * - * @param array|WP_REST_Request $request Request to check. + * @param array|string|WP_REST_Request $request Request to check. * @return WP_REST_Request REST request instance. */ function rest_ensure_request( $request ) { @@ -491,30 +523,44 @@ return $request; } + if ( is_string( $request ) ) { + return new WP_REST_Request( 'GET', $request ); + } + return new WP_REST_Request( 'GET', '', $request ); } /** * Ensures a REST response is a response object (for consistency). * - * This implements WP_HTTP_Response, allowing usage of `set_status`/`header`/etc + * This implements WP_REST_Response, allowing usage of `set_status`/`header`/etc * without needing to double-check the object. Will also allow WP_Error to indicate error * responses, so users should immediately check for this value. * * @since 4.4.0 * - * @param WP_Error|WP_HTTP_Response|mixed $response Response to check. - * @return WP_REST_Response|mixed If response generated an error, WP_Error, if response - * is already an instance, WP_HTTP_Response, otherwise - * returns a new WP_REST_Response instance. + * @param WP_REST_Response|WP_Error|WP_HTTP_Response|mixed $response Response to check. + * @return WP_REST_Response|WP_Error If response generated an error, WP_Error, if response + * is already an instance, WP_REST_Response, otherwise + * returns a new WP_REST_Response instance. */ function rest_ensure_response( $response ) { if ( is_wp_error( $response ) ) { return $response; } + if ( $response instanceof WP_REST_Response ) { + return $response; + } + + // While WP_HTTP_Response is the base class of WP_REST_Response, it doesn't provide + // all the required methods used in WP_REST_Server::dispatch(). if ( $response instanceof WP_HTTP_Response ) { - return $response; + return new WP_REST_Response( + $response->get_data(), + $response->get_status(), + $response->get_headers() + ); } return new WP_REST_Response( $response ); @@ -534,10 +580,10 @@ return; } if ( ! empty( $replacement ) ) { - /* translators: 1: function name, 2: WordPress version number, 3: new function name */ + /* translators: 1: Function name, 2: WordPress version number, 3: New function name. */ $string = sprintf( __( '%1$s (since %2$s; use %3$s instead)' ), $function, $version, $replacement ); } else { - /* translators: 1: function name, 2: WordPress version number */ + /* translators: 1: Function name, 2: WordPress version number. */ $string = sprintf( __( '%1$s (since %2$s; no alternative available)' ), $function, $version ); } @@ -557,11 +603,11 @@ if ( ! WP_DEBUG || headers_sent() ) { return; } - if ( ! empty( $message ) ) { - /* translators: 1: function name, 2: WordPress version number, 3: error message */ + if ( $message ) { + /* translators: 1: Function name, 2: WordPress version number, 3: Error message. */ $string = sprintf( __( '%1$s (since %2$s; %3$s)' ), $function, $version, $message ); } else { - /* translators: 1: function name, 2: WordPress version number */ + /* translators: 1: Function name, 2: WordPress version number. */ $string = sprintf( __( '%1$s (since %2$s; no alternative available)' ), $function, $version ); } @@ -569,6 +615,33 @@ } /** + * Handles _doing_it_wrong errors. + * + * @since 5.5.0 + * + * @param string $function The function that was called. + * @param string $message A message explaining what has been done incorrectly. + * @param string|null $version The version of WordPress where the message was added. + */ +function rest_handle_doing_it_wrong( $function, $message, $version ) { + if ( ! WP_DEBUG || headers_sent() ) { + return; + } + + if ( $version ) { + /* translators: Developer debugging message. 1: PHP function name, 2: WordPress version number, 3: Explanatory message. */ + $string = __( '%1$s (since %2$s; %3$s)' ); + $string = sprintf( $string, $function, $version, $message ); + } else { + /* translators: Developer debugging message. 1: PHP function name, 2: Explanatory message. */ + $string = __( '%1$s (%2$s)' ); + $string = sprintf( $string, $function, $message ); + } + + header( sprintf( 'X-WP-DoingItWrong: %s', $string ) ); +} + +/** * Sends Cross-Origin Resource Sharing headers with API requests. * * @since 4.4.0 @@ -580,7 +653,7 @@ $origin = get_http_origin(); if ( $origin ) { - // Requests from file:// and data: URLs send "Origin: null" + // Requests from file:// and data: URLs send "Origin: null". if ( 'null' !== $origin ) { $origin = esc_url_raw( $origin ); } @@ -589,7 +662,7 @@ header( 'Access-Control-Allow-Credentials: true' ); header( 'Vary: Origin', false ); } elseif ( ! headers_sent() && 'GET' === $_SERVER['REQUEST_METHOD'] && ! is_user_logged_in() ) { - header( 'Vary: Origin' ); + header( 'Vary: Origin', false ); } return $value; @@ -631,7 +704,7 @@ } foreach ( $endpoints as $endpoint ) { - // Remove the redundant preg_match argument. + // Remove the redundant preg_match() argument. unset( $args[0] ); $request->set_url_params( $args ); @@ -694,6 +767,26 @@ } /** + * Recursively computes the intersection of arrays using keys for comparison. + * + * @since 5.3.0 + * + * @param array $array1 The array with master keys to check. + * @param array $array2 An array to compare keys against. + * @return array An associative array containing all the entries of array1 which have keys + * that are present in all arguments. + */ +function _rest_array_intersect_key_recursive( $array1, $array2 ) { + $array1 = array_intersect_key( $array1, $array2 ); + foreach ( $array1 as $key => $value ) { + if ( is_array( $value ) && is_array( $array2[ $key ] ) ) { + $array1[ $key ] = _rest_array_intersect_key_recursive( $value, $array2[ $key ] ); + } + } + return $array1; +} + +/** * Filter the API response to include only a white-listed set of response object fields. * * @since 4.8.0 @@ -701,7 +794,6 @@ * @param WP_REST_Response $response Current response being served. * @param WP_REST_Server $server ResponseHandler instance (usually WP_REST_Server). * @param WP_REST_Request $request The request that was used to make current response. - * * @return WP_REST_Response Response to be served, trimmed down to contain a subset of fields. */ function rest_filter_response_fields( $response, $server, $request ) { @@ -720,15 +812,31 @@ // Trim off outside whitespace from the comma delimited list. $fields = array_map( 'trim', $fields ); - $fields_as_keyed = array_combine( $fields, array_fill( 0, count( $fields ), true ) ); + // Create nested array of accepted field hierarchy. + $fields_as_keyed = array(); + foreach ( $fields as $field ) { + $parts = explode( '.', $field ); + $ref = &$fields_as_keyed; + while ( count( $parts ) > 1 ) { + $next = array_shift( $parts ); + if ( isset( $ref[ $next ] ) && true === $ref[ $next ] ) { + // Skip any sub-properties if their parent prop is already marked for inclusion. + break 2; + } + $ref[ $next ] = isset( $ref[ $next ] ) ? $ref[ $next ] : array(); + $ref = &$ref[ $next ]; + } + $last = array_shift( $parts ); + $ref[ $last ] = true; + } if ( wp_is_numeric_array( $data ) ) { $new_data = array(); foreach ( $data as $item ) { - $new_data[] = array_intersect_key( $item, $fields_as_keyed ); + $new_data[] = _rest_array_intersect_key_recursive( $item, $fields_as_keyed ); } } else { - $new_data = array_intersect_key( $data, $fields_as_keyed ); + $new_data = _rest_array_intersect_key_recursive( $data, $fields_as_keyed ); } $response->set_data( $new_data ); @@ -737,6 +845,43 @@ } /** + * Given an array of fields to include in a response, some of which may be + * `nested.fields`, determine whether the provided field should be included + * in the response body. + * + * If a parent field is passed in, the presence of any nested field within + * that parent will cause the method to return `true`. For example "title" + * will return true if any of `title`, `title.raw` or `title.rendered` is + * provided. + * + * @since 5.3.0 + * + * @param string $field A field to test for inclusion in the response body. + * @param array $fields An array of string fields supported by the endpoint. + * @return bool Whether to include the field or not. + */ +function rest_is_field_included( $field, $fields ) { + if ( in_array( $field, $fields, true ) ) { + return true; + } + + foreach ( $fields as $accepted_field ) { + // Check to see if $field is the parent of any item in $fields. + // A field "parent" should be accepted if "parent.child" is accepted. + if ( strpos( $accepted_field, "$field." ) === 0 ) { + return true; + } + // Conversely, if "parent" is accepted, all "parent.child" fields + // should also be accepted. + if ( strpos( $field, "$accepted_field." ) === 0 ) { + return true; + } + } + + return false; +} + +/** * Adds the REST API URL to the WP RSD endpoint. * * @since 4.4.0 @@ -768,7 +913,13 @@ return; } - echo "\n"; + printf( '', esc_url( $api_root ) ); + + $resource = rest_get_queried_resource_route(); + + if ( $resource ) { + printf( '', esc_url( rest_url( $resource ) ) ); + } } /** @@ -787,7 +938,13 @@ return; } - header( 'Link: <' . esc_url_raw( $api_root ) . '>; rel="https://api.w.org/"', false ); + header( sprintf( 'Link: <%s>; rel="https://api.w.org/"', esc_url_raw( $api_root ) ), false ); + + $resource = rest_get_queried_resource_route(); + + if ( $resource ) { + header( sprintf( 'Link: <%s>; rel="alternate"; type="application/json"', esc_url_raw( rest_url( $resource ) ) ), false ); + } } /** @@ -802,8 +959,7 @@ * @global mixed $wp_rest_auth_cookie * * @param WP_Error|mixed $result Error from another authentication handler, - * null if we should handle it, or another value - * if not. + * null if we should handle it, or another value if not. * @return WP_Error|mixed|bool WP_Error if the cookie is invalid, the $result, otherwise true. */ function rest_cookie_check_errors( $result ) { @@ -874,6 +1030,50 @@ } /** + * 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. * * @since 4.4.0 @@ -898,6 +1098,23 @@ } /** + * Parses a 3 or 6 digit hex color (with #). + * + * @since 5.4.0 + * + * @param string $color 3 or 6 digit hex color (with #). + * @return string|false + */ +function rest_parse_hex_color( $color ) { + $regex = '|^#([A-Fa-f0-9]{3}){1,2}$|'; + if ( ! preg_match( $regex, $color, $matches ) ) { + return false; + } + + return $color; +} + +/** * Parses a date into both its local and UTC equivalent, in MySQL datetime format. * * @since 4.4.0 @@ -910,9 +1127,11 @@ * null on failure. */ function rest_get_date_with_gmt( $date, $is_utc = false ) { - // Whether or not the original date actually has a timezone string - // changes the way we need to do timezone conversion. Store this info - // before parsing the date, and use it later. + /* + * Whether or not the original date actually has a timezone string + * changes the way we need to do timezone conversion. + * Store this info before parsing the date, and use it later. + */ $has_timezone = preg_match( '#(Z|[+-]\d{2}(:\d{2})?)$#', $date ); $date = rest_parse_date( $date ); @@ -921,15 +1140,16 @@ return null; } - // At this point $date could either be a local date (if we were passed a - // *local* date without a timezone offset) or a UTC date (otherwise). - // Timezone conversion needs to be handled differently between these two - // cases. + /* + * At this point $date could either be a local date (if we were passed + * a *local* date without a timezone offset) or a UTC date (otherwise). + * Timezone conversion needs to be handled differently between these two cases. + */ if ( ! $is_utc && ! $has_timezone ) { - $local = date( 'Y-m-d H:i:s', $date ); + $local = gmdate( 'Y-m-d H:i:s', $date ); $utc = get_gmt_from_date( $local ); } else { - $utc = date( 'Y-m-d H:i:s', $date ); + $utc = gmdate( 'Y-m-d H:i:s', $date ); $local = get_date_from_gmt( $utc ); } @@ -952,10 +1172,10 @@ * * @since 4.7.0 * - * @param mixed $value - * @param WP_REST_Request $request - * @param string $param - * @return WP_Error|boolean + * @param mixed $value + * @param WP_REST_Request $request + * @param string $param + * @return true|WP_Error */ function rest_validate_request_arg( $value, $request, $param ) { $attributes = $request->get_attributes(); @@ -972,9 +1192,9 @@ * * @since 4.7.0 * - * @param mixed $value - * @param WP_REST_Request $request - * @param string $param + * @param mixed $value + * @param WP_REST_Request $request + * @param string $param * @return mixed */ function rest_sanitize_request_arg( $value, $request, $param ) { @@ -984,7 +1204,7 @@ } $args = $attributes['args'][ $param ]; - return rest_sanitize_value_from_schema( $value, $args ); + return rest_sanitize_value_from_schema( $value, $args, $param ); } /** @@ -995,9 +1215,9 @@ * * @since 4.7.0 * - * @param mixed $value - * @param WP_REST_Request $request - * @param string $param + * @param mixed $value + * @param WP_REST_Request $request + * @param string $param * @return mixed */ function rest_parse_request_arg( $value, $request, $param ) { @@ -1019,7 +1239,7 @@ * * @since 4.7.0 * - * @param string $ip IP address. + * @param string $ip IP address. * @return string|false The valid IP address, otherwise false. */ function rest_is_ip_address( $ip ) { @@ -1087,52 +1307,250 @@ } /** - * Retrieves the avatar urls in various sizes based on a given email address. + * Determines if a given value is integer-like. + * + * @since 5.5.0 * - * @since 4.7.0 + * @param mixed $maybe_integer The value being evaluated. + * @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 ); +} + +/** + * Determines if a given value is array-like. * - * @see get_avatar_url() + * @since 5.5.0 * - * @param string $email Email address. - * @return array $urls Gravatar url for each size. + * @param mixed $maybe_array The value being evaluated. + * @return bool */ -function rest_get_avatar_urls( $email ) { - $avatar_sizes = rest_get_avatar_sizes(); +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 ); +} - $urls = array(); - foreach ( $avatar_sizes as $size ) { - $urls[ $size ] = get_avatar_url( $email, array( 'size' => $size ) ); +/** + * 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(); } - return $urls; + // 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; } /** - * Retrieves the pixel sizes for avatars. + * Gets the best type for a value. + * + * @since 5.5.0 * - * @since 4.7.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. * - * @return array List of pixel sizes for avatars. Default `[ 24, 48, 96 ]`. + * 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_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 array $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_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; +} + +/** + * 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. * * @since 4.7.0 + * @since 4.9.0 Support the "object" type. + * @since 5.2.0 Support validating "additionalProperties" against a schema. + * @since 5.3.0 Support multiple types. + * @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. * * @param mixed $value The value to validate. * @param array $args Schema array to use for validation. @@ -1140,29 +1558,88 @@ * @return true|WP_Error */ function rest_validate_value_from_schema( $value, $args, $param = '' ) { + $allowed_types = array( 'array', 'object', 'string', 'number', 'integer', 'boolean', 'null' ); + + if ( ! isset( $args['type'] ) ) { + /* 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'] ) ) { + $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'] ) ) ); + } + + $args['type'] = $best_type; + } + + if ( ! in_array( $args['type'], $allowed_types, true ) ) { + _doing_it_wrong( + __FUNCTION__, + /* 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 ( ! is_null( $value ) ) { - $value = wp_parse_list( $value ); - } - if ( ! wp_is_numeric_array( $value ) ) { - /* translators: 1: parameter, 2: type name */ + 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' ) ); } - 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; + + $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 ) ); + } } if ( 'object' === $args['type'] ) { - if ( $value instanceof stdClass ) { - $value = (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' ) ); } - if ( ! 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, '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 ) { @@ -1171,41 +1648,104 @@ if ( is_wp_error( $is_valid ) ) { return $is_valid; } - } elseif ( isset( $args['additionalProperties'] ) && false === $args['additionalProperties'] ) { - return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not a valid property of Object.' ), $property ) ); + } 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 ( ! empty( $args['enum'] ) ) { if ( ! in_array( $value, $args['enum'], true ) ) { - /* translators: 1: parameter, 2: list of valid values */ + /* 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' ) ) && ! is_numeric( $value ) ) { - /* translators: 1: parameter, 2: type name */ + 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'] && round( floatval( $value ) ) !== floatval( $value ) ) { - /* translators: 1: parameter, 2: type name */ + 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.' ), $value, 'boolean' ) ); + /* 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'] && ! 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 ( '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'] ) ); + } + } } - if ( isset( $args['format'] ) ) { + // The "format" keyword should only be applied to strings. However, for backward compatibility, + // we allow the "format" keyword if the type keyword was not specified, or was set to an invalid value. + if ( isset( $args['format'] ) + && ( ! isset( $args['type'] ) || 'string' === $args['type'] || ! in_array( $args['type'], $allowed_types, true ) ) + ) { switch ( $args['format'] ) { + case 'hex-color': + if ( ! rest_parse_hex_color( $value ) ) { + return new WP_Error( 'rest_invalid_hex_color', __( 'Invalid hex color.' ) ); + } + break; + case 'date-time': if ( ! rest_parse_date( $value ) ) { return new WP_Error( 'rest_invalid_date', __( 'Invalid date.' ) ); @@ -1219,8 +1759,14 @@ break; 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.' ), $value ) ); + /* translators: %s: IP address. */ + return new WP_Error( 'rest_invalid_param', 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. */ + return new WP_Error( 'rest_invalid_uuid', sprintf( __( '%s is not a valid UUID.' ), $param ) ); } break; } @@ -1229,39 +1775,39 @@ 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 */ + /* 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 */ + /* 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 */ + /* 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 */ + /* 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'] ) ); } } 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 */ + /* 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 */ + /* 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 */ + /* 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 */ + /* 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'] ) ); } } @@ -1275,45 +1821,79 @@ * 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. - * @return true|WP_Error + * @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|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 ) { +function rest_sanitize_value_from_schema( $value, $args, $param = '' ) { + $allowed_types = array( 'array', 'object', 'string', 'number', 'integer', 'boolean', 'null' ); + + if ( ! isset( $args['type'] ) ) { + /* 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'] ) ) { + $best_type = rest_handle_multi_type_schema( $value, $args, $param ); + + if ( ! $best_type ) { + return null; + } + + $args['type'] = $best_type; + } + + if ( ! in_array( $args['type'], $allowed_types, true ) ) { + _doing_it_wrong( + __FUNCTION__, + /* 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 ( empty( $args['items'] ) ) { - return (array) $value; + $value = rest_sanitize_array( $value ); + + if ( ! empty( $args['items'] ) ) { + foreach ( $value as $index => $v ) { + $value[ $index ] = rest_sanitize_value_from_schema( $v, $args['items'], $param . '[' . $index . ']' ); + } } - $value = wp_parse_list( $value ); - foreach ( $value as $index => $v ) { - $value[ $index ] = rest_sanitize_value_from_schema( $v, $args['items'] ); + + 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 ) ); } - // 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 ( ! 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 ] ); - } elseif ( isset( $args['additionalProperties'] ) && false === $args['additionalProperties'] ) { - unset( $value[ $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'], $param . '[' . $property . ']' ); + } } } return $value; } + if ( 'null' === $args['type'] ) { + return null; + } + if ( 'integer' === $args['type'] ) { return (int) $value; } @@ -1326,15 +1906,19 @@ return rest_sanitize_boolean( $value ); } - if ( isset( $args['format'] ) ) { + // This behavior matches rest_validate_value_from_schema(). + if ( isset( $args['format'] ) + && ( ! isset( $args['type'] ) || 'string' === $args['type'] || ! in_array( $args['type'], $allowed_types, true ) ) + ) { switch ( $args['format'] ) { + case 'hex-color': + return (string) sanitize_hex_color( $value ); + case 'date-time': return sanitize_text_field( $value ); case 'email': - /* - * sanitize_email() validates, which would be unexpected. - */ + // sanitize_email() validates, which would be unexpected. return sanitize_text_field( $value ); case 'uri': @@ -1342,6 +1926,9 @@ case 'ip': return sanitize_text_field( $value ); + + case 'uuid': + return sanitize_text_field( $value ); } } @@ -1358,12 +1945,13 @@ * * @since 5.0.0 * - * @param array $memo Reduce accumulator. - * @param string $path REST API path to preload. - * @return array Modified reduce accumulator. + * @param array $memo Reduce accumulator. + * @param string $path REST API path to preload. + * @return array Modified reduce accumulator. */ function rest_preload_api_request( $memo, $path ) { - // array_reduce() doesn't support passing an array in PHP 5.2, so we need to make sure we start with one. + // array_reduce() doesn't support passing an array in PHP 5.2, + // so we need to make sure we start with one. if ( ! is_array( $memo ) ) { $memo = array(); } @@ -1397,7 +1985,7 @@ if ( 200 === $response->status ) { $server = rest_get_server(); $data = (array) $response->get_data(); - $links = $server->get_compact_response_links( $response ); + $links = $server::get_compact_response_links( $response ); if ( ! empty( $links ) ) { $data['_links'] = $links; } @@ -1419,3 +2007,254 @@ return $memo; } + +/** + * Parses the "_embed" parameter into the list of resources to embed. + * + * @since 5.4.0 + * + * @param string|array $embed Raw "_embed" parameter value. + * @return true|string[] Either true to embed all embeds, or a list of relations to embed. + */ +function rest_parse_embed_param( $embed ) { + if ( ! $embed || 'true' === $embed || '1' === $embed ) { + return true; + } + + $rels = wp_parse_list( $embed ); + + if ( ! $rels ) { + return true; + } + + return $rels; +} + +/** + * Filters the response to remove any fields not available in the given context. + * + * @since 5.5.0 + * + * @param array|object $data The response data to modify. + * @param array $schema The schema for the endpoint used to filter the response. + * @param string $context The requested context. + * @return array|object The filtered response data. + */ +function rest_filter_response_by_context( $data, $schema, $context ) { + if ( ! is_array( $data ) && ! is_object( $data ) ) { + return $data; + } + + if ( isset( $schema['type'] ) ) { + $type = $schema['type']; + } elseif ( isset( $schema['properties'] ) ) { + $type = 'object'; // Back compat if a developer accidentally omitted the type. + } else { + return $data; + } + + $is_array_type = 'array' === $type || ( is_array( $type ) && in_array( 'array', $type, true ) ); + $is_object_type = 'object' === $type || ( is_array( $type ) && in_array( 'object', $type, true ) ); + + if ( $is_array_type && $is_object_type ) { + if ( rest_is_array( $data ) ) { + $is_object_type = false; + } else { + $is_array_type = false; + } + } + + $has_additional_properties = $is_object_type && isset( $schema['additionalProperties'] ) && is_array( $schema['additionalProperties'] ); + + foreach ( $data as $key => $value ) { + $check = array(); + + if ( $is_array_type ) { + $check = isset( $schema['items'] ) ? $schema['items'] : array(); + } elseif ( $is_object_type ) { + if ( isset( $schema['properties'][ $key ] ) ) { + $check = $schema['properties'][ $key ]; + } elseif ( $has_additional_properties ) { + $check = $schema['additionalProperties']; + } + } + + if ( ! isset( $check['context'] ) ) { + continue; + } + + if ( ! in_array( $context, $check['context'], true ) ) { + if ( $is_array_type ) { + // All array items share schema, so there's no need to check each one. + $data = array(); + break; + } + + if ( is_object( $data ) ) { + unset( $data->$key ); + } else { + unset( $data[ $key ] ); + } + } elseif ( is_array( $value ) || is_object( $value ) ) { + $new_value = rest_filter_response_by_context( $value, $check, $context ); + + if ( is_object( $data ) ) { + $data->$key = $new_value; + } else { + $data[ $key ] = $new_value; + } + } + } + + return $data; +} + +/** + * Sets the "additionalProperties" to false by default for all object definitions in the schema. + * + * @since 5.5.0 + * + * @param array $schema The schema to modify. + * @return array The modified schema. + */ +function rest_default_additional_properties_to_false( $schema ) { + $type = (array) $schema['type']; + + if ( in_array( 'object', $type, true ) ) { + if ( isset( $schema['properties'] ) ) { + foreach ( $schema['properties'] as $key => $child_schema ) { + $schema['properties'][ $key ] = rest_default_additional_properties_to_false( $child_schema ); + } + } + + if ( ! isset( $schema['additionalProperties'] ) ) { + $schema['additionalProperties'] = false; + } + } + + if ( in_array( 'array', $type, true ) ) { + if ( isset( $schema['items'] ) ) { + $schema['items'] = rest_default_additional_properties_to_false( $schema['items'] ); + } + } + + return $schema; +} + +/** + * Gets the REST API route for a post. + * + * @since 5.5.0 + * + * @param int|WP_Post $post Post ID or post object. + * @return string The route path with a leading slash for the given post, or an empty string if there is not a route. + */ +function rest_get_route_for_post( $post ) { + $post = get_post( $post ); + + if ( ! $post instanceof WP_Post ) { + return ''; + } + + $post_type = get_post_type_object( $post->post_type ); + if ( ! $post_type ) { + return ''; + } + + $controller = $post_type->get_rest_controller(); + if ( ! $controller ) { + return ''; + } + + $route = ''; + + // The only two controllers that we can detect are the Attachments and Posts controllers. + if ( in_array( get_class( $controller ), array( 'WP_REST_Attachments_Controller', 'WP_REST_Posts_Controller' ), true ) ) { + $namespace = 'wp/v2'; + $rest_base = ! empty( $post_type->rest_base ) ? $post_type->rest_base : $post_type->name; + $route = sprintf( '/%s/%s/%d', $namespace, $rest_base, $post->ID ); + } + + /** + * Filters the REST API route for a post. + * + * @since 5.5.0 + * + * @param string $route The route path. + * @param WP_Post $post The post object. + */ + return apply_filters( 'rest_route_for_post', $route, $post ); +} + +/** + * Gets the REST API route for a term. + * + * @since 5.5.0 + * + * @param int|WP_Term $term Term ID or term object. + * @return string The route path with a leading slash for the given term, or an empty string if there is not a route. + */ +function rest_get_route_for_term( $term ) { + $term = get_term( $term ); + + if ( ! $term instanceof WP_Term ) { + return ''; + } + + $taxonomy = get_taxonomy( $term->taxonomy ); + if ( ! $taxonomy ) { + return ''; + } + + $controller = $taxonomy->get_rest_controller(); + if ( ! $controller ) { + return ''; + } + + $route = ''; + + // The only controller that works is the Terms controller. + if ( 'WP_REST_Terms_Controller' === get_class( $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 ); + } + + /** + * Filters the REST API route for a term. + * + * @since 5.5.0 + * + * @param string $route The route path. + * @param WP_Term $term The term object. + */ + return apply_filters( 'rest_route_for_term', $route, $term ); +} + +/** + * Gets the REST route for the currently queried object. + * + * @since 5.5.0 + * + * @return string The REST route of the resource, or an empty string if no resource identified. + */ +function rest_get_queried_resource_route() { + if ( is_singular() ) { + $route = rest_get_route_for_post( get_queried_object() ); + } elseif ( is_category() || is_tag() || is_tax() ) { + $route = rest_get_route_for_term( get_queried_object() ); + } elseif ( is_author() ) { + $route = '/wp/v2/users/' . get_queried_object_id(); + } else { + $route = ''; + } + + /** + * Filters the REST route for the currently queried object. + * + * @since 5.5.0 + * + * @param string $link The route with a leading slash, or an empty string. + */ + return apply_filters( 'rest_queried_resource_route', $route ); +}