]> scripts.mit.edu Git - autoinstalls/wordpress.git/blob - wp-includes/rest-api/class-wp-rest-server.php
WordPress 4.4
[autoinstalls/wordpress.git] / wp-includes / rest-api / class-wp-rest-server.php
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 class 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          * @access protected
62          * @var array
63          */
64         protected $namespaces = array();
65
66         /**
67          * Endpoints registered to the server.
68          *
69          * @since 4.4.0
70          * @access protected
71          * @var array
72          */
73         protected $endpoints = array();
74
75         /**
76          * Options defined for the routes.
77          *
78          * @since 4.4.0
79          * @access protected
80          * @var array
81          */
82         protected $route_options = array();
83
84         /**
85          * Instantiates the REST server.
86          *
87          * @since 4.4.0
88          * @access public
89          */
90         public function __construct() {
91                 $this->endpoints = array(
92                         // Meta endpoints.
93                         '/' => array(
94                                 'callback' => array( $this, 'get_index' ),
95                                 'methods' => 'GET',
96                                 'args' => array(
97                                         'context' => array(
98                                                 'default' => 'view',
99                                         ),
100                                 ),
101                         ),
102                 );
103         }
104
105
106         /**
107          * Checks the authentication headers if supplied.
108          *
109          * @since 4.4.0
110          * @access public
111          *
112          * @return WP_Error|null WP_Error indicates unsuccessful login, null indicates successful
113          *                       or no authentication provided
114          */
115         public function check_authentication() {
116                 /**
117                  * Pass an authentication error to the API
118                  *
119                  * This is used to pass a WP_Error from an authentication method back to
120                  * the API.
121                  *
122                  * Authentication methods should check first if they're being used, as
123                  * multiple authentication methods can be enabled on a site (cookies,
124                  * HTTP basic auth, OAuth). If the authentication method hooked in is
125                  * not actually being attempted, null should be returned to indicate
126                  * another authentication method should check instead. Similarly,
127                  * callbacks should ensure the value is `null` before checking for
128                  * errors.
129                  *
130                  * A WP_Error instance can be returned if an error occurs, and this should
131                  * match the format used by API methods internally (that is, the `status`
132                  * data should be used). A callback can return `true` to indicate that
133                  * the authentication method was used, and it succeeded.
134                  *
135                  * @since 4.4.0
136                  *
137                  * @param WP_Error|null|bool WP_Error if authentication error, null if authentication
138                  *                              method wasn't used, true if authentication succeeded.
139                  */
140                 return apply_filters( 'rest_authentication_errors', null );
141         }
142
143         /**
144          * Converts an error to a response object.
145          *
146          * This iterates over all error codes and messages to change it into a flat
147          * array. This enables simpler client behaviour, as it is represented as a
148          * list in JSON rather than an object/map.
149          *
150          * @since 4.4.0
151          * @access protected
152          *
153          * @param WP_Error $error WP_Error instance.
154          * @return WP_REST_Response List of associative arrays with code and message keys.
155          */
156         protected function error_to_response( $error ) {
157                 $error_data = $error->get_error_data();
158
159                 if ( is_array( $error_data ) && isset( $error_data['status'] ) ) {
160                         $status = $error_data['status'];
161                 } else {
162                         $status = 500;
163                 }
164
165                 $errors = array();
166
167                 foreach ( (array) $error->errors as $code => $messages ) {
168                         foreach ( (array) $messages as $message ) {
169                                 $errors[] = array( 'code' => $code, 'message' => $message, 'data' => $error->get_error_data( $code ) );
170                         }
171                 }
172
173                 $data = $errors[0];
174                 if ( count( $errors ) > 1 ) {
175                         // Remove the primary error.
176                         array_shift( $errors );
177                         $data['additional_errors'] = $errors;
178                 }
179
180                 $response = new WP_REST_Response( $data, $status );
181
182                 return $response;
183         }
184
185         /**
186          * Retrieves an appropriate error representation in JSON.
187          *
188          * Note: This should only be used in WP_REST_Server::serve_request(), as it
189          * cannot handle WP_Error internally. All callbacks and other internal methods
190          * should instead return a WP_Error with the data set to an array that includes
191          * a 'status' key, with the value being the HTTP status to send.
192          *
193          * @since 4.4.0
194          * @access protected
195          *
196          * @param string $code    WP_Error-style code.
197          * @param string $message Human-readable message.
198          * @param int    $status  Optional. HTTP status code to send. Default null.
199          * @return string JSON representation of the error
200          */
201         protected function json_error( $code, $message, $status = null ) {
202                 if ( $status ) {
203                         $this->set_status( $status );
204                 }
205
206                 $error = compact( 'code', 'message' );
207
208                 return wp_json_encode( $error );
209         }
210
211         /**
212          * Handles serving an API request.
213          *
214          * Matches the current server URI to a route and runs the first matching
215          * callback then outputs a JSON representation of the returned value.
216          *
217          * @since 4.4.0
218          * @access public
219          *
220          * @see WP_REST_Server::dispatch()
221          *
222          * @param string $path Optional. The request route. If not set, `$_SERVER['PATH_INFO']` will be used.
223          *                     Default null.
224          * @return false|null Null if not served and a HEAD request, false otherwise.
225          */
226         public function serve_request( $path = null ) {
227                 $content_type = isset( $_GET['_jsonp'] ) ? 'application/javascript' : 'application/json';
228                 $this->send_header( 'Content-Type', $content_type . '; charset=' . get_option( 'blog_charset' ) );
229
230                 /*
231                  * Mitigate possible JSONP Flash attacks.
232                  *
233                  * http://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
234                  */
235                 $this->send_header( 'X-Content-Type-Options', 'nosniff' );
236                 $this->send_header( 'Access-Control-Expose-Headers', 'X-WP-Total, X-WP-TotalPages' );
237                 $this->send_header( 'Access-Control-Allow-Headers', 'Authorization' );
238
239                 /**
240                  * Send nocache headers on authenticated requests.
241                  *
242                  * @since 4.4.0
243                  *
244                  * @param bool $rest_send_nocache_headers Whether to send no-cache headers.
245                  */
246                 $send_no_cache_headers = apply_filters( 'rest_send_nocache_headers', is_user_logged_in() );
247                 if ( $send_no_cache_headers ) {
248                         foreach ( wp_get_nocache_headers() as $header => $header_value ) {
249                                 $this->send_header( $header, $header_value );
250                         }
251                 }
252
253                 /**
254                  * Filter whether the REST API is enabled.
255                  *
256                  * @since 4.4.0
257                  *
258                  * @param bool $rest_enabled Whether the REST API is enabled. Default true.
259                  */
260                 $enabled = apply_filters( 'rest_enabled', true );
261
262                 /**
263                  * Filter whether jsonp is enabled.
264                  *
265                  * @since 4.4.0
266                  *
267                  * @param bool $jsonp_enabled Whether jsonp is enabled. Default true.
268                  */
269                 $jsonp_enabled = apply_filters( 'rest_jsonp_enabled', true );
270
271                 $jsonp_callback = null;
272
273                 if ( ! $enabled ) {
274                         echo $this->json_error( 'rest_disabled', __( 'The REST API is disabled on this site.' ), 404 );
275                         return false;
276                 }
277                 if ( isset( $_GET['_jsonp'] ) ) {
278                         if ( ! $jsonp_enabled ) {
279                                 echo $this->json_error( 'rest_callback_disabled', __( 'JSONP support is disabled on this site.' ), 400 );
280                                 return false;
281                         }
282
283                         // Check for invalid characters (only alphanumeric allowed).
284                         if ( is_string( $_GET['_jsonp'] ) ) {
285                                 $jsonp_callback = preg_replace( '/[^\w\.]/', '', wp_unslash( $_GET['_jsonp'] ), -1, $illegal_char_count );
286                                 if ( 0 !== $illegal_char_count ) {
287                                         $jsonp_callback = null;
288                                 }
289                         }
290                         if ( null === $jsonp_callback ) {
291                                 echo $this->json_error( 'rest_callback_invalid', __( 'The JSONP callback function is invalid.' ), 400 );
292                                 return false;
293                         }
294                 }
295
296                 if ( empty( $path ) ) {
297                         if ( isset( $_SERVER['PATH_INFO'] ) ) {
298                                 $path = $_SERVER['PATH_INFO'];
299                         } else {
300                                 $path = '/';
301                         }
302                 }
303
304                 $request = new WP_REST_Request( $_SERVER['REQUEST_METHOD'], $path );
305
306                 $request->set_query_params( $_GET );
307                 $request->set_body_params( $_POST );
308                 $request->set_file_params( $_FILES );
309                 $request->set_headers( $this->get_headers( $_SERVER ) );
310                 $request->set_body( $this->get_raw_data() );
311
312                 /*
313                  * HTTP method override for clients that can't use PUT/PATCH/DELETE. First, we check
314                  * $_GET['_method']. If that is not set, we check for the HTTP_X_HTTP_METHOD_OVERRIDE
315                  * header.
316                  */
317                 if ( isset( $_GET['_method'] ) ) {
318                         $request->set_method( $_GET['_method'] );
319                 } elseif ( isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ) {
320                         $request->set_method( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] );
321                 }
322
323                 $result = $this->check_authentication();
324
325                 if ( ! is_wp_error( $result ) ) {
326                         $result = $this->dispatch( $request );
327                 }
328
329                 // Normalize to either WP_Error or WP_REST_Response...
330                 $result = rest_ensure_response( $result );
331
332                 // ...then convert WP_Error across.
333                 if ( is_wp_error( $result ) ) {
334                         $result = $this->error_to_response( $result );
335                 }
336
337                 /**
338                  * Filter the API response.
339                  *
340                  * Allows modification of the response before returning.
341                  *
342                  * @since 4.4.0
343                  *
344                  * @param WP_HTTP_Response $result  Result to send to the client. Usually a WP_REST_Response.
345                  * @param WP_REST_Server   $this    Server instance.
346                  * @param WP_REST_Request  $request Request used to generate the response.
347                  */
348                 $result = apply_filters( 'rest_post_dispatch', rest_ensure_response( $result ), $this, $request );
349
350                 // Wrap the response in an envelope if asked for.
351                 if ( isset( $_GET['_envelope'] ) ) {
352                         $result = $this->envelope_response( $result, isset( $_GET['_embed'] ) );
353                 }
354
355                 // Send extra data from response objects.
356                 $headers = $result->get_headers();
357                 $this->send_headers( $headers );
358
359                 $code = $result->get_status();
360                 $this->set_status( $code );
361
362                 /**
363                  * Filter whether the request has already been served.
364                  *
365                  * Allow sending the request manually - by returning true, the API result
366                  * will not be sent to the client.
367                  *
368                  * @since 4.4.0
369                  *
370                  * @param bool             $served  Whether the request has already been served.
371                  *                                           Default false.
372                  * @param WP_HTTP_Response $result  Result to send to the client. Usually a WP_REST_Response.
373                  * @param WP_REST_Request  $request Request used to generate the response.
374                  * @param WP_REST_Server   $this    Server instance.
375                  */
376                 $served = apply_filters( 'rest_pre_serve_request', false, $result, $request, $this );
377
378                 if ( ! $served ) {
379                         if ( 'HEAD' === $request->get_method() ) {
380                                 return null;
381                         }
382
383                         // Embed links inside the request.
384                         $result = $this->response_to_data( $result, isset( $_GET['_embed'] ) );
385
386                         $result = wp_json_encode( $result );
387
388                         $json_error_message = $this->get_json_last_error();
389                         if ( $json_error_message ) {
390                                 $json_error_obj = new WP_Error( 'rest_encode_error', $json_error_message, array( 'status' => 500 ) );
391                                 $result = $this->error_to_response( $json_error_obj );
392                                 $result = wp_json_encode( $result->data[0] );
393                         }
394
395                         if ( $jsonp_callback ) {
396                                 // Prepend '/**/' to mitigate possible JSONP Flash attacks
397                                 // http://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
398                                 echo '/**/' . $jsonp_callback . '(' . $result . ')';
399                         } else {
400                                 echo $result;
401                         }
402                 }
403                 return null;
404         }
405
406         /**
407          * Converts a response to data to send.
408          *
409          * @since 4.4.0
410          * @access public
411          *
412          * @param WP_REST_Response $response Response object.
413          * @param bool             $embed    Whether links should be embedded.
414          * @return array {
415          *     Data with sub-requests embedded.
416          *
417          *     @type array [$_links]    Links.
418          *     @type array [$_embedded] Embeddeds.
419          * }
420          */
421         public function response_to_data( $response, $embed ) {
422                 $data  = $response->get_data();
423                 $links = $this->get_response_links( $response );
424
425                 if ( ! empty( $links ) ) {
426                         // Convert links to part of the data.
427                         $data['_links'] = $links;
428                 }
429                 if ( $embed ) {
430                         // Determine if this is a numeric array.
431                         if ( wp_is_numeric_array( $data ) ) {
432                                 $data = array_map( array( $this, 'embed_links' ), $data );
433                         } else {
434                                 $data = $this->embed_links( $data );
435                         }
436                 }
437
438                 return $data;
439         }
440
441         /**
442          * Retrieves links from a response.
443          *
444          * Extracts the links from a response into a structured hash, suitable for
445          * direct output.
446          *
447          * @since 4.4.0
448          * @access public
449          * @static
450          *
451          * @param WP_REST_Response $response Response to extract links from.
452          * @return array Map of link relation to list of link hashes.
453          */
454         public static function get_response_links( $response ) {
455                 $links = $response->get_links();
456
457                 if ( empty( $links ) ) {
458                         return array();
459                 }
460
461                 // Convert links to part of the data.
462                 $data = array();
463                 foreach ( $links as $rel => $items ) {
464                         $data[ $rel ] = array();
465
466                         foreach ( $items as $item ) {
467                                 $attributes = $item['attributes'];
468                                 $attributes['href'] = $item['href'];
469                                 $data[ $rel ][] = $attributes;
470                         }
471                 }
472
473                 return $data;
474         }
475
476         /**
477          * Embeds the links from the data into the request.
478          *
479          * @since 4.4.0
480          * @access protected
481          *
482          * @param array $data Data from the request.
483          * @return array {
484          *     Data with sub-requests embedded.
485          *
486          *     @type array [$_links]    Links.
487          *     @type array [$_embedded] Embeddeds.
488          * }
489          */
490         protected function embed_links( $data ) {
491                 if ( empty( $data['_links'] ) ) {
492                         return $data;
493                 }
494
495                 $embedded = array();
496                 $api_root = rest_url();
497
498                 foreach ( $data['_links'] as $rel => $links ) {
499                         // Ignore links to self, for obvious reasons.
500                         if ( 'self' === $rel ) {
501                                 continue;
502                         }
503
504                         $embeds = array();
505
506                         foreach ( $links as $item ) {
507                                 // Determine if the link is embeddable.
508                                 if ( empty( $item['embeddable'] ) || strpos( $item['href'], $api_root ) !== 0 ) {
509                                         // Ensure we keep the same order.
510                                         $embeds[] = array();
511                                         continue;
512                                 }
513
514                                 // Run through our internal routing and serve.
515                                 $route = substr( $item['href'], strlen( untrailingslashit( $api_root ) ) );
516                                 $query_params = array();
517
518                                 // Parse out URL query parameters.
519                                 $parsed = parse_url( $route );
520                                 if ( empty( $parsed['path'] ) ) {
521                                         $embeds[] = array();
522                                         continue;
523                                 }
524
525                                 if ( ! empty( $parsed['query'] ) ) {
526                                         parse_str( $parsed['query'], $query_params );
527
528                                         // Ensure magic quotes are stripped.
529                                         if ( get_magic_quotes_gpc() ) {
530                                                 $query_params = stripslashes_deep( $query_params );
531                                         }
532                                 }
533
534                                 // Embedded resources get passed context=embed.
535                                 if ( empty( $query_params['context'] ) ) {
536                                         $query_params['context'] = 'embed';
537                                 }
538
539                                 $request = new WP_REST_Request( 'GET', $parsed['path'] );
540
541                                 $request->set_query_params( $query_params );
542                                 $response = $this->dispatch( $request );
543
544                                 $embeds[] = $this->response_to_data( $response, false );
545                         }
546
547                         // Determine if any real links were found.
548                         $has_links = count( array_filter( $embeds ) );
549                         if ( $has_links ) {
550                                 $embedded[ $rel ] = $embeds;
551                         }
552                 }
553
554                 if ( ! empty( $embedded ) ) {
555                         $data['_embedded'] = $embedded;
556                 }
557
558                 return $data;
559         }
560
561         /**
562          * Wraps the response in an envelope.
563          *
564          * The enveloping technique is used to work around browser/client
565          * compatibility issues. Essentially, it converts the full HTTP response to
566          * data instead.
567          *
568          * @since 4.4.0
569          * @access public
570          *
571          * @param WP_REST_Response $response Response object.
572          * @param bool             $embed    Whether links should be embedded.
573          * @return WP_REST_Response New response with wrapped data
574          */
575         public function envelope_response( $response, $embed ) {
576                 $envelope = array(
577                         'body'    => $this->response_to_data( $response, $embed ),
578                         'status'  => $response->get_status(),
579                         'headers' => $response->get_headers(),
580                 );
581
582                 /**
583                  * Filter the enveloped form of a response.
584                  *
585                  * @since 4.4.0
586                  *
587                  * @param array            $envelope Envelope data.
588                  * @param WP_REST_Response $response Original response data.
589                  */
590                 $envelope = apply_filters( 'rest_envelope_response', $envelope, $response );
591
592                 // Ensure it's still a response and return.
593                 return rest_ensure_response( $envelope );
594         }
595
596         /**
597          * Registers a route to the server.
598          *
599          * @since 4.4.0
600          * @access public
601          *
602          * @param string $namespace  Namespace.
603          * @param string $route      The REST route.
604          * @param array  $route_args Route arguments.
605          * @param bool   $override   Optional. Whether the route should be overriden if it already exists.
606          *                           Default false.
607          */
608         public function register_route( $namespace, $route, $route_args, $override = false ) {
609                 if ( ! isset( $this->namespaces[ $namespace ] ) ) {
610                         $this->namespaces[ $namespace ] = array();
611
612                         $this->register_route( $namespace, '/' . $namespace, array(
613                                 array(
614                                         'methods' => self::READABLE,
615                                         'callback' => array( $this, 'get_namespace_index' ),
616                                         'args' => array(
617                                                 'namespace' => array(
618                                                         'default' => $namespace,
619                                                 ),
620                                                 'context' => array(
621                                                         'default' => 'view',
622                                                 ),
623                                         ),
624                                 ),
625                         ) );
626                 }
627
628                 // Associative to avoid double-registration.
629                 $this->namespaces[ $namespace ][ $route ] = true;
630                 $route_args['namespace'] = $namespace;
631
632                 if ( $override || empty( $this->endpoints[ $route ] ) ) {
633                         $this->endpoints[ $route ] = $route_args;
634                 } else {
635                         $this->endpoints[ $route ] = array_merge( $this->endpoints[ $route ], $route_args );
636                 }
637         }
638
639         /**
640          * Retrieves the route map.
641          *
642          * The route map is an associative array with path regexes as the keys. The
643          * value is an indexed array with the callback function/method as the first
644          * item, and a bitmask of HTTP methods as the second item (see the class
645          * constants).
646          *
647          * Each route can be mapped to more than one callback by using an array of
648          * the indexed arrays. This allows mapping e.g. GET requests to one callback
649          * and POST requests to another.
650          *
651          * Note that the path regexes (array keys) must have @ escaped, as this is
652          * used as the delimiter with preg_match()
653          *
654          * @since 4.4.0
655          * @access public
656          *
657          * @return array `'/path/regex' => array( $callback, $bitmask )` or
658          *               `'/path/regex' => array( array( $callback, $bitmask ), ...)`.
659          */
660         public function get_routes() {
661
662                 /**
663                  * Filter the array of available endpoints.
664                  *
665                  * @since 4.4.0
666                  *
667                  * @param array $endpoints The available endpoints. An array of matching regex patterns, each mapped
668                  *                         to an array of callbacks for the endpoint. These take the format
669                  *                         `'/path/regex' => array( $callback, $bitmask )` or
670                  *                         `'/path/regex' => array( array( $callback, $bitmask ).
671                  */
672                 $endpoints = apply_filters( 'rest_endpoints', $this->endpoints );
673
674                 // Normalise the endpoints.
675                 $defaults = array(
676                         'methods'       => '',
677                         'accept_json'   => false,
678                         'accept_raw'    => false,
679                         'show_in_index' => true,
680                         'args'          => array(),
681                 );
682
683                 foreach ( $endpoints as $route => &$handlers ) {
684
685                         if ( isset( $handlers['callback'] ) ) {
686                                 // Single endpoint, add one deeper.
687                                 $handlers = array( $handlers );
688                         }
689
690                         if ( ! isset( $this->route_options[ $route ] ) ) {
691                                 $this->route_options[ $route ] = array();
692                         }
693
694                         foreach ( $handlers as $key => &$handler ) {
695
696                                 if ( ! is_numeric( $key ) ) {
697                                         // Route option, move it to the options.
698                                         $this->route_options[ $route ][ $key ] = $handler;
699                                         unset( $handlers[ $key ] );
700                                         continue;
701                                 }
702
703                                 $handler = wp_parse_args( $handler, $defaults );
704
705                                 // Allow comma-separated HTTP methods.
706                                 if ( is_string( $handler['methods'] ) ) {
707                                         $methods = explode( ',', $handler['methods'] );
708                                 } else if ( is_array( $handler['methods'] ) ) {
709                                         $methods = $handler['methods'];
710                                 } else {
711                                         $methods = array();
712                                 }
713
714                                 $handler['methods'] = array();
715
716                                 foreach ( $methods as $method ) {
717                                         $method = strtoupper( trim( $method ) );
718                                         $handler['methods'][ $method ] = true;
719                                 }
720                         }
721                 }
722                 return $endpoints;
723         }
724
725         /**
726          * Retrieves namespaces registered on the server.
727          *
728          * @since 4.4.0
729          * @access public
730          *
731          * @return array List of registered namespaces.
732          */
733         public function get_namespaces() {
734                 return array_keys( $this->namespaces );
735         }
736
737         /**
738          * Retrieves specified options for a route.
739          *
740          * @since 4.4.0
741          * @access public
742          *
743          * @param string $route Route pattern to fetch options for.
744          * @return array|null Data as an associative array if found, or null if not found.
745          */
746         public function get_route_options( $route ) {
747                 if ( ! isset( $this->route_options[ $route ] ) ) {
748                         return null;
749                 }
750
751                 return $this->route_options[ $route ];
752         }
753
754         /**
755          * Matches the request to a callback and call it.
756          *
757          * @since 4.4.0
758          * @access public
759          *
760          * @param WP_REST_Request $request Request to attempt dispatching.
761          * @return WP_REST_Response Response returned by the callback.
762          */
763         public function dispatch( $request ) {
764                 /**
765                  * Filter the pre-calculated result of a REST dispatch request.
766                  *
767                  * Allow hijacking the request before dispatching by returning a non-empty. The returned value
768                  * will be used to serve the request instead.
769                  *
770                  * @since 4.4.0
771                  *
772                  * @param mixed           $result  Response to replace the requested version with. Can be anything
773                  *                                 a normal endpoint can return, or null to not hijack the request.
774                  * @param WP_REST_Server  $this    Server instance.
775                  * @param WP_REST_Request $request Request used to generate the response.
776                  */
777                 $result = apply_filters( 'rest_pre_dispatch', null, $this, $request );
778
779                 if ( ! empty( $result ) ) {
780                         return $result;
781                 }
782
783                 $method = $request->get_method();
784                 $path   = $request->get_route();
785
786                 foreach ( $this->get_routes() as $route => $handlers ) {
787                         $match = preg_match( '@^' . $route . '$@i', $path, $args );
788
789                         if ( ! $match ) {
790                                 continue;
791                         }
792
793                         foreach ( $handlers as $handler ) {
794                                 $callback  = $handler['callback'];
795                                 $response = null;
796
797                                 $checked_method = 'HEAD' === $method ? 'GET' : $method;
798                                 if ( empty( $handler['methods'][ $checked_method ] ) ) {
799                                         continue;
800                                 }
801
802                                 if ( ! is_callable( $callback ) ) {
803                                         $response = new WP_Error( 'rest_invalid_handler', __( 'The handler for the route is invalid' ), array( 'status' => 500 ) );
804                                 }
805
806                                 if ( ! is_wp_error( $response ) ) {
807                                         // Remove the redundant preg_match argument.
808                                         unset( $args[0] );
809
810                                         $request->set_url_params( $args );
811                                         $request->set_attributes( $handler );
812
813                                         $request->sanitize_params();
814
815                                         $defaults = array();
816
817                                         foreach ( $handler['args'] as $arg => $options ) {
818                                                 if ( isset( $options['default'] ) ) {
819                                                         $defaults[ $arg ] = $options['default'];
820                                                 }
821                                         }
822
823                                         $request->set_default_params( $defaults );
824
825                                         $check_required = $request->has_valid_params();
826                                         if ( is_wp_error( $check_required ) ) {
827                                                 $response = $check_required;
828                                         }
829                                 }
830
831                                 if ( ! is_wp_error( $response ) ) {
832                                         // Check permission specified on the route.
833                                         if ( ! empty( $handler['permission_callback'] ) ) {
834                                                 $permission = call_user_func( $handler['permission_callback'], $request );
835
836                                                 if ( is_wp_error( $permission ) ) {
837                                                         $response = $permission;
838                                                 } else if ( false === $permission || null === $permission ) {
839                                                         $response = new WP_Error( 'rest_forbidden', __( "You don't have permission to do this." ), array( 'status' => 403 ) );
840                                                 }
841                                         }
842                                 }
843
844                                 if ( ! is_wp_error( $response ) ) {
845                                         /**
846                                          * Filter the REST dispatch request result.
847                                          *
848                                          * Allow plugins to override dispatching the request.
849                                          *
850                                          * @since 4.4.0
851                                          *
852                                          * @param bool            $dispatch_result Dispatch result, will be used if not empty.
853                                          * @param WP_REST_Request $request         Request used to generate the response.
854                                          */
855                                         $dispatch_result = apply_filters( 'rest_dispatch_request', null, $request );
856
857                                         // Allow plugins to halt the request via this filter.
858                                         if ( null !== $dispatch_result ) {
859                                                 $response = $dispatch_result;
860                                         } else {
861                                                 $response = call_user_func( $callback, $request );
862                                         }
863                                 }
864
865                                 if ( is_wp_error( $response ) ) {
866                                         $response = $this->error_to_response( $response );
867                                 } else {
868                                         $response = rest_ensure_response( $response );
869                                 }
870
871                                 $response->set_matched_route( $route );
872                                 $response->set_matched_handler( $handler );
873
874                                 return $response;
875                         }
876                 }
877
878                 return $this->error_to_response( new WP_Error( 'rest_no_route', __( 'No route was found matching the URL and request method' ), array( 'status' => 404 ) ) );
879         }
880
881         /**
882          * Returns if an error occurred during most recent JSON encode/decode.
883          *
884          * Strings to be translated will be in format like
885          * "Encoding error: Maximum stack depth exceeded".
886          *
887          * @since 4.4.0
888          * @access protected
889          *
890          * @return bool|string Boolean false or string error message.
891          */
892         protected function get_json_last_error() {
893                 // See https://core.trac.wordpress.org/ticket/27799.
894                 if ( ! function_exists( 'json_last_error' ) ) {
895                         return false;
896                 }
897
898                 $last_error_code = json_last_error();
899
900                 if ( ( defined( 'JSON_ERROR_NONE' ) && JSON_ERROR_NONE === $last_error_code ) || empty( $last_error_code ) ) {
901                         return false;
902                 }
903
904                 return json_last_error_msg();
905         }
906
907         /**
908          * Retrieves the site index.
909          *
910          * This endpoint describes the capabilities of the site.
911          *
912          * @since 4.4.0
913          * @access public
914          *
915          * @param array $request {
916          *     Request.
917          *
918          *     @type string $context Context.
919          * }
920          * @return array Index entity
921          */
922         public function get_index( $request ) {
923                 // General site data.
924                 $available = array(
925                         'name'           => get_option( 'blogname' ),
926                         'description'    => get_option( 'blogdescription' ),
927                         'url'            => get_option( 'siteurl' ),
928                         'namespaces'     => array_keys( $this->namespaces ),
929                         'authentication' => array(),
930                         'routes'         => $this->get_data_for_routes( $this->get_routes(), $request['context'] ),
931                 );
932
933                 $response = new WP_REST_Response( $available );
934
935                 $response->add_link( 'help', 'http://v2.wp-api.org/' );
936
937                 /**
938                  * Filter the API root index data.
939                  *
940                  * This contains the data describing the API. This includes information
941                  * about supported authentication schemes, supported namespaces, routes
942                  * available on the API, and a small amount of data about the site.
943                  *
944                  * @since 4.4.0
945                  *
946                  * @param WP_REST_Response $response Response data.
947                  */
948                 return apply_filters( 'rest_index', $response );
949         }
950
951         /**
952          * Retrieves the index for a namespace.
953          *
954          * @since 4.4.0
955          * @access public
956          *
957          * @param WP_REST_Request $request REST request instance.
958          * @return WP_REST_Response|WP_Error WP_REST_Response instance if the index was found,
959          *                                   WP_Error if the namespace isn't set.
960          */
961         public function get_namespace_index( $request ) {
962                 $namespace = $request['namespace'];
963
964                 if ( ! isset( $this->namespaces[ $namespace ] ) ) {
965                         return new WP_Error( 'rest_invalid_namespace', __( 'The specified namespace could not be found.' ), array( 'status' => 404 ) );
966                 }
967
968                 $routes = $this->namespaces[ $namespace ];
969                 $endpoints = array_intersect_key( $this->get_routes(), $routes );
970
971                 $data = array(
972                         'namespace' => $namespace,
973                         'routes' => $this->get_data_for_routes( $endpoints, $request['context'] ),
974                 );
975                 $response = rest_ensure_response( $data );
976
977                 // Link to the root index.
978                 $response->add_link( 'up', rest_url( '/' ) );
979
980                 /**
981                  * Filter the namespace index data.
982                  *
983                  * This typically is just the route data for the namespace, but you can
984                  * add any data you'd like here.
985                  *
986                  * @since 4.4.0
987                  *
988                  * @param WP_REST_Response $response Response data.
989                  * @param WP_REST_Request  $request  Request data. The namespace is passed as the 'namespace' parameter.
990                  */
991                 return apply_filters( 'rest_namespace_index', $response, $request );
992         }
993
994         /**
995          * Retrieves the publicly-visible data for routes.
996          *
997          * @since 4.4.0
998          * @access public
999          *
1000          * @param array  $routes  Routes to get data for.
1001          * @param string $context Optional. Context for data. Accepts 'view' or 'help'. Default 'view'.
1002          * @return array Route data to expose in indexes.
1003          */
1004         public function get_data_for_routes( $routes, $context = 'view' ) {
1005                 $available = array();
1006
1007                 // Find the available routes.
1008                 foreach ( $routes as $route => $callbacks ) {
1009                         $data = $this->get_data_for_route( $route, $callbacks, $context );
1010                         if ( empty( $data ) ) {
1011                                 continue;
1012                         }
1013
1014                         /**
1015                          * Filter the REST endpoint data.
1016                          *
1017                          * @since 4.4.0
1018                          *
1019                          * @param WP_REST_Request $request Request data. The namespace is passed as the 'namespace' parameter.
1020                          */
1021                         $available[ $route ] = apply_filters( 'rest_endpoints_description', $data );
1022                 }
1023
1024                 /**
1025                  * Filter the publicly-visible data for routes.
1026                  *
1027                  * This data is exposed on indexes and can be used by clients or
1028                  * developers to investigate the site and find out how to use it. It
1029                  * acts as a form of self-documentation.
1030                  *
1031                  * @since 4.4.0
1032                  *
1033                  * @param array $available Map of route to route data.
1034                  * @param array $routes    Internal route data as an associative array.
1035                  */
1036                 return apply_filters( 'rest_route_data', $available, $routes );
1037         }
1038
1039         /**
1040          * Retrieves publicly-visible data for the route.
1041          *
1042          * @since 4.4.0
1043          * @access public
1044          *
1045          * @param string $route     Route to get data for.
1046          * @param array  $callbacks Callbacks to convert to data.
1047          * @param string $context   Optional. Context for the data. Accepts 'view' or 'help'. Default 'view'.
1048          * @return array|null Data for the route, or null if no publicly-visible data.
1049          */
1050         public function get_data_for_route( $route, $callbacks, $context = 'view' ) {
1051                 $data = array(
1052                         'namespace' => '',
1053                         'methods' => array(),
1054                         'endpoints' => array(),
1055                 );
1056
1057                 if ( isset( $this->route_options[ $route ] ) ) {
1058                         $options = $this->route_options[ $route ];
1059
1060                         if ( isset( $options['namespace'] ) ) {
1061                                 $data['namespace'] = $options['namespace'];
1062                         }
1063
1064                         if ( isset( $options['schema'] ) && 'help' === $context ) {
1065                                 $data['schema'] = call_user_func( $options['schema'] );
1066                         }
1067                 }
1068
1069                 $route = preg_replace( '#\(\?P<(\w+?)>.*?\)#', '{$1}', $route );
1070
1071                 foreach ( $callbacks as $callback ) {
1072                         // Skip to the next route if any callback is hidden.
1073                         if ( empty( $callback['show_in_index'] ) ) {
1074                                 continue;
1075                         }
1076
1077                         $data['methods'] = array_merge( $data['methods'], array_keys( $callback['methods'] ) );
1078                         $endpoint_data = array(
1079                                 'methods' => array_keys( $callback['methods'] ),
1080                         );
1081
1082                         if ( isset( $callback['args'] ) ) {
1083                                 $endpoint_data['args'] = array();
1084                                 foreach ( $callback['args'] as $key => $opts ) {
1085                                         $arg_data = array(
1086                                                 'required' => ! empty( $opts['required'] ),
1087                                         );
1088                                         if ( isset( $opts['default'] ) ) {
1089                                                 $arg_data['default'] = $opts['default'];
1090                                         }
1091                                         if ( isset( $opts['enum'] ) ) {
1092                                                 $arg_data['enum'] = $opts['enum'];
1093                                         }
1094                                         if ( isset( $opts['description'] ) ) {
1095                                                 $arg_data['description'] = $opts['description'];
1096                                         }
1097                                         $endpoint_data['args'][ $key ] = $arg_data;
1098                                 }
1099                         }
1100
1101                         $data['endpoints'][] = $endpoint_data;
1102
1103                         // For non-variable routes, generate links.
1104                         if ( strpos( $route, '{' ) === false ) {
1105                                 $data['_links'] = array(
1106                                         'self' => rest_url( $route ),
1107                                 );
1108                         }
1109                 }
1110
1111                 if ( empty( $data['methods'] ) ) {
1112                         // No methods supported, hide the route.
1113                         return null;
1114                 }
1115
1116                 return $data;
1117         }
1118
1119         /**
1120          * Sends an HTTP status code.
1121          *
1122          * @since 4.4.0
1123          * @access protected
1124          *
1125          * @param int $code HTTP status.
1126          */
1127         protected function set_status( $code ) {
1128                 status_header( $code );
1129         }
1130
1131         /**
1132          * Sends an HTTP header.
1133          *
1134          * @since 4.4.0
1135          * @access public
1136          *
1137          * @param string $key Header key.
1138          * @param string $value Header value.
1139          */
1140         public function send_header( $key, $value ) {
1141                 /*
1142                  * Sanitize as per RFC2616 (Section 4.2):
1143                  *
1144                  * Any LWS that occurs between field-content MAY be replaced with a
1145                  * single SP before interpreting the field value or forwarding the
1146                  * message downstream.
1147                  */
1148                 $value = preg_replace( '/\s+/', ' ', $value );
1149                 header( sprintf( '%s: %s', $key, $value ) );
1150         }
1151
1152         /**
1153          * Sends multiple HTTP headers.
1154          *
1155          * @since 4.4.0
1156          * @access public
1157          *
1158          * @param array $headers Map of header name to header value.
1159          */
1160         public function send_headers( $headers ) {
1161                 foreach ( $headers as $key => $value ) {
1162                         $this->send_header( $key, $value );
1163                 }
1164         }
1165
1166         /**
1167          * Retrieves the raw request entity (body).
1168          *
1169          * @since 4.4.0
1170          * @access public
1171          *
1172          * @global string $HTTP_RAW_POST_DATA Raw post data.
1173          *
1174          * @return string Raw request data.
1175          */
1176         public static function get_raw_data() {
1177                 global $HTTP_RAW_POST_DATA;
1178
1179                 /*
1180                  * A bug in PHP < 5.2.2 makes $HTTP_RAW_POST_DATA not set by default,
1181                  * but we can do it ourself.
1182                  */
1183                 if ( ! isset( $HTTP_RAW_POST_DATA ) ) {
1184                         $HTTP_RAW_POST_DATA = file_get_contents( 'php://input' );
1185                 }
1186
1187                 return $HTTP_RAW_POST_DATA;
1188         }
1189
1190         /**
1191          * Extracts headers from a PHP-style $_SERVER array.
1192          *
1193          * @since 4.4.0
1194          * @access public
1195          *
1196          * @param array $server Associative array similar to `$_SERVER`.
1197          * @return array Headers extracted from the input.
1198          */
1199         public function get_headers( $server ) {
1200                 $headers = array();
1201
1202                 // CONTENT_* headers are not prefixed with HTTP_.
1203                 $additional = array( 'CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true );
1204
1205                 foreach ( $server as $key => $value ) {
1206                         if ( strpos( $key, 'HTTP_' ) === 0 ) {
1207                                 $headers[ substr( $key, 5 ) ] = $value;
1208                         } elseif ( isset( $additional[ $key ] ) ) {
1209                                 $headers[ $key ] = $value;
1210                         }
1211                 }
1212
1213                 return $headers;
1214         }
1215 }