1<?php
2/**
3 * REST API: WP_REST_Server class
4 *
5 * @package WordPress
6 * @subpackage REST_API
7 * @since 4.4.0
8 */
9
10/**
11 * Core class used to implement the WordPress REST API server.
12 *
13 * @since 4.4.0
14 */
15class WP_REST_Server {
16
17	/**
18	 * Alias for GET transport method.
19	 *
20	 * @since 4.4.0
21	 * @var string
22	 */
23	const READABLE = 'GET';
24
25	/**
26	 * Alias for POST transport method.
27	 *
28	 * @since 4.4.0
29	 * @var string
30	 */
31	const CREATABLE = 'POST';
32
33	/**
34	 * Alias for POST, PUT, PATCH transport methods together.
35	 *
36	 * @since 4.4.0
37	 * @var string
38	 */
39	const EDITABLE = 'POST, PUT, PATCH';
40
41	/**
42	 * Alias for DELETE transport method.
43	 *
44	 * @since 4.4.0
45	 * @var string
46	 */
47	const DELETABLE = 'DELETE';
48
49	/**
50	 * Alias for GET, POST, PUT, PATCH & DELETE transport methods together.
51	 *
52	 * @since 4.4.0
53	 * @var string
54	 */
55	const ALLMETHODS = 'GET, POST, PUT, PATCH, DELETE';
56
57	/**
58	 * Namespaces registered to the server.
59	 *
60	 * @since 4.4.0
61	 * @var array
62	 */
63	protected $namespaces = array();
64
65	/**
66	 * Endpoints registered to the server.
67	 *
68	 * @since 4.4.0
69	 * @var array
70	 */
71	protected $endpoints = array();
72
73	/**
74	 * Options defined for the routes.
75	 *
76	 * @since 4.4.0
77	 * @var array
78	 */
79	protected $route_options = array();
80
81	/**
82	 * Caches embedded requests.
83	 *
84	 * @since 5.4.0
85	 * @var array
86	 */
87	protected $embed_cache = array();
88
89	/**
90	 * Instantiates the REST server.
91	 *
92	 * @since 4.4.0
93	 */
94	public function __construct() {
95		$this->endpoints = array(
96			// Meta endpoints.
97			'/'         => array(
98				'callback' => array( $this, 'get_index' ),
99				'methods'  => 'GET',
100				'args'     => array(
101					'context' => array(
102						'default' => 'view',
103					),
104				),
105			),
106			'/batch/v1' => array(
107				'callback' => array( $this, 'serve_batch_request_v1' ),
108				'methods'  => 'POST',
109				'args'     => array(
110					'validation' => array(
111						'type'    => 'string',
112						'enum'    => array( 'require-all-validate', 'normal' ),
113						'default' => 'normal',
114					),
115					'requests'   => array(
116						'required' => true,
117						'type'     => 'array',
118						'maxItems' => $this->get_max_batch_size(),
119						'items'    => array(
120							'type'       => 'object',
121							'properties' => array(
122								'method'  => array(
123									'type'    => 'string',
124									'enum'    => array( 'POST', 'PUT', 'PATCH', 'DELETE' ),
125									'default' => 'POST',
126								),
127								'path'    => array(
128									'type'     => 'string',
129									'required' => true,
130								),
131								'body'    => array(
132									'type'                 => 'object',
133									'properties'           => array(),
134									'additionalProperties' => true,
135								),
136								'headers' => array(
137									'type'                 => 'object',
138									'properties'           => array(),
139									'additionalProperties' => array(
140										'type'  => array( 'string', 'array' ),
141										'items' => array(
142											'type' => 'string',
143										),
144									),
145								),
146							),
147						),
148					),
149				),
150			),
151		);
152	}
153
154
155	/**
156	 * Checks the authentication headers if supplied.
157	 *
158	 * @since 4.4.0
159	 *
160	 * @return WP_Error|null WP_Error indicates unsuccessful login, null indicates successful
161	 *                       or no authentication provided
162	 */
163	public function check_authentication() {
164		/**
165		 * Filters REST API authentication errors.
166		 *
167		 * This is used to pass a WP_Error from an authentication method back to
168		 * the API.
169		 *
170		 * Authentication methods should check first if they're being used, as
171		 * multiple authentication methods can be enabled on a site (cookies,
172		 * HTTP basic auth, OAuth). If the authentication method hooked in is
173		 * not actually being attempted, null should be returned to indicate
174		 * another authentication method should check instead. Similarly,
175		 * callbacks should ensure the value is `null` before checking for
176		 * errors.
177		 *
178		 * A WP_Error instance can be returned if an error occurs, and this should
179		 * match the format used by API methods internally (that is, the `status`
180		 * data should be used). A callback can return `true` to indicate that
181		 * the authentication method was used, and it succeeded.
182		 *
183		 * @since 4.4.0
184		 *
185		 * @param WP_Error|null|true $errors WP_Error if authentication error, null if authentication
186		 *                                   method wasn't used, true if authentication succeeded.
187		 */
188		return apply_filters( 'rest_authentication_errors', null );
189	}
190
191	/**
192	 * Converts an error to a response object.
193	 *
194	 * This iterates over all error codes and messages to change it into a flat
195	 * array. This enables simpler client behaviour, as it is represented as a
196	 * list in JSON rather than an object/map.
197	 *
198	 * @since 4.4.0
199	 * @since 5.7.0 Converted to a wrapper of {@see rest_convert_error_to_response()}.
200	 *
201	 * @param WP_Error $error WP_Error instance.
202	 * @return WP_REST_Response List of associative arrays with code and message keys.
203	 */
204	protected function error_to_response( $error ) {
205		return rest_convert_error_to_response( $error );
206	}
207
208	/**
209	 * Retrieves an appropriate error representation in JSON.
210	 *
211	 * Note: This should only be used in WP_REST_Server::serve_request(), as it
212	 * cannot handle WP_Error internally. All callbacks and other internal methods
213	 * should instead return a WP_Error with the data set to an array that includes
214	 * a 'status' key, with the value being the HTTP status to send.
215	 *
216	 * @since 4.4.0
217	 *
218	 * @param string $code    WP_Error-style code.
219	 * @param string $message Human-readable message.
220	 * @param int    $status  Optional. HTTP status code to send. Default null.
221	 * @return string JSON representation of the error
222	 */
223	protected function json_error( $code, $message, $status = null ) {
224		if ( $status ) {
225			$this->set_status( $status );
226		}
227
228		$error = compact( 'code', 'message' );
229
230		return wp_json_encode( $error );
231	}
232
233	/**
234	 * Handles serving a REST API request.
235	 *
236	 * Matches the current server URI to a route and runs the first matching
237	 * callback then outputs a JSON representation of the returned value.
238	 *
239	 * @since 4.4.0
240	 *
241	 * @see WP_REST_Server::dispatch()
242	 *
243	 * @global WP_User $current_user The currently authenticated user.
244	 *
245	 * @param string $path Optional. The request route. If not set, `$_SERVER['PATH_INFO']` will be used.
246	 *                     Default null.
247	 * @return null|false Null if not served and a HEAD request, false otherwise.
248	 */
249	public function serve_request( $path = null ) {
250		/* @var WP_User|null $current_user */
251		global $current_user;
252
253		if ( $current_user instanceof WP_User && ! $current_user->exists() ) {
254			/*
255			 * If there is no current user authenticated via other means, clear
256			 * the cached lack of user, so that an authenticate check can set it
257			 * properly.
258			 *
259			 * This is done because for authentications such as Application
260			 * Passwords, we don't want it to be accepted unless the current HTTP
261			 * request is a REST API request, which can't always be identified early
262			 * enough in evaluation.
263			 */
264			$current_user = null;
265		}
266
267		/**
268		 * Filters whether JSONP is enabled for the REST API.
269		 *
270		 * @since 4.4.0
271		 *
272		 * @param bool $jsonp_enabled Whether JSONP is enabled. Default true.
273		 */
274		$jsonp_enabled = apply_filters( 'rest_jsonp_enabled', true );
275
276		$jsonp_callback = false;
277		if ( isset( $_GET['_jsonp'] ) ) {
278			$jsonp_callback = $_GET['_jsonp'];
279		}
280
281		$content_type = ( $jsonp_callback && $jsonp_enabled ) ? 'application/javascript' : 'application/json';
282		$this->send_header( 'Content-Type', $content_type . '; charset=' . get_option( 'blog_charset' ) );
283		$this->send_header( 'X-Robots-Tag', 'noindex' );
284
285		$api_root = get_rest_url();
286		if ( ! empty( $api_root ) ) {
287			$this->send_header( 'Link', '<' . esc_url_raw( $api_root ) . '>; rel="https://api.w.org/"' );
288		}
289
290		/*
291		 * Mitigate possible JSONP Flash attacks.
292		 *
293		 * https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
294		 */
295		$this->send_header( 'X-Content-Type-Options', 'nosniff' );
296		$expose_headers = array( 'X-WP-Total', 'X-WP-TotalPages', 'Link' );
297
298		/**
299		 * Filters the list of response headers that are exposed to REST API CORS requests.
300		 *
301		 * @since 5.5.0
302		 *
303		 * @param string[] $expose_headers The list of response headers to expose.
304		 */
305		$expose_headers = apply_filters( 'rest_exposed_cors_headers', $expose_headers );
306
307		$this->send_header( 'Access-Control-Expose-Headers', implode( ', ', $expose_headers ) );
308
309		$allow_headers = array(
310			'Authorization',
311			'X-WP-Nonce',
312			'Content-Disposition',
313			'Content-MD5',
314			'Content-Type',
315		);
316
317		/**
318		 * Filters the list of request headers that are allowed for REST API CORS requests.
319		 *
320		 * The allowed headers are passed to the browser to specify which
321		 * headers can be passed to the REST API. By default, we allow the
322		 * Content-* headers needed to upload files to the media endpoints.
323		 * As well as the Authorization and Nonce headers for allowing authentication.
324		 *
325		 * @since 5.5.0
326		 *
327		 * @param string[] $allow_headers The list of request headers to allow.
328		 */
329		$allow_headers = apply_filters( 'rest_allowed_cors_headers', $allow_headers );
330
331		$this->send_header( 'Access-Control-Allow-Headers', implode( ', ', $allow_headers ) );
332
333		/**
334		 * Filters whether to send nocache headers on a REST API request.
335		 *
336		 * @since 4.4.0
337		 *
338		 * @param bool $rest_send_nocache_headers Whether to send no-cache headers.
339		 */
340		$send_no_cache_headers = apply_filters( 'rest_send_nocache_headers', is_user_logged_in() );
341		if ( $send_no_cache_headers ) {
342			foreach ( wp_get_nocache_headers() as $header => $header_value ) {
343				if ( empty( $header_value ) ) {
344					$this->remove_header( $header );
345				} else {
346					$this->send_header( $header, $header_value );
347				}
348			}
349		}
350
351		/**
352		 * Filters whether the REST API is enabled.
353		 *
354		 * @since 4.4.0
355		 * @deprecated 4.7.0 Use the {@see 'rest_authentication_errors'} filter to
356		 *                   restrict access to the REST API.
357		 *
358		 * @param bool $rest_enabled Whether the REST API is enabled. Default true.
359		 */
360		apply_filters_deprecated(
361			'rest_enabled',
362			array( true ),
363			'4.7.0',
364			'rest_authentication_errors',
365			sprintf(
366				/* translators: %s: rest_authentication_errors */
367				__( 'The REST API can no longer be completely disabled, the %s filter can be used to restrict access to the API, instead.' ),
368				'rest_authentication_errors'
369			)
370		);
371
372		if ( $jsonp_callback ) {
373			if ( ! $jsonp_enabled ) {
374				echo $this->json_error( 'rest_callback_disabled', __( 'JSONP support is disabled on this site.' ), 400 );
375				return false;
376			}
377
378			if ( ! wp_check_jsonp_callback( $jsonp_callback ) ) {
379				echo $this->json_error( 'rest_callback_invalid', __( 'Invalid JSONP callback function.' ), 400 );
380				return false;
381			}
382		}
383
384		if ( empty( $path ) ) {
385			if ( isset( $_SERVER['PATH_INFO'] ) ) {
386				$path = $_SERVER['PATH_INFO'];
387			} else {
388				$path = '/';
389			}
390		}
391
392		$request = new WP_REST_Request( $_SERVER['REQUEST_METHOD'], $path );
393
394		$request->set_query_params( wp_unslash( $_GET ) );
395		$request->set_body_params( wp_unslash( $_POST ) );
396		$request->set_file_params( $_FILES );
397		$request->set_headers( $this->get_headers( wp_unslash( $_SERVER ) ) );
398		$request->set_body( self::get_raw_data() );
399
400		/*
401		 * HTTP method override for clients that can't use PUT/PATCH/DELETE. First, we check
402		 * $_GET['_method']. If that is not set, we check for the HTTP_X_HTTP_METHOD_OVERRIDE
403		 * header.
404		 */
405		if ( isset( $_GET['_method'] ) ) {
406			$request->set_method( $_GET['_method'] );
407		} elseif ( isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ) {
408			$request->set_method( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] );
409		}
410
411		$result = $this->check_authentication();
412
413		if ( ! is_wp_error( $result ) ) {
414			$result = $this->dispatch( $request );
415		}
416
417		// Normalize to either WP_Error or WP_REST_Response...
418		$result = rest_ensure_response( $result );
419
420		// ...then convert WP_Error across.
421		if ( is_wp_error( $result ) ) {
422			$result = $this->error_to_response( $result );
423		}
424
425		/**
426		 * Filters the REST API response.
427		 *
428		 * Allows modification of the response before returning.
429		 *
430		 * @since 4.4.0
431		 * @since 4.5.0 Applied to embedded responses.
432		 *
433		 * @param WP_HTTP_Response $result  Result to send to the client. Usually a `WP_REST_Response`.
434		 * @param WP_REST_Server   $server  Server instance.
435		 * @param WP_REST_Request  $request Request used to generate the response.
436		 */
437		$result = apply_filters( 'rest_post_dispatch', rest_ensure_response( $result ), $this, $request );
438
439		// Wrap the response in an envelope if asked for.
440		if ( isset( $_GET['_envelope'] ) ) {
441			$result = $this->envelope_response( $result, isset( $_GET['_embed'] ) );
442		}
443
444		// Send extra data from response objects.
445		$headers = $result->get_headers();
446		$this->send_headers( $headers );
447
448		$code = $result->get_status();
449		$this->set_status( $code );
450
451		/**
452		 * Filters whether the REST API request has already been served.
453		 *
454		 * Allow sending the request manually - by returning true, the API result
455		 * will not be sent to the client.
456		 *
457		 * @since 4.4.0
458		 *
459		 * @param bool             $served  Whether the request has already been served.
460		 *                                           Default false.
461		 * @param WP_HTTP_Response $result  Result to send to the client. Usually a `WP_REST_Response`.
462		 * @param WP_REST_Request  $request Request used to generate the response.
463		 * @param WP_REST_Server   $server  Server instance.
464		 */
465		$served = apply_filters( 'rest_pre_serve_request', false, $result, $request, $this );
466
467		if ( ! $served ) {
468			if ( 'HEAD' === $request->get_method() ) {
469				return null;
470			}
471
472			// Embed links inside the request.
473			$embed  = isset( $_GET['_embed'] ) ? rest_parse_embed_param( $_GET['_embed'] ) : false;
474			$result = $this->response_to_data( $result, $embed );
475
476			/**
477			 * Filters the REST API response.
478			 *
479			 * Allows modification of the response data after inserting
480			 * embedded data (if any) and before echoing the response data.
481			 *
482			 * @since 4.8.1
483			 *
484			 * @param array            $result  Response data to send to the client.
485			 * @param WP_REST_Server   $server  Server instance.
486			 * @param WP_REST_Request  $request Request used to generate the response.
487			 */
488			$result = apply_filters( 'rest_pre_echo_response', $result, $this, $request );
489
490			// The 204 response shouldn't have a body.
491			if ( 204 === $code || null === $result ) {
492				return null;
493			}
494
495			$result = wp_json_encode( $result );
496
497			$json_error_message = $this->get_json_last_error();
498
499			if ( $json_error_message ) {
500				$json_error_obj = new WP_Error(
501					'rest_encode_error',
502					$json_error_message,
503					array( 'status' => 500 )
504				);
505
506				$result = $this->error_to_response( $json_error_obj );
507				$result = wp_json_encode( $result->data );
508			}
509
510			if ( $jsonp_callback ) {
511				// Prepend '/**/' to mitigate possible JSONP Flash attacks.
512				// https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
513				echo '/**/' . $jsonp_callback . '(' . $result . ')';
514			} else {
515				echo $result;
516			}
517		}
518
519		return null;
520	}
521
522	/**
523	 * Converts a response to data to send.
524	 *
525	 * @since 4.4.0
526	 * @since 5.4.0 The $embed parameter can now contain a list of link relations to include.
527	 *
528	 * @param WP_REST_Response $response Response object.
529	 * @param bool|string[]    $embed    Whether to embed all links, a filtered list of link relations, or no links.
530	 * @return array {
531	 *     Data with sub-requests embedded.
532	 *
533	 *     @type array $_links    Links.
534	 *     @type array $_embedded Embedded objects.
535	 * }
536	 */
537	public function response_to_data( $response, $embed ) {
538		$data  = $response->get_data();
539		$links = self::get_compact_response_links( $response );
540
541		if ( ! empty( $links ) ) {
542			// Convert links to part of the data.
543			$data['_links'] = $links;
544		}
545
546		if ( $embed ) {
547			$this->embed_cache = array();
548			// Determine if this is a numeric array.
549			if ( wp_is_numeric_array( $data ) ) {
550				foreach ( $data as $key => $item ) {
551					$data[ $key ] = $this->embed_links( $item, $embed );
552				}
553			} else {
554				$data = $this->embed_links( $data, $embed );
555			}
556			$this->embed_cache = array();
557		}
558
559		return $data;
560	}
561
562	/**
563	 * Retrieves links from a response.
564	 *
565	 * Extracts the links from a response into a structured hash, suitable for
566	 * direct output.
567	 *
568	 * @since 4.4.0
569	 *
570	 * @param WP_REST_Response $response Response to extract links from.
571	 * @return array Map of link relation to list of link hashes.
572	 */
573	public static function get_response_links( $response ) {
574		$links = $response->get_links();
575
576		if ( empty( $links ) ) {
577			return array();
578		}
579
580		// Convert links to part of the data.
581		$data = array();
582		foreach ( $links as $rel => $items ) {
583			$data[ $rel ] = array();
584
585			foreach ( $items as $item ) {
586				$attributes         = $item['attributes'];
587				$attributes['href'] = $item['href'];
588				$data[ $rel ][]     = $attributes;
589			}
590		}
591
592		return $data;
593	}
594
595	/**
596	 * Retrieves the CURIEs (compact URIs) used for relations.
597	 *
598	 * Extracts the links from a response into a structured hash, suitable for
599	 * direct output.
600	 *
601	 * @since 4.5.0
602	 *
603	 * @param WP_REST_Response $response Response to extract links from.
604	 * @return array Map of link relation to list of link hashes.
605	 */
606	public static function get_compact_response_links( $response ) {
607		$links = self::get_response_links( $response );
608
609		if ( empty( $links ) ) {
610			return array();
611		}
612
613		$curies      = $response->get_curies();
614		$used_curies = array();
615
616		foreach ( $links as $rel => $items ) {
617
618			// Convert $rel URIs to their compact versions if they exist.
619			foreach ( $curies as $curie ) {
620				$href_prefix = substr( $curie['href'], 0, strpos( $curie['href'], '{rel}' ) );
621				if ( strpos( $rel, $href_prefix ) !== 0 ) {
622					continue;
623				}
624
625				// Relation now changes from '$uri' to '$curie:$relation'.
626				$rel_regex = str_replace( '\{rel\}', '(.+)', preg_quote( $curie['href'], '!' ) );
627				preg_match( '!' . $rel_regex . '!', $rel, $matches );
628				if ( $matches ) {
629					$new_rel                       = $curie['name'] . ':' . $matches[1];
630					$used_curies[ $curie['name'] ] = $curie;
631					$links[ $new_rel ]             = $items;
632					unset( $links[ $rel ] );
633					break;
634				}
635			}
636		}
637
638		// Push the curies onto the start of the links array.
639		if ( $used_curies ) {
640			$links['curies'] = array_values( $used_curies );
641		}
642
643		return $links;
644	}
645
646	/**
647	 * Embeds the links from the data into the request.
648	 *
649	 * @since 4.4.0
650	 * @since 5.4.0 The $embed parameter can now contain a list of link relations to include.
651	 *
652	 * @param array         $data  Data from the request.
653	 * @param bool|string[] $embed Whether to embed all links or a filtered list of link relations.
654	 * @return array {
655	 *     Data with sub-requests embedded.
656	 *
657	 *     @type array $_links    Links.
658	 *     @type array $_embedded Embedded objects.
659	 * }
660	 */
661	protected function embed_links( $data, $embed = true ) {
662		if ( empty( $data['_links'] ) ) {
663			return $data;
664		}
665
666		$embedded = array();
667
668		foreach ( $data['_links'] as $rel => $links ) {
669			// If a list of relations was specified, and the link relation
670			// is not in the list of allowed relations, don't process the link.
671			if ( is_array( $embed ) && ! in_array( $rel, $embed, true ) ) {
672				continue;
673			}
674
675			$embeds = array();
676
677			foreach ( $links as $item ) {
678				// Determine if the link is embeddable.
679				if ( empty( $item['embeddable'] ) ) {
680					// Ensure we keep the same order.
681					$embeds[] = array();
682					continue;
683				}
684
685				if ( ! array_key_exists( $item['href'], $this->embed_cache ) ) {
686					// Run through our internal routing and serve.
687					$request = WP_REST_Request::from_url( $item['href'] );
688					if ( ! $request ) {
689						$embeds[] = array();
690						continue;
691					}
692
693					// Embedded resources get passed context=embed.
694					if ( empty( $request['context'] ) ) {
695						$request['context'] = 'embed';
696					}
697
698					$response = $this->dispatch( $request );
699
700					/** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */
701					$response = apply_filters( 'rest_post_dispatch', rest_ensure_response( $response ), $this, $request );
702
703					$this->embed_cache[ $item['href'] ] = $this->response_to_data( $response, false );
704				}
705
706				$embeds[] = $this->embed_cache[ $item['href'] ];
707			}
708
709			// Determine if any real links were found.
710			$has_links = count( array_filter( $embeds ) );
711
712			if ( $has_links ) {
713				$embedded[ $rel ] = $embeds;
714			}
715		}
716
717		if ( ! empty( $embedded ) ) {
718			$data['_embedded'] = $embedded;
719		}
720
721		return $data;
722	}
723
724	/**
725	 * Wraps the response in an envelope.
726	 *
727	 * The enveloping technique is used to work around browser/client
728	 * compatibility issues. Essentially, it converts the full HTTP response to
729	 * data instead.
730	 *
731	 * @since 4.4.0
732	 *
733	 * @param WP_REST_Response $response Response object.
734	 * @param bool             $embed    Whether links should be embedded.
735	 * @return WP_REST_Response New response with wrapped data
736	 */
737	public function envelope_response( $response, $embed ) {
738		$envelope = array(
739			'body'    => $this->response_to_data( $response, $embed ),
740			'status'  => $response->get_status(),
741			'headers' => $response->get_headers(),
742		);
743
744		/**
745		 * Filters the enveloped form of a REST API response.
746		 *
747		 * @since 4.4.0
748		 *
749		 * @param array            $envelope {
750		 *     Envelope data.
751		 *
752		 *     @type array $body    Response data.
753		 *     @type int   $status  The 3-digit HTTP status code.
754		 *     @type array $headers Map of header name to header value.
755		 * }
756		 * @param WP_REST_Response $response Original response data.
757		 */
758		$envelope = apply_filters( 'rest_envelope_response', $envelope, $response );
759
760		// Ensure it's still a response and return.
761		return rest_ensure_response( $envelope );
762	}
763
764	/**
765	 * Registers a route to the server.
766	 *
767	 * @since 4.4.0
768	 *
769	 * @param string $namespace  Namespace.
770	 * @param string $route      The REST route.
771	 * @param array  $route_args Route arguments.
772	 * @param bool   $override   Optional. Whether the route should be overridden if it already exists.
773	 *                           Default false.
774	 */
775	public function register_route( $namespace, $route, $route_args, $override = false ) {
776		if ( ! isset( $this->namespaces[ $namespace ] ) ) {
777			$this->namespaces[ $namespace ] = array();
778
779			$this->register_route(
780				$namespace,
781				'/' . $namespace,
782				array(
783					array(
784						'methods'  => self::READABLE,
785						'callback' => array( $this, 'get_namespace_index' ),
786						'args'     => array(
787							'namespace' => array(
788								'default' => $namespace,
789							),
790							'context'   => array(
791								'default' => 'view',
792							),
793						),
794					),
795				)
796			);
797		}
798
799		// Associative to avoid double-registration.
800		$this->namespaces[ $namespace ][ $route ] = true;
801		$route_args['namespace']                  = $namespace;
802
803		if ( $override || empty( $this->endpoints[ $route ] ) ) {
804			$this->endpoints[ $route ] = $route_args;
805		} else {
806			$this->endpoints[ $route ] = array_merge( $this->endpoints[ $route ], $route_args );
807		}
808	}
809
810	/**
811	 * Retrieves the route map.
812	 *
813	 * The route map is an associative array with path regexes as the keys. The
814	 * value is an indexed array with the callback function/method as the first
815	 * item, and a bitmask of HTTP methods as the second item (see the class
816	 * constants).
817	 *
818	 * Each route can be mapped to more than one callback by using an array of
819	 * the indexed arrays. This allows mapping e.g. GET requests to one callback
820	 * and POST requests to another.
821	 *
822	 * Note that the path regexes (array keys) must have @ escaped, as this is
823	 * used as the delimiter with preg_match()
824	 *
825	 * @since 4.4.0
826	 * @since 5.4.0 Add $namespace parameter.
827	 *
828	 * @param string $namespace Optionally, only return routes in the given namespace.
829	 * @return array `'/path/regex' => array( $callback, $bitmask )` or
830	 *               `'/path/regex' => array( array( $callback, $bitmask ), ...)`.
831	 */
832	public function get_routes( $namespace = '' ) {
833		$endpoints = $this->endpoints;
834
835		if ( $namespace ) {
836			$endpoints = wp_list_filter( $endpoints, array( 'namespace' => $namespace ) );
837		}
838
839		/**
840		 * Filters the array of available REST API endpoints.
841		 *
842		 * @since 4.4.0
843		 *
844		 * @param array $endpoints The available endpoints. An array of matching regex patterns, each mapped
845		 *                         to an array of callbacks for the endpoint. These take the format
846		 *                         `'/path/regex' => array( $callback, $bitmask )` or
847		 *                         `'/path/regex' => array( array( $callback, $bitmask ).
848		 */
849		$endpoints = apply_filters( 'rest_endpoints', $endpoints );
850
851		// Normalise the endpoints.
852		$defaults = array(
853			'methods'       => '',
854			'accept_json'   => false,
855			'accept_raw'    => false,
856			'show_in_index' => true,
857			'args'          => array(),
858		);
859
860		foreach ( $endpoints as $route => &$handlers ) {
861
862			if ( isset( $handlers['callback'] ) ) {
863				// Single endpoint, add one deeper.
864				$handlers = array( $handlers );
865			}
866
867			if ( ! isset( $this->route_options[ $route ] ) ) {
868				$this->route_options[ $route ] = array();
869			}
870
871			foreach ( $handlers as $key => &$handler ) {
872
873				if ( ! is_numeric( $key ) ) {
874					// Route option, move it to the options.
875					$this->route_options[ $route ][ $key ] = $handler;
876					unset( $handlers[ $key ] );
877					continue;
878				}
879
880				$handler = wp_parse_args( $handler, $defaults );
881
882				// Allow comma-separated HTTP methods.
883				if ( is_string( $handler['methods'] ) ) {
884					$methods = explode( ',', $handler['methods'] );
885				} elseif ( is_array( $handler['methods'] ) ) {
886					$methods = $handler['methods'];
887				} else {
888					$methods = array();
889				}
890
891				$handler['methods'] = array();
892
893				foreach ( $methods as $method ) {
894					$method                        = strtoupper( trim( $method ) );
895					$handler['methods'][ $method ] = true;
896				}
897			}
898		}
899
900		return $endpoints;
901	}
902
903	/**
904	 * Retrieves namespaces registered on the server.
905	 *
906	 * @since 4.4.0
907	 *
908	 * @return string[] List of registered namespaces.
909	 */
910	public function get_namespaces() {
911		return array_keys( $this->namespaces );
912	}
913
914	/**
915	 * Retrieves specified options for a route.
916	 *
917	 * @since 4.4.0
918	 *
919	 * @param string $route Route pattern to fetch options for.
920	 * @return array|null Data as an associative array if found, or null if not found.
921	 */
922	public function get_route_options( $route ) {
923		if ( ! isset( $this->route_options[ $route ] ) ) {
924			return null;
925		}
926
927		return $this->route_options[ $route ];
928	}
929
930	/**
931	 * Matches the request to a callback and call it.
932	 *
933	 * @since 4.4.0
934	 *
935	 * @param WP_REST_Request $request Request to attempt dispatching.
936	 * @return WP_REST_Response Response returned by the callback.
937	 */
938	public function dispatch( $request ) {
939		/**
940		 * Filters the pre-calculated result of a REST API dispatch request.
941		 *
942		 * Allow hijacking the request before dispatching by returning a non-empty. The returned value
943		 * will be used to serve the request instead.
944		 *
945		 * @since 4.4.0
946		 *
947		 * @param mixed           $result  Response to replace the requested version with. Can be anything
948		 *                                 a normal endpoint can return, or null to not hijack the request.
949		 * @param WP_REST_Server  $server  Server instance.
950		 * @param WP_REST_Request $request Request used to generate the response.
951		 */
952		$result = apply_filters( 'rest_pre_dispatch', null, $this, $request );
953
954		if ( ! empty( $result ) ) {
955			return $result;
956		}
957
958		$error   = null;
959		$matched = $this->match_request_to_handler( $request );
960
961		if ( is_wp_error( $matched ) ) {
962			return $this->error_to_response( $matched );
963		}
964
965		list( $route, $handler ) = $matched;
966
967		if ( ! is_callable( $handler['callback'] ) ) {
968			$error = new WP_Error(
969				'rest_invalid_handler',
970				__( 'The handler for the route is invalid.' ),
971				array( 'status' => 500 )
972			);
973		}
974
975		if ( ! is_wp_error( $error ) ) {
976			$check_required = $request->has_valid_params();
977			if ( is_wp_error( $check_required ) ) {
978				$error = $check_required;
979			} else {
980				$check_sanitized = $request->sanitize_params();
981				if ( is_wp_error( $check_sanitized ) ) {
982					$error = $check_sanitized;
983				}
984			}
985		}
986
987		return $this->respond_to_request( $request, $route, $handler, $error );
988	}
989
990	/**
991	 * Matches a request object to its handler.
992	 *
993	 * @access private
994	 * @since 5.6.0
995	 *
996	 * @param WP_REST_Request $request The request object.
997	 * @return array|WP_Error The route and request handler on success or a WP_Error instance if no handler was found.
998	 */
999	protected function match_request_to_handler( $request ) {
1000		$method = $request->get_method();
1001		$path   = $request->get_route();
1002
1003		$with_namespace = array();
1004
1005		foreach ( $this->get_namespaces() as $namespace ) {
1006			if ( 0 === strpos( trailingslashit( ltrim( $path, '/' ) ), $namespace ) ) {
1007				$with_namespace[] = $this->get_routes( $namespace );
1008			}
1009		}
1010
1011		if ( $with_namespace ) {
1012			$routes = array_merge( ...$with_namespace );
1013		} else {
1014			$routes = $this->get_routes();
1015		}
1016
1017		foreach ( $routes as $route => $handlers ) {
1018			$match = preg_match( '@^' . $route . '$@i', $path, $matches );
1019
1020			if ( ! $match ) {
1021				continue;
1022			}
1023
1024			$args = array();
1025
1026			foreach ( $matches as $param => $value ) {
1027				if ( ! is_int( $param ) ) {
1028					$args[ $param ] = $value;
1029				}
1030			}
1031
1032			foreach ( $handlers as $handler ) {
1033				$callback = $handler['callback'];
1034				$response = null;
1035
1036				// Fallback to GET method if no HEAD method is registered.
1037				$checked_method = $method;
1038				if ( 'HEAD' === $method && empty( $handler['methods']['HEAD'] ) ) {
1039					$checked_method = 'GET';
1040				}
1041				if ( empty( $handler['methods'][ $checked_method ] ) ) {
1042					continue;
1043				}
1044
1045				if ( ! is_callable( $callback ) ) {
1046					return array( $route, $handler );
1047				}
1048
1049				$request->set_url_params( $args );
1050				$request->set_attributes( $handler );
1051
1052				$defaults = array();
1053
1054				foreach ( $handler['args'] as $arg => $options ) {
1055					if ( isset( $options['default'] ) ) {
1056						$defaults[ $arg ] = $options['default'];
1057					}
1058				}
1059
1060				$request->set_default_params( $defaults );
1061
1062				return array( $route, $handler );
1063			}
1064		}
1065
1066		return new WP_Error(
1067			'rest_no_route',
1068			__( 'No route was found matching the URL and request method.' ),
1069			array( 'status' => 404 )
1070		);
1071	}
1072
1073	/**
1074	 * Dispatches the request to the callback handler.
1075	 *
1076	 * @access private
1077	 * @since 5.6.0
1078	 *
1079	 * @param WP_REST_Request $request  The request object.
1080	 * @param array           $handler  The matched route handler.
1081	 * @param string          $route    The matched route regex.
1082	 * @param WP_Error|null   $response The current error object if any.
1083	 * @return WP_REST_Response
1084	 */
1085	protected function respond_to_request( $request, $route, $handler, $response ) {
1086		/**
1087		 * Filters the response before executing any REST API callbacks.
1088		 *
1089		 * Allows plugins to perform additional validation after a
1090		 * request is initialized and matched to a registered route,
1091		 * but before it is executed.
1092		 *
1093		 * Note that this filter will not be called for requests that
1094		 * fail to authenticate or match to a registered route.
1095		 *
1096		 * @since 4.7.0
1097		 *
1098		 * @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client.
1099		 *                                                                   Usually a WP_REST_Response or WP_Error.
1100		 * @param array                                            $handler  Route handler used for the request.
1101		 * @param WP_REST_Request                                  $request  Request used to generate the response.
1102		 */
1103		$response = apply_filters( 'rest_request_before_callbacks', $response, $handler, $request );
1104
1105		// Check permission specified on the route.
1106		if ( ! is_wp_error( $response ) && ! empty( $handler['permission_callback'] ) ) {
1107			$permission = call_user_func( $handler['permission_callback'], $request );
1108
1109			if ( is_wp_error( $permission ) ) {
1110				$response = $permission;
1111			} elseif ( false === $permission || null === $permission ) {
1112				$response = new WP_Error(
1113					'rest_forbidden',
1114					__( 'Sorry, you are not allowed to do that.' ),
1115					array( 'status' => rest_authorization_required_code() )
1116				);
1117			}
1118		}
1119
1120		if ( ! is_wp_error( $response ) ) {
1121			/**
1122			 * Filters the REST API dispatch request result.
1123			 *
1124			 * Allow plugins to override dispatching the request.
1125			 *
1126			 * @since 4.4.0
1127			 * @since 4.5.0 Added `$route` and `$handler` parameters.
1128			 *
1129			 * @param mixed           $dispatch_result Dispatch result, will be used if not empty.
1130			 * @param WP_REST_Request $request         Request used to generate the response.
1131			 * @param string          $route           Route matched for the request.
1132			 * @param array           $handler         Route handler used for the request.
1133			 */
1134			$dispatch_result = apply_filters( 'rest_dispatch_request', null, $request, $route, $handler );
1135
1136			// Allow plugins to halt the request via this filter.
1137			if ( null !== $dispatch_result ) {
1138				$response = $dispatch_result;
1139			} else {
1140				$response = call_user_func( $handler['callback'], $request );
1141			}
1142		}
1143
1144		/**
1145		 * Filters the response immediately after executing any REST API
1146		 * callbacks.
1147		 *
1148		 * Allows plugins to perform any needed cleanup, for example,
1149		 * to undo changes made during the {@see 'rest_request_before_callbacks'}
1150		 * filter.
1151		 *
1152		 * Note that this filter will not be called for requests that
1153		 * fail to authenticate or match to a registered route.
1154		 *
1155		 * Note that an endpoint's `permission_callback` can still be
1156		 * called after this filter - see `rest_send_allow_header()`.
1157		 *
1158		 * @since 4.7.0
1159		 *
1160		 * @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client.
1161		 *                                                                   Usually a WP_REST_Response or WP_Error.
1162		 * @param array                                            $handler  Route handler used for the request.
1163		 * @param WP_REST_Request                                  $request  Request used to generate the response.
1164		 */
1165		$response = apply_filters( 'rest_request_after_callbacks', $response, $handler, $request );
1166
1167		if ( is_wp_error( $response ) ) {
1168			$response = $this->error_to_response( $response );
1169		} else {
1170			$response = rest_ensure_response( $response );
1171		}
1172
1173		$response->set_matched_route( $route );
1174		$response->set_matched_handler( $handler );
1175
1176		return $response;
1177	}
1178
1179	/**
1180	 * Returns if an error occurred during most recent JSON encode/decode.
1181	 *
1182	 * Strings to be translated will be in format like
1183	 * "Encoding error: Maximum stack depth exceeded".
1184	 *
1185	 * @since 4.4.0
1186	 *
1187	 * @return false|string Boolean false or string error message.
1188	 */
1189	protected function get_json_last_error() {
1190		$last_error_code = json_last_error();
1191
1192		if ( JSON_ERROR_NONE === $last_error_code || empty( $last_error_code ) ) {
1193			return false;
1194		}
1195
1196		return json_last_error_msg();
1197	}
1198
1199	/**
1200	 * Retrieves the site index.
1201	 *
1202	 * This endpoint describes the capabilities of the site.
1203	 *
1204	 * @since 4.4.0
1205	 *
1206	 * @param array $request {
1207	 *     Request.
1208	 *
1209	 *     @type string $context Context.
1210	 * }
1211	 * @return WP_REST_Response The API root index data.
1212	 */
1213	public function get_index( $request ) {
1214		// General site data.
1215		$available = array(
1216			'name'            => get_option( 'blogname' ),
1217			'description'     => get_option( 'blogdescription' ),
1218			'url'             => get_option( 'siteurl' ),
1219			'home'            => home_url(),
1220			'gmt_offset'      => get_option( 'gmt_offset' ),
1221			'timezone_string' => get_option( 'timezone_string' ),
1222			'namespaces'      => array_keys( $this->namespaces ),
1223			'authentication'  => array(),
1224			'routes'          => $this->get_data_for_routes( $this->get_routes(), $request['context'] ),
1225		);
1226
1227		$response = new WP_REST_Response( $available );
1228		$response->add_link( 'help', 'https://developer.wordpress.org/rest-api/' );
1229		$this->add_active_theme_link_to_index( $response );
1230		$this->add_site_logo_to_index( $response );
1231
1232		/**
1233		 * Filters the REST API root index data.
1234		 *
1235		 * This contains the data describing the API. This includes information
1236		 * about supported authentication schemes, supported namespaces, routes
1237		 * available on the API, and a small amount of data about the site.
1238		 *
1239		 * @since 4.4.0
1240		 *
1241		 * @param WP_REST_Response $response Response data.
1242		 */
1243		return apply_filters( 'rest_index', $response );
1244	}
1245
1246	/**
1247	 * Adds a link to the active theme for users who have proper permissions.
1248	 *
1249	 * @since 5.7.0
1250	 *
1251	 * @param WP_REST_Response $response REST API response.
1252	 */
1253	protected function add_active_theme_link_to_index( WP_REST_Response $response ) {
1254		$should_add = current_user_can( 'switch_themes' ) || current_user_can( 'manage_network_themes' );
1255
1256		if ( ! $should_add && current_user_can( 'edit_posts' ) ) {
1257			$should_add = true;
1258		}
1259
1260		if ( ! $should_add ) {
1261			foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
1262				if ( current_user_can( $post_type->cap->edit_posts ) ) {
1263					$should_add = true;
1264					break;
1265				}
1266			}
1267		}
1268
1269		if ( $should_add ) {
1270			$theme = wp_get_theme();
1271			$response->add_link( 'https://api.w.org/active-theme', rest_url( 'wp/v2/themes/' . $theme->get_stylesheet() ) );
1272		}
1273	}
1274
1275	/**
1276	 * Exposes the site logo through the WordPress REST API.
1277	 * This is used for fetching this information when user has no rights
1278	 * to update settings.
1279	 *
1280	 * @since 5.8.0
1281	 *
1282	 * @param WP_REST_Response $response REST API response.
1283	 */
1284	protected function add_site_logo_to_index( WP_REST_Response $response ) {
1285		$site_logo_id                = get_theme_mod( 'custom_logo' );
1286		$response->data['site_logo'] = $site_logo_id;
1287		if ( $site_logo_id ) {
1288			$response->add_link(
1289				'https://api.w.org/featuredmedia',
1290				rest_url( 'wp/v2/media/' . $site_logo_id ),
1291				array(
1292					'embeddable' => true,
1293				)
1294			);
1295		}
1296	}
1297
1298	/**
1299	 * Retrieves the index for a namespace.
1300	 *
1301	 * @since 4.4.0
1302	 *
1303	 * @param WP_REST_Request $request REST request instance.
1304	 * @return WP_REST_Response|WP_Error WP_REST_Response instance if the index was found,
1305	 *                                   WP_Error if the namespace isn't set.
1306	 */
1307	public function get_namespace_index( $request ) {
1308		$namespace = $request['namespace'];
1309
1310		if ( ! isset( $this->namespaces[ $namespace ] ) ) {
1311			return new WP_Error(
1312				'rest_invalid_namespace',
1313				__( 'The specified namespace could not be found.' ),
1314				array( 'status' => 404 )
1315			);
1316		}
1317
1318		$routes    = $this->namespaces[ $namespace ];
1319		$endpoints = array_intersect_key( $this->get_routes(), $routes );
1320
1321		$data     = array(
1322			'namespace' => $namespace,
1323			'routes'    => $this->get_data_for_routes( $endpoints, $request['context'] ),
1324		);
1325		$response = rest_ensure_response( $data );
1326
1327		// Link to the root index.
1328		$response->add_link( 'up', rest_url( '/' ) );
1329
1330		/**
1331		 * Filters the REST API namespace index data.
1332		 *
1333		 * This typically is just the route data for the namespace, but you can
1334		 * add any data you'd like here.
1335		 *
1336		 * @since 4.4.0
1337		 *
1338		 * @param WP_REST_Response $response Response data.
1339		 * @param WP_REST_Request  $request  Request data. The namespace is passed as the 'namespace' parameter.
1340		 */
1341		return apply_filters( 'rest_namespace_index', $response, $request );
1342	}
1343
1344	/**
1345	 * Retrieves the publicly-visible data for routes.
1346	 *
1347	 * @since 4.4.0
1348	 *
1349	 * @param array  $routes  Routes to get data for.
1350	 * @param string $context Optional. Context for data. Accepts 'view' or 'help'. Default 'view'.
1351	 * @return array[] Route data to expose in indexes, keyed by route.
1352	 */
1353	public function get_data_for_routes( $routes, $context = 'view' ) {
1354		$available = array();
1355
1356		// Find the available routes.
1357		foreach ( $routes as $route => $callbacks ) {
1358			$data = $this->get_data_for_route( $route, $callbacks, $context );
1359			if ( empty( $data ) ) {
1360				continue;
1361			}
1362
1363			/**
1364			 * Filters the REST API endpoint data.
1365			 *
1366			 * @since 4.4.0
1367			 *
1368			 * @param WP_REST_Request $request Request data. The namespace is passed as the 'namespace' parameter.
1369			 */
1370			$available[ $route ] = apply_filters( 'rest_endpoints_description', $data );
1371		}
1372
1373		/**
1374		 * Filters the publicly-visible data for REST API routes.
1375		 *
1376		 * This data is exposed on indexes and can be used by clients or
1377		 * developers to investigate the site and find out how to use it. It
1378		 * acts as a form of self-documentation.
1379		 *
1380		 * @since 4.4.0
1381		 *
1382		 * @param array[] $available Route data to expose in indexes, keyed by route.
1383		 * @param array   $routes    Internal route data as an associative array.
1384		 */
1385		return apply_filters( 'rest_route_data', $available, $routes );
1386	}
1387
1388	/**
1389	 * Retrieves publicly-visible data for the route.
1390	 *
1391	 * @since 4.4.0
1392	 *
1393	 * @param string $route     Route to get data for.
1394	 * @param array  $callbacks Callbacks to convert to data.
1395	 * @param string $context   Optional. Context for the data. Accepts 'view' or 'help'. Default 'view'.
1396	 * @return array|null Data for the route, or null if no publicly-visible data.
1397	 */
1398	public function get_data_for_route( $route, $callbacks, $context = 'view' ) {
1399		$data = array(
1400			'namespace' => '',
1401			'methods'   => array(),
1402			'endpoints' => array(),
1403		);
1404
1405		if ( isset( $this->route_options[ $route ] ) ) {
1406			$options = $this->route_options[ $route ];
1407
1408			if ( isset( $options['namespace'] ) ) {
1409				$data['namespace'] = $options['namespace'];
1410			}
1411
1412			if ( isset( $options['schema'] ) && 'help' === $context ) {
1413				$data['schema'] = call_user_func( $options['schema'] );
1414			}
1415		}
1416
1417		$allowed_schema_keywords = array_flip( rest_get_allowed_schema_keywords() );
1418
1419		$route = preg_replace( '#\(\?P<(\w+?)>.*?\)#', '{$1}', $route );
1420
1421		foreach ( $callbacks as $callback ) {
1422			// Skip to the next route if any callback is hidden.
1423			if ( empty( $callback['show_in_index'] ) ) {
1424				continue;
1425			}
1426
1427			$data['methods'] = array_merge( $data['methods'], array_keys( $callback['methods'] ) );
1428			$endpoint_data   = array(
1429				'methods' => array_keys( $callback['methods'] ),
1430			);
1431
1432			if ( isset( $callback['args'] ) ) {
1433				$endpoint_data['args'] = array();
1434
1435				foreach ( $callback['args'] as $key => $opts ) {
1436					$arg_data             = array_intersect_key( $opts, $allowed_schema_keywords );
1437					$arg_data['required'] = ! empty( $opts['required'] );
1438
1439					$endpoint_data['args'][ $key ] = $arg_data;
1440				}
1441			}
1442
1443			$data['endpoints'][] = $endpoint_data;
1444
1445			// For non-variable routes, generate links.
1446			if ( strpos( $route, '{' ) === false ) {
1447				$data['_links'] = array(
1448					'self' => array(
1449						array(
1450							'href' => rest_url( $route ),
1451						),
1452					),
1453				);
1454			}
1455		}
1456
1457		if ( empty( $data['methods'] ) ) {
1458			// No methods supported, hide the route.
1459			return null;
1460		}
1461
1462		return $data;
1463	}
1464
1465	/**
1466	 * Gets the maximum number of requests that can be included in a batch.
1467	 *
1468	 * @since 5.6.0
1469	 *
1470	 * @return int The maximum requests.
1471	 */
1472	protected function get_max_batch_size() {
1473		/**
1474		 * Filters the maximum number of REST API requests that can be included in a batch.
1475		 *
1476		 * @since 5.6.0
1477		 *
1478		 * @param int $max_size The maximum size.
1479		 */
1480		return apply_filters( 'rest_get_max_batch_size', 25 );
1481	}
1482
1483	/**
1484	 * Serves the batch/v1 request.
1485	 *
1486	 * @since 5.6.0
1487	 *
1488	 * @param WP_REST_Request $batch_request The batch request object.
1489	 * @return WP_REST_Response The generated response object.
1490	 */
1491	public function serve_batch_request_v1( WP_REST_Request $batch_request ) {
1492		$requests = array();
1493
1494		foreach ( $batch_request['requests'] as $args ) {
1495			$parsed_url = wp_parse_url( $args['path'] );
1496
1497			if ( false === $parsed_url ) {
1498				$requests[] = new WP_Error( 'parse_path_failed', __( 'Could not parse the path.' ), array( 'status' => 400 ) );
1499
1500				continue;
1501			}
1502
1503			$single_request = new WP_REST_Request( isset( $args['method'] ) ? $args['method'] : 'POST', $parsed_url['path'] );
1504
1505			if ( ! empty( $parsed_url['query'] ) ) {
1506				$query_args = null; // Satisfy linter.
1507				wp_parse_str( $parsed_url['query'], $query_args );
1508				$single_request->set_query_params( $query_args );
1509			}
1510
1511			if ( ! empty( $args['body'] ) ) {
1512				$single_request->set_body_params( $args['body'] );
1513			}
1514
1515			if ( ! empty( $args['headers'] ) ) {
1516				$single_request->set_headers( $args['headers'] );
1517			}
1518
1519			$requests[] = $single_request;
1520		}
1521
1522		$matches    = array();
1523		$validation = array();
1524		$has_error  = false;
1525
1526		foreach ( $requests as $single_request ) {
1527			$match     = $this->match_request_to_handler( $single_request );
1528			$matches[] = $match;
1529			$error     = null;
1530
1531			if ( is_wp_error( $match ) ) {
1532				$error = $match;
1533			}
1534
1535			if ( ! $error ) {
1536				list( $route, $handler ) = $match;
1537
1538				if ( isset( $handler['allow_batch'] ) ) {
1539					$allow_batch = $handler['allow_batch'];
1540				} else {
1541					$route_options = $this->get_route_options( $route );
1542					$allow_batch   = isset( $route_options['allow_batch'] ) ? $route_options['allow_batch'] : false;
1543				}
1544
1545				if ( ! is_array( $allow_batch ) || empty( $allow_batch['v1'] ) ) {
1546					$error = new WP_Error(
1547						'rest_batch_not_allowed',
1548						__( 'The requested route does not support batch requests.' ),
1549						array( 'status' => 400 )
1550					);
1551				}
1552			}
1553
1554			if ( ! $error ) {
1555				$check_required = $single_request->has_valid_params();
1556				if ( is_wp_error( $check_required ) ) {
1557					$error = $check_required;
1558				}
1559			}
1560
1561			if ( ! $error ) {
1562				$check_sanitized = $single_request->sanitize_params();
1563				if ( is_wp_error( $check_sanitized ) ) {
1564					$error = $check_sanitized;
1565				}
1566			}
1567
1568			if ( $error ) {
1569				$has_error    = true;
1570				$validation[] = $error;
1571			} else {
1572				$validation[] = true;
1573			}
1574		}
1575
1576		$responses = array();
1577
1578		if ( $has_error && 'require-all-validate' === $batch_request['validation'] ) {
1579			foreach ( $validation as $valid ) {
1580				if ( is_wp_error( $valid ) ) {
1581					$responses[] = $this->envelope_response( $this->error_to_response( $valid ), false )->get_data();
1582				} else {
1583					$responses[] = null;
1584				}
1585			}
1586
1587			return new WP_REST_Response(
1588				array(
1589					'failed'    => 'validation',
1590					'responses' => $responses,
1591				),
1592				WP_Http::MULTI_STATUS
1593			);
1594		}
1595
1596		foreach ( $requests as $i => $single_request ) {
1597			$clean_request = clone $single_request;
1598			$clean_request->set_url_params( array() );
1599			$clean_request->set_attributes( array() );
1600			$clean_request->set_default_params( array() );
1601
1602			/** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */
1603			$result = apply_filters( 'rest_pre_dispatch', null, $this, $clean_request );
1604
1605			if ( empty( $result ) ) {
1606				$match = $matches[ $i ];
1607				$error = null;
1608
1609				if ( is_wp_error( $validation[ $i ] ) ) {
1610					$error = $validation[ $i ];
1611				}
1612
1613				if ( is_wp_error( $match ) ) {
1614					$result = $this->error_to_response( $match );
1615				} else {
1616					list( $route, $handler ) = $match;
1617
1618					if ( ! $error && ! is_callable( $handler['callback'] ) ) {
1619						$error = new WP_Error(
1620							'rest_invalid_handler',
1621							__( 'The handler for the route is invalid' ),
1622							array( 'status' => 500 )
1623						);
1624					}
1625
1626					$result = $this->respond_to_request( $single_request, $route, $handler, $error );
1627				}
1628			}
1629
1630			/** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */
1631			$result = apply_filters( 'rest_post_dispatch', rest_ensure_response( $result ), $this, $single_request );
1632
1633			$responses[] = $this->envelope_response( $result, false )->get_data();
1634		}
1635
1636		return new WP_REST_Response( array( 'responses' => $responses ), WP_Http::MULTI_STATUS );
1637	}
1638
1639	/**
1640	 * Sends an HTTP status code.
1641	 *
1642	 * @since 4.4.0
1643	 *
1644	 * @param int $code HTTP status.
1645	 */
1646	protected function set_status( $code ) {
1647		status_header( $code );
1648	}
1649
1650	/**
1651	 * Sends an HTTP header.
1652	 *
1653	 * @since 4.4.0
1654	 *
1655	 * @param string $key Header key.
1656	 * @param string $value Header value.
1657	 */
1658	public function send_header( $key, $value ) {
1659		/*
1660		 * Sanitize as per RFC2616 (Section 4.2):
1661		 *
1662		 * Any LWS that occurs between field-content MAY be replaced with a
1663		 * single SP before interpreting the field value or forwarding the
1664		 * message downstream.
1665		 */
1666		$value = preg_replace( '/\s+/', ' ', $value );
1667		header( sprintf( '%s: %s', $key, $value ) );
1668	}
1669
1670	/**
1671	 * Sends multiple HTTP headers.
1672	 *
1673	 * @since 4.4.0
1674	 *
1675	 * @param array $headers Map of header name to header value.
1676	 */
1677	public function send_headers( $headers ) {
1678		foreach ( $headers as $key => $value ) {
1679			$this->send_header( $key, $value );
1680		}
1681	}
1682
1683	/**
1684	 * Removes an HTTP header from the current response.
1685	 *
1686	 * @since 4.8.0
1687	 *
1688	 * @param string $key Header key.
1689	 */
1690	public function remove_header( $key ) {
1691		header_remove( $key );
1692	}
1693
1694	/**
1695	 * Retrieves the raw request entity (body).
1696	 *
1697	 * @since 4.4.0
1698	 *
1699	 * @global string $HTTP_RAW_POST_DATA Raw post data.
1700	 *
1701	 * @return string Raw request data.
1702	 */
1703	public static function get_raw_data() {
1704		// phpcs:disable PHPCompatibility.Variables.RemovedPredefinedGlobalVariables.http_raw_post_dataDeprecatedRemoved
1705		global $HTTP_RAW_POST_DATA;
1706
1707		// $HTTP_RAW_POST_DATA was deprecated in PHP 5.6 and removed in PHP 7.0.
1708		if ( ! isset( $HTTP_RAW_POST_DATA ) ) {
1709			$HTTP_RAW_POST_DATA = file_get_contents( 'php://input' );
1710		}
1711
1712		return $HTTP_RAW_POST_DATA;
1713		// phpcs:enable
1714	}
1715
1716	/**
1717	 * Extracts headers from a PHP-style $_SERVER array.
1718	 *
1719	 * @since 4.4.0
1720	 *
1721	 * @param array $server Associative array similar to `$_SERVER`.
1722	 * @return array Headers extracted from the input.
1723	 */
1724	public function get_headers( $server ) {
1725		$headers = array();
1726
1727		// CONTENT_* headers are not prefixed with HTTP_.
1728		$additional = array(
1729			'CONTENT_LENGTH' => true,
1730			'CONTENT_MD5'    => true,
1731			'CONTENT_TYPE'   => true,
1732		);
1733
1734		foreach ( $server as $key => $value ) {
1735			if ( strpos( $key, 'HTTP_' ) === 0 ) {
1736				$headers[ substr( $key, 5 ) ] = $value;
1737			} elseif ( 'REDIRECT_HTTP_AUTHORIZATION' === $key && empty( $server['HTTP_AUTHORIZATION'] ) ) {
1738				/*
1739				 * In some server configurations, the authorization header is passed in this alternate location.
1740				 * Since it would not be passed in in both places we do not check for both headers and resolve.
1741				 */
1742				$headers['AUTHORIZATION'] = $value;
1743			} elseif ( isset( $additional[ $key ] ) ) {
1744				$headers[ $key ] = $value;
1745			}
1746		}
1747
1748		return $headers;
1749	}
1750}
1751