Make WordPress Core

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

Last change on this file since 35758 was 35758, checked in by markjaquith, 10 years ago

Route HEAD API requests through the GET callback method

fixes #34837
props danielbachhuber

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