]> scripts.mit.edu Git - autoinstalls/wordpress.git/blob - wp-includes/rest-api.php
WordPress 4.7.2
[autoinstalls/wordpress.git] / wp-includes / rest-api.php
1 <?php
2 /**
3  * REST API functions.
4  *
5  * @package WordPress
6  * @subpackage REST_API
7  * @since 4.4.0
8  */
9
10 /**
11  * Version number for our API.
12  *
13  * @var string
14  */
15 define( 'REST_API_VERSION', '2.0' );
16
17 /**
18  * Registers a REST API route.
19  *
20  * @since 4.4.0
21  *
22  * @global WP_REST_Server $wp_rest_server ResponseHandler instance (usually WP_REST_Server).
23  *
24  * @param string $namespace The first URL segment after core prefix. Should be unique to your package/plugin.
25  * @param string $route     The base URL for route you are adding.
26  * @param array  $args      Optional. Either an array of options for the endpoint, or an array of arrays for
27  *                          multiple methods. Default empty array.
28  * @param bool   $override  Optional. If the route already exists, should we override it? True overrides,
29  *                          false merges (with newer overriding if duplicate keys exist). Default false.
30  * @return bool True on success, false on error.
31  */
32 function register_rest_route( $namespace, $route, $args = array(), $override = false ) {
33         /** @var WP_REST_Server $wp_rest_server */
34         global $wp_rest_server;
35
36         if ( empty( $namespace ) ) {
37                 /*
38                  * Non-namespaced routes are not allowed, with the exception of the main
39                  * and namespace indexes. If you really need to register a
40                  * non-namespaced route, call `WP_REST_Server::register_route` directly.
41                  */
42                 _doing_it_wrong( 'register_rest_route', __( 'Routes must be namespaced with plugin or theme name and version.' ), '4.4.0' );
43                 return false;
44         } else if ( empty( $route ) ) {
45                 _doing_it_wrong( 'register_rest_route', __( 'Route must be specified.' ), '4.4.0' );
46                 return false;
47         }
48
49         if ( isset( $args['args'] ) ) {
50                 $common_args = $args['args'];
51                 unset( $args['args'] );
52         } else {
53                 $common_args = array();
54         }
55
56         if ( isset( $args['callback'] ) ) {
57                 // Upgrade a single set to multiple.
58                 $args = array( $args );
59         }
60
61         $defaults = array(
62                 'methods'         => 'GET',
63                 'callback'        => null,
64                 'args'            => array(),
65         );
66         foreach ( $args as $key => &$arg_group ) {
67                 if ( ! is_numeric( $key ) ) {
68                         // Route option, skip here.
69                         continue;
70                 }
71
72                 $arg_group = array_merge( $defaults, $arg_group );
73                 $arg_group['args'] = array_merge( $common_args, $arg_group['args'] );
74         }
75
76         $full_route = '/' . trim( $namespace, '/' ) . '/' . trim( $route, '/' );
77         $wp_rest_server->register_route( $namespace, $full_route, $args, $override );
78         return true;
79 }
80
81 /**
82  * Registers a new field on an existing WordPress object type.
83  *
84  * @since 4.7.0
85  *
86  * @global array $wp_rest_additional_fields Holds registered fields, organized
87  *                                          by object type.
88  *
89  * @param string|array $object_type Object(s) the field is being registered
90  *                                  to, "post"|"term"|"comment" etc.
91  * @param string $attribute         The attribute name.
92  * @param array  $args {
93  *     Optional. An array of arguments used to handle the registered field.
94  *
95  *     @type string|array|null $get_callback    Optional. The callback function used to retrieve the field
96  *                                              value. Default is 'null', the field will not be returned in
97  *                                              the response.
98  *     @type string|array|null $update_callback Optional. The callback function used to set and update the
99  *                                              field value. Default is 'null', the value cannot be set or
100  *                                              updated.
101  *     @type string|array|null $schema          Optional. The callback function used to create the schema for
102  *                                              this field. Default is 'null', no schema entry will be returned.
103  * }
104  */
105 function register_rest_field( $object_type, $attribute, $args = array() ) {
106         $defaults = array(
107                 'get_callback'    => null,
108                 'update_callback' => null,
109                 'schema'          => null,
110         );
111
112         $args = wp_parse_args( $args, $defaults );
113
114         global $wp_rest_additional_fields;
115
116         $object_types = (array) $object_type;
117
118         foreach ( $object_types as $object_type ) {
119                 $wp_rest_additional_fields[ $object_type ][ $attribute ] = $args;
120         }
121 }
122
123 /**
124  * Registers rewrite rules for the API.
125  *
126  * @since 4.4.0
127  *
128  * @see rest_api_register_rewrites()
129  * @global WP $wp Current WordPress environment instance.
130  */
131 function rest_api_init() {
132         rest_api_register_rewrites();
133
134         global $wp;
135         $wp->add_query_var( 'rest_route' );
136 }
137
138 /**
139  * Adds REST rewrite rules.
140  *
141  * @since 4.4.0
142  *
143  * @see add_rewrite_rule()
144  * @global WP_Rewrite $wp_rewrite
145  */
146 function rest_api_register_rewrites() {
147         global $wp_rewrite;
148
149         add_rewrite_rule( '^' . rest_get_url_prefix() . '/?$','index.php?rest_route=/','top' );
150         add_rewrite_rule( '^' . rest_get_url_prefix() . '/(.*)?','index.php?rest_route=/$matches[1]','top' );
151         add_rewrite_rule( '^' . $wp_rewrite->index . '/' . rest_get_url_prefix() . '/?$','index.php?rest_route=/','top' );
152         add_rewrite_rule( '^' . $wp_rewrite->index . '/' . rest_get_url_prefix() . '/(.*)?','index.php?rest_route=/$matches[1]','top' );
153 }
154
155 /**
156  * Registers the default REST API filters.
157  *
158  * Attached to the {@see 'rest_api_init'} action
159  * to make testing and disabling these filters easier.
160  *
161  * @since 4.4.0
162  */
163 function rest_api_default_filters() {
164         // Deprecated reporting.
165         add_action( 'deprecated_function_run', 'rest_handle_deprecated_function', 10, 3 );
166         add_filter( 'deprecated_function_trigger_error', '__return_false' );
167         add_action( 'deprecated_argument_run', 'rest_handle_deprecated_argument', 10, 3 );
168         add_filter( 'deprecated_argument_trigger_error', '__return_false' );
169
170         // Default serving.
171         add_filter( 'rest_pre_serve_request', 'rest_send_cors_headers' );
172         add_filter( 'rest_post_dispatch', 'rest_send_allow_header', 10, 3 );
173
174         add_filter( 'rest_pre_dispatch', 'rest_handle_options_request', 10, 3 );
175 }
176
177 /**
178  * Registers default REST API routes.
179  *
180  * @since 4.7.0
181  */
182 function create_initial_rest_routes() {
183         foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
184                 $class = ! empty( $post_type->rest_controller_class ) ? $post_type->rest_controller_class : 'WP_REST_Posts_Controller';
185
186                 if ( ! class_exists( $class ) ) {
187                         continue;
188                 }
189                 $controller = new $class( $post_type->name );
190                 if ( ! is_subclass_of( $controller, 'WP_REST_Controller' ) ) {
191                         continue;
192                 }
193
194                 $controller->register_routes();
195
196                 if ( post_type_supports( $post_type->name, 'revisions' ) ) {
197                         $revisions_controller = new WP_REST_Revisions_Controller( $post_type->name );
198                         $revisions_controller->register_routes();
199                 }
200         }
201
202         // Post types.
203         $controller = new WP_REST_Post_Types_Controller;
204         $controller->register_routes();
205
206         // Post statuses.
207         $controller = new WP_REST_Post_Statuses_Controller;
208         $controller->register_routes();
209
210         // Taxonomies.
211         $controller = new WP_REST_Taxonomies_Controller;
212         $controller->register_routes();
213
214         // Terms.
215         foreach ( get_taxonomies( array( 'show_in_rest' => true ), 'object' ) as $taxonomy ) {
216                 $class = ! empty( $taxonomy->rest_controller_class ) ? $taxonomy->rest_controller_class : 'WP_REST_Terms_Controller';
217
218                 if ( ! class_exists( $class ) ) {
219                         continue;
220                 }
221                 $controller = new $class( $taxonomy->name );
222                 if ( ! is_subclass_of( $controller, 'WP_REST_Controller' ) ) {
223                         continue;
224                 }
225
226                 $controller->register_routes();
227         }
228
229         // Users.
230         $controller = new WP_REST_Users_Controller;
231         $controller->register_routes();
232
233         // Comments.
234         $controller = new WP_REST_Comments_Controller;
235         $controller->register_routes();
236
237         // Settings.
238         $controller = new WP_REST_Settings_Controller;
239         $controller->register_routes();
240 }
241
242 /**
243  * Loads the REST API.
244  *
245  * @since 4.4.0
246  *
247  * @global WP             $wp             Current WordPress environment instance.
248  * @global WP_REST_Server $wp_rest_server ResponseHandler instance (usually WP_REST_Server).
249  */
250 function rest_api_loaded() {
251         if ( empty( $GLOBALS['wp']->query_vars['rest_route'] ) ) {
252                 return;
253         }
254
255         /**
256          * Whether this is a REST Request.
257          *
258          * @since 4.4.0
259          * @var bool
260          */
261         define( 'REST_REQUEST', true );
262
263         // Initialize the server.
264         $server = rest_get_server();
265
266         // Fire off the request.
267         $server->serve_request( untrailingslashit( $GLOBALS['wp']->query_vars['rest_route'] ) );
268
269         // We're done.
270         die();
271 }
272
273 /**
274  * Retrieves the URL prefix for any API resource.
275  *
276  * @since 4.4.0
277  *
278  * @return string Prefix.
279  */
280 function rest_get_url_prefix() {
281         /**
282          * Filters the REST URL prefix.
283          *
284          * @since 4.4.0
285          *
286          * @param string $prefix URL prefix. Default 'wp-json'.
287          */
288         return apply_filters( 'rest_url_prefix', 'wp-json' );
289 }
290
291 /**
292  * Retrieves the URL to a REST endpoint on a site.
293  *
294  * Note: The returned URL is NOT escaped.
295  *
296  * @since 4.4.0
297  *
298  * @todo Check if this is even necessary
299  * @global WP_Rewrite $wp_rewrite
300  *
301  * @param int    $blog_id Optional. Blog ID. Default of null returns URL for current blog.
302  * @param string $path    Optional. REST route. Default '/'.
303  * @param string $scheme  Optional. Sanitization scheme. Default 'rest'.
304  * @return string Full URL to the endpoint.
305  */
306 function get_rest_url( $blog_id = null, $path = '/', $scheme = 'rest' ) {
307         if ( empty( $path ) ) {
308                 $path = '/';
309         }
310
311         if ( is_multisite() && get_blog_option( $blog_id, 'permalink_structure' ) || get_option( 'permalink_structure' ) ) {
312                 global $wp_rewrite;
313
314                 if ( $wp_rewrite->using_index_permalinks() ) {
315                         $url = get_home_url( $blog_id, $wp_rewrite->index . '/' . rest_get_url_prefix(), $scheme );
316                 } else {
317                         $url = get_home_url( $blog_id, rest_get_url_prefix(), $scheme );
318                 }
319
320                 $url .= '/' . ltrim( $path, '/' );
321         } else {
322                 $url = trailingslashit( get_home_url( $blog_id, '', $scheme ) );
323
324                 $path = '/' . ltrim( $path, '/' );
325
326                 $url = add_query_arg( 'rest_route', $path, $url );
327         }
328
329         if ( is_ssl() ) {
330                 // If the current host is the same as the REST URL host, force the REST URL scheme to HTTPS.
331                 if ( $_SERVER['SERVER_NAME'] === parse_url( get_home_url( $blog_id ), PHP_URL_HOST ) ) {
332                         $url = set_url_scheme( $url, 'https' );
333                 }
334         }
335
336         /**
337          * Filters the REST URL.
338          *
339          * Use this filter to adjust the url returned by the get_rest_url() function.
340          *
341          * @since 4.4.0
342          *
343          * @param string $url     REST URL.
344          * @param string $path    REST route.
345          * @param int    $blog_id Blog ID.
346          * @param string $scheme  Sanitization scheme.
347          */
348         return apply_filters( 'rest_url', $url, $path, $blog_id, $scheme );
349 }
350
351 /**
352  * Retrieves the URL to a REST endpoint.
353  *
354  * Note: The returned URL is NOT escaped.
355  *
356  * @since 4.4.0
357  *
358  * @param string $path   Optional. REST route. Default empty.
359  * @param string $scheme Optional. Sanitization scheme. Default 'json'.
360  * @return string Full URL to the endpoint.
361  */
362 function rest_url( $path = '', $scheme = 'json' ) {
363         return get_rest_url( null, $path, $scheme );
364 }
365
366 /**
367  * Do a REST request.
368  *
369  * Used primarily to route internal requests through WP_REST_Server.
370  *
371  * @since 4.4.0
372  *
373  * @global WP_REST_Server $wp_rest_server ResponseHandler instance (usually WP_REST_Server).
374  *
375  * @param WP_REST_Request|string $request Request.
376  * @return WP_REST_Response REST response.
377  */
378 function rest_do_request( $request ) {
379         $request = rest_ensure_request( $request );
380         return rest_get_server()->dispatch( $request );
381 }
382
383 /**
384  * Retrieves the current REST server instance.
385  *
386  * Instantiates a new instance if none exists already.
387  *
388  * @since 4.5.0
389  *
390  * @global WP_REST_Server $wp_rest_server REST server instance.
391  *
392  * @return WP_REST_Server REST server instance.
393  */
394 function rest_get_server() {
395         /* @var WP_REST_Server $wp_rest_server */
396         global $wp_rest_server;
397
398         if ( empty( $wp_rest_server ) ) {
399                 /**
400                  * Filters the REST Server Class.
401                  *
402                  * This filter allows you to adjust the server class used by the API, using a
403                  * different class to handle requests.
404                  *
405                  * @since 4.4.0
406                  *
407                  * @param string $class_name The name of the server class. Default 'WP_REST_Server'.
408                  */
409                 $wp_rest_server_class = apply_filters( 'wp_rest_server_class', 'WP_REST_Server' );
410                 $wp_rest_server = new $wp_rest_server_class;
411
412                 /**
413                  * Fires when preparing to serve an API request.
414                  *
415                  * Endpoint objects should be created and register their hooks on this action rather
416                  * than another action to ensure they're only loaded when needed.
417                  *
418                  * @since 4.4.0
419                  *
420                  * @param WP_REST_Server $wp_rest_server Server object.
421                  */
422                 do_action( 'rest_api_init', $wp_rest_server );
423         }
424
425         return $wp_rest_server;
426 }
427
428 /**
429  * Ensures request arguments are a request object (for consistency).
430  *
431  * @since 4.4.0
432  *
433  * @param array|WP_REST_Request $request Request to check.
434  * @return WP_REST_Request REST request instance.
435  */
436 function rest_ensure_request( $request ) {
437         if ( $request instanceof WP_REST_Request ) {
438                 return $request;
439         }
440
441         return new WP_REST_Request( 'GET', '', $request );
442 }
443
444 /**
445  * Ensures a REST response is a response object (for consistency).
446  *
447  * This implements WP_HTTP_Response, allowing usage of `set_status`/`header`/etc
448  * without needing to double-check the object. Will also allow WP_Error to indicate error
449  * responses, so users should immediately check for this value.
450  *
451  * @since 4.4.0
452  *
453  * @param WP_Error|WP_HTTP_Response|mixed $response Response to check.
454  * @return WP_REST_Response|mixed If response generated an error, WP_Error, if response
455  *                                is already an instance, WP_HTTP_Response, otherwise
456  *                                returns a new WP_REST_Response instance.
457  */
458 function rest_ensure_response( $response ) {
459         if ( is_wp_error( $response ) ) {
460                 return $response;
461         }
462
463         if ( $response instanceof WP_HTTP_Response ) {
464                 return $response;
465         }
466
467         return new WP_REST_Response( $response );
468 }
469
470 /**
471  * Handles _deprecated_function() errors.
472  *
473  * @since 4.4.0
474  *
475  * @param string $function    The function that was called.
476  * @param string $replacement The function that should have been called.
477  * @param string $version     Version.
478  */
479 function rest_handle_deprecated_function( $function, $replacement, $version ) {
480         if ( ! empty( $replacement ) ) {
481                 /* translators: 1: function name, 2: WordPress version number, 3: new function name */
482                 $string = sprintf( __( '%1$s (since %2$s; use %3$s instead)' ), $function, $version, $replacement );
483         } else {
484                 /* translators: 1: function name, 2: WordPress version number */
485                 $string = sprintf( __( '%1$s (since %2$s; no alternative available)' ), $function, $version );
486         }
487
488         header( sprintf( 'X-WP-DeprecatedFunction: %s', $string ) );
489 }
490
491 /**
492  * Handles _deprecated_argument() errors.
493  *
494  * @since 4.4.0
495  *
496  * @param string $function    The function that was called.
497  * @param string $message     A message regarding the change.
498  * @param string $version     Version.
499  */
500 function rest_handle_deprecated_argument( $function, $message, $version ) {
501         if ( ! empty( $message ) ) {
502                 /* translators: 1: function name, 2: WordPress version number, 3: error message */
503                 $string = sprintf( __( '%1$s (since %2$s; %3$s)' ), $function, $version, $message );
504         } else {
505                 /* translators: 1: function name, 2: WordPress version number */
506                 $string = sprintf( __( '%1$s (since %2$s; no alternative available)' ), $function, $version );
507         }
508
509         header( sprintf( 'X-WP-DeprecatedParam: %s', $string ) );
510 }
511
512 /**
513  * Sends Cross-Origin Resource Sharing headers with API requests.
514  *
515  * @since 4.4.0
516  *
517  * @param mixed $value Response data.
518  * @return mixed Response data.
519  */
520 function rest_send_cors_headers( $value ) {
521         $origin = get_http_origin();
522
523         if ( $origin ) {
524                 header( 'Access-Control-Allow-Origin: ' . esc_url_raw( $origin ) );
525                 header( 'Access-Control-Allow-Methods: OPTIONS, GET, POST, PUT, PATCH, DELETE' );
526                 header( 'Access-Control-Allow-Credentials: true' );
527                 header( 'Vary: Origin' );
528         }
529
530         return $value;
531 }
532
533 /**
534  * Handles OPTIONS requests for the server.
535  *
536  * This is handled outside of the server code, as it doesn't obey normal route
537  * mapping.
538  *
539  * @since 4.4.0
540  *
541  * @param mixed           $response Current response, either response or `null` to indicate pass-through.
542  * @param WP_REST_Server  $handler  ResponseHandler instance (usually WP_REST_Server).
543  * @param WP_REST_Request $request  The request that was used to make current response.
544  * @return WP_REST_Response Modified response, either response or `null` to indicate pass-through.
545  */
546 function rest_handle_options_request( $response, $handler, $request ) {
547         if ( ! empty( $response ) || $request->get_method() !== 'OPTIONS' ) {
548                 return $response;
549         }
550
551         $response = new WP_REST_Response();
552         $data = array();
553
554         foreach ( $handler->get_routes() as $route => $endpoints ) {
555                 $match = preg_match( '@^' . $route . '$@i', $request->get_route() );
556
557                 if ( ! $match ) {
558                         continue;
559                 }
560
561                 $data = $handler->get_data_for_route( $route, $endpoints, 'help' );
562                 $response->set_matched_route( $route );
563                 break;
564         }
565
566         $response->set_data( $data );
567         return $response;
568 }
569
570 /**
571  * Sends the "Allow" header to state all methods that can be sent to the current route.
572  *
573  * @since 4.4.0
574  *
575  * @param WP_REST_Response $response Current response being served.
576  * @param WP_REST_Server   $server   ResponseHandler instance (usually WP_REST_Server).
577  * @param WP_REST_Request  $request  The request that was used to make current response.
578  * @return WP_REST_Response Response to be served, with "Allow" header if route has allowed methods.
579  */
580 function rest_send_allow_header( $response, $server, $request ) {
581         $matched_route = $response->get_matched_route();
582
583         if ( ! $matched_route ) {
584                 return $response;
585         }
586
587         $routes = $server->get_routes();
588
589         $allowed_methods = array();
590
591         // Get the allowed methods across the routes.
592         foreach ( $routes[ $matched_route ] as $_handler ) {
593                 foreach ( $_handler['methods'] as $handler_method => $value ) {
594
595                         if ( ! empty( $_handler['permission_callback'] ) ) {
596
597                                 $permission = call_user_func( $_handler['permission_callback'], $request );
598
599                                 $allowed_methods[ $handler_method ] = true === $permission;
600                         } else {
601                                 $allowed_methods[ $handler_method ] = true;
602                         }
603                 }
604         }
605
606         // Strip out all the methods that are not allowed (false values).
607         $allowed_methods = array_filter( $allowed_methods );
608
609         if ( $allowed_methods ) {
610                 $response->header( 'Allow', implode( ', ', array_map( 'strtoupper', array_keys( $allowed_methods ) ) ) );
611         }
612
613         return $response;
614 }
615
616 /**
617  * Adds the REST API URL to the WP RSD endpoint.
618  *
619  * @since 4.4.0
620  *
621  * @see get_rest_url()
622  */
623 function rest_output_rsd() {
624         $api_root = get_rest_url();
625
626         if ( empty( $api_root ) ) {
627                 return;
628         }
629         ?>
630         <api name="WP-API" blogID="1" preferred="false" apiLink="<?php echo esc_url( $api_root ); ?>" />
631         <?php
632 }
633
634 /**
635  * Outputs the REST API link tag into page header.
636  *
637  * @since 4.4.0
638  *
639  * @see get_rest_url()
640  */
641 function rest_output_link_wp_head() {
642         $api_root = get_rest_url();
643
644         if ( empty( $api_root ) ) {
645                 return;
646         }
647
648         echo "<link rel='https://api.w.org/' href='" . esc_url( $api_root ) . "' />\n";
649 }
650
651 /**
652  * Sends a Link header for the REST API.
653  *
654  * @since 4.4.0
655  */
656 function rest_output_link_header() {
657         if ( headers_sent() ) {
658                 return;
659         }
660
661         $api_root = get_rest_url();
662
663         if ( empty( $api_root ) ) {
664                 return;
665         }
666
667         header( 'Link: <' . esc_url_raw( $api_root ) . '>; rel="https://api.w.org/"', false );
668 }
669
670 /**
671  * Checks for errors when using cookie-based authentication.
672  *
673  * WordPress' built-in cookie authentication is always active
674  * for logged in users. However, the API has to check nonces
675  * for each request to ensure users are not vulnerable to CSRF.
676  *
677  * @since 4.4.0
678  *
679  * @global mixed          $wp_rest_auth_cookie
680  * @global WP_REST_Server $wp_rest_server      REST server instance.
681  *
682  * @param WP_Error|mixed $result Error from another authentication handler,
683  *                               null if we should handle it, or another value
684  *                               if not.
685  * @return WP_Error|mixed|bool WP_Error if the cookie is invalid, the $result, otherwise true.
686  */
687 function rest_cookie_check_errors( $result ) {
688         if ( ! empty( $result ) ) {
689                 return $result;
690         }
691
692         global $wp_rest_auth_cookie, $wp_rest_server;
693
694         /*
695          * Is cookie authentication being used? (If we get an auth
696          * error, but we're still logged in, another authentication
697          * must have been used).
698          */
699         if ( true !== $wp_rest_auth_cookie && is_user_logged_in() ) {
700                 return $result;
701         }
702
703         // Determine if there is a nonce.
704         $nonce = null;
705
706         if ( isset( $_REQUEST['_wpnonce'] ) ) {
707                 $nonce = $_REQUEST['_wpnonce'];
708         } elseif ( isset( $_SERVER['HTTP_X_WP_NONCE'] ) ) {
709                 $nonce = $_SERVER['HTTP_X_WP_NONCE'];
710         }
711
712         if ( null === $nonce ) {
713                 // No nonce at all, so act as if it's an unauthenticated request.
714                 wp_set_current_user( 0 );
715                 return true;
716         }
717
718         // Check the nonce.
719         $result = wp_verify_nonce( $nonce, 'wp_rest' );
720
721         if ( ! $result ) {
722                 return new WP_Error( 'rest_cookie_invalid_nonce', __( 'Cookie nonce is invalid' ), array( 'status' => 403 ) );
723         }
724
725         // Send a refreshed nonce in header.
726         $wp_rest_server->send_header( 'X-WP-Nonce', wp_create_nonce( 'wp_rest' ) );
727
728         return true;
729 }
730
731 /**
732  * Collects cookie authentication status.
733  *
734  * Collects errors from wp_validate_auth_cookie for use by rest_cookie_check_errors.
735  *
736  * @since 4.4.0
737  *
738  * @see current_action()
739  * @global mixed $wp_rest_auth_cookie
740  */
741 function rest_cookie_collect_status() {
742         global $wp_rest_auth_cookie;
743
744         $status_type = current_action();
745
746         if ( 'auth_cookie_valid' !== $status_type ) {
747                 $wp_rest_auth_cookie = substr( $status_type, 12 );
748                 return;
749         }
750
751         $wp_rest_auth_cookie = true;
752 }
753
754 /**
755  * Parses an RFC3339 time into a Unix timestamp.
756  *
757  * @since 4.4.0
758  *
759  * @param string $date      RFC3339 timestamp.
760  * @param bool   $force_utc Optional. Whether to force UTC timezone instead of using
761  *                          the timestamp's timezone. Default false.
762  * @return int Unix timestamp.
763  */
764 function rest_parse_date( $date, $force_utc = false ) {
765         if ( $force_utc ) {
766                 $date = preg_replace( '/[+-]\d+:?\d+$/', '+00:00', $date );
767         }
768
769         $regex = '#^\d{4}-\d{2}-\d{2}[Tt ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}(?::\d{2})?)?$#';
770
771         if ( ! preg_match( $regex, $date, $matches ) ) {
772                 return false;
773         }
774
775         return strtotime( $date );
776 }
777
778 /**
779  * Retrieves a local date with its GMT equivalent, in MySQL datetime format.
780  *
781  * @since 4.4.0
782  *
783  * @see rest_parse_date()
784  *
785  * @param string $date      RFC3339 timestamp.
786  * @param bool   $force_utc Whether a UTC timestamp should be forced. Default false.
787  * @return array|null Local and UTC datetime strings, in MySQL datetime format (Y-m-d H:i:s),
788  *                    null on failure.
789  */
790 function rest_get_date_with_gmt( $date, $force_utc = false ) {
791         $date = rest_parse_date( $date, $force_utc );
792
793         if ( empty( $date ) ) {
794                 return null;
795         }
796
797         $utc = date( 'Y-m-d H:i:s', $date );
798         $local = get_date_from_gmt( $utc );
799
800         return array( $local, $utc );
801 }
802
803 /**
804  * Returns a contextual HTTP error code for authorization failure.
805  *
806  * @since 4.7.0
807  *
808  * @return integer 401 if the user is not logged in, 403 if the user is logged in.
809  */
810 function rest_authorization_required_code() {
811         return is_user_logged_in() ? 403 : 401;
812 }
813
814 /**
815  * Validate a request argument based on details registered to the route.
816  *
817  * @since 4.7.0
818  *
819  * @param  mixed            $value
820  * @param  WP_REST_Request  $request
821  * @param  string           $param
822  * @return WP_Error|boolean
823  */
824 function rest_validate_request_arg( $value, $request, $param ) {
825         $attributes = $request->get_attributes();
826         if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) {
827                 return true;
828         }
829         $args = $attributes['args'][ $param ];
830
831         return rest_validate_value_from_schema( $value, $args, $param );
832 }
833
834 /**
835  * Sanitize a request argument based on details registered to the route.
836  *
837  * @since 4.7.0
838  *
839  * @param  mixed            $value
840  * @param  WP_REST_Request  $request
841  * @param  string           $param
842  * @return mixed
843  */
844 function rest_sanitize_request_arg( $value, $request, $param ) {
845         $attributes = $request->get_attributes();
846         if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) {
847                 return $value;
848         }
849         $args = $attributes['args'][ $param ];
850
851         return rest_sanitize_value_from_schema( $value, $args );
852 }
853
854 /**
855  * Parse a request argument based on details registered to the route.
856  *
857  * Runs a validation check and sanitizes the value, primarily to be used via
858  * the `sanitize_callback` arguments in the endpoint args registration.
859  *
860  * @since 4.7.0
861  *
862  * @param  mixed            $value
863  * @param  WP_REST_Request  $request
864  * @param  string           $param
865  * @return mixed
866  */
867 function rest_parse_request_arg( $value, $request, $param ) {
868         $is_valid = rest_validate_request_arg( $value, $request, $param );
869
870         if ( is_wp_error( $is_valid ) ) {
871                 return $is_valid;
872         }
873
874         $value = rest_sanitize_request_arg( $value, $request, $param );
875
876         return $value;
877 }
878
879 /**
880  * Determines if an IP address is valid.
881  *
882  * Handles both IPv4 and IPv6 addresses.
883  *
884  * @since 4.7.0
885  *
886  * @param  string $ip IP address.
887  * @return string|false The valid IP address, otherwise false.
888  */
889 function rest_is_ip_address( $ip ) {
890         $ipv4_pattern = '/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/';
891
892         if ( ! preg_match( $ipv4_pattern, $ip ) && ! Requests_IPv6::check_ipv6( $ip ) ) {
893                 return false;
894         }
895
896         return $ip;
897 }
898
899 /**
900  * Changes a boolean-like value into the proper boolean value.
901  *
902  * @since 4.7.0
903  *
904  * @param bool|string|int $value The value being evaluated.
905  * @return boolean Returns the proper associated boolean value.
906  */
907 function rest_sanitize_boolean( $value ) {
908         // String values are translated to `true`; make sure 'false' is false.
909         if ( is_string( $value )  ) {
910                 $value = strtolower( $value );
911                 if ( in_array( $value, array( 'false', '0' ), true ) ) {
912                         $value = false;
913                 }
914         }
915
916         // Everything else will map nicely to boolean.
917         return (boolean) $value;
918 }
919
920 /**
921  * Determines if a given value is boolean-like.
922  *
923  * @since 4.7.0
924  *
925  * @param bool|string $maybe_bool The value being evaluated.
926  * @return boolean True if a boolean, otherwise false.
927  */
928 function rest_is_boolean( $maybe_bool ) {
929         if ( is_bool( $maybe_bool ) ) {
930                 return true;
931         }
932
933         if ( is_string( $maybe_bool ) ) {
934                 $maybe_bool = strtolower( $maybe_bool );
935
936                 $valid_boolean_values = array(
937                         'false',
938                         'true',
939                         '0',
940                         '1',
941                 );
942
943                 return in_array( $maybe_bool, $valid_boolean_values, true );
944         }
945
946         if ( is_int( $maybe_bool ) ) {
947                 return in_array( $maybe_bool, array( 0, 1 ), true );
948         }
949
950         return false;
951 }
952
953 /**
954  * Retrieves the avatar urls in various sizes based on a given email address.
955  *
956  * @since 4.7.0
957  *
958  * @see get_avatar_url()
959  *
960  * @param string $email Email address.
961  * @return array $urls Gravatar url for each size.
962  */
963 function rest_get_avatar_urls( $email ) {
964         $avatar_sizes = rest_get_avatar_sizes();
965
966         $urls = array();
967         foreach ( $avatar_sizes as $size ) {
968                 $urls[ $size ] = get_avatar_url( $email, array( 'size' => $size ) );
969         }
970
971         return $urls;
972 }
973
974 /**
975  * Retrieves the pixel sizes for avatars.
976  *
977  * @since 4.7.0
978  *
979  * @return array List of pixel sizes for avatars. Default `[ 24, 48, 96 ]`.
980  */
981 function rest_get_avatar_sizes() {
982         /**
983          * Filter the REST avatar sizes.
984          *
985          * Use this filter to adjust the array of sizes returned by the
986          * `rest_get_avatar_sizes` function.
987          *
988          * @since 4.4.0
989          *
990          * @param array $sizes An array of int values that are the pixel sizes for avatars.
991          *                     Default `[ 24, 48, 96 ]`.
992          */
993         return apply_filters( 'rest_avatar_sizes', array( 24, 48, 96 ) );
994 }
995
996 /**
997  * Validate a value based on a schema.
998  *
999  * @param mixed  $value The value to validate.
1000  * @param array  $args  Schema array to use for validation.
1001  * @param string $param The parameter name, used in error messages.
1002  * @return true|WP_Error
1003  */
1004 function rest_validate_value_from_schema( $value, $args, $param = '' ) {
1005         if ( 'array' === $args['type'] ) {
1006                 if ( ! is_array( $value ) ) {
1007                         $value = preg_split( '/[\s,]+/', $value );
1008                 }
1009                 if ( ! wp_is_numeric_array( $value ) ) {
1010                         /* translators: 1: parameter, 2: type name */
1011                         return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'array' ) );
1012                 }
1013                 foreach ( $value as $index => $v ) {
1014                         $is_valid = rest_validate_value_from_schema( $v, $args['items'], $param . '[' . $index . ']' );
1015                         if ( is_wp_error( $is_valid ) ) {
1016                                 return $is_valid;
1017                         }
1018                 }
1019         }
1020         if ( ! empty( $args['enum'] ) ) {
1021                 if ( ! in_array( $value, $args['enum'], true ) ) {
1022                         /* translators: 1: parameter, 2: list of valid values */
1023                         return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not one of %2$s.' ), $param, implode( ', ', $args['enum'] ) ) );
1024                 }
1025         }
1026
1027         if ( in_array( $args['type'], array( 'integer', 'number' ) ) && ! is_numeric( $value ) ) {
1028                 /* translators: 1: parameter, 2: type name */
1029                 return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, $args['type'] ) );
1030         }
1031
1032         if ( 'integer' === $args['type'] && round( floatval( $value ) ) !== floatval( $value ) ) {
1033                 /* translators: 1: parameter, 2: type name */
1034                 return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'integer' ) );
1035         }
1036
1037         if ( 'boolean' === $args['type'] && ! rest_is_boolean( $value ) ) {
1038                 /* translators: 1: parameter, 2: type name */
1039                 return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $value, 'boolean' ) );
1040         }
1041
1042         if ( 'string' === $args['type'] && ! is_string( $value ) ) {
1043                 /* translators: 1: parameter, 2: type name */
1044                 return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'string' ) );
1045         }
1046
1047         if ( isset( $args['format'] ) ) {
1048                 switch ( $args['format'] ) {
1049                         case 'date-time' :
1050                                 if ( ! rest_parse_date( $value ) ) {
1051                                         return new WP_Error( 'rest_invalid_date', __( 'Invalid date.' ) );
1052                                 }
1053                                 break;
1054
1055                         case 'email' :
1056                                 // is_email() checks for 3 characters (a@b), but
1057                                 // wp_handle_comment_submission() requires 6 characters (a@b.co)
1058                                 //
1059                                 // https://core.trac.wordpress.org/ticket/38506
1060                                 if ( ! is_email( $value ) || strlen( $value ) < 6 ) {
1061                                         return new WP_Error( 'rest_invalid_email', __( 'Invalid email address.' ) );
1062                                 }
1063                                 break;
1064                         case 'ip' :
1065                                 if ( ! rest_is_ip_address( $value ) ) {
1066                                         /* translators: %s: IP address */
1067                                         return new WP_Error( 'rest_invalid_param', sprintf( __( '%s is not a valid IP address.' ), $value ) );
1068                                 }
1069                                 break;
1070                 }
1071         }
1072
1073         if ( in_array( $args['type'], array( 'number', 'integer' ), true ) && ( isset( $args['minimum'] ) || isset( $args['maximum'] ) ) ) {
1074                 if ( isset( $args['minimum'] ) && ! isset( $args['maximum'] ) ) {
1075                         if ( ! empty( $args['exclusiveMinimum'] ) && $value <= $args['minimum'] ) {
1076                                 /* translators: 1: parameter, 2: minimum number */
1077                                 return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be greater than %2$d (exclusive)' ), $param, $args['minimum'] ) );
1078                         } elseif ( empty( $args['exclusiveMinimum'] ) && $value < $args['minimum'] ) {
1079                                 /* translators: 1: parameter, 2: minimum number */
1080                                 return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be greater than %2$d (inclusive)' ), $param, $args['minimum'] ) );
1081                         }
1082                 } elseif ( isset( $args['maximum'] ) && ! isset( $args['minimum'] ) ) {
1083                         if ( ! empty( $args['exclusiveMaximum'] ) && $value >= $args['maximum'] ) {
1084                                 /* translators: 1: parameter, 2: maximum number */
1085                                 return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be less than %2$d (exclusive)' ), $param, $args['maximum'] ) );
1086                         } elseif ( empty( $args['exclusiveMaximum'] ) && $value > $args['maximum'] ) {
1087                                 /* translators: 1: parameter, 2: maximum number */
1088                                 return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be less than %2$d (inclusive)' ), $param, $args['maximum'] ) );
1089                         }
1090                 } elseif ( isset( $args['maximum'] ) && isset( $args['minimum'] ) ) {
1091                         if ( ! empty( $args['exclusiveMinimum'] ) && ! empty( $args['exclusiveMaximum'] ) ) {
1092                                 if ( $value >= $args['maximum'] || $value <= $args['minimum'] ) {
1093                                         /* translators: 1: parameter, 2: minimum number, 3: maximum number */
1094                                         return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be between %2$d (exclusive) and %3$d (exclusive)' ), $param, $args['minimum'], $args['maximum'] ) );
1095                                 }
1096                         } elseif ( empty( $args['exclusiveMinimum'] ) && ! empty( $args['exclusiveMaximum'] ) ) {
1097                                 if ( $value >= $args['maximum'] || $value < $args['minimum'] ) {
1098                                         /* translators: 1: parameter, 2: minimum number, 3: maximum number */
1099                                         return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be between %2$d (inclusive) and %3$d (exclusive)' ), $param, $args['minimum'], $args['maximum'] ) );
1100                                 }
1101                         } elseif ( ! empty( $args['exclusiveMinimum'] ) && empty( $args['exclusiveMaximum'] ) ) {
1102                                 if ( $value > $args['maximum'] || $value <= $args['minimum'] ) {
1103                                         /* translators: 1: parameter, 2: minimum number, 3: maximum number */
1104                                         return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be between %2$d (exclusive) and %3$d (inclusive)' ), $param, $args['minimum'], $args['maximum'] ) );
1105                                 }
1106                         } elseif ( empty( $args['exclusiveMinimum'] ) && empty( $args['exclusiveMaximum'] ) ) {
1107                                 if ( $value > $args['maximum'] || $value < $args['minimum'] ) {
1108                                         /* translators: 1: parameter, 2: minimum number, 3: maximum number */
1109                                         return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be between %2$d (inclusive) and %3$d (inclusive)' ), $param, $args['minimum'], $args['maximum'] ) );
1110                                 }
1111                         }
1112                 }
1113         }
1114
1115         return true;
1116 }
1117
1118 /**
1119  * Sanitize a value based on a schema.
1120  *
1121  * @param mixed $value The value to sanitize.
1122  * @param array $args  Schema array to use for sanitization.
1123  * @return true|WP_Error
1124  */
1125 function rest_sanitize_value_from_schema( $value, $args ) {
1126         if ( 'array' === $args['type'] ) {
1127                 if ( empty( $args['items'] ) ) {
1128                         return (array) $value;
1129                 }
1130                 if ( ! is_array( $value ) ) {
1131                         $value = preg_split( '/[\s,]+/', $value );
1132                 }
1133                 foreach ( $value as $index => $v ) {
1134                         $value[ $index ] = rest_sanitize_value_from_schema( $v, $args['items'] );
1135                 }
1136                 // Normalize to numeric array so nothing unexpected
1137                 // is in the keys.
1138                 $value = array_values( $value );
1139                 return $value;
1140         }
1141         if ( 'integer' === $args['type'] ) {
1142                 return (int) $value;
1143         }
1144
1145         if ( 'number' === $args['type'] ) {
1146                 return (float) $value;
1147         }
1148
1149         if ( 'boolean' === $args['type'] ) {
1150                 return rest_sanitize_boolean( $value );
1151         }
1152
1153         if ( isset( $args['format'] ) ) {
1154                 switch ( $args['format'] ) {
1155                         case 'date-time' :
1156                                 return sanitize_text_field( $value );
1157
1158                         case 'email' :
1159                                 /*
1160                                  * sanitize_email() validates, which would be unexpected.
1161                                  */
1162                                 return sanitize_text_field( $value );
1163
1164                         case 'uri' :
1165                                 return esc_url_raw( $value );
1166
1167                         case 'ip' :
1168                                 return sanitize_text_field( $value );
1169                 }
1170         }
1171
1172         if ( 'string' === $args['type'] ) {
1173                 return strval( $value );
1174         }
1175
1176         return $value;
1177 }