diff -r 34716fd837a4 -r be944660c56a wp/wp-includes/rest-api/class-wp-rest-server.php --- a/wp/wp-includes/rest-api/class-wp-rest-server.php Tue Dec 15 15:52:01 2020 +0100 +++ b/wp/wp-includes/rest-api/class-wp-rest-server.php Wed Sep 21 18:19:35 2022 +0200 @@ -94,7 +94,7 @@ public function __construct() { $this->endpoints = array( // Meta endpoints. - '/' => array( + '/' => array( 'callback' => array( $this, 'get_index' ), 'methods' => 'GET', 'args' => array( @@ -103,6 +103,51 @@ ), ), ), + '/batch/v1' => array( + 'callback' => array( $this, 'serve_batch_request_v1' ), + 'methods' => 'POST', + 'args' => array( + 'validation' => array( + 'type' => 'string', + 'enum' => array( 'require-all-validate', 'normal' ), + 'default' => 'normal', + ), + 'requests' => array( + 'required' => true, + 'type' => 'array', + 'maxItems' => $this->get_max_batch_size(), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'method' => array( + 'type' => 'string', + 'enum' => array( 'POST', 'PUT', 'PATCH', 'DELETE' ), + 'default' => 'POST', + ), + 'path' => array( + 'type' => 'string', + 'required' => true, + ), + 'body' => array( + 'type' => 'object', + 'properties' => array(), + 'additionalProperties' => true, + ), + 'headers' => array( + 'type' => 'object', + 'properties' => array(), + 'additionalProperties' => array( + 'type' => array( 'string', 'array' ), + 'items' => array( + 'type' => 'string', + ), + ), + ), + ), + ), + ), + ), + ), ); } @@ -117,7 +162,7 @@ */ public function check_authentication() { /** - * Filters REST authentication errors. + * Filters REST API authentication errors. * * This is used to pass a WP_Error from an authentication method back to * the API. @@ -151,41 +196,13 @@ * list in JSON rather than an object/map. * * @since 4.4.0 + * @since 5.7.0 Converted to a wrapper of {@see rest_convert_error_to_response()}. * * @param WP_Error $error WP_Error instance. * @return WP_REST_Response List of associative arrays with code and message keys. */ protected function error_to_response( $error ) { - $error_data = $error->get_error_data(); - - if ( is_array( $error_data ) && isset( $error_data['status'] ) ) { - $status = $error_data['status']; - } else { - $status = 500; - } - - $errors = array(); - - foreach ( (array) $error->errors as $code => $messages ) { - foreach ( (array) $messages as $message ) { - $errors[] = array( - 'code' => $code, - 'message' => $message, - 'data' => $error->get_error_data( $code ), - ); - } - } - - $data = $errors[0]; - if ( count( $errors ) > 1 ) { - // Remove the primary error. - array_shift( $errors ); - $data['additional_errors'] = $errors; - } - - $response = new WP_REST_Response( $data, $status ); - - return $response; + return rest_convert_error_to_response( $error ); } /** @@ -214,7 +231,7 @@ } /** - * Handles serving an API request. + * Handles serving a REST API request. * * Matches the current server URI to a route and runs the first matching * callback then outputs a JSON representation of the returned value. @@ -223,12 +240,45 @@ * * @see WP_REST_Server::dispatch() * + * @global WP_User $current_user The currently authenticated user. + * * @param string $path Optional. The request route. If not set, `$_SERVER['PATH_INFO']` will be used. * Default null. * @return null|false Null if not served and a HEAD request, false otherwise. */ public function serve_request( $path = null ) { - $content_type = isset( $_GET['_jsonp'] ) ? 'application/javascript' : 'application/json'; + /* @var WP_User|null $current_user */ + global $current_user; + + if ( $current_user instanceof WP_User && ! $current_user->exists() ) { + /* + * If there is no current user authenticated via other means, clear + * the cached lack of user, so that an authenticate check can set it + * properly. + * + * This is done because for authentications such as Application + * Passwords, we don't want it to be accepted unless the current HTTP + * request is a REST API request, which can't always be identified early + * enough in evaluation. + */ + $current_user = null; + } + + /** + * Filters whether JSONP is enabled for the REST API. + * + * @since 4.4.0 + * + * @param bool $jsonp_enabled Whether JSONP is enabled. Default true. + */ + $jsonp_enabled = apply_filters( 'rest_jsonp_enabled', true ); + + $jsonp_callback = false; + if ( isset( $_GET['_jsonp'] ) ) { + $jsonp_callback = $_GET['_jsonp']; + } + + $content_type = ( $jsonp_callback && $jsonp_enabled ) ? 'application/javascript' : 'application/json'; $this->send_header( 'Content-Type', $content_type . '; charset=' . get_option( 'blog_charset' ) ); $this->send_header( 'X-Robots-Tag', 'noindex' ); @@ -246,11 +296,11 @@ $expose_headers = array( 'X-WP-Total', 'X-WP-TotalPages', 'Link' ); /** - * Filters the list of response headers that are exposed to CORS requests. + * Filters the list of response headers that are exposed to REST API CORS requests. * * @since 5.5.0 * - * @param string[] $expose_headers The list of headers to expose. + * @param string[] $expose_headers The list of response headers to expose. */ $expose_headers = apply_filters( 'rest_exposed_cors_headers', $expose_headers ); @@ -265,7 +315,7 @@ ); /** - * Filters the list of request headers that are allowed for CORS requests. + * Filters the list of request headers that are allowed for REST API CORS requests. * * The allowed headers are passed to the browser to specify which * headers can be passed to the REST API. By default, we allow the @@ -274,14 +324,14 @@ * * @since 5.5.0 * - * @param string[] $allow_headers The list of headers to allow. + * @param string[] $allow_headers The list of request headers to allow. */ $allow_headers = apply_filters( 'rest_allowed_cors_headers', $allow_headers ); $this->send_header( 'Access-Control-Allow-Headers', implode( ', ', $allow_headers ) ); /** - * Send nocache headers on authenticated requests. + * Filters whether to send nocache headers on a REST API request. * * @since 4.4.0 * @@ -303,7 +353,7 @@ * * @since 4.4.0 * @deprecated 4.7.0 Use the {@see 'rest_authentication_errors'} filter to - * restrict access to the API. + * restrict access to the REST API. * * @param bool $rest_enabled Whether the REST API is enabled. Default true. */ @@ -319,24 +369,12 @@ ) ); - /** - * Filters whether jsonp is enabled. - * - * @since 4.4.0 - * - * @param bool $jsonp_enabled Whether jsonp is enabled. Default true. - */ - $jsonp_enabled = apply_filters( 'rest_jsonp_enabled', true ); - - $jsonp_callback = null; - - if ( isset( $_GET['_jsonp'] ) ) { + if ( $jsonp_callback ) { if ( ! $jsonp_enabled ) { echo $this->json_error( 'rest_callback_disabled', __( 'JSONP support is disabled on this site.' ), 400 ); return false; } - $jsonp_callback = $_GET['_jsonp']; if ( ! wp_check_jsonp_callback( $jsonp_callback ) ) { echo $this->json_error( 'rest_callback_invalid', __( 'Invalid JSONP callback function.' ), 400 ); return false; @@ -385,15 +423,15 @@ } /** - * Filters the API response. + * Filters the REST API response. * * Allows modification of the response before returning. * * @since 4.4.0 * @since 4.5.0 Applied to embedded responses. * - * @param WP_HTTP_Response $result Result to send to the client. Usually a WP_REST_Response. - * @param WP_REST_Server $this Server instance. + * @param WP_HTTP_Response $result Result to send to the client. Usually a `WP_REST_Response`. + * @param WP_REST_Server $server Server instance. * @param WP_REST_Request $request Request used to generate the response. */ $result = apply_filters( 'rest_post_dispatch', rest_ensure_response( $result ), $this, $request ); @@ -411,7 +449,7 @@ $this->set_status( $code ); /** - * Filters whether the request has already been served. + * Filters whether the REST API request has already been served. * * Allow sending the request manually - by returning true, the API result * will not be sent to the client. @@ -420,9 +458,9 @@ * * @param bool $served Whether the request has already been served. * Default false. - * @param WP_HTTP_Response $result Result to send to the client. Usually a WP_REST_Response. + * @param WP_HTTP_Response $result Result to send to the client. Usually a `WP_REST_Response`. * @param WP_REST_Request $request Request used to generate the response. - * @param WP_REST_Server $this Server instance. + * @param WP_REST_Server $server Server instance. */ $served = apply_filters( 'rest_pre_serve_request', false, $result, $request, $this ); @@ -436,7 +474,7 @@ $result = $this->response_to_data( $result, $embed ); /** - * Filters the API response. + * Filters the REST API response. * * Allows modification of the response data after inserting * embedded data (if any) and before echoing the response data. @@ -444,7 +482,7 @@ * @since 4.8.1 * * @param array $result Response data to send to the client. - * @param WP_REST_Server $this Server instance. + * @param WP_REST_Server $server Server instance. * @param WP_REST_Request $request Request used to generate the response. */ $result = apply_filters( 'rest_pre_echo_response', $result, $this, $request ); @@ -466,7 +504,7 @@ ); $result = $this->error_to_response( $json_error_obj ); - $result = wp_json_encode( $result->data[0] ); + $result = wp_json_encode( $result->data ); } if ( $jsonp_callback ) { @@ -493,7 +531,7 @@ * Data with sub-requests embedded. * * @type array $_links Links. - * @type array $_embedded Embeddeds. + * @type array $_embedded Embedded objects. * } */ public function response_to_data( $response, $embed ) { @@ -617,7 +655,7 @@ * Data with sub-requests embedded. * * @type array $_links Links. - * @type array $_embedded Embeddeds. + * @type array $_embedded Embedded objects. * } */ protected function embed_links( $data, $embed = true ) { @@ -704,11 +742,17 @@ ); /** - * Filters the enveloped form of a response. + * Filters the enveloped form of a REST API response. * * @since 4.4.0 * - * @param array $envelope Envelope data. + * @param array $envelope { + * Envelope data. + * + * @type array $body Response data. + * @type int $status The 3-digit HTTP status code. + * @type array $headers Map of header name to header value. + * } * @param WP_REST_Response $response Original response data. */ $envelope = apply_filters( 'rest_envelope_response', $envelope, $response ); @@ -793,7 +837,7 @@ } /** - * Filters the array of available endpoints. + * Filters the array of available REST API endpoints. * * @since 4.4.0 * @@ -893,7 +937,7 @@ */ public function dispatch( $request ) { /** - * Filters the pre-calculated result of a REST dispatch request. + * Filters the pre-calculated result of a REST API dispatch request. * * Allow hijacking the request before dispatching by returning a non-empty. The returned value * will be used to serve the request instead. @@ -902,7 +946,7 @@ * * @param mixed $result Response to replace the requested version with. Can be anything * a normal endpoint can return, or null to not hijack the request. - * @param WP_REST_Server $this Server instance. + * @param WP_REST_Server $server Server instance. * @param WP_REST_Request $request Request used to generate the response. */ $result = apply_filters( 'rest_pre_dispatch', null, $this, $request ); @@ -911,6 +955,48 @@ return $result; } + $error = null; + $matched = $this->match_request_to_handler( $request ); + + if ( is_wp_error( $matched ) ) { + return $this->error_to_response( $matched ); + } + + list( $route, $handler ) = $matched; + + if ( ! is_callable( $handler['callback'] ) ) { + $error = new WP_Error( + 'rest_invalid_handler', + __( 'The handler for the route is invalid.' ), + array( 'status' => 500 ) + ); + } + + if ( ! is_wp_error( $error ) ) { + $check_required = $request->has_valid_params(); + if ( is_wp_error( $check_required ) ) { + $error = $check_required; + } else { + $check_sanitized = $request->sanitize_params(); + if ( is_wp_error( $check_sanitized ) ) { + $error = $check_sanitized; + } + } + } + + return $this->respond_to_request( $request, $route, $handler, $error ); + } + + /** + * Matches a request object to its handler. + * + * @access private + * @since 5.6.0 + * + * @param WP_REST_Request $request The request object. + * @return array|WP_Error The route and request handler on success or a WP_Error instance if no handler was found. + */ + protected function match_request_to_handler( $request ) { $method = $request->get_method(); $path = $request->get_route(); @@ -957,145 +1043,140 @@ } if ( ! is_callable( $callback ) ) { - $response = new WP_Error( - 'rest_invalid_handler', - __( 'The handler for the route is invalid' ), - array( 'status' => 500 ) - ); + return array( $route, $handler ); } - if ( ! is_wp_error( $response ) ) { - // Remove the redundant preg_match argument. - unset( $args[0] ); - - $request->set_url_params( $args ); - $request->set_attributes( $handler ); - - $defaults = array(); + $request->set_url_params( $args ); + $request->set_attributes( $handler ); - foreach ( $handler['args'] as $arg => $options ) { - if ( isset( $options['default'] ) ) { - $defaults[ $arg ] = $options['default']; - } - } - - $request->set_default_params( $defaults ); + $defaults = array(); - $check_required = $request->has_valid_params(); - if ( is_wp_error( $check_required ) ) { - $response = $check_required; - } else { - $check_sanitized = $request->sanitize_params(); - if ( is_wp_error( $check_sanitized ) ) { - $response = $check_sanitized; - } + foreach ( $handler['args'] as $arg => $options ) { + if ( isset( $options['default'] ) ) { + $defaults[ $arg ] = $options['default']; } } - /** - * Filters the response before executing any REST API callbacks. - * - * Allows plugins to perform additional validation after a - * request is initialized and matched to a registered route, - * but before it is executed. - * - * Note that this filter will not be called for requests that - * fail to authenticate or match to a registered route. - * - * @since 4.7.0 - * - * @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client. Usually a WP_REST_Response or WP_Error. - * @param array $handler Route handler used for the request. - * @param WP_REST_Request $request Request used to generate the response. - */ - $response = apply_filters( 'rest_request_before_callbacks', $response, $handler, $request ); - - if ( ! is_wp_error( $response ) ) { - // Check permission specified on the route. - if ( ! empty( $handler['permission_callback'] ) ) { - $permission = call_user_func( $handler['permission_callback'], $request ); - - if ( is_wp_error( $permission ) ) { - $response = $permission; - } elseif ( false === $permission || null === $permission ) { - $response = new WP_Error( - 'rest_forbidden', - __( 'Sorry, you are not allowed to do that.' ), - array( 'status' => rest_authorization_required_code() ) - ); - } - } - } + $request->set_default_params( $defaults ); - if ( ! is_wp_error( $response ) ) { - /** - * Filters the REST dispatch request result. - * - * Allow plugins to override dispatching the request. - * - * @since 4.4.0 - * @since 4.5.0 Added `$route` and `$handler` parameters. - * - * @param mixed $dispatch_result Dispatch result, will be used if not empty. - * @param WP_REST_Request $request Request used to generate the response. - * @param string $route Route matched for the request. - * @param array $handler Route handler used for the request. - */ - $dispatch_result = apply_filters( 'rest_dispatch_request', null, $request, $route, $handler ); - - // Allow plugins to halt the request via this filter. - if ( null !== $dispatch_result ) { - $response = $dispatch_result; - } else { - $response = call_user_func( $callback, $request ); - } - } - - /** - * Filters the response immediately after executing any REST API - * callbacks. - * - * Allows plugins to perform any needed cleanup, for example, - * to undo changes made during the {@see 'rest_request_before_callbacks'} - * filter. - * - * Note that this filter will not be called for requests that - * fail to authenticate or match to a registered route. - * - * Note that an endpoint's `permission_callback` can still be - * called after this filter - see `rest_send_allow_header()`. - * - * @since 4.7.0 - * - * @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client. Usually a WP_REST_Response or WP_Error. - * @param array $handler Route handler used for the request. - * @param WP_REST_Request $request Request used to generate the response. - */ - $response = apply_filters( 'rest_request_after_callbacks', $response, $handler, $request ); - - if ( is_wp_error( $response ) ) { - $response = $this->error_to_response( $response ); - } else { - $response = rest_ensure_response( $response ); - } - - $response->set_matched_route( $route ); - $response->set_matched_handler( $handler ); - - return $response; + return array( $route, $handler ); } } - return $this->error_to_response( - new WP_Error( - 'rest_no_route', - __( 'No route was found matching the URL and request method' ), - array( 'status' => 404 ) - ) + return new WP_Error( + 'rest_no_route', + __( 'No route was found matching the URL and request method.' ), + array( 'status' => 404 ) ); } /** + * Dispatches the request to the callback handler. + * + * @access private + * @since 5.6.0 + * + * @param WP_REST_Request $request The request object. + * @param array $handler The matched route handler. + * @param string $route The matched route regex. + * @param WP_Error|null $response The current error object if any. + * @return WP_REST_Response + */ + protected function respond_to_request( $request, $route, $handler, $response ) { + /** + * Filters the response before executing any REST API callbacks. + * + * Allows plugins to perform additional validation after a + * request is initialized and matched to a registered route, + * but before it is executed. + * + * Note that this filter will not be called for requests that + * fail to authenticate or match to a registered route. + * + * @since 4.7.0 + * + * @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client. + * Usually a WP_REST_Response or WP_Error. + * @param array $handler Route handler used for the request. + * @param WP_REST_Request $request Request used to generate the response. + */ + $response = apply_filters( 'rest_request_before_callbacks', $response, $handler, $request ); + + // Check permission specified on the route. + if ( ! is_wp_error( $response ) && ! empty( $handler['permission_callback'] ) ) { + $permission = call_user_func( $handler['permission_callback'], $request ); + + if ( is_wp_error( $permission ) ) { + $response = $permission; + } elseif ( false === $permission || null === $permission ) { + $response = new WP_Error( + 'rest_forbidden', + __( 'Sorry, you are not allowed to do that.' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + } + + if ( ! is_wp_error( $response ) ) { + /** + * Filters the REST API dispatch request result. + * + * Allow plugins to override dispatching the request. + * + * @since 4.4.0 + * @since 4.5.0 Added `$route` and `$handler` parameters. + * + * @param mixed $dispatch_result Dispatch result, will be used if not empty. + * @param WP_REST_Request $request Request used to generate the response. + * @param string $route Route matched for the request. + * @param array $handler Route handler used for the request. + */ + $dispatch_result = apply_filters( 'rest_dispatch_request', null, $request, $route, $handler ); + + // Allow plugins to halt the request via this filter. + if ( null !== $dispatch_result ) { + $response = $dispatch_result; + } else { + $response = call_user_func( $handler['callback'], $request ); + } + } + + /** + * Filters the response immediately after executing any REST API + * callbacks. + * + * Allows plugins to perform any needed cleanup, for example, + * to undo changes made during the {@see 'rest_request_before_callbacks'} + * filter. + * + * Note that this filter will not be called for requests that + * fail to authenticate or match to a registered route. + * + * Note that an endpoint's `permission_callback` can still be + * called after this filter - see `rest_send_allow_header()`. + * + * @since 4.7.0 + * + * @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client. + * Usually a WP_REST_Response or WP_Error. + * @param array $handler Route handler used for the request. + * @param WP_REST_Request $request Request used to generate the response. + */ + $response = apply_filters( 'rest_request_after_callbacks', $response, $handler, $request ); + + if ( is_wp_error( $response ) ) { + $response = $this->error_to_response( $response ); + } else { + $response = rest_ensure_response( $response ); + } + + $response->set_matched_route( $route ); + $response->set_matched_handler( $handler ); + + return $response; + } + + /** * Returns if an error occurred during most recent JSON encode/decode. * * Strings to be translated will be in format like @@ -1103,7 +1184,7 @@ * * @since 4.4.0 * - * @return bool|string Boolean false or string error message. + * @return false|string Boolean false or string error message. */ protected function get_json_last_error() { $last_error_code = json_last_error(); @@ -1144,11 +1225,12 @@ ); $response = new WP_REST_Response( $available ); - - $response->add_link( 'help', 'http://v2.wp-api.org/' ); + $response->add_link( 'help', 'https://developer.wordpress.org/rest-api/' ); + $this->add_active_theme_link_to_index( $response ); + $this->add_site_logo_to_index( $response ); /** - * Filters the API root index data. + * Filters the REST API root index data. * * This contains the data describing the API. This includes information * about supported authentication schemes, supported namespaces, routes @@ -1162,6 +1244,58 @@ } /** + * Adds a link to the active theme for users who have proper permissions. + * + * @since 5.7.0 + * + * @param WP_REST_Response $response REST API response. + */ + protected function add_active_theme_link_to_index( WP_REST_Response $response ) { + $should_add = current_user_can( 'switch_themes' ) || current_user_can( 'manage_network_themes' ); + + if ( ! $should_add && current_user_can( 'edit_posts' ) ) { + $should_add = true; + } + + if ( ! $should_add ) { + foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) { + if ( current_user_can( $post_type->cap->edit_posts ) ) { + $should_add = true; + break; + } + } + } + + if ( $should_add ) { + $theme = wp_get_theme(); + $response->add_link( 'https://api.w.org/active-theme', rest_url( 'wp/v2/themes/' . $theme->get_stylesheet() ) ); + } + } + + /** + * Exposes the site logo through the WordPress REST API. + * This is used for fetching this information when user has no rights + * to update settings. + * + * @since 5.8.0 + * + * @param WP_REST_Response $response REST API response. + */ + protected function add_site_logo_to_index( WP_REST_Response $response ) { + $site_logo_id = get_theme_mod( 'custom_logo' ); + $response->data['site_logo'] = $site_logo_id; + if ( $site_logo_id ) { + $response->add_link( + 'https://api.w.org/featuredmedia', + rest_url( 'wp/v2/media/' . $site_logo_id ), + array( + 'embeddable' => true, + ) + ); + } + } + + /** * Retrieves the index for a namespace. * * @since 4.4.0 @@ -1194,7 +1328,7 @@ $response->add_link( 'up', rest_url( '/' ) ); /** - * Filters the namespace index data. + * Filters the REST API namespace index data. * * This typically is just the route data for the namespace, but you can * add any data you'd like here. @@ -1227,7 +1361,7 @@ } /** - * Filters the REST endpoint data. + * Filters the REST API endpoint data. * * @since 4.4.0 * @@ -1237,7 +1371,7 @@ } /** - * Filters the publicly-visible data for routes. + * Filters the publicly-visible data for REST API routes. * * This data is exposed on indexes and can be used by clients or * developers to investigate the site and find out how to use it. It @@ -1280,6 +1414,8 @@ } } + $allowed_schema_keywords = array_flip( rest_get_allowed_schema_keywords() ); + $route = preg_replace( '#\(\?P<(\w+?)>.*?\)#', '{$1}', $route ); foreach ( $callbacks as $callback ) { @@ -1297,24 +1433,9 @@ $endpoint_data['args'] = array(); foreach ( $callback['args'] as $key => $opts ) { - $arg_data = array( - 'required' => ! empty( $opts['required'] ), - ); - if ( isset( $opts['default'] ) ) { - $arg_data['default'] = $opts['default']; - } - if ( isset( $opts['enum'] ) ) { - $arg_data['enum'] = $opts['enum']; - } - if ( isset( $opts['description'] ) ) { - $arg_data['description'] = $opts['description']; - } - if ( isset( $opts['type'] ) ) { - $arg_data['type'] = $opts['type']; - } - if ( isset( $opts['items'] ) ) { - $arg_data['items'] = $opts['items']; - } + $arg_data = array_intersect_key( $opts, $allowed_schema_keywords ); + $arg_data['required'] = ! empty( $opts['required'] ); + $endpoint_data['args'][ $key ] = $arg_data; } } @@ -1342,6 +1463,180 @@ } /** + * Gets the maximum number of requests that can be included in a batch. + * + * @since 5.6.0 + * + * @return int The maximum requests. + */ + protected function get_max_batch_size() { + /** + * Filters the maximum number of REST API requests that can be included in a batch. + * + * @since 5.6.0 + * + * @param int $max_size The maximum size. + */ + return apply_filters( 'rest_get_max_batch_size', 25 ); + } + + /** + * Serves the batch/v1 request. + * + * @since 5.6.0 + * + * @param WP_REST_Request $batch_request The batch request object. + * @return WP_REST_Response The generated response object. + */ + public function serve_batch_request_v1( WP_REST_Request $batch_request ) { + $requests = array(); + + foreach ( $batch_request['requests'] as $args ) { + $parsed_url = wp_parse_url( $args['path'] ); + + if ( false === $parsed_url ) { + $requests[] = new WP_Error( 'parse_path_failed', __( 'Could not parse the path.' ), array( 'status' => 400 ) ); + + continue; + } + + $single_request = new WP_REST_Request( isset( $args['method'] ) ? $args['method'] : 'POST', $parsed_url['path'] ); + + if ( ! empty( $parsed_url['query'] ) ) { + $query_args = null; // Satisfy linter. + wp_parse_str( $parsed_url['query'], $query_args ); + $single_request->set_query_params( $query_args ); + } + + if ( ! empty( $args['body'] ) ) { + $single_request->set_body_params( $args['body'] ); + } + + if ( ! empty( $args['headers'] ) ) { + $single_request->set_headers( $args['headers'] ); + } + + $requests[] = $single_request; + } + + $matches = array(); + $validation = array(); + $has_error = false; + + foreach ( $requests as $single_request ) { + $match = $this->match_request_to_handler( $single_request ); + $matches[] = $match; + $error = null; + + if ( is_wp_error( $match ) ) { + $error = $match; + } + + if ( ! $error ) { + list( $route, $handler ) = $match; + + if ( isset( $handler['allow_batch'] ) ) { + $allow_batch = $handler['allow_batch']; + } else { + $route_options = $this->get_route_options( $route ); + $allow_batch = isset( $route_options['allow_batch'] ) ? $route_options['allow_batch'] : false; + } + + if ( ! is_array( $allow_batch ) || empty( $allow_batch['v1'] ) ) { + $error = new WP_Error( + 'rest_batch_not_allowed', + __( 'The requested route does not support batch requests.' ), + array( 'status' => 400 ) + ); + } + } + + if ( ! $error ) { + $check_required = $single_request->has_valid_params(); + if ( is_wp_error( $check_required ) ) { + $error = $check_required; + } + } + + if ( ! $error ) { + $check_sanitized = $single_request->sanitize_params(); + if ( is_wp_error( $check_sanitized ) ) { + $error = $check_sanitized; + } + } + + if ( $error ) { + $has_error = true; + $validation[] = $error; + } else { + $validation[] = true; + } + } + + $responses = array(); + + if ( $has_error && 'require-all-validate' === $batch_request['validation'] ) { + foreach ( $validation as $valid ) { + if ( is_wp_error( $valid ) ) { + $responses[] = $this->envelope_response( $this->error_to_response( $valid ), false )->get_data(); + } else { + $responses[] = null; + } + } + + return new WP_REST_Response( + array( + 'failed' => 'validation', + 'responses' => $responses, + ), + WP_Http::MULTI_STATUS + ); + } + + foreach ( $requests as $i => $single_request ) { + $clean_request = clone $single_request; + $clean_request->set_url_params( array() ); + $clean_request->set_attributes( array() ); + $clean_request->set_default_params( array() ); + + /** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */ + $result = apply_filters( 'rest_pre_dispatch', null, $this, $clean_request ); + + if ( empty( $result ) ) { + $match = $matches[ $i ]; + $error = null; + + if ( is_wp_error( $validation[ $i ] ) ) { + $error = $validation[ $i ]; + } + + if ( is_wp_error( $match ) ) { + $result = $this->error_to_response( $match ); + } else { + list( $route, $handler ) = $match; + + if ( ! $error && ! is_callable( $handler['callback'] ) ) { + $error = new WP_Error( + 'rest_invalid_handler', + __( 'The handler for the route is invalid' ), + array( 'status' => 500 ) + ); + } + + $result = $this->respond_to_request( $single_request, $route, $handler, $error ); + } + } + + /** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */ + $result = apply_filters( 'rest_post_dispatch', rest_ensure_response( $result ), $this, $single_request ); + + $responses[] = $this->envelope_response( $result, false )->get_data(); + } + + return new WP_REST_Response( array( 'responses' => $responses ), WP_Http::MULTI_STATUS ); + } + + /** * Sends an HTTP status code. * * @since 4.4.0