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