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