Make WordPress Core

source: trunk/src/wp-includes/rest-api/class-wp-rest-server.php @ 35773

Last change on this file since 35773 was 35773, checked in by wonderboymusic, 10 years ago

REST API: Core typically sends nocache headers on all auth'ed responses, as in wp, admin-ajax, etc. Because the REST API infrastructure is hooked in pre-wp, we should be setting this ourselves.

Adds unit tests.

Props joehoyle.
Fixes #34832.

File size: 33.0 KB
Line 
1<?php
2/**
3 * REST API: WP_REST_Server class
4 *
5 * @package WordPress
6 * @subpackage REST_API
7 * @since 4.4.0
8 */
9
10/**
11 * Core class used to implement the WordPress REST API server.
12 *
13 * @since 4.4.0
14 */
15class WP_REST_Server {
16
17        /**
18         * Alias for GET transport method.
19         *
20         * @since 4.4.0
21         * @var string
22         */
23        const READABLE = 'GET';
24
25        /**
26         * Alias for POST transport method.
27         *
28         * @since 4.4.0
29         * @var string
30         */
31        const CREATABLE = 'POST';
32
33        /**
34         * Alias for POST, PUT, PATCH transport methods together.
35         *
36         * @since 4.4.0
37         * @var string
38         */
39        const EDITABLE = 'POST, PUT, PATCH';
40
41        /**
42         * Alias for DELETE transport method.
43         *
44         * @since 4.4.0
45         * @var string
46         */
47        const DELETABLE = 'DELETE';
48
49        /**
50         * Alias for GET, POST, PUT, PATCH & DELETE transport methods together.
51         *
52         * @since 4.4.0
53         * @var string
54         */
55        const ALLMETHODS = 'GET, POST, PUT, PATCH, DELETE';
56
57        /**
58         * Namespaces registered to the server.
59         *
60         * @since 4.4.0
61         * @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}
Note: See TracBrowser for help on using the repository browser.