wp/wp-includes/rest-api/class-wp-rest-server.php
changeset 18 be944660c56a
parent 16 a86126ab1dd4
child 19 3d72ae0968f4
--- 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