]> scripts.mit.edu Git - autoinstalls/wordpress.git/blob - wp-includes/rest-api/class-wp-rest-server.php
Wordpress 4.6
[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                 $this->send_header( 'X-Robots-Tag', 'noindex' );
230
231                 $api_root = get_rest_url();
232                 if ( ! empty( $api_root ) ) {
233                         $this->send_header( 'Link', '<' . esc_url_raw( $api_root ) . '>; rel="https://api.w.org/"' );
234                 }
235
236                 /*
237                  * Mitigate possible JSONP Flash attacks.
238                  *
239                  * https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
240                  */
241                 $this->send_header( 'X-Content-Type-Options', 'nosniff' );
242                 $this->send_header( 'Access-Control-Expose-Headers', 'X-WP-Total, X-WP-TotalPages' );
243                 $this->send_header( 'Access-Control-Allow-Headers', 'Authorization' );
244
245                 /**
246                  * Send nocache headers on authenticated requests.
247                  *
248                  * @since 4.4.0
249                  *
250                  * @param bool $rest_send_nocache_headers Whether to send no-cache headers.
251                  */
252                 $send_no_cache_headers = apply_filters( 'rest_send_nocache_headers', is_user_logged_in() );
253                 if ( $send_no_cache_headers ) {
254                         foreach ( wp_get_nocache_headers() as $header => $header_value ) {
255                                 $this->send_header( $header, $header_value );
256                         }
257                 }
258
259                 /**
260                  * Filters whether the REST API is enabled.
261                  *
262                  * @since 4.4.0
263                  *
264                  * @param bool $rest_enabled Whether the REST API is enabled. Default true.
265                  */
266                 $enabled = apply_filters( 'rest_enabled', true );
267
268                 /**
269                  * Filters whether jsonp is enabled.
270                  *
271                  * @since 4.4.0
272                  *
273                  * @param bool $jsonp_enabled Whether jsonp is enabled. Default true.
274                  */
275                 $jsonp_enabled = apply_filters( 'rest_jsonp_enabled', true );
276
277                 $jsonp_callback = null;
278
279                 if ( ! $enabled ) {
280                         echo $this->json_error( 'rest_disabled', __( 'The REST API is disabled on this site.' ), 404 );
281                         return false;
282                 }
283                 if ( isset( $_GET['_jsonp'] ) ) {
284                         if ( ! $jsonp_enabled ) {
285                                 echo $this->json_error( 'rest_callback_disabled', __( 'JSONP support is disabled on this site.' ), 400 );
286                                 return false;
287                         }
288
289                         $jsonp_callback = $_GET['_jsonp'];
290                         if ( ! wp_check_jsonp_callback( $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( wp_unslash( $_GET ) );
307                 $request->set_body_params( wp_unslash( $_POST ) );
308                 $request->set_file_params( $_FILES );
309                 $request->set_headers( $this->get_headers( wp_unslash( $_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                  * Filters the API response.
339                  *
340                  * Allows modification of the response before returning.
341                  *
342                  * @since 4.4.0
343                  * @since 4.5.0 Applied to embedded responses.
344                  *
345                  * @param WP_HTTP_Response $result  Result to send to the client. Usually a WP_REST_Response.
346                  * @param WP_REST_Server   $this    Server instance.
347                  * @param WP_REST_Request  $request Request used to generate the response.
348                  */
349                 $result = apply_filters( 'rest_post_dispatch', rest_ensure_response( $result ), $this, $request );
350
351                 // Wrap the response in an envelope if asked for.
352                 if ( isset( $_GET['_envelope'] ) ) {
353                         $result = $this->envelope_response( $result, isset( $_GET['_embed'] ) );
354                 }
355
356                 // Send extra data from response objects.
357                 $headers = $result->get_headers();
358                 $this->send_headers( $headers );
359
360                 $code = $result->get_status();
361                 $this->set_status( $code );
362
363                 /**
364                  * Filters whether the request has already been served.
365                  *
366                  * Allow sending the request manually - by returning true, the API result
367                  * will not be sent to the client.
368                  *
369                  * @since 4.4.0
370                  *
371                  * @param bool             $served  Whether the request has already been served.
372                  *                                           Default false.
373                  * @param WP_HTTP_Response $result  Result to send to the client. Usually a WP_REST_Response.
374                  * @param WP_REST_Request  $request Request used to generate the response.
375                  * @param WP_REST_Server   $this    Server instance.
376                  */
377                 $served = apply_filters( 'rest_pre_serve_request', false, $result, $request, $this );
378
379                 if ( ! $served ) {
380                         if ( 'HEAD' === $request->get_method() ) {
381                                 return null;
382                         }
383
384                         // Embed links inside the request.
385                         $result = $this->response_to_data( $result, isset( $_GET['_embed'] ) );
386
387                         $result = wp_json_encode( $result );
388
389                         $json_error_message = $this->get_json_last_error();
390                         if ( $json_error_message ) {
391                                 $json_error_obj = new WP_Error( 'rest_encode_error', $json_error_message, array( 'status' => 500 ) );
392                                 $result = $this->error_to_response( $json_error_obj );
393                                 $result = wp_json_encode( $result->data[0] );
394                         }
395
396                         if ( $jsonp_callback ) {
397                                 // Prepend '/**/' to mitigate possible JSONP Flash attacks
398                                 // https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
399                                 echo '/**/' . $jsonp_callback . '(' . $result . ')';
400                         } else {
401                                 echo $result;
402                         }
403                 }
404                 return null;
405         }
406
407         /**
408          * Converts a response to data to send.
409          *
410          * @since 4.4.0
411          * @access public
412          *
413          * @param WP_REST_Response $response Response object.
414          * @param bool             $embed    Whether links should be embedded.
415          * @return array {
416          *     Data with sub-requests embedded.
417          *
418          *     @type array [$_links]    Links.
419          *     @type array [$_embedded] Embeddeds.
420          * }
421          */
422         public function response_to_data( $response, $embed ) {
423                 $data  = $response->get_data();
424                 $links = $this->get_compact_response_links( $response );
425
426                 if ( ! empty( $links ) ) {
427                         // Convert links to part of the data.
428                         $data['_links'] = $links;
429                 }
430                 if ( $embed ) {
431                         // Determine if this is a numeric array.
432                         if ( wp_is_numeric_array( $data ) ) {
433                                 $data = array_map( array( $this, 'embed_links' ), $data );
434                         } else {
435                                 $data = $this->embed_links( $data );
436                         }
437                 }
438
439                 return $data;
440         }
441
442         /**
443          * Retrieves links from a response.
444          *
445          * Extracts the links from a response into a structured hash, suitable for
446          * direct output.
447          *
448          * @since 4.4.0
449          * @access public
450          * @static
451          *
452          * @param WP_REST_Response $response Response to extract links from.
453          * @return array Map of link relation to list of link hashes.
454          */
455         public static function get_response_links( $response ) {
456                 $links = $response->get_links();
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          * Retrieves the CURIEs (compact URIs) used for relations.
478          *
479          * Extracts the links from a response into a structured hash, suitable for
480          * direct output.
481          *
482          * @since 4.5.0
483          * @access public
484          * @static
485          *
486          * @param WP_REST_Response $response Response to extract links from.
487          * @return array Map of link relation to list of link hashes.
488          */
489         public static function get_compact_response_links( $response ) {
490                 $links = self::get_response_links( $response );
491
492                 if ( empty( $links ) ) {
493                         return array();
494                 }
495
496                 $curies = $response->get_curies();
497                 $used_curies = array();
498
499                 foreach ( $links as $rel => $items ) {
500
501                         // Convert $rel URIs to their compact versions if they exist.
502                         foreach ( $curies as $curie ) {
503                                 $href_prefix = substr( $curie['href'], 0, strpos( $curie['href'], '{rel}' ) );
504                                 if ( strpos( $rel, $href_prefix ) !== 0 ) {
505                                         continue;
506                                 }
507
508                                 // Relation now changes from '$uri' to '$curie:$relation'
509                                 $rel_regex = str_replace( '\{rel\}', '(.+)', preg_quote( $curie['href'], '!' ) );
510                                 preg_match( '!' . $rel_regex . '!', $rel, $matches );
511                                 if ( $matches ) {
512                                         $new_rel = $curie['name'] . ':' . $matches[1];
513                                         $used_curies[ $curie['name'] ] = $curie;
514                                         $links[ $new_rel ] = $items;
515                                         unset( $links[ $rel ] );
516                                         break;
517                                 }
518                         }
519                 }
520
521                 // Push the curies onto the start of the links array.
522                 if ( $used_curies ) {
523                         $links['curies'] = array_values( $used_curies );
524                 }
525
526                 return $links;
527         }
528
529         /**
530          * Embeds the links from the data into the request.
531          *
532          * @since 4.4.0
533          * @access protected
534          *
535          * @param array $data Data from the request.
536          * @return array {
537          *     Data with sub-requests embedded.
538          *
539          *     @type array [$_links]    Links.
540          *     @type array [$_embedded] Embeddeds.
541          * }
542          */
543         protected function embed_links( $data ) {
544                 if ( empty( $data['_links'] ) ) {
545                         return $data;
546                 }
547
548                 $embedded = array();
549
550                 foreach ( $data['_links'] as $rel => $links ) {
551                         // Ignore links to self, for obvious reasons.
552                         if ( 'self' === $rel ) {
553                                 continue;
554                         }
555
556                         $embeds = array();
557
558                         foreach ( $links as $item ) {
559                                 // Determine if the link is embeddable.
560                                 if ( empty( $item['embeddable'] ) ) {
561                                         // Ensure we keep the same order.
562                                         $embeds[] = array();
563                                         continue;
564                                 }
565
566                                 // Run through our internal routing and serve.
567                                 $request = WP_REST_Request::from_url( $item['href'] );
568                                 if ( ! $request ) {
569                                         $embeds[] = array();
570                                         continue;
571                                 }
572
573                                 // Embedded resources get passed context=embed.
574                                 if ( empty( $request['context'] ) ) {
575                                         $request['context'] = 'embed';
576                                 }
577
578                                 $response = $this->dispatch( $request );
579
580                                 /** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */
581                                 $response = apply_filters( 'rest_post_dispatch', rest_ensure_response( $response ), $this, $request );
582
583                                 $embeds[] = $this->response_to_data( $response, false );
584                         }
585
586                         // Determine if any real links were found.
587                         $has_links = count( array_filter( $embeds ) );
588                         if ( $has_links ) {
589                                 $embedded[ $rel ] = $embeds;
590                         }
591                 }
592
593                 if ( ! empty( $embedded ) ) {
594                         $data['_embedded'] = $embedded;
595                 }
596
597                 return $data;
598         }
599
600         /**
601          * Wraps the response in an envelope.
602          *
603          * The enveloping technique is used to work around browser/client
604          * compatibility issues. Essentially, it converts the full HTTP response to
605          * data instead.
606          *
607          * @since 4.4.0
608          * @access public
609          *
610          * @param WP_REST_Response $response Response object.
611          * @param bool             $embed    Whether links should be embedded.
612          * @return WP_REST_Response New response with wrapped data
613          */
614         public function envelope_response( $response, $embed ) {
615                 $envelope = array(
616                         'body'    => $this->response_to_data( $response, $embed ),
617                         'status'  => $response->get_status(),
618                         'headers' => $response->get_headers(),
619                 );
620
621                 /**
622                  * Filters the enveloped form of a response.
623                  *
624                  * @since 4.4.0
625                  *
626                  * @param array            $envelope Envelope data.
627                  * @param WP_REST_Response $response Original response data.
628                  */
629                 $envelope = apply_filters( 'rest_envelope_response', $envelope, $response );
630
631                 // Ensure it's still a response and return.
632                 return rest_ensure_response( $envelope );
633         }
634
635         /**
636          * Registers a route to the server.
637          *
638          * @since 4.4.0
639          * @access public
640          *
641          * @param string $namespace  Namespace.
642          * @param string $route      The REST route.
643          * @param array  $route_args Route arguments.
644          * @param bool   $override   Optional. Whether the route should be overriden if it already exists.
645          *                           Default false.
646          */
647         public function register_route( $namespace, $route, $route_args, $override = false ) {
648                 if ( ! isset( $this->namespaces[ $namespace ] ) ) {
649                         $this->namespaces[ $namespace ] = array();
650
651                         $this->register_route( $namespace, '/' . $namespace, array(
652                                 array(
653                                         'methods' => self::READABLE,
654                                         'callback' => array( $this, 'get_namespace_index' ),
655                                         'args' => array(
656                                                 'namespace' => array(
657                                                         'default' => $namespace,
658                                                 ),
659                                                 'context' => array(
660                                                         'default' => 'view',
661                                                 ),
662                                         ),
663                                 ),
664                         ) );
665                 }
666
667                 // Associative to avoid double-registration.
668                 $this->namespaces[ $namespace ][ $route ] = true;
669                 $route_args['namespace'] = $namespace;
670
671                 if ( $override || empty( $this->endpoints[ $route ] ) ) {
672                         $this->endpoints[ $route ] = $route_args;
673                 } else {
674                         $this->endpoints[ $route ] = array_merge( $this->endpoints[ $route ], $route_args );
675                 }
676         }
677
678         /**
679          * Retrieves the route map.
680          *
681          * The route map is an associative array with path regexes as the keys. The
682          * value is an indexed array with the callback function/method as the first
683          * item, and a bitmask of HTTP methods as the second item (see the class
684          * constants).
685          *
686          * Each route can be mapped to more than one callback by using an array of
687          * the indexed arrays. This allows mapping e.g. GET requests to one callback
688          * and POST requests to another.
689          *
690          * Note that the path regexes (array keys) must have @ escaped, as this is
691          * used as the delimiter with preg_match()
692          *
693          * @since 4.4.0
694          * @access public
695          *
696          * @return array `'/path/regex' => array( $callback, $bitmask )` or
697          *               `'/path/regex' => array( array( $callback, $bitmask ), ...)`.
698          */
699         public function get_routes() {
700
701                 /**
702                  * Filters the array of available endpoints.
703                  *
704                  * @since 4.4.0
705                  *
706                  * @param array $endpoints The available endpoints. An array of matching regex patterns, each mapped
707                  *                         to an array of callbacks for the endpoint. These take the format
708                  *                         `'/path/regex' => array( $callback, $bitmask )` or
709                  *                         `'/path/regex' => array( array( $callback, $bitmask ).
710                  */
711                 $endpoints = apply_filters( 'rest_endpoints', $this->endpoints );
712
713                 // Normalise the endpoints.
714                 $defaults = array(
715                         'methods'       => '',
716                         'accept_json'   => false,
717                         'accept_raw'    => false,
718                         'show_in_index' => true,
719                         'args'          => array(),
720                 );
721
722                 foreach ( $endpoints as $route => &$handlers ) {
723
724                         if ( isset( $handlers['callback'] ) ) {
725                                 // Single endpoint, add one deeper.
726                                 $handlers = array( $handlers );
727                         }
728
729                         if ( ! isset( $this->route_options[ $route ] ) ) {
730                                 $this->route_options[ $route ] = array();
731                         }
732
733                         foreach ( $handlers as $key => &$handler ) {
734
735                                 if ( ! is_numeric( $key ) ) {
736                                         // Route option, move it to the options.
737                                         $this->route_options[ $route ][ $key ] = $handler;
738                                         unset( $handlers[ $key ] );
739                                         continue;
740                                 }
741
742                                 $handler = wp_parse_args( $handler, $defaults );
743
744                                 // Allow comma-separated HTTP methods.
745                                 if ( is_string( $handler['methods'] ) ) {
746                                         $methods = explode( ',', $handler['methods'] );
747                                 } else if ( is_array( $handler['methods'] ) ) {
748                                         $methods = $handler['methods'];
749                                 } else {
750                                         $methods = array();
751                                 }
752
753                                 $handler['methods'] = array();
754
755                                 foreach ( $methods as $method ) {
756                                         $method = strtoupper( trim( $method ) );
757                                         $handler['methods'][ $method ] = true;
758                                 }
759                         }
760                 }
761                 return $endpoints;
762         }
763
764         /**
765          * Retrieves namespaces registered on the server.
766          *
767          * @since 4.4.0
768          * @access public
769          *
770          * @return array List of registered namespaces.
771          */
772         public function get_namespaces() {
773                 return array_keys( $this->namespaces );
774         }
775
776         /**
777          * Retrieves specified options for a route.
778          *
779          * @since 4.4.0
780          * @access public
781          *
782          * @param string $route Route pattern to fetch options for.
783          * @return array|null Data as an associative array if found, or null if not found.
784          */
785         public function get_route_options( $route ) {
786                 if ( ! isset( $this->route_options[ $route ] ) ) {
787                         return null;
788                 }
789
790                 return $this->route_options[ $route ];
791         }
792
793         /**
794          * Matches the request to a callback and call it.
795          *
796          * @since 4.4.0
797          * @access public
798          *
799          * @param WP_REST_Request $request Request to attempt dispatching.
800          * @return WP_REST_Response Response returned by the callback.
801          */
802         public function dispatch( $request ) {
803                 /**
804                  * Filters the pre-calculated result of a REST dispatch request.
805                  *
806                  * Allow hijacking the request before dispatching by returning a non-empty. The returned value
807                  * will be used to serve the request instead.
808                  *
809                  * @since 4.4.0
810                  *
811                  * @param mixed           $result  Response to replace the requested version with. Can be anything
812                  *                                 a normal endpoint can return, or null to not hijack the request.
813                  * @param WP_REST_Server  $this    Server instance.
814                  * @param WP_REST_Request $request Request used to generate the response.
815                  */
816                 $result = apply_filters( 'rest_pre_dispatch', null, $this, $request );
817
818                 if ( ! empty( $result ) ) {
819                         return $result;
820                 }
821
822                 $method = $request->get_method();
823                 $path   = $request->get_route();
824
825                 foreach ( $this->get_routes() as $route => $handlers ) {
826                         $match = preg_match( '@^' . $route . '$@i', $path, $args );
827
828                         if ( ! $match ) {
829                                 continue;
830                         }
831
832                         foreach ( $handlers as $handler ) {
833                                 $callback  = $handler['callback'];
834                                 $response = null;
835
836                                 // Fallback to GET method if no HEAD method is registered.
837                                 $checked_method = $method;
838                                 if ( 'HEAD' === $method && empty( $handler['methods']['HEAD'] ) ) {
839                                         $checked_method = 'GET';
840                                 }
841                                 if ( empty( $handler['methods'][ $checked_method ] ) ) {
842                                         continue;
843                                 }
844
845                                 if ( ! is_callable( $callback ) ) {
846                                         $response = new WP_Error( 'rest_invalid_handler', __( 'The handler for the route is invalid' ), array( 'status' => 500 ) );
847                                 }
848
849                                 if ( ! is_wp_error( $response ) ) {
850                                         // Remove the redundant preg_match argument.
851                                         unset( $args[0] );
852
853                                         $request->set_url_params( $args );
854                                         $request->set_attributes( $handler );
855
856                                         $defaults = array();
857
858                                         foreach ( $handler['args'] as $arg => $options ) {
859                                                 if ( isset( $options['default'] ) ) {
860                                                         $defaults[ $arg ] = $options['default'];
861                                                 }
862                                         }
863
864                                         $request->set_default_params( $defaults );
865
866                                         $check_required = $request->has_valid_params();
867                                         if ( is_wp_error( $check_required ) ) {
868                                                 $response = $check_required;
869                                         }
870
871                                         $request->sanitize_params();
872                                 }
873
874                                 if ( ! is_wp_error( $response ) ) {
875                                         // Check permission specified on the route.
876                                         if ( ! empty( $handler['permission_callback'] ) ) {
877                                                 $permission = call_user_func( $handler['permission_callback'], $request );
878
879                                                 if ( is_wp_error( $permission ) ) {
880                                                         $response = $permission;
881                                                 } else if ( false === $permission || null === $permission ) {
882                                                         $response = new WP_Error( 'rest_forbidden', __( 'Sorry, you are not allowed to do that.' ), array( 'status' => 403 ) );
883                                                 }
884                                         }
885                                 }
886
887                                 if ( ! is_wp_error( $response ) ) {
888                                         /**
889                                          * Filters the REST dispatch request result.
890                                          *
891                                          * Allow plugins to override dispatching the request.
892                                          *
893                                          * @since 4.4.0
894                                          * @since 4.5.0 Added `$route` and `$handler` parameters.
895                                          *
896                                          * @param bool            $dispatch_result Dispatch result, will be used if not empty.
897                                          * @param WP_REST_Request $request         Request used to generate the response.
898                                          * @param string          $route           Route matched for the request.
899                                          * @param array           $handler         Route handler used for the request.
900                                          */
901                                         $dispatch_result = apply_filters( 'rest_dispatch_request', null, $request, $route, $handler );
902
903                                         // Allow plugins to halt the request via this filter.
904                                         if ( null !== $dispatch_result ) {
905                                                 $response = $dispatch_result;
906                                         } else {
907                                                 $response = call_user_func( $callback, $request );
908                                         }
909                                 }
910
911                                 if ( is_wp_error( $response ) ) {
912                                         $response = $this->error_to_response( $response );
913                                 } else {
914                                         $response = rest_ensure_response( $response );
915                                 }
916
917                                 $response->set_matched_route( $route );
918                                 $response->set_matched_handler( $handler );
919
920                                 return $response;
921                         }
922                 }
923
924                 return $this->error_to_response( new WP_Error( 'rest_no_route', __( 'No route was found matching the URL and request method' ), array( 'status' => 404 ) ) );
925         }
926
927         /**
928          * Returns if an error occurred during most recent JSON encode/decode.
929          *
930          * Strings to be translated will be in format like
931          * "Encoding error: Maximum stack depth exceeded".
932          *
933          * @since 4.4.0
934          * @access protected
935          *
936          * @return bool|string Boolean false or string error message.
937          */
938         protected function get_json_last_error() {
939                 // See https://core.trac.wordpress.org/ticket/27799.
940                 if ( ! function_exists( 'json_last_error' ) ) {
941                         return false;
942                 }
943
944                 $last_error_code = json_last_error();
945
946                 if ( ( defined( 'JSON_ERROR_NONE' ) && JSON_ERROR_NONE === $last_error_code ) || empty( $last_error_code ) ) {
947                         return false;
948                 }
949
950                 return json_last_error_msg();
951         }
952
953         /**
954          * Retrieves the site index.
955          *
956          * This endpoint describes the capabilities of the site.
957          *
958          * @since 4.4.0
959          * @access public
960          *
961          * @param array $request {
962          *     Request.
963          *
964          *     @type string $context Context.
965          * }
966          * @return array Index entity
967          */
968         public function get_index( $request ) {
969                 // General site data.
970                 $available = array(
971                         'name'           => get_option( 'blogname' ),
972                         'description'    => get_option( 'blogdescription' ),
973                         'url'            => get_option( 'siteurl' ),
974                         'home'           => home_url(),
975                         'namespaces'     => array_keys( $this->namespaces ),
976                         'authentication' => array(),
977                         'routes'         => $this->get_data_for_routes( $this->get_routes(), $request['context'] ),
978                 );
979
980                 $response = new WP_REST_Response( $available );
981
982                 $response->add_link( 'help', 'http://v2.wp-api.org/' );
983
984                 /**
985                  * Filters the API root index data.
986                  *
987                  * This contains the data describing the API. This includes information
988                  * about supported authentication schemes, supported namespaces, routes
989                  * available on the API, and a small amount of data about the site.
990                  *
991                  * @since 4.4.0
992                  *
993                  * @param WP_REST_Response $response Response data.
994                  */
995                 return apply_filters( 'rest_index', $response );
996         }
997
998         /**
999          * Retrieves the index for a namespace.
1000          *
1001          * @since 4.4.0
1002          * @access public
1003          *
1004          * @param WP_REST_Request $request REST request instance.
1005          * @return WP_REST_Response|WP_Error WP_REST_Response instance if the index was found,
1006          *                                   WP_Error if the namespace isn't set.
1007          */
1008         public function get_namespace_index( $request ) {
1009                 $namespace = $request['namespace'];
1010
1011                 if ( ! isset( $this->namespaces[ $namespace ] ) ) {
1012                         return new WP_Error( 'rest_invalid_namespace', __( 'The specified namespace could not be found.' ), array( 'status' => 404 ) );
1013                 }
1014
1015                 $routes = $this->namespaces[ $namespace ];
1016                 $endpoints = array_intersect_key( $this->get_routes(), $routes );
1017
1018                 $data = array(
1019                         'namespace' => $namespace,
1020                         'routes' => $this->get_data_for_routes( $endpoints, $request['context'] ),
1021                 );
1022                 $response = rest_ensure_response( $data );
1023
1024                 // Link to the root index.
1025                 $response->add_link( 'up', rest_url( '/' ) );
1026
1027                 /**
1028                  * Filters the namespace index data.
1029                  *
1030                  * This typically is just the route data for the namespace, but you can
1031                  * add any data you'd like here.
1032                  *
1033                  * @since 4.4.0
1034                  *
1035                  * @param WP_REST_Response $response Response data.
1036                  * @param WP_REST_Request  $request  Request data. The namespace is passed as the 'namespace' parameter.
1037                  */
1038                 return apply_filters( 'rest_namespace_index', $response, $request );
1039         }
1040
1041         /**
1042          * Retrieves the publicly-visible data for routes.
1043          *
1044          * @since 4.4.0
1045          * @access public
1046          *
1047          * @param array  $routes  Routes to get data for.
1048          * @param string $context Optional. Context for data. Accepts 'view' or 'help'. Default 'view'.
1049          * @return array Route data to expose in indexes.
1050          */
1051         public function get_data_for_routes( $routes, $context = 'view' ) {
1052                 $available = array();
1053
1054                 // Find the available routes.
1055                 foreach ( $routes as $route => $callbacks ) {
1056                         $data = $this->get_data_for_route( $route, $callbacks, $context );
1057                         if ( empty( $data ) ) {
1058                                 continue;
1059                         }
1060
1061                         /**
1062                          * Filters the REST endpoint data.
1063                          *
1064                          * @since 4.4.0
1065                          *
1066                          * @param WP_REST_Request $request Request data. The namespace is passed as the 'namespace' parameter.
1067                          */
1068                         $available[ $route ] = apply_filters( 'rest_endpoints_description', $data );
1069                 }
1070
1071                 /**
1072                  * Filters the publicly-visible data for routes.
1073                  *
1074                  * This data is exposed on indexes and can be used by clients or
1075                  * developers to investigate the site and find out how to use it. It
1076                  * acts as a form of self-documentation.
1077                  *
1078                  * @since 4.4.0
1079                  *
1080                  * @param array $available Map of route to route data.
1081                  * @param array $routes    Internal route data as an associative array.
1082                  */
1083                 return apply_filters( 'rest_route_data', $available, $routes );
1084         }
1085
1086         /**
1087          * Retrieves publicly-visible data for the route.
1088          *
1089          * @since 4.4.0
1090          * @access public
1091          *
1092          * @param string $route     Route to get data for.
1093          * @param array  $callbacks Callbacks to convert to data.
1094          * @param string $context   Optional. Context for the data. Accepts 'view' or 'help'. Default 'view'.
1095          * @return array|null Data for the route, or null if no publicly-visible data.
1096          */
1097         public function get_data_for_route( $route, $callbacks, $context = 'view' ) {
1098                 $data = array(
1099                         'namespace' => '',
1100                         'methods' => array(),
1101                         'endpoints' => array(),
1102                 );
1103
1104                 if ( isset( $this->route_options[ $route ] ) ) {
1105                         $options = $this->route_options[ $route ];
1106
1107                         if ( isset( $options['namespace'] ) ) {
1108                                 $data['namespace'] = $options['namespace'];
1109                         }
1110
1111                         if ( isset( $options['schema'] ) && 'help' === $context ) {
1112                                 $data['schema'] = call_user_func( $options['schema'] );
1113                         }
1114                 }
1115
1116                 $route = preg_replace( '#\(\?P<(\w+?)>.*?\)#', '{$1}', $route );
1117
1118                 foreach ( $callbacks as $callback ) {
1119                         // Skip to the next route if any callback is hidden.
1120                         if ( empty( $callback['show_in_index'] ) ) {
1121                                 continue;
1122                         }
1123
1124                         $data['methods'] = array_merge( $data['methods'], array_keys( $callback['methods'] ) );
1125                         $endpoint_data = array(
1126                                 'methods' => array_keys( $callback['methods'] ),
1127                         );
1128
1129                         if ( isset( $callback['args'] ) ) {
1130                                 $endpoint_data['args'] = array();
1131                                 foreach ( $callback['args'] as $key => $opts ) {
1132                                         $arg_data = array(
1133                                                 'required' => ! empty( $opts['required'] ),
1134                                         );
1135                                         if ( isset( $opts['default'] ) ) {
1136                                                 $arg_data['default'] = $opts['default'];
1137                                         }
1138                                         if ( isset( $opts['enum'] ) ) {
1139                                                 $arg_data['enum'] = $opts['enum'];
1140                                         }
1141                                         if ( isset( $opts['description'] ) ) {
1142                                                 $arg_data['description'] = $opts['description'];
1143                                         }
1144                                         $endpoint_data['args'][ $key ] = $arg_data;
1145                                 }
1146                         }
1147
1148                         $data['endpoints'][] = $endpoint_data;
1149
1150                         // For non-variable routes, generate links.
1151                         if ( strpos( $route, '{' ) === false ) {
1152                                 $data['_links'] = array(
1153                                         'self' => rest_url( $route ),
1154                                 );
1155                         }
1156                 }
1157
1158                 if ( empty( $data['methods'] ) ) {
1159                         // No methods supported, hide the route.
1160                         return null;
1161                 }
1162
1163                 return $data;
1164         }
1165
1166         /**
1167          * Sends an HTTP status code.
1168          *
1169          * @since 4.4.0
1170          * @access protected
1171          *
1172          * @param int $code HTTP status.
1173          */
1174         protected function set_status( $code ) {
1175                 status_header( $code );
1176         }
1177
1178         /**
1179          * Sends an HTTP header.
1180          *
1181          * @since 4.4.0
1182          * @access public
1183          *
1184          * @param string $key Header key.
1185          * @param string $value Header value.
1186          */
1187         public function send_header( $key, $value ) {
1188                 /*
1189                  * Sanitize as per RFC2616 (Section 4.2):
1190                  *
1191                  * Any LWS that occurs between field-content MAY be replaced with a
1192                  * single SP before interpreting the field value or forwarding the
1193                  * message downstream.
1194                  */
1195                 $value = preg_replace( '/\s+/', ' ', $value );
1196                 header( sprintf( '%s: %s', $key, $value ) );
1197         }
1198
1199         /**
1200          * Sends multiple HTTP headers.
1201          *
1202          * @since 4.4.0
1203          * @access public
1204          *
1205          * @param array $headers Map of header name to header value.
1206          */
1207         public function send_headers( $headers ) {
1208                 foreach ( $headers as $key => $value ) {
1209                         $this->send_header( $key, $value );
1210                 }
1211         }
1212
1213         /**
1214          * Retrieves the raw request entity (body).
1215          *
1216          * @since 4.4.0
1217          * @access public
1218          *
1219          * @global string $HTTP_RAW_POST_DATA Raw post data.
1220          *
1221          * @return string Raw request data.
1222          */
1223         public static function get_raw_data() {
1224                 global $HTTP_RAW_POST_DATA;
1225
1226                 /*
1227                  * A bug in PHP < 5.2.2 makes $HTTP_RAW_POST_DATA not set by default,
1228                  * but we can do it ourself.
1229                  */
1230                 if ( ! isset( $HTTP_RAW_POST_DATA ) ) {
1231                         $HTTP_RAW_POST_DATA = file_get_contents( 'php://input' );
1232                 }
1233
1234                 return $HTTP_RAW_POST_DATA;
1235         }
1236
1237         /**
1238          * Extracts headers from a PHP-style $_SERVER array.
1239          *
1240          * @since 4.4.0
1241          * @access public
1242          *
1243          * @param array $server Associative array similar to `$_SERVER`.
1244          * @return array Headers extracted from the input.
1245          */
1246         public function get_headers( $server ) {
1247                 $headers = array();
1248
1249                 // CONTENT_* headers are not prefixed with HTTP_.
1250                 $additional = array( 'CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true );
1251
1252                 foreach ( $server as $key => $value ) {
1253                         if ( strpos( $key, 'HTTP_' ) === 0 ) {
1254                                 $headers[ substr( $key, 5 ) ] = $value;
1255                         } elseif ( isset( $additional[ $key ] ) ) {
1256                                 $headers[ $key ] = $value;
1257                         }
1258                 }
1259
1260                 return $headers;
1261         }
1262 }