rest-api.php (93374B)
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 * Note: Do not use before the {@see 'rest_api_init'} hook. 21 * 22 * @since 4.4.0 23 * @since 5.1.0 Added a `_doing_it_wrong()` notice when not called on or after the `rest_api_init` hook. 24 * @since 5.5.0 Added a `_doing_it_wrong()` notice when the required `permission_callback` argument is not set. 25 * 26 * @param string $namespace The first URL segment after core prefix. Should be unique to your package/plugin. 27 * @param string $route The base URL for route you are adding. 28 * @param array $args Optional. Either an array of options for the endpoint, or an array of arrays for 29 * multiple methods. Default empty array. 30 * @param bool $override Optional. If the route already exists, should we override it? True overrides, 31 * false merges (with newer overriding if duplicate keys exist). Default false. 32 * @return bool True on success, false on error. 33 */ 34 function register_rest_route( $namespace, $route, $args = array(), $override = false ) { 35 if ( empty( $namespace ) ) { 36 /* 37 * Non-namespaced routes are not allowed, with the exception of the main 38 * and namespace indexes. If you really need to register a 39 * non-namespaced route, call `WP_REST_Server::register_route` directly. 40 */ 41 _doing_it_wrong( 'register_rest_route', __( 'Routes must be namespaced with plugin or theme name and version.' ), '4.4.0' ); 42 return false; 43 } elseif ( empty( $route ) ) { 44 _doing_it_wrong( 'register_rest_route', __( 'Route must be specified.' ), '4.4.0' ); 45 return false; 46 } 47 48 $clean_namespace = trim( $namespace, '/' ); 49 50 if ( $clean_namespace !== $namespace ) { 51 _doing_it_wrong( __FUNCTION__, __( 'Namespace must not start or end with a slash.' ), '5.4.2' ); 52 } 53 54 if ( ! did_action( 'rest_api_init' ) ) { 55 _doing_it_wrong( 56 'register_rest_route', 57 sprintf( 58 /* translators: %s: rest_api_init */ 59 __( 'REST API routes must be registered on the %s action.' ), 60 '<code>rest_api_init</code>' 61 ), 62 '5.1.0' 63 ); 64 } 65 66 if ( isset( $args['args'] ) ) { 67 $common_args = $args['args']; 68 unset( $args['args'] ); 69 } else { 70 $common_args = array(); 71 } 72 73 if ( isset( $args['callback'] ) ) { 74 // Upgrade a single set to multiple. 75 $args = array( $args ); 76 } 77 78 $defaults = array( 79 'methods' => 'GET', 80 'callback' => null, 81 'args' => array(), 82 ); 83 84 foreach ( $args as $key => &$arg_group ) { 85 if ( ! is_numeric( $key ) ) { 86 // Route option, skip here. 87 continue; 88 } 89 90 $arg_group = array_merge( $defaults, $arg_group ); 91 $arg_group['args'] = array_merge( $common_args, $arg_group['args'] ); 92 93 if ( ! isset( $arg_group['permission_callback'] ) ) { 94 _doing_it_wrong( 95 __FUNCTION__, 96 sprintf( 97 /* translators: 1: The REST API route being registered, 2: The argument name, 3: The suggested function name. */ 98 __( 'The REST API route definition for %1$s is missing the required %2$s argument. For REST API routes that are intended to be public, use %3$s as the permission callback.' ), 99 '<code>' . $clean_namespace . '/' . trim( $route, '/' ) . '</code>', 100 '<code>permission_callback</code>', 101 '<code>__return_true</code>' 102 ), 103 '5.5.0' 104 ); 105 } 106 } 107 108 $full_route = '/' . $clean_namespace . '/' . trim( $route, '/' ); 109 rest_get_server()->register_route( $clean_namespace, $full_route, $args, $override ); 110 return true; 111 } 112 113 /** 114 * Registers a new field on an existing WordPress object type. 115 * 116 * @since 4.7.0 117 * 118 * @global array $wp_rest_additional_fields Holds registered fields, organized 119 * by object type. 120 * 121 * @param string|array $object_type Object(s) the field is being registered 122 * to, "post"|"term"|"comment" etc. 123 * @param string $attribute The attribute name. 124 * @param array $args { 125 * Optional. An array of arguments used to handle the registered field. 126 * 127 * @type callable|null $get_callback Optional. The callback function used to retrieve the field value. Default is 128 * 'null', the field will not be returned in the response. The function will 129 * be passed the prepared object data. 130 * @type callable|null $update_callback Optional. The callback function used to set and update the field value. Default 131 * is 'null', the value cannot be set or updated. The function will be passed 132 * the model object, like WP_Post. 133 * @type array|null $schema Optional. The schema for this field. 134 * Default is 'null', no schema entry will be returned. 135 * } 136 */ 137 function register_rest_field( $object_type, $attribute, $args = array() ) { 138 $defaults = array( 139 'get_callback' => null, 140 'update_callback' => null, 141 'schema' => null, 142 ); 143 144 $args = wp_parse_args( $args, $defaults ); 145 146 global $wp_rest_additional_fields; 147 148 $object_types = (array) $object_type; 149 150 foreach ( $object_types as $object_type ) { 151 $wp_rest_additional_fields[ $object_type ][ $attribute ] = $args; 152 } 153 } 154 155 /** 156 * Registers rewrite rules for the REST API. 157 * 158 * @since 4.4.0 159 * 160 * @see rest_api_register_rewrites() 161 * @global WP $wp Current WordPress environment instance. 162 */ 163 function rest_api_init() { 164 rest_api_register_rewrites(); 165 166 global $wp; 167 $wp->add_query_var( 'rest_route' ); 168 } 169 170 /** 171 * Adds REST rewrite rules. 172 * 173 * @since 4.4.0 174 * 175 * @see add_rewrite_rule() 176 * @global WP_Rewrite $wp_rewrite WordPress rewrite component. 177 */ 178 function rest_api_register_rewrites() { 179 global $wp_rewrite; 180 181 add_rewrite_rule( '^' . rest_get_url_prefix() . '/?$', 'index.php?rest_route=/', 'top' ); 182 add_rewrite_rule( '^' . rest_get_url_prefix() . '/(.*)?', 'index.php?rest_route=/$matches[1]', 'top' ); 183 add_rewrite_rule( '^' . $wp_rewrite->index . '/' . rest_get_url_prefix() . '/?$', 'index.php?rest_route=/', 'top' ); 184 add_rewrite_rule( '^' . $wp_rewrite->index . '/' . rest_get_url_prefix() . '/(.*)?', 'index.php?rest_route=/$matches[1]', 'top' ); 185 } 186 187 /** 188 * Registers the default REST API filters. 189 * 190 * Attached to the {@see 'rest_api_init'} action 191 * to make testing and disabling these filters easier. 192 * 193 * @since 4.4.0 194 */ 195 function rest_api_default_filters() { 196 if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { 197 // Deprecated reporting. 198 add_action( 'deprecated_function_run', 'rest_handle_deprecated_function', 10, 3 ); 199 add_filter( 'deprecated_function_trigger_error', '__return_false' ); 200 add_action( 'deprecated_argument_run', 'rest_handle_deprecated_argument', 10, 3 ); 201 add_filter( 'deprecated_argument_trigger_error', '__return_false' ); 202 add_action( 'doing_it_wrong_run', 'rest_handle_doing_it_wrong', 10, 3 ); 203 add_filter( 'doing_it_wrong_trigger_error', '__return_false' ); 204 } 205 206 // Default serving. 207 add_filter( 'rest_pre_serve_request', 'rest_send_cors_headers' ); 208 add_filter( 'rest_post_dispatch', 'rest_send_allow_header', 10, 3 ); 209 add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 ); 210 211 add_filter( 'rest_pre_dispatch', 'rest_handle_options_request', 10, 3 ); 212 add_filter( 'rest_index', 'rest_add_application_passwords_to_index' ); 213 } 214 215 /** 216 * Registers default REST API routes. 217 * 218 * @since 4.7.0 219 */ 220 function create_initial_rest_routes() { 221 foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) { 222 $controller = $post_type->get_rest_controller(); 223 224 if ( ! $controller ) { 225 continue; 226 } 227 228 $controller->register_routes(); 229 230 if ( post_type_supports( $post_type->name, 'revisions' ) ) { 231 $revisions_controller = new WP_REST_Revisions_Controller( $post_type->name ); 232 $revisions_controller->register_routes(); 233 } 234 235 if ( 'attachment' !== $post_type->name ) { 236 $autosaves_controller = new WP_REST_Autosaves_Controller( $post_type->name ); 237 $autosaves_controller->register_routes(); 238 } 239 } 240 241 // Post types. 242 $controller = new WP_REST_Post_Types_Controller; 243 $controller->register_routes(); 244 245 // Post statuses. 246 $controller = new WP_REST_Post_Statuses_Controller; 247 $controller->register_routes(); 248 249 // Taxonomies. 250 $controller = new WP_REST_Taxonomies_Controller; 251 $controller->register_routes(); 252 253 // Terms. 254 foreach ( get_taxonomies( array( 'show_in_rest' => true ), 'object' ) as $taxonomy ) { 255 $controller = $taxonomy->get_rest_controller(); 256 257 if ( ! $controller ) { 258 continue; 259 } 260 261 $controller->register_routes(); 262 } 263 264 // Users. 265 $controller = new WP_REST_Users_Controller; 266 $controller->register_routes(); 267 268 // Application Passwords 269 $controller = new WP_REST_Application_Passwords_Controller(); 270 $controller->register_routes(); 271 272 // Comments. 273 $controller = new WP_REST_Comments_Controller; 274 $controller->register_routes(); 275 276 $search_handlers = array( 277 new WP_REST_Post_Search_Handler(), 278 new WP_REST_Term_Search_Handler(), 279 new WP_REST_Post_Format_Search_Handler(), 280 ); 281 282 /** 283 * Filters the search handlers to use in the REST search controller. 284 * 285 * @since 5.0.0 286 * 287 * @param array $search_handlers List of search handlers to use in the controller. Each search 288 * handler instance must extend the `WP_REST_Search_Handler` class. 289 * Default is only a handler for posts. 290 */ 291 $search_handlers = apply_filters( 'wp_rest_search_handlers', $search_handlers ); 292 293 $controller = new WP_REST_Search_Controller( $search_handlers ); 294 $controller->register_routes(); 295 296 // Block Renderer. 297 $controller = new WP_REST_Block_Renderer_Controller; 298 $controller->register_routes(); 299 300 // Block Types. 301 $controller = new WP_REST_Block_Types_Controller(); 302 $controller->register_routes(); 303 304 // Settings. 305 $controller = new WP_REST_Settings_Controller; 306 $controller->register_routes(); 307 308 // Themes. 309 $controller = new WP_REST_Themes_Controller; 310 $controller->register_routes(); 311 312 // Plugins. 313 $controller = new WP_REST_Plugins_Controller(); 314 $controller->register_routes(); 315 316 // Sidebars. 317 $controller = new WP_REST_Sidebars_Controller(); 318 $controller->register_routes(); 319 320 // Widget Types. 321 $controller = new WP_REST_Widget_Types_Controller(); 322 $controller->register_routes(); 323 324 // Widgets. 325 $controller = new WP_REST_Widgets_Controller(); 326 $controller->register_routes(); 327 328 // Block Directory. 329 $controller = new WP_REST_Block_Directory_Controller(); 330 $controller->register_routes(); 331 332 // Pattern Directory. 333 $controller = new WP_REST_Pattern_Directory_Controller(); 334 $controller->register_routes(); 335 336 // Site Health. 337 $site_health = WP_Site_Health::get_instance(); 338 $controller = new WP_REST_Site_Health_Controller( $site_health ); 339 $controller->register_routes(); 340 } 341 342 /** 343 * Loads the REST API. 344 * 345 * @since 4.4.0 346 * 347 * @global WP $wp Current WordPress environment instance. 348 */ 349 function rest_api_loaded() { 350 if ( empty( $GLOBALS['wp']->query_vars['rest_route'] ) ) { 351 return; 352 } 353 354 /** 355 * Whether this is a REST Request. 356 * 357 * @since 4.4.0 358 * @var bool 359 */ 360 define( 'REST_REQUEST', true ); 361 362 // Initialize the server. 363 $server = rest_get_server(); 364 365 // Fire off the request. 366 $route = untrailingslashit( $GLOBALS['wp']->query_vars['rest_route'] ); 367 if ( empty( $route ) ) { 368 $route = '/'; 369 } 370 $server->serve_request( $route ); 371 372 // We're done. 373 die(); 374 } 375 376 /** 377 * Retrieves the URL prefix for any API resource. 378 * 379 * @since 4.4.0 380 * 381 * @return string Prefix. 382 */ 383 function rest_get_url_prefix() { 384 /** 385 * Filters the REST URL prefix. 386 * 387 * @since 4.4.0 388 * 389 * @param string $prefix URL prefix. Default 'wp-json'. 390 */ 391 return apply_filters( 'rest_url_prefix', 'wp-json' ); 392 } 393 394 /** 395 * Retrieves the URL to a REST endpoint on a site. 396 * 397 * Note: The returned URL is NOT escaped. 398 * 399 * @since 4.4.0 400 * 401 * @todo Check if this is even necessary 402 * @global WP_Rewrite $wp_rewrite WordPress rewrite component. 403 * 404 * @param int|null $blog_id Optional. Blog ID. Default of null returns URL for current blog. 405 * @param string $path Optional. REST route. Default '/'. 406 * @param string $scheme Optional. Sanitization scheme. Default 'rest'. 407 * @return string Full URL to the endpoint. 408 */ 409 function get_rest_url( $blog_id = null, $path = '/', $scheme = 'rest' ) { 410 if ( empty( $path ) ) { 411 $path = '/'; 412 } 413 414 $path = '/' . ltrim( $path, '/' ); 415 416 if ( is_multisite() && get_blog_option( $blog_id, 'permalink_structure' ) || get_option( 'permalink_structure' ) ) { 417 global $wp_rewrite; 418 419 if ( $wp_rewrite->using_index_permalinks() ) { 420 $url = get_home_url( $blog_id, $wp_rewrite->index . '/' . rest_get_url_prefix(), $scheme ); 421 } else { 422 $url = get_home_url( $blog_id, rest_get_url_prefix(), $scheme ); 423 } 424 425 $url .= $path; 426 } else { 427 $url = trailingslashit( get_home_url( $blog_id, '', $scheme ) ); 428 // nginx only allows HTTP/1.0 methods when redirecting from / to /index.php. 429 // To work around this, we manually add index.php to the URL, avoiding the redirect. 430 if ( 'index.php' !== substr( $url, 9 ) ) { 431 $url .= 'index.php'; 432 } 433 434 $url = add_query_arg( 'rest_route', $path, $url ); 435 } 436 437 if ( is_ssl() && isset( $_SERVER['SERVER_NAME'] ) ) { 438 // If the current host is the same as the REST URL host, force the REST URL scheme to HTTPS. 439 if ( parse_url( get_home_url( $blog_id ), PHP_URL_HOST ) === $_SERVER['SERVER_NAME'] ) { 440 $url = set_url_scheme( $url, 'https' ); 441 } 442 } 443 444 if ( is_admin() && force_ssl_admin() ) { 445 /* 446 * In this situation the home URL may be http:, and `is_ssl()` may be false, 447 * but the admin is served over https: (one way or another), so REST API usage 448 * will be blocked by browsers unless it is also served over HTTPS. 449 */ 450 $url = set_url_scheme( $url, 'https' ); 451 } 452 453 /** 454 * Filters the REST URL. 455 * 456 * Use this filter to adjust the url returned by the get_rest_url() function. 457 * 458 * @since 4.4.0 459 * 460 * @param string $url REST URL. 461 * @param string $path REST route. 462 * @param int|null $blog_id Blog ID. 463 * @param string $scheme Sanitization scheme. 464 */ 465 return apply_filters( 'rest_url', $url, $path, $blog_id, $scheme ); 466 } 467 468 /** 469 * Retrieves the URL to a REST endpoint. 470 * 471 * Note: The returned URL is NOT escaped. 472 * 473 * @since 4.4.0 474 * 475 * @param string $path Optional. REST route. Default empty. 476 * @param string $scheme Optional. Sanitization scheme. Default 'rest'. 477 * @return string Full URL to the endpoint. 478 */ 479 function rest_url( $path = '', $scheme = 'rest' ) { 480 return get_rest_url( null, $path, $scheme ); 481 } 482 483 /** 484 * Do a REST request. 485 * 486 * Used primarily to route internal requests through WP_REST_Server. 487 * 488 * @since 4.4.0 489 * 490 * @param WP_REST_Request|string $request Request. 491 * @return WP_REST_Response REST response. 492 */ 493 function rest_do_request( $request ) { 494 $request = rest_ensure_request( $request ); 495 return rest_get_server()->dispatch( $request ); 496 } 497 498 /** 499 * Retrieves the current REST server instance. 500 * 501 * Instantiates a new instance if none exists already. 502 * 503 * @since 4.5.0 504 * 505 * @global WP_REST_Server $wp_rest_server REST server instance. 506 * 507 * @return WP_REST_Server REST server instance. 508 */ 509 function rest_get_server() { 510 /* @var WP_REST_Server $wp_rest_server */ 511 global $wp_rest_server; 512 513 if ( empty( $wp_rest_server ) ) { 514 /** 515 * Filters the REST Server Class. 516 * 517 * This filter allows you to adjust the server class used by the REST API, using a 518 * different class to handle requests. 519 * 520 * @since 4.4.0 521 * 522 * @param string $class_name The name of the server class. Default 'WP_REST_Server'. 523 */ 524 $wp_rest_server_class = apply_filters( 'wp_rest_server_class', 'WP_REST_Server' ); 525 $wp_rest_server = new $wp_rest_server_class; 526 527 /** 528 * Fires when preparing to serve a REST API request. 529 * 530 * Endpoint objects should be created and register their hooks on this action rather 531 * than another action to ensure they're only loaded when needed. 532 * 533 * @since 4.4.0 534 * 535 * @param WP_REST_Server $wp_rest_server Server object. 536 */ 537 do_action( 'rest_api_init', $wp_rest_server ); 538 } 539 540 return $wp_rest_server; 541 } 542 543 /** 544 * Ensures request arguments are a request object (for consistency). 545 * 546 * @since 4.4.0 547 * @since 5.3.0 Accept string argument for the request path. 548 * 549 * @param array|string|WP_REST_Request $request Request to check. 550 * @return WP_REST_Request REST request instance. 551 */ 552 function rest_ensure_request( $request ) { 553 if ( $request instanceof WP_REST_Request ) { 554 return $request; 555 } 556 557 if ( is_string( $request ) ) { 558 return new WP_REST_Request( 'GET', $request ); 559 } 560 561 return new WP_REST_Request( 'GET', '', $request ); 562 } 563 564 /** 565 * Ensures a REST response is a response object (for consistency). 566 * 567 * This implements WP_REST_Response, allowing usage of `set_status`/`header`/etc 568 * without needing to double-check the object. Will also allow WP_Error to indicate error 569 * responses, so users should immediately check for this value. 570 * 571 * @since 4.4.0 572 * 573 * @param WP_REST_Response|WP_Error|WP_HTTP_Response|mixed $response Response to check. 574 * @return WP_REST_Response|WP_Error If response generated an error, WP_Error, if response 575 * is already an instance, WP_REST_Response, otherwise 576 * returns a new WP_REST_Response instance. 577 */ 578 function rest_ensure_response( $response ) { 579 if ( is_wp_error( $response ) ) { 580 return $response; 581 } 582 583 if ( $response instanceof WP_REST_Response ) { 584 return $response; 585 } 586 587 // While WP_HTTP_Response is the base class of WP_REST_Response, it doesn't provide 588 // all the required methods used in WP_REST_Server::dispatch(). 589 if ( $response instanceof WP_HTTP_Response ) { 590 return new WP_REST_Response( 591 $response->get_data(), 592 $response->get_status(), 593 $response->get_headers() 594 ); 595 } 596 597 return new WP_REST_Response( $response ); 598 } 599 600 /** 601 * Handles _deprecated_function() errors. 602 * 603 * @since 4.4.0 604 * 605 * @param string $function The function that was called. 606 * @param string $replacement The function that should have been called. 607 * @param string $version Version. 608 */ 609 function rest_handle_deprecated_function( $function, $replacement, $version ) { 610 if ( ! WP_DEBUG || headers_sent() ) { 611 return; 612 } 613 if ( ! empty( $replacement ) ) { 614 /* translators: 1: Function name, 2: WordPress version number, 3: New function name. */ 615 $string = sprintf( __( '%1$s (since %2$s; use %3$s instead)' ), $function, $version, $replacement ); 616 } else { 617 /* translators: 1: Function name, 2: WordPress version number. */ 618 $string = sprintf( __( '%1$s (since %2$s; no alternative available)' ), $function, $version ); 619 } 620 621 header( sprintf( 'X-WP-DeprecatedFunction: %s', $string ) ); 622 } 623 624 /** 625 * Handles _deprecated_argument() errors. 626 * 627 * @since 4.4.0 628 * 629 * @param string $function The function that was called. 630 * @param string $message A message regarding the change. 631 * @param string $version Version. 632 */ 633 function rest_handle_deprecated_argument( $function, $message, $version ) { 634 if ( ! WP_DEBUG || headers_sent() ) { 635 return; 636 } 637 if ( $message ) { 638 /* translators: 1: Function name, 2: WordPress version number, 3: Error message. */ 639 $string = sprintf( __( '%1$s (since %2$s; %3$s)' ), $function, $version, $message ); 640 } else { 641 /* translators: 1: Function name, 2: WordPress version number. */ 642 $string = sprintf( __( '%1$s (since %2$s; no alternative available)' ), $function, $version ); 643 } 644 645 header( sprintf( 'X-WP-DeprecatedParam: %s', $string ) ); 646 } 647 648 /** 649 * Handles _doing_it_wrong errors. 650 * 651 * @since 5.5.0 652 * 653 * @param string $function The function that was called. 654 * @param string $message A message explaining what has been done incorrectly. 655 * @param string|null $version The version of WordPress where the message was added. 656 */ 657 function rest_handle_doing_it_wrong( $function, $message, $version ) { 658 if ( ! WP_DEBUG || headers_sent() ) { 659 return; 660 } 661 662 if ( $version ) { 663 /* translators: Developer debugging message. 1: PHP function name, 2: WordPress version number, 3: Explanatory message. */ 664 $string = __( '%1$s (since %2$s; %3$s)' ); 665 $string = sprintf( $string, $function, $version, $message ); 666 } else { 667 /* translators: Developer debugging message. 1: PHP function name, 2: Explanatory message. */ 668 $string = __( '%1$s (%2$s)' ); 669 $string = sprintf( $string, $function, $message ); 670 } 671 672 header( sprintf( 'X-WP-DoingItWrong: %s', $string ) ); 673 } 674 675 /** 676 * Sends Cross-Origin Resource Sharing headers with API requests. 677 * 678 * @since 4.4.0 679 * 680 * @param mixed $value Response data. 681 * @return mixed Response data. 682 */ 683 function rest_send_cors_headers( $value ) { 684 $origin = get_http_origin(); 685 686 if ( $origin ) { 687 // Requests from file:// and data: URLs send "Origin: null". 688 if ( 'null' !== $origin ) { 689 $origin = esc_url_raw( $origin ); 690 } 691 header( 'Access-Control-Allow-Origin: ' . $origin ); 692 header( 'Access-Control-Allow-Methods: OPTIONS, GET, POST, PUT, PATCH, DELETE' ); 693 header( 'Access-Control-Allow-Credentials: true' ); 694 header( 'Vary: Origin', false ); 695 } elseif ( ! headers_sent() && 'GET' === $_SERVER['REQUEST_METHOD'] && ! is_user_logged_in() ) { 696 header( 'Vary: Origin', false ); 697 } 698 699 return $value; 700 } 701 702 /** 703 * Handles OPTIONS requests for the server. 704 * 705 * This is handled outside of the server code, as it doesn't obey normal route 706 * mapping. 707 * 708 * @since 4.4.0 709 * 710 * @param mixed $response Current response, either response or `null` to indicate pass-through. 711 * @param WP_REST_Server $handler ResponseHandler instance (usually WP_REST_Server). 712 * @param WP_REST_Request $request The request that was used to make current response. 713 * @return WP_REST_Response Modified response, either response or `null` to indicate pass-through. 714 */ 715 function rest_handle_options_request( $response, $handler, $request ) { 716 if ( ! empty( $response ) || $request->get_method() !== 'OPTIONS' ) { 717 return $response; 718 } 719 720 $response = new WP_REST_Response(); 721 $data = array(); 722 723 foreach ( $handler->get_routes() as $route => $endpoints ) { 724 $match = preg_match( '@^' . $route . '$@i', $request->get_route(), $matches ); 725 726 if ( ! $match ) { 727 continue; 728 } 729 730 $args = array(); 731 foreach ( $matches as $param => $value ) { 732 if ( ! is_int( $param ) ) { 733 $args[ $param ] = $value; 734 } 735 } 736 737 foreach ( $endpoints as $endpoint ) { 738 // Remove the redundant preg_match() argument. 739 unset( $args[0] ); 740 741 $request->set_url_params( $args ); 742 $request->set_attributes( $endpoint ); 743 } 744 745 $data = $handler->get_data_for_route( $route, $endpoints, 'help' ); 746 $response->set_matched_route( $route ); 747 break; 748 } 749 750 $response->set_data( $data ); 751 return $response; 752 } 753 754 /** 755 * Sends the "Allow" header to state all methods that can be sent to the current route. 756 * 757 * @since 4.4.0 758 * 759 * @param WP_REST_Response $response Current response being served. 760 * @param WP_REST_Server $server ResponseHandler instance (usually WP_REST_Server). 761 * @param WP_REST_Request $request The request that was used to make current response. 762 * @return WP_REST_Response Response to be served, with "Allow" header if route has allowed methods. 763 */ 764 function rest_send_allow_header( $response, $server, $request ) { 765 $matched_route = $response->get_matched_route(); 766 767 if ( ! $matched_route ) { 768 return $response; 769 } 770 771 $routes = $server->get_routes(); 772 773 $allowed_methods = array(); 774 775 // Get the allowed methods across the routes. 776 foreach ( $routes[ $matched_route ] as $_handler ) { 777 foreach ( $_handler['methods'] as $handler_method => $value ) { 778 779 if ( ! empty( $_handler['permission_callback'] ) ) { 780 781 $permission = call_user_func( $_handler['permission_callback'], $request ); 782 783 $allowed_methods[ $handler_method ] = true === $permission; 784 } else { 785 $allowed_methods[ $handler_method ] = true; 786 } 787 } 788 } 789 790 // Strip out all the methods that are not allowed (false values). 791 $allowed_methods = array_filter( $allowed_methods ); 792 793 if ( $allowed_methods ) { 794 $response->header( 'Allow', implode( ', ', array_map( 'strtoupper', array_keys( $allowed_methods ) ) ) ); 795 } 796 797 return $response; 798 } 799 800 /** 801 * Recursively computes the intersection of arrays using keys for comparison. 802 * 803 * @since 5.3.0 804 * 805 * @param array $array1 The array with master keys to check. 806 * @param array $array2 An array to compare keys against. 807 * @return array An associative array containing all the entries of array1 which have keys 808 * that are present in all arguments. 809 */ 810 function _rest_array_intersect_key_recursive( $array1, $array2 ) { 811 $array1 = array_intersect_key( $array1, $array2 ); 812 foreach ( $array1 as $key => $value ) { 813 if ( is_array( $value ) && is_array( $array2[ $key ] ) ) { 814 $array1[ $key ] = _rest_array_intersect_key_recursive( $value, $array2[ $key ] ); 815 } 816 } 817 return $array1; 818 } 819 820 /** 821 * Filters the REST API response to include only a white-listed set of response object fields. 822 * 823 * @since 4.8.0 824 * 825 * @param WP_REST_Response $response Current response being served. 826 * @param WP_REST_Server $server ResponseHandler instance (usually WP_REST_Server). 827 * @param WP_REST_Request $request The request that was used to make current response. 828 * @return WP_REST_Response Response to be served, trimmed down to contain a subset of fields. 829 */ 830 function rest_filter_response_fields( $response, $server, $request ) { 831 if ( ! isset( $request['_fields'] ) || $response->is_error() ) { 832 return $response; 833 } 834 835 $data = $response->get_data(); 836 837 $fields = wp_parse_list( $request['_fields'] ); 838 839 if ( 0 === count( $fields ) ) { 840 return $response; 841 } 842 843 // Trim off outside whitespace from the comma delimited list. 844 $fields = array_map( 'trim', $fields ); 845 846 // Create nested array of accepted field hierarchy. 847 $fields_as_keyed = array(); 848 foreach ( $fields as $field ) { 849 $parts = explode( '.', $field ); 850 $ref = &$fields_as_keyed; 851 while ( count( $parts ) > 1 ) { 852 $next = array_shift( $parts ); 853 if ( isset( $ref[ $next ] ) && true === $ref[ $next ] ) { 854 // Skip any sub-properties if their parent prop is already marked for inclusion. 855 break 2; 856 } 857 $ref[ $next ] = isset( $ref[ $next ] ) ? $ref[ $next ] : array(); 858 $ref = &$ref[ $next ]; 859 } 860 $last = array_shift( $parts ); 861 $ref[ $last ] = true; 862 } 863 864 if ( wp_is_numeric_array( $data ) ) { 865 $new_data = array(); 866 foreach ( $data as $item ) { 867 $new_data[] = _rest_array_intersect_key_recursive( $item, $fields_as_keyed ); 868 } 869 } else { 870 $new_data = _rest_array_intersect_key_recursive( $data, $fields_as_keyed ); 871 } 872 873 $response->set_data( $new_data ); 874 875 return $response; 876 } 877 878 /** 879 * Given an array of fields to include in a response, some of which may be 880 * `nested.fields`, determine whether the provided field should be included 881 * in the response body. 882 * 883 * If a parent field is passed in, the presence of any nested field within 884 * that parent will cause the method to return `true`. For example "title" 885 * will return true if any of `title`, `title.raw` or `title.rendered` is 886 * provided. 887 * 888 * @since 5.3.0 889 * 890 * @param string $field A field to test for inclusion in the response body. 891 * @param array $fields An array of string fields supported by the endpoint. 892 * @return bool Whether to include the field or not. 893 */ 894 function rest_is_field_included( $field, $fields ) { 895 if ( in_array( $field, $fields, true ) ) { 896 return true; 897 } 898 899 foreach ( $fields as $accepted_field ) { 900 // Check to see if $field is the parent of any item in $fields. 901 // A field "parent" should be accepted if "parent.child" is accepted. 902 if ( strpos( $accepted_field, "$field." ) === 0 ) { 903 return true; 904 } 905 // Conversely, if "parent" is accepted, all "parent.child" fields 906 // should also be accepted. 907 if ( strpos( $field, "$accepted_field." ) === 0 ) { 908 return true; 909 } 910 } 911 912 return false; 913 } 914 915 /** 916 * Adds the REST API URL to the WP RSD endpoint. 917 * 918 * @since 4.4.0 919 * 920 * @see get_rest_url() 921 */ 922 function rest_output_rsd() { 923 $api_root = get_rest_url(); 924 925 if ( empty( $api_root ) ) { 926 return; 927 } 928 ?> 929 <api name="WP-API" blogID="1" preferred="false" apiLink="<?php echo esc_url( $api_root ); ?>" /> 930 <?php 931 } 932 933 /** 934 * Outputs the REST API link tag into page header. 935 * 936 * @since 4.4.0 937 * 938 * @see get_rest_url() 939 */ 940 function rest_output_link_wp_head() { 941 $api_root = get_rest_url(); 942 943 if ( empty( $api_root ) ) { 944 return; 945 } 946 947 printf( '<link rel="https://api.w.org/" href="%s" />', esc_url( $api_root ) ); 948 949 $resource = rest_get_queried_resource_route(); 950 951 if ( $resource ) { 952 printf( '<link rel="alternate" type="application/json" href="%s" />', esc_url( rest_url( $resource ) ) ); 953 } 954 } 955 956 /** 957 * Sends a Link header for the REST API. 958 * 959 * @since 4.4.0 960 */ 961 function rest_output_link_header() { 962 if ( headers_sent() ) { 963 return; 964 } 965 966 $api_root = get_rest_url(); 967 968 if ( empty( $api_root ) ) { 969 return; 970 } 971 972 header( sprintf( 'Link: <%s>; rel="https://api.w.org/"', esc_url_raw( $api_root ) ), false ); 973 974 $resource = rest_get_queried_resource_route(); 975 976 if ( $resource ) { 977 header( sprintf( 'Link: <%s>; rel="alternate"; type="application/json"', esc_url_raw( rest_url( $resource ) ) ), false ); 978 } 979 } 980 981 /** 982 * Checks for errors when using cookie-based authentication. 983 * 984 * WordPress' built-in cookie authentication is always active 985 * for logged in users. However, the API has to check nonces 986 * for each request to ensure users are not vulnerable to CSRF. 987 * 988 * @since 4.4.0 989 * 990 * @global mixed $wp_rest_auth_cookie 991 * 992 * @param WP_Error|mixed $result Error from another authentication handler, 993 * null if we should handle it, or another value if not. 994 * @return WP_Error|mixed|bool WP_Error if the cookie is invalid, the $result, otherwise true. 995 */ 996 function rest_cookie_check_errors( $result ) { 997 if ( ! empty( $result ) ) { 998 return $result; 999 } 1000 1001 global $wp_rest_auth_cookie; 1002 1003 /* 1004 * Is cookie authentication being used? (If we get an auth 1005 * error, but we're still logged in, another authentication 1006 * must have been used). 1007 */ 1008 if ( true !== $wp_rest_auth_cookie && is_user_logged_in() ) { 1009 return $result; 1010 } 1011 1012 // Determine if there is a nonce. 1013 $nonce = null; 1014 1015 if ( isset( $_REQUEST['_wpnonce'] ) ) { 1016 $nonce = $_REQUEST['_wpnonce']; 1017 } elseif ( isset( $_SERVER['HTTP_X_WP_NONCE'] ) ) { 1018 $nonce = $_SERVER['HTTP_X_WP_NONCE']; 1019 } 1020 1021 if ( null === $nonce ) { 1022 // No nonce at all, so act as if it's an unauthenticated request. 1023 wp_set_current_user( 0 ); 1024 return true; 1025 } 1026 1027 // Check the nonce. 1028 $result = wp_verify_nonce( $nonce, 'wp_rest' ); 1029 1030 if ( ! $result ) { 1031 return new WP_Error( 'rest_cookie_invalid_nonce', __( 'Cookie check failed' ), array( 'status' => 403 ) ); 1032 } 1033 1034 // Send a refreshed nonce in header. 1035 rest_get_server()->send_header( 'X-WP-Nonce', wp_create_nonce( 'wp_rest' ) ); 1036 1037 return true; 1038 } 1039 1040 /** 1041 * Collects cookie authentication status. 1042 * 1043 * Collects errors from wp_validate_auth_cookie for use by rest_cookie_check_errors. 1044 * 1045 * @since 4.4.0 1046 * 1047 * @see current_action() 1048 * @global mixed $wp_rest_auth_cookie 1049 */ 1050 function rest_cookie_collect_status() { 1051 global $wp_rest_auth_cookie; 1052 1053 $status_type = current_action(); 1054 1055 if ( 'auth_cookie_valid' !== $status_type ) { 1056 $wp_rest_auth_cookie = substr( $status_type, 12 ); 1057 return; 1058 } 1059 1060 $wp_rest_auth_cookie = true; 1061 } 1062 1063 /** 1064 * Collects the status of authenticating with an application password. 1065 * 1066 * @since 5.6.0 1067 * @since 5.7.0 Added the `$app_password` parameter. 1068 * 1069 * @global WP_User|WP_Error|null $wp_rest_application_password_status 1070 * @global string|null $wp_rest_application_password_uuid 1071 * 1072 * @param WP_Error $user_or_error The authenticated user or error instance. 1073 * @param array $app_password The Application Password used to authenticate. 1074 */ 1075 function rest_application_password_collect_status( $user_or_error, $app_password = array() ) { 1076 global $wp_rest_application_password_status, $wp_rest_application_password_uuid; 1077 1078 $wp_rest_application_password_status = $user_or_error; 1079 1080 if ( empty( $app_password['uuid'] ) ) { 1081 $wp_rest_application_password_uuid = null; 1082 } else { 1083 $wp_rest_application_password_uuid = $app_password['uuid']; 1084 } 1085 } 1086 1087 /** 1088 * Gets the Application Password used for authenticating the request. 1089 * 1090 * @since 5.7.0 1091 * 1092 * @global string|null $wp_rest_application_password_uuid 1093 * 1094 * @return string|null The App Password UUID, or null if Application Passwords was not used. 1095 */ 1096 function rest_get_authenticated_app_password() { 1097 global $wp_rest_application_password_uuid; 1098 1099 return $wp_rest_application_password_uuid; 1100 } 1101 1102 /** 1103 * Checks for errors when using application password-based authentication. 1104 * 1105 * @since 5.6.0 1106 * 1107 * @global WP_User|WP_Error|null $wp_rest_application_password_status 1108 * 1109 * @param WP_Error|null|true $result Error from another authentication handler, 1110 * null if we should handle it, or another value if not. 1111 * @return WP_Error|null|true WP_Error if the application password is invalid, the $result, otherwise true. 1112 */ 1113 function rest_application_password_check_errors( $result ) { 1114 global $wp_rest_application_password_status; 1115 1116 if ( ! empty( $result ) ) { 1117 return $result; 1118 } 1119 1120 if ( is_wp_error( $wp_rest_application_password_status ) ) { 1121 $data = $wp_rest_application_password_status->get_error_data(); 1122 1123 if ( ! isset( $data['status'] ) ) { 1124 $data['status'] = 401; 1125 } 1126 1127 $wp_rest_application_password_status->add_data( $data ); 1128 1129 return $wp_rest_application_password_status; 1130 } 1131 1132 if ( $wp_rest_application_password_status instanceof WP_User ) { 1133 return true; 1134 } 1135 1136 return $result; 1137 } 1138 1139 /** 1140 * Adds Application Passwords info to the REST API index. 1141 * 1142 * @since 5.6.0 1143 * 1144 * @param WP_REST_Response $response The index response object. 1145 * @return WP_REST_Response 1146 */ 1147 function rest_add_application_passwords_to_index( $response ) { 1148 if ( ! wp_is_application_passwords_available() ) { 1149 return $response; 1150 } 1151 1152 $response->data['authentication']['application-passwords'] = array( 1153 'endpoints' => array( 1154 'authorization' => admin_url( 'authorize-application.php' ), 1155 ), 1156 ); 1157 1158 return $response; 1159 } 1160 1161 /** 1162 * Retrieves the avatar urls in various sizes. 1163 * 1164 * @since 4.7.0 1165 * 1166 * @see get_avatar_url() 1167 * 1168 * @param mixed $id_or_email The Gravatar to retrieve a URL for. Accepts a user_id, gravatar md5 hash, 1169 * user email, WP_User object, WP_Post object, or WP_Comment object. 1170 * @return array Avatar URLs keyed by size. Each value can be a URL string or boolean false. 1171 */ 1172 function rest_get_avatar_urls( $id_or_email ) { 1173 $avatar_sizes = rest_get_avatar_sizes(); 1174 1175 $urls = array(); 1176 foreach ( $avatar_sizes as $size ) { 1177 $urls[ $size ] = get_avatar_url( $id_or_email, array( 'size' => $size ) ); 1178 } 1179 1180 return $urls; 1181 } 1182 1183 /** 1184 * Retrieves the pixel sizes for avatars. 1185 * 1186 * @since 4.7.0 1187 * 1188 * @return int[] List of pixel sizes for avatars. Default `[ 24, 48, 96 ]`. 1189 */ 1190 function rest_get_avatar_sizes() { 1191 /** 1192 * Filters the REST avatar sizes. 1193 * 1194 * Use this filter to adjust the array of sizes returned by the 1195 * `rest_get_avatar_sizes` function. 1196 * 1197 * @since 4.4.0 1198 * 1199 * @param int[] $sizes An array of int values that are the pixel sizes for avatars. 1200 * Default `[ 24, 48, 96 ]`. 1201 */ 1202 return apply_filters( 'rest_avatar_sizes', array( 24, 48, 96 ) ); 1203 } 1204 1205 /** 1206 * Parses an RFC3339 time into a Unix timestamp. 1207 * 1208 * @since 4.4.0 1209 * 1210 * @param string $date RFC3339 timestamp. 1211 * @param bool $force_utc Optional. Whether to force UTC timezone instead of using 1212 * the timestamp's timezone. Default false. 1213 * @return int Unix timestamp. 1214 */ 1215 function rest_parse_date( $date, $force_utc = false ) { 1216 if ( $force_utc ) { 1217 $date = preg_replace( '/[+-]\d+:?\d+$/', '+00:00', $date ); 1218 } 1219 1220 $regex = '#^\d{4}-\d{2}-\d{2}[Tt ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}(?::\d{2})?)?$#'; 1221 1222 if ( ! preg_match( $regex, $date, $matches ) ) { 1223 return false; 1224 } 1225 1226 return strtotime( $date ); 1227 } 1228 1229 /** 1230 * Parses a 3 or 6 digit hex color (with #). 1231 * 1232 * @since 5.4.0 1233 * 1234 * @param string $color 3 or 6 digit hex color (with #). 1235 * @return string|false 1236 */ 1237 function rest_parse_hex_color( $color ) { 1238 $regex = '|^#([A-Fa-f0-9]{3}){1,2}$|'; 1239 if ( ! preg_match( $regex, $color, $matches ) ) { 1240 return false; 1241 } 1242 1243 return $color; 1244 } 1245 1246 /** 1247 * Parses a date into both its local and UTC equivalent, in MySQL datetime format. 1248 * 1249 * @since 4.4.0 1250 * 1251 * @see rest_parse_date() 1252 * 1253 * @param string $date RFC3339 timestamp. 1254 * @param bool $is_utc Whether the provided date should be interpreted as UTC. Default false. 1255 * @return array|null Local and UTC datetime strings, in MySQL datetime format (Y-m-d H:i:s), 1256 * null on failure. 1257 */ 1258 function rest_get_date_with_gmt( $date, $is_utc = false ) { 1259 /* 1260 * Whether or not the original date actually has a timezone string 1261 * changes the way we need to do timezone conversion. 1262 * Store this info before parsing the date, and use it later. 1263 */ 1264 $has_timezone = preg_match( '#(Z|[+-]\d{2}(:\d{2})?)$#', $date ); 1265 1266 $date = rest_parse_date( $date ); 1267 1268 if ( empty( $date ) ) { 1269 return null; 1270 } 1271 1272 /* 1273 * At this point $date could either be a local date (if we were passed 1274 * a *local* date without a timezone offset) or a UTC date (otherwise). 1275 * Timezone conversion needs to be handled differently between these two cases. 1276 */ 1277 if ( ! $is_utc && ! $has_timezone ) { 1278 $local = gmdate( 'Y-m-d H:i:s', $date ); 1279 $utc = get_gmt_from_date( $local ); 1280 } else { 1281 $utc = gmdate( 'Y-m-d H:i:s', $date ); 1282 $local = get_date_from_gmt( $utc ); 1283 } 1284 1285 return array( $local, $utc ); 1286 } 1287 1288 /** 1289 * Returns a contextual HTTP error code for authorization failure. 1290 * 1291 * @since 4.7.0 1292 * 1293 * @return int 401 if the user is not logged in, 403 if the user is logged in. 1294 */ 1295 function rest_authorization_required_code() { 1296 return is_user_logged_in() ? 403 : 401; 1297 } 1298 1299 /** 1300 * Validate a request argument based on details registered to the route. 1301 * 1302 * @since 4.7.0 1303 * 1304 * @param mixed $value 1305 * @param WP_REST_Request $request 1306 * @param string $param 1307 * @return true|WP_Error 1308 */ 1309 function rest_validate_request_arg( $value, $request, $param ) { 1310 $attributes = $request->get_attributes(); 1311 if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) { 1312 return true; 1313 } 1314 $args = $attributes['args'][ $param ]; 1315 1316 return rest_validate_value_from_schema( $value, $args, $param ); 1317 } 1318 1319 /** 1320 * Sanitize a request argument based on details registered to the route. 1321 * 1322 * @since 4.7.0 1323 * 1324 * @param mixed $value 1325 * @param WP_REST_Request $request 1326 * @param string $param 1327 * @return mixed 1328 */ 1329 function rest_sanitize_request_arg( $value, $request, $param ) { 1330 $attributes = $request->get_attributes(); 1331 if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) { 1332 return $value; 1333 } 1334 $args = $attributes['args'][ $param ]; 1335 1336 return rest_sanitize_value_from_schema( $value, $args, $param ); 1337 } 1338 1339 /** 1340 * Parse a request argument based on details registered to the route. 1341 * 1342 * Runs a validation check and sanitizes the value, primarily to be used via 1343 * the `sanitize_callback` arguments in the endpoint args registration. 1344 * 1345 * @since 4.7.0 1346 * 1347 * @param mixed $value 1348 * @param WP_REST_Request $request 1349 * @param string $param 1350 * @return mixed 1351 */ 1352 function rest_parse_request_arg( $value, $request, $param ) { 1353 $is_valid = rest_validate_request_arg( $value, $request, $param ); 1354 1355 if ( is_wp_error( $is_valid ) ) { 1356 return $is_valid; 1357 } 1358 1359 $value = rest_sanitize_request_arg( $value, $request, $param ); 1360 1361 return $value; 1362 } 1363 1364 /** 1365 * Determines if an IP address is valid. 1366 * 1367 * Handles both IPv4 and IPv6 addresses. 1368 * 1369 * @since 4.7.0 1370 * 1371 * @param string $ip IP address. 1372 * @return string|false The valid IP address, otherwise false. 1373 */ 1374 function rest_is_ip_address( $ip ) { 1375 $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]?)$/'; 1376 1377 if ( ! preg_match( $ipv4_pattern, $ip ) && ! Requests_IPv6::check_ipv6( $ip ) ) { 1378 return false; 1379 } 1380 1381 return $ip; 1382 } 1383 1384 /** 1385 * Changes a boolean-like value into the proper boolean value. 1386 * 1387 * @since 4.7.0 1388 * 1389 * @param bool|string|int $value The value being evaluated. 1390 * @return bool Returns the proper associated boolean value. 1391 */ 1392 function rest_sanitize_boolean( $value ) { 1393 // String values are translated to `true`; make sure 'false' is false. 1394 if ( is_string( $value ) ) { 1395 $value = strtolower( $value ); 1396 if ( in_array( $value, array( 'false', '0' ), true ) ) { 1397 $value = false; 1398 } 1399 } 1400 1401 // Everything else will map nicely to boolean. 1402 return (bool) $value; 1403 } 1404 1405 /** 1406 * Determines if a given value is boolean-like. 1407 * 1408 * @since 4.7.0 1409 * 1410 * @param bool|string $maybe_bool The value being evaluated. 1411 * @return bool True if a boolean, otherwise false. 1412 */ 1413 function rest_is_boolean( $maybe_bool ) { 1414 if ( is_bool( $maybe_bool ) ) { 1415 return true; 1416 } 1417 1418 if ( is_string( $maybe_bool ) ) { 1419 $maybe_bool = strtolower( $maybe_bool ); 1420 1421 $valid_boolean_values = array( 1422 'false', 1423 'true', 1424 '0', 1425 '1', 1426 ); 1427 1428 return in_array( $maybe_bool, $valid_boolean_values, true ); 1429 } 1430 1431 if ( is_int( $maybe_bool ) ) { 1432 return in_array( $maybe_bool, array( 0, 1 ), true ); 1433 } 1434 1435 return false; 1436 } 1437 1438 /** 1439 * Determines if a given value is integer-like. 1440 * 1441 * @since 5.5.0 1442 * 1443 * @param mixed $maybe_integer The value being evaluated. 1444 * @return bool True if an integer, otherwise false. 1445 */ 1446 function rest_is_integer( $maybe_integer ) { 1447 return is_numeric( $maybe_integer ) && round( (float) $maybe_integer ) === (float) $maybe_integer; 1448 } 1449 1450 /** 1451 * Determines if a given value is array-like. 1452 * 1453 * @since 5.5.0 1454 * 1455 * @param mixed $maybe_array The value being evaluated. 1456 * @return bool 1457 */ 1458 function rest_is_array( $maybe_array ) { 1459 if ( is_scalar( $maybe_array ) ) { 1460 $maybe_array = wp_parse_list( $maybe_array ); 1461 } 1462 1463 return wp_is_numeric_array( $maybe_array ); 1464 } 1465 1466 /** 1467 * Converts an array-like value to an array. 1468 * 1469 * @since 5.5.0 1470 * 1471 * @param mixed $maybe_array The value being evaluated. 1472 * @return array Returns the array extracted from the value. 1473 */ 1474 function rest_sanitize_array( $maybe_array ) { 1475 if ( is_scalar( $maybe_array ) ) { 1476 return wp_parse_list( $maybe_array ); 1477 } 1478 1479 if ( ! is_array( $maybe_array ) ) { 1480 return array(); 1481 } 1482 1483 // Normalize to numeric array so nothing unexpected is in the keys. 1484 return array_values( $maybe_array ); 1485 } 1486 1487 /** 1488 * Determines if a given value is object-like. 1489 * 1490 * @since 5.5.0 1491 * 1492 * @param mixed $maybe_object The value being evaluated. 1493 * @return bool True if object like, otherwise false. 1494 */ 1495 function rest_is_object( $maybe_object ) { 1496 if ( '' === $maybe_object ) { 1497 return true; 1498 } 1499 1500 if ( $maybe_object instanceof stdClass ) { 1501 return true; 1502 } 1503 1504 if ( $maybe_object instanceof JsonSerializable ) { 1505 $maybe_object = $maybe_object->jsonSerialize(); 1506 } 1507 1508 return is_array( $maybe_object ); 1509 } 1510 1511 /** 1512 * Converts an object-like value to an object. 1513 * 1514 * @since 5.5.0 1515 * 1516 * @param mixed $maybe_object The value being evaluated. 1517 * @return array Returns the object extracted from the value. 1518 */ 1519 function rest_sanitize_object( $maybe_object ) { 1520 if ( '' === $maybe_object ) { 1521 return array(); 1522 } 1523 1524 if ( $maybe_object instanceof stdClass ) { 1525 return (array) $maybe_object; 1526 } 1527 1528 if ( $maybe_object instanceof JsonSerializable ) { 1529 $maybe_object = $maybe_object->jsonSerialize(); 1530 } 1531 1532 if ( ! is_array( $maybe_object ) ) { 1533 return array(); 1534 } 1535 1536 return $maybe_object; 1537 } 1538 1539 /** 1540 * Gets the best type for a value. 1541 * 1542 * @since 5.5.0 1543 * 1544 * @param mixed $value The value to check. 1545 * @param array $types The list of possible types. 1546 * @return string The best matching type, an empty string if no types match. 1547 */ 1548 function rest_get_best_type_for_value( $value, $types ) { 1549 static $checks = array( 1550 'array' => 'rest_is_array', 1551 'object' => 'rest_is_object', 1552 'integer' => 'rest_is_integer', 1553 'number' => 'is_numeric', 1554 'boolean' => 'rest_is_boolean', 1555 'string' => 'is_string', 1556 'null' => 'is_null', 1557 ); 1558 1559 // Both arrays and objects allow empty strings to be converted to their types. 1560 // But the best answer for this type is a string. 1561 if ( '' === $value && in_array( 'string', $types, true ) ) { 1562 return 'string'; 1563 } 1564 1565 foreach ( $types as $type ) { 1566 if ( isset( $checks[ $type ] ) && $checks[ $type ]( $value ) ) { 1567 return $type; 1568 } 1569 } 1570 1571 return ''; 1572 } 1573 1574 /** 1575 * Handles getting the best type for a multi-type schema. 1576 * 1577 * This is a wrapper for {@see rest_get_best_type_for_value()} that handles 1578 * backward compatibility for schemas that use invalid types. 1579 * 1580 * @since 5.5.0 1581 * 1582 * @param mixed $value The value to check. 1583 * @param array $args The schema array to use. 1584 * @param string $param The parameter name, used in error messages. 1585 * @return string 1586 */ 1587 function rest_handle_multi_type_schema( $value, $args, $param = '' ) { 1588 $allowed_types = array( 'array', 'object', 'string', 'number', 'integer', 'boolean', 'null' ); 1589 $invalid_types = array_diff( $args['type'], $allowed_types ); 1590 1591 if ( $invalid_types ) { 1592 _doing_it_wrong( 1593 __FUNCTION__, 1594 /* translators: 1: Parameter, 2: List of allowed types. */ 1595 wp_sprintf( __( 'The "type" schema keyword for %1$s can only contain the built-in types: %2$l.' ), $param, $allowed_types ), 1596 '5.5.0' 1597 ); 1598 } 1599 1600 $best_type = rest_get_best_type_for_value( $value, $args['type'] ); 1601 1602 if ( ! $best_type ) { 1603 if ( ! $invalid_types ) { 1604 return ''; 1605 } 1606 1607 // Backward compatibility for previous behavior which allowed the value if there was an invalid type used. 1608 $best_type = reset( $invalid_types ); 1609 } 1610 1611 return $best_type; 1612 } 1613 1614 /** 1615 * Checks if an array is made up of unique items. 1616 * 1617 * @since 5.5.0 1618 * 1619 * @param array $array The array to check. 1620 * @return bool True if the array contains unique items, false otherwise. 1621 */ 1622 function rest_validate_array_contains_unique_items( $array ) { 1623 $seen = array(); 1624 1625 foreach ( $array as $item ) { 1626 $stabilized = rest_stabilize_value( $item ); 1627 $key = serialize( $stabilized ); 1628 1629 if ( ! isset( $seen[ $key ] ) ) { 1630 $seen[ $key ] = true; 1631 1632 continue; 1633 } 1634 1635 return false; 1636 } 1637 1638 return true; 1639 } 1640 1641 /** 1642 * Stabilizes a value following JSON Schema semantics. 1643 * 1644 * For lists, order is preserved. For objects, properties are reordered alphabetically. 1645 * 1646 * @since 5.5.0 1647 * 1648 * @param mixed $value The value to stabilize. Must already be sanitized. Objects should have been converted to arrays. 1649 * @return mixed The stabilized value. 1650 */ 1651 function rest_stabilize_value( $value ) { 1652 if ( is_scalar( $value ) || is_null( $value ) ) { 1653 return $value; 1654 } 1655 1656 if ( is_object( $value ) ) { 1657 _doing_it_wrong( __FUNCTION__, __( 'Cannot stabilize objects. Convert the object to an array first.' ), '5.5.0' ); 1658 1659 return $value; 1660 } 1661 1662 ksort( $value ); 1663 1664 foreach ( $value as $k => $v ) { 1665 $value[ $k ] = rest_stabilize_value( $v ); 1666 } 1667 1668 return $value; 1669 } 1670 1671 /** 1672 * Validates if the JSON Schema pattern matches a value. 1673 * 1674 * @since 5.6.0 1675 * 1676 * @param string $pattern The pattern to match against. 1677 * @param string $value The value to check. 1678 * @return bool True if the pattern matches the given value, false otherwise. 1679 */ 1680 function rest_validate_json_schema_pattern( $pattern, $value ) { 1681 $escaped_pattern = str_replace( '#', '\\#', $pattern ); 1682 1683 return 1 === preg_match( '#' . $escaped_pattern . '#u', $value ); 1684 } 1685 1686 /** 1687 * Finds the schema for a property using the patternProperties keyword. 1688 * 1689 * @since 5.6.0 1690 * 1691 * @param string $property The property name to check. 1692 * @param array $args The schema array to use. 1693 * @return array|null The schema of matching pattern property, or null if no patterns match. 1694 */ 1695 function rest_find_matching_pattern_property_schema( $property, $args ) { 1696 if ( isset( $args['patternProperties'] ) ) { 1697 foreach ( $args['patternProperties'] as $pattern => $child_schema ) { 1698 if ( rest_validate_json_schema_pattern( $pattern, $property ) ) { 1699 return $child_schema; 1700 } 1701 } 1702 } 1703 1704 return null; 1705 } 1706 1707 /** 1708 * Formats a combining operation error into a WP_Error object. 1709 * 1710 * @since 5.6.0 1711 * 1712 * @param string $param The parameter name. 1713 * @param array $error The error details. 1714 * @return WP_Error 1715 */ 1716 function rest_format_combining_operation_error( $param, $error ) { 1717 $position = $error['index']; 1718 $reason = $error['error_object']->get_error_message(); 1719 1720 if ( isset( $error['schema']['title'] ) ) { 1721 $title = $error['schema']['title']; 1722 1723 return new WP_Error( 1724 'rest_no_matching_schema', 1725 /* translators: 1: Parameter, 2: Schema title, 3: Reason. */ 1726 sprintf( __( '%1$s is not a valid %2$s. Reason: %3$s' ), $param, $title, $reason ), 1727 array( 'position' => $position ) 1728 ); 1729 } 1730 1731 return new WP_Error( 1732 'rest_no_matching_schema', 1733 /* translators: 1: Parameter, 2: Reason. */ 1734 sprintf( __( '%1$s does not match the expected format. Reason: %2$s' ), $param, $reason ), 1735 array( 'position' => $position ) 1736 ); 1737 } 1738 1739 /** 1740 * Gets the error of combining operation. 1741 * 1742 * @since 5.6.0 1743 * 1744 * @param array $value The value to validate. 1745 * @param string $param The parameter name, used in error messages. 1746 * @param array $errors The errors array, to search for possible error. 1747 * @return WP_Error The combining operation error. 1748 */ 1749 function rest_get_combining_operation_error( $value, $param, $errors ) { 1750 // If there is only one error, simply return it. 1751 if ( 1 === count( $errors ) ) { 1752 return rest_format_combining_operation_error( $param, $errors[0] ); 1753 } 1754 1755 // Filter out all errors related to type validation. 1756 $filtered_errors = array(); 1757 foreach ( $errors as $error ) { 1758 $error_code = $error['error_object']->get_error_code(); 1759 $error_data = $error['error_object']->get_error_data(); 1760 1761 if ( 'rest_invalid_type' !== $error_code || ( isset( $error_data['param'] ) && $param !== $error_data['param'] ) ) { 1762 $filtered_errors[] = $error; 1763 } 1764 } 1765 1766 // If there is only one error left, simply return it. 1767 if ( 1 === count( $filtered_errors ) ) { 1768 return rest_format_combining_operation_error( $param, $filtered_errors[0] ); 1769 } 1770 1771 // If there are only errors related to object validation, try choosing the most appropriate one. 1772 if ( count( $filtered_errors ) > 1 && 'object' === $filtered_errors[0]['schema']['type'] ) { 1773 $result = null; 1774 $number = 0; 1775 1776 foreach ( $filtered_errors as $error ) { 1777 if ( isset( $error['schema']['properties'] ) ) { 1778 $n = count( array_intersect_key( $error['schema']['properties'], $value ) ); 1779 if ( $n > $number ) { 1780 $result = $error; 1781 $number = $n; 1782 } 1783 } 1784 } 1785 1786 if ( null !== $result ) { 1787 return rest_format_combining_operation_error( $param, $result ); 1788 } 1789 } 1790 1791 // If each schema has a title, include those titles in the error message. 1792 $schema_titles = array(); 1793 foreach ( $errors as $error ) { 1794 if ( isset( $error['schema']['title'] ) ) { 1795 $schema_titles[] = $error['schema']['title']; 1796 } 1797 } 1798 1799 if ( count( $schema_titles ) === count( $errors ) ) { 1800 /* translators: 1: Parameter, 2: Schema titles. */ 1801 return new WP_Error( 'rest_no_matching_schema', wp_sprintf( __( '%1$s is not a valid %2$l.' ), $param, $schema_titles ) ); 1802 } 1803 1804 /* translators: %s: Parameter. */ 1805 return new WP_Error( 'rest_no_matching_schema', sprintf( __( '%s does not match any of the expected formats.' ), $param ) ); 1806 } 1807 1808 /** 1809 * Finds the matching schema among the "anyOf" schemas. 1810 * 1811 * @since 5.6.0 1812 * 1813 * @param mixed $value The value to validate. 1814 * @param array $args The schema array to use. 1815 * @param string $param The parameter name, used in error messages. 1816 * @return array|WP_Error The matching schema or WP_Error instance if all schemas do not match. 1817 */ 1818 function rest_find_any_matching_schema( $value, $args, $param ) { 1819 $errors = array(); 1820 1821 foreach ( $args['anyOf'] as $index => $schema ) { 1822 if ( ! isset( $schema['type'] ) && isset( $args['type'] ) ) { 1823 $schema['type'] = $args['type']; 1824 } 1825 1826 $is_valid = rest_validate_value_from_schema( $value, $schema, $param ); 1827 if ( ! is_wp_error( $is_valid ) ) { 1828 return $schema; 1829 } 1830 1831 $errors[] = array( 1832 'error_object' => $is_valid, 1833 'schema' => $schema, 1834 'index' => $index, 1835 ); 1836 } 1837 1838 return rest_get_combining_operation_error( $value, $param, $errors ); 1839 } 1840 1841 /** 1842 * Finds the matching schema among the "oneOf" schemas. 1843 * 1844 * @since 5.6.0 1845 * 1846 * @param mixed $value The value to validate. 1847 * @param array $args The schema array to use. 1848 * @param string $param The parameter name, used in error messages. 1849 * @param bool $stop_after_first_match Optional. Whether the process should stop after the first successful match. 1850 * @return array|WP_Error The matching schema or WP_Error instance if the number of matching schemas is not equal to one. 1851 */ 1852 function rest_find_one_matching_schema( $value, $args, $param, $stop_after_first_match = false ) { 1853 $matching_schemas = array(); 1854 $errors = array(); 1855 1856 foreach ( $args['oneOf'] as $index => $schema ) { 1857 if ( ! isset( $schema['type'] ) && isset( $args['type'] ) ) { 1858 $schema['type'] = $args['type']; 1859 } 1860 1861 $is_valid = rest_validate_value_from_schema( $value, $schema, $param ); 1862 if ( ! is_wp_error( $is_valid ) ) { 1863 if ( $stop_after_first_match ) { 1864 return $schema; 1865 } 1866 1867 $matching_schemas[] = array( 1868 'schema_object' => $schema, 1869 'index' => $index, 1870 ); 1871 } else { 1872 $errors[] = array( 1873 'error_object' => $is_valid, 1874 'schema' => $schema, 1875 'index' => $index, 1876 ); 1877 } 1878 } 1879 1880 if ( ! $matching_schemas ) { 1881 return rest_get_combining_operation_error( $value, $param, $errors ); 1882 } 1883 1884 if ( count( $matching_schemas ) > 1 ) { 1885 $schema_positions = array(); 1886 $schema_titles = array(); 1887 1888 foreach ( $matching_schemas as $schema ) { 1889 $schema_positions[] = $schema['index']; 1890 1891 if ( isset( $schema['schema_object']['title'] ) ) { 1892 $schema_titles[] = $schema['schema_object']['title']; 1893 } 1894 } 1895 1896 // If each schema has a title, include those titles in the error message. 1897 if ( count( $schema_titles ) === count( $matching_schemas ) ) { 1898 return new WP_Error( 1899 'rest_one_of_multiple_matches', 1900 /* translators: 1: Parameter, 2: Schema titles. */ 1901 wp_sprintf( __( '%1$s matches %2$l, but should match only one.' ), $param, $schema_titles ), 1902 array( 'positions' => $schema_positions ) 1903 ); 1904 } 1905 1906 return new WP_Error( 1907 'rest_one_of_multiple_matches', 1908 /* translators: %s: Parameter. */ 1909 sprintf( __( '%s matches more than one of the expected formats.' ), $param ), 1910 array( 'positions' => $schema_positions ) 1911 ); 1912 } 1913 1914 return $matching_schemas[0]['schema_object']; 1915 } 1916 1917 /** 1918 * Checks the equality of two values, following JSON Schema semantics. 1919 * 1920 * Property order is ignored for objects. 1921 * 1922 * Values must have been previously sanitized/coerced to their native types. 1923 * 1924 * @since 5.7.0 1925 * 1926 * @param mixed $value1 The first value to check. 1927 * @param mixed $value2 The second value to check. 1928 * @return bool True if the values are equal or false otherwise. 1929 */ 1930 function rest_are_values_equal( $value1, $value2 ) { 1931 if ( is_array( $value1 ) && is_array( $value2 ) ) { 1932 if ( count( $value1 ) !== count( $value2 ) ) { 1933 return false; 1934 } 1935 1936 foreach ( $value1 as $index => $value ) { 1937 if ( ! array_key_exists( $index, $value2 ) || ! rest_are_values_equal( $value, $value2[ $index ] ) ) { 1938 return false; 1939 } 1940 } 1941 1942 return true; 1943 } 1944 1945 if ( is_int( $value1 ) && is_float( $value2 ) 1946 || is_float( $value1 ) && is_int( $value2 ) 1947 ) { 1948 return (float) $value1 === (float) $value2; 1949 } 1950 1951 return $value1 === $value2; 1952 } 1953 1954 /** 1955 * Validates that the given value is a member of the JSON Schema "enum". 1956 * 1957 * @since 5.7.0 1958 * 1959 * @param mixed $value The value to validate. 1960 * @param array $args The schema array to use. 1961 * @param string $param The parameter name, used in error messages. 1962 * @return true|WP_Error True if the "enum" contains the value or a WP_Error instance otherwise. 1963 */ 1964 function rest_validate_enum( $value, $args, $param ) { 1965 $sanitized_value = rest_sanitize_value_from_schema( $value, $args, $param ); 1966 if ( is_wp_error( $sanitized_value ) ) { 1967 return $sanitized_value; 1968 } 1969 1970 foreach ( $args['enum'] as $enum_value ) { 1971 if ( rest_are_values_equal( $sanitized_value, $enum_value ) ) { 1972 return true; 1973 } 1974 } 1975 1976 $encoded_enum_values = array(); 1977 foreach ( $args['enum'] as $enum_value ) { 1978 $encoded_enum_values[] = is_scalar( $enum_value ) ? $enum_value : wp_json_encode( $enum_value ); 1979 } 1980 1981 if ( count( $encoded_enum_values ) === 1 ) { 1982 /* translators: 1: Parameter, 2: Valid values. */ 1983 return new WP_Error( 'rest_not_in_enum', wp_sprintf( __( '%1$s is not %2$s.' ), $param, $encoded_enum_values[0] ) ); 1984 } 1985 1986 /* translators: 1: Parameter, 2: List of valid values. */ 1987 return new WP_Error( 'rest_not_in_enum', wp_sprintf( __( '%1$s is not one of %2$l.' ), $param, $encoded_enum_values ) ); 1988 } 1989 1990 /** 1991 * Get all valid JSON schema properties. 1992 * 1993 * @since 5.6.0 1994 * 1995 * @return string[] All valid JSON schema properties. 1996 */ 1997 function rest_get_allowed_schema_keywords() { 1998 return array( 1999 'title', 2000 'description', 2001 'default', 2002 'type', 2003 'format', 2004 'enum', 2005 'items', 2006 'properties', 2007 'additionalProperties', 2008 'patternProperties', 2009 'minProperties', 2010 'maxProperties', 2011 'minimum', 2012 'maximum', 2013 'exclusiveMinimum', 2014 'exclusiveMaximum', 2015 'multipleOf', 2016 'minLength', 2017 'maxLength', 2018 'pattern', 2019 'minItems', 2020 'maxItems', 2021 'uniqueItems', 2022 'anyOf', 2023 'oneOf', 2024 ); 2025 } 2026 2027 /** 2028 * Validate a value based on a schema. 2029 * 2030 * @since 4.7.0 2031 * @since 4.9.0 Support the "object" type. 2032 * @since 5.2.0 Support validating "additionalProperties" against a schema. 2033 * @since 5.3.0 Support multiple types. 2034 * @since 5.4.0 Convert an empty string to an empty object. 2035 * @since 5.5.0 Add the "uuid" and "hex-color" formats. 2036 * Support the "minLength", "maxLength" and "pattern" keywords for strings. 2037 * Support the "minItems", "maxItems" and "uniqueItems" keywords for arrays. 2038 * Validate required properties. 2039 * @since 5.6.0 Support the "minProperties" and "maxProperties" keywords for objects. 2040 * Support the "multipleOf" keyword for numbers and integers. 2041 * Support the "patternProperties" keyword for objects. 2042 * Support the "anyOf" and "oneOf" keywords. 2043 * 2044 * @param mixed $value The value to validate. 2045 * @param array $args Schema array to use for validation. 2046 * @param string $param The parameter name, used in error messages. 2047 * @return true|WP_Error 2048 */ 2049 function rest_validate_value_from_schema( $value, $args, $param = '' ) { 2050 if ( isset( $args['anyOf'] ) ) { 2051 $matching_schema = rest_find_any_matching_schema( $value, $args, $param ); 2052 if ( is_wp_error( $matching_schema ) ) { 2053 return $matching_schema; 2054 } 2055 2056 if ( ! isset( $args['type'] ) && isset( $matching_schema['type'] ) ) { 2057 $args['type'] = $matching_schema['type']; 2058 } 2059 } 2060 2061 if ( isset( $args['oneOf'] ) ) { 2062 $matching_schema = rest_find_one_matching_schema( $value, $args, $param ); 2063 if ( is_wp_error( $matching_schema ) ) { 2064 return $matching_schema; 2065 } 2066 2067 if ( ! isset( $args['type'] ) && isset( $matching_schema['type'] ) ) { 2068 $args['type'] = $matching_schema['type']; 2069 } 2070 } 2071 2072 $allowed_types = array( 'array', 'object', 'string', 'number', 'integer', 'boolean', 'null' ); 2073 2074 if ( ! isset( $args['type'] ) ) { 2075 /* translators: %s: Parameter. */ 2076 _doing_it_wrong( __FUNCTION__, sprintf( __( 'The "type" schema keyword for %s is required.' ), $param ), '5.5.0' ); 2077 } 2078 2079 if ( is_array( $args['type'] ) ) { 2080 $best_type = rest_handle_multi_type_schema( $value, $args, $param ); 2081 2082 if ( ! $best_type ) { 2083 return new WP_Error( 2084 'rest_invalid_type', 2085 /* translators: 1: Parameter, 2: List of types. */ 2086 sprintf( __( '%1$s is not of type %2$s.' ), $param, implode( ',', $args['type'] ) ), 2087 array( 'param' => $param ) 2088 ); 2089 } 2090 2091 $args['type'] = $best_type; 2092 } 2093 2094 if ( ! in_array( $args['type'], $allowed_types, true ) ) { 2095 _doing_it_wrong( 2096 __FUNCTION__, 2097 /* translators: 1: Parameter, 2: The list of allowed types. */ 2098 wp_sprintf( __( 'The "type" schema keyword for %1$s can only be one of the built-in types: %2$l.' ), $param, $allowed_types ), 2099 '5.5.0' 2100 ); 2101 } 2102 2103 switch ( $args['type'] ) { 2104 case 'null': 2105 $is_valid = rest_validate_null_value_from_schema( $value, $param ); 2106 break; 2107 case 'boolean': 2108 $is_valid = rest_validate_boolean_value_from_schema( $value, $param ); 2109 break; 2110 case 'object': 2111 $is_valid = rest_validate_object_value_from_schema( $value, $args, $param ); 2112 break; 2113 case 'array': 2114 $is_valid = rest_validate_array_value_from_schema( $value, $args, $param ); 2115 break; 2116 case 'number': 2117 $is_valid = rest_validate_number_value_from_schema( $value, $args, $param ); 2118 break; 2119 case 'string': 2120 $is_valid = rest_validate_string_value_from_schema( $value, $args, $param ); 2121 break; 2122 case 'integer': 2123 $is_valid = rest_validate_integer_value_from_schema( $value, $args, $param ); 2124 break; 2125 default: 2126 $is_valid = true; 2127 break; 2128 } 2129 2130 if ( is_wp_error( $is_valid ) ) { 2131 return $is_valid; 2132 } 2133 2134 if ( ! empty( $args['enum'] ) ) { 2135 $enum_contains_value = rest_validate_enum( $value, $args, $param ); 2136 if ( is_wp_error( $enum_contains_value ) ) { 2137 return $enum_contains_value; 2138 } 2139 } 2140 2141 // The "format" keyword should only be applied to strings. However, for backward compatibility, 2142 // we allow the "format" keyword if the type keyword was not specified, or was set to an invalid value. 2143 if ( isset( $args['format'] ) 2144 && ( ! isset( $args['type'] ) || 'string' === $args['type'] || ! in_array( $args['type'], $allowed_types, true ) ) 2145 ) { 2146 switch ( $args['format'] ) { 2147 case 'hex-color': 2148 if ( ! rest_parse_hex_color( $value ) ) { 2149 return new WP_Error( 'rest_invalid_hex_color', __( 'Invalid hex color.' ) ); 2150 } 2151 break; 2152 2153 case 'date-time': 2154 if ( ! rest_parse_date( $value ) ) { 2155 return new WP_Error( 'rest_invalid_date', __( 'Invalid date.' ) ); 2156 } 2157 break; 2158 2159 case 'email': 2160 if ( ! is_email( $value ) ) { 2161 return new WP_Error( 'rest_invalid_email', __( 'Invalid email address.' ) ); 2162 } 2163 break; 2164 case 'ip': 2165 if ( ! rest_is_ip_address( $value ) ) { 2166 /* translators: %s: IP address. */ 2167 return new WP_Error( 'rest_invalid_ip', sprintf( __( '%s is not a valid IP address.' ), $param ) ); 2168 } 2169 break; 2170 case 'uuid': 2171 if ( ! wp_is_uuid( $value ) ) { 2172 /* translators: %s: The name of a JSON field expecting a valid UUID. */ 2173 return new WP_Error( 'rest_invalid_uuid', sprintf( __( '%s is not a valid UUID.' ), $param ) ); 2174 } 2175 break; 2176 } 2177 } 2178 2179 return true; 2180 } 2181 2182 /** 2183 * Validates a null value based on a schema. 2184 * 2185 * @since 5.7.0 2186 * 2187 * @param mixed $value The value to validate. 2188 * @param string $param The parameter name, used in error messages. 2189 * @return true|WP_Error 2190 */ 2191 function rest_validate_null_value_from_schema( $value, $param ) { 2192 if ( null !== $value ) { 2193 return new WP_Error( 2194 'rest_invalid_type', 2195 /* translators: 1: Parameter, 2: Type name. */ 2196 sprintf( __( '%1$s is not of type %2$s.' ), $param, 'null' ), 2197 array( 'param' => $param ) 2198 ); 2199 } 2200 2201 return true; 2202 } 2203 2204 /** 2205 * Validates a boolean value based on a schema. 2206 * 2207 * @since 5.7.0 2208 * 2209 * @param mixed $value The value to validate. 2210 * @param string $param The parameter name, used in error messages. 2211 * @return true|WP_Error 2212 */ 2213 function rest_validate_boolean_value_from_schema( $value, $param ) { 2214 if ( ! rest_is_boolean( $value ) ) { 2215 return new WP_Error( 2216 'rest_invalid_type', 2217 /* translators: 1: Parameter, 2: Type name. */ 2218 sprintf( __( '%1$s is not of type %2$s.' ), $param, 'boolean' ), 2219 array( 'param' => $param ) 2220 ); 2221 } 2222 2223 return true; 2224 } 2225 2226 /** 2227 * Validates an object value based on a schema. 2228 * 2229 * @since 5.7.0 2230 * 2231 * @param mixed $value The value to validate. 2232 * @param array $args Schema array to use for validation. 2233 * @param string $param The parameter name, used in error messages. 2234 * @return true|WP_Error 2235 */ 2236 function rest_validate_object_value_from_schema( $value, $args, $param ) { 2237 if ( ! rest_is_object( $value ) ) { 2238 return new WP_Error( 2239 'rest_invalid_type', 2240 /* translators: 1: Parameter, 2: Type name. */ 2241 sprintf( __( '%1$s is not of type %2$s.' ), $param, 'object' ), 2242 array( 'param' => $param ) 2243 ); 2244 } 2245 2246 $value = rest_sanitize_object( $value ); 2247 2248 if ( isset( $args['required'] ) && is_array( $args['required'] ) ) { // schema version 4 2249 foreach ( $args['required'] as $name ) { 2250 if ( ! array_key_exists( $name, $value ) ) { 2251 return new WP_Error( 2252 'rest_property_required', 2253 /* translators: 1: Property of an object, 2: Parameter. */ 2254 sprintf( __( '%1$s is a required property of %2$s.' ), $name, $param ) 2255 ); 2256 } 2257 } 2258 } elseif ( isset( $args['properties'] ) ) { // schema version 3 2259 foreach ( $args['properties'] as $name => $property ) { 2260 if ( isset( $property['required'] ) && true === $property['required'] && ! array_key_exists( $name, $value ) ) { 2261 return new WP_Error( 2262 'rest_property_required', 2263 /* translators: 1: Property of an object, 2: Parameter. */ 2264 sprintf( __( '%1$s is a required property of %2$s.' ), $name, $param ) 2265 ); 2266 } 2267 } 2268 } 2269 2270 foreach ( $value as $property => $v ) { 2271 if ( isset( $args['properties'][ $property ] ) ) { 2272 $is_valid = rest_validate_value_from_schema( $v, $args['properties'][ $property ], $param . '[' . $property . ']' ); 2273 if ( is_wp_error( $is_valid ) ) { 2274 return $is_valid; 2275 } 2276 continue; 2277 } 2278 2279 $pattern_property_schema = rest_find_matching_pattern_property_schema( $property, $args ); 2280 if ( null !== $pattern_property_schema ) { 2281 $is_valid = rest_validate_value_from_schema( $v, $pattern_property_schema, $param . '[' . $property . ']' ); 2282 if ( is_wp_error( $is_valid ) ) { 2283 return $is_valid; 2284 } 2285 continue; 2286 } 2287 2288 if ( isset( $args['additionalProperties'] ) ) { 2289 if ( false === $args['additionalProperties'] ) { 2290 return new WP_Error( 2291 'rest_additional_properties_forbidden', 2292 /* translators: %s: Property of an object. */ 2293 sprintf( __( '%1$s is not a valid property of Object.' ), $property ) 2294 ); 2295 } 2296 2297 if ( is_array( $args['additionalProperties'] ) ) { 2298 $is_valid = rest_validate_value_from_schema( $v, $args['additionalProperties'], $param . '[' . $property . ']' ); 2299 if ( is_wp_error( $is_valid ) ) { 2300 return $is_valid; 2301 } 2302 } 2303 } 2304 } 2305 2306 if ( isset( $args['minProperties'] ) && count( $value ) < $args['minProperties'] ) { 2307 return new WP_Error( 2308 'rest_too_few_properties', 2309 sprintf( 2310 /* translators: 1: Parameter, 2: Number. */ 2311 _n( 2312 '%1$s must contain at least %2$s property.', 2313 '%1$s must contain at least %2$s properties.', 2314 $args['minProperties'] 2315 ), 2316 $param, 2317 number_format_i18n( $args['minProperties'] ) 2318 ) 2319 ); 2320 } 2321 2322 if ( isset( $args['maxProperties'] ) && count( $value ) > $args['maxProperties'] ) { 2323 return new WP_Error( 2324 'rest_too_many_properties', 2325 sprintf( 2326 /* translators: 1: Parameter, 2: Number. */ 2327 _n( 2328 '%1$s must contain at most %2$s property.', 2329 '%1$s must contain at most %2$s properties.', 2330 $args['maxProperties'] 2331 ), 2332 $param, 2333 number_format_i18n( $args['maxProperties'] ) 2334 ) 2335 ); 2336 } 2337 2338 return true; 2339 } 2340 2341 /** 2342 * Validates an array value based on a schema. 2343 * 2344 * @since 5.7.0 2345 * 2346 * @param mixed $value The value to validate. 2347 * @param array $args Schema array to use for validation. 2348 * @param string $param The parameter name, used in error messages. 2349 * @return true|WP_Error 2350 */ 2351 function rest_validate_array_value_from_schema( $value, $args, $param ) { 2352 if ( ! rest_is_array( $value ) ) { 2353 return new WP_Error( 2354 'rest_invalid_type', 2355 /* translators: 1: Parameter, 2: Type name. */ 2356 sprintf( __( '%1$s is not of type %2$s.' ), $param, 'array' ), 2357 array( 'param' => $param ) 2358 ); 2359 } 2360 2361 $value = rest_sanitize_array( $value ); 2362 2363 if ( isset( $args['items'] ) ) { 2364 foreach ( $value as $index => $v ) { 2365 $is_valid = rest_validate_value_from_schema( $v, $args['items'], $param . '[' . $index . ']' ); 2366 if ( is_wp_error( $is_valid ) ) { 2367 return $is_valid; 2368 } 2369 } 2370 } 2371 2372 if ( isset( $args['minItems'] ) && count( $value ) < $args['minItems'] ) { 2373 return new WP_Error( 2374 'rest_too_few_items', 2375 sprintf( 2376 /* translators: 1: Parameter, 2: Number. */ 2377 _n( 2378 '%1$s must contain at least %2$s item.', 2379 '%1$s must contain at least %2$s items.', 2380 $args['minItems'] 2381 ), 2382 $param, 2383 number_format_i18n( $args['minItems'] ) 2384 ) 2385 ); 2386 } 2387 2388 if ( isset( $args['maxItems'] ) && count( $value ) > $args['maxItems'] ) { 2389 return new WP_Error( 2390 'rest_too_many_items', 2391 sprintf( 2392 /* translators: 1: Parameter, 2: Number. */ 2393 _n( 2394 '%1$s must contain at most %2$s item.', 2395 '%1$s must contain at most %2$s items.', 2396 $args['maxItems'] 2397 ), 2398 $param, 2399 number_format_i18n( $args['maxItems'] ) 2400 ) 2401 ); 2402 } 2403 2404 if ( ! empty( $args['uniqueItems'] ) && ! rest_validate_array_contains_unique_items( $value ) ) { 2405 /* translators: %s: Parameter. */ 2406 return new WP_Error( 'rest_duplicate_items', sprintf( __( '%s has duplicate items.' ), $param ) ); 2407 } 2408 2409 return true; 2410 } 2411 2412 /** 2413 * Validates a number value based on a schema. 2414 * 2415 * @since 5.7.0 2416 * 2417 * @param mixed $value The value to validate. 2418 * @param array $args Schema array to use for validation. 2419 * @param string $param The parameter name, used in error messages. 2420 * @return true|WP_Error 2421 */ 2422 function rest_validate_number_value_from_schema( $value, $args, $param ) { 2423 if ( ! is_numeric( $value ) ) { 2424 return new WP_Error( 2425 'rest_invalid_type', 2426 /* translators: 1: Parameter, 2: Type name. */ 2427 sprintf( __( '%1$s is not of type %2$s.' ), $param, $args['type'] ), 2428 array( 'param' => $param ) 2429 ); 2430 } 2431 2432 if ( isset( $args['multipleOf'] ) && fmod( $value, $args['multipleOf'] ) !== 0.0 ) { 2433 return new WP_Error( 2434 'rest_invalid_multiple', 2435 /* translators: 1: Parameter, 2: Multiplier. */ 2436 sprintf( __( '%1$s must be a multiple of %2$s.' ), $param, $args['multipleOf'] ) 2437 ); 2438 } 2439 2440 if ( isset( $args['minimum'] ) && ! isset( $args['maximum'] ) ) { 2441 if ( ! empty( $args['exclusiveMinimum'] ) && $value <= $args['minimum'] ) { 2442 return new WP_Error( 2443 'rest_out_of_bounds', 2444 /* translators: 1: Parameter, 2: Minimum number. */ 2445 sprintf( __( '%1$s must be greater than %2$d' ), $param, $args['minimum'] ) 2446 ); 2447 } 2448 2449 if ( empty( $args['exclusiveMinimum'] ) && $value < $args['minimum'] ) { 2450 return new WP_Error( 2451 'rest_out_of_bounds', 2452 /* translators: 1: Parameter, 2: Minimum number. */ 2453 sprintf( __( '%1$s must be greater than or equal to %2$d' ), $param, $args['minimum'] ) 2454 ); 2455 } 2456 } 2457 2458 if ( isset( $args['maximum'] ) && ! isset( $args['minimum'] ) ) { 2459 if ( ! empty( $args['exclusiveMaximum'] ) && $value >= $args['maximum'] ) { 2460 return new WP_Error( 2461 'rest_out_of_bounds', 2462 /* translators: 1: Parameter, 2: Maximum number. */ 2463 sprintf( __( '%1$s must be less than %2$d' ), $param, $args['maximum'] ) 2464 ); 2465 } 2466 2467 if ( empty( $args['exclusiveMaximum'] ) && $value > $args['maximum'] ) { 2468 return new WP_Error( 2469 'rest_out_of_bounds', 2470 /* translators: 1: Parameter, 2: Maximum number. */ 2471 sprintf( __( '%1$s must be less than or equal to %2$d' ), $param, $args['maximum'] ) 2472 ); 2473 } 2474 } 2475 2476 if ( isset( $args['minimum'], $args['maximum'] ) ) { 2477 if ( ! empty( $args['exclusiveMinimum'] ) && ! empty( $args['exclusiveMaximum'] ) ) { 2478 if ( $value >= $args['maximum'] || $value <= $args['minimum'] ) { 2479 return new WP_Error( 2480 'rest_out_of_bounds', 2481 sprintf( 2482 /* translators: 1: Parameter, 2: Minimum number, 3: Maximum number. */ 2483 __( '%1$s must be between %2$d (exclusive) and %3$d (exclusive)' ), 2484 $param, 2485 $args['minimum'], 2486 $args['maximum'] 2487 ) 2488 ); 2489 } 2490 } 2491 2492 if ( ! empty( $args['exclusiveMinimum'] ) && empty( $args['exclusiveMaximum'] ) ) { 2493 if ( $value > $args['maximum'] || $value <= $args['minimum'] ) { 2494 return new WP_Error( 2495 'rest_out_of_bounds', 2496 sprintf( 2497 /* translators: 1: Parameter, 2: Minimum number, 3: Maximum number. */ 2498 __( '%1$s must be between %2$d (exclusive) and %3$d (inclusive)' ), 2499 $param, 2500 $args['minimum'], 2501 $args['maximum'] 2502 ) 2503 ); 2504 } 2505 } 2506 2507 if ( ! empty( $args['exclusiveMaximum'] ) && empty( $args['exclusiveMinimum'] ) ) { 2508 if ( $value >= $args['maximum'] || $value < $args['minimum'] ) { 2509 return new WP_Error( 2510 'rest_out_of_bounds', 2511 sprintf( 2512 /* translators: 1: Parameter, 2: Minimum number, 3: Maximum number. */ 2513 __( '%1$s must be between %2$d (inclusive) and %3$d (exclusive)' ), 2514 $param, 2515 $args['minimum'], 2516 $args['maximum'] 2517 ) 2518 ); 2519 } 2520 } 2521 2522 if ( empty( $args['exclusiveMinimum'] ) && empty( $args['exclusiveMaximum'] ) ) { 2523 if ( $value > $args['maximum'] || $value < $args['minimum'] ) { 2524 return new WP_Error( 2525 'rest_out_of_bounds', 2526 sprintf( 2527 /* translators: 1: Parameter, 2: Minimum number, 3: Maximum number. */ 2528 __( '%1$s must be between %2$d (inclusive) and %3$d (inclusive)' ), 2529 $param, 2530 $args['minimum'], 2531 $args['maximum'] 2532 ) 2533 ); 2534 } 2535 } 2536 } 2537 2538 return true; 2539 } 2540 2541 /** 2542 * Validates a string value based on a schema. 2543 * 2544 * @since 5.7.0 2545 * 2546 * @param mixed $value The value to validate. 2547 * @param array $args Schema array to use for validation. 2548 * @param string $param The parameter name, used in error messages. 2549 * @return true|WP_Error 2550 */ 2551 function rest_validate_string_value_from_schema( $value, $args, $param ) { 2552 if ( ! is_string( $value ) ) { 2553 return new WP_Error( 2554 'rest_invalid_type', 2555 /* translators: 1: Parameter, 2: Type name. */ 2556 sprintf( __( '%1$s is not of type %2$s.' ), $param, 'string' ), 2557 array( 'param' => $param ) 2558 ); 2559 } 2560 2561 if ( isset( $args['minLength'] ) && mb_strlen( $value ) < $args['minLength'] ) { 2562 return new WP_Error( 2563 'rest_too_short', 2564 sprintf( 2565 /* translators: 1: Parameter, 2: Number of characters. */ 2566 _n( 2567 '%1$s must be at least %2$s character long.', 2568 '%1$s must be at least %2$s characters long.', 2569 $args['minLength'] 2570 ), 2571 $param, 2572 number_format_i18n( $args['minLength'] ) 2573 ) 2574 ); 2575 } 2576 2577 if ( isset( $args['maxLength'] ) && mb_strlen( $value ) > $args['maxLength'] ) { 2578 return new WP_Error( 2579 'rest_too_long', 2580 sprintf( 2581 /* translators: 1: Parameter, 2: Number of characters. */ 2582 _n( 2583 '%1$s must be at most %2$s character long.', 2584 '%1$s must be at most %2$s characters long.', 2585 $args['maxLength'] 2586 ), 2587 $param, 2588 number_format_i18n( $args['maxLength'] ) 2589 ) 2590 ); 2591 } 2592 2593 if ( isset( $args['pattern'] ) && ! rest_validate_json_schema_pattern( $args['pattern'], $value ) ) { 2594 return new WP_Error( 2595 'rest_invalid_pattern', 2596 /* translators: 1: Parameter, 2: Pattern. */ 2597 sprintf( __( '%1$s does not match pattern %2$s.' ), $param, $args['pattern'] ) 2598 ); 2599 } 2600 2601 return true; 2602 } 2603 2604 /** 2605 * Validates an integer value based on a schema. 2606 * 2607 * @since 5.7.0 2608 * 2609 * @param mixed $value The value to validate. 2610 * @param array $args Schema array to use for validation. 2611 * @param string $param The parameter name, used in error messages. 2612 * @return true|WP_Error 2613 */ 2614 function rest_validate_integer_value_from_schema( $value, $args, $param ) { 2615 $is_valid_number = rest_validate_number_value_from_schema( $value, $args, $param ); 2616 if ( is_wp_error( $is_valid_number ) ) { 2617 return $is_valid_number; 2618 } 2619 2620 if ( ! rest_is_integer( $value ) ) { 2621 return new WP_Error( 2622 'rest_invalid_type', 2623 /* translators: 1: Parameter, 2: Type name. */ 2624 sprintf( __( '%1$s is not of type %2$s.' ), $param, 'integer' ), 2625 array( 'param' => $param ) 2626 ); 2627 } 2628 2629 return true; 2630 } 2631 2632 /** 2633 * Sanitize a value based on a schema. 2634 * 2635 * @since 4.7.0 2636 * @since 5.5.0 Added the `$param` parameter. 2637 * @since 5.6.0 Support the "anyOf" and "oneOf" keywords. 2638 * 2639 * @param mixed $value The value to sanitize. 2640 * @param array $args Schema array to use for sanitization. 2641 * @param string $param The parameter name, used in error messages. 2642 * @return mixed|WP_Error The sanitized value or a WP_Error instance if the value cannot be safely sanitized. 2643 */ 2644 function rest_sanitize_value_from_schema( $value, $args, $param = '' ) { 2645 if ( isset( $args['anyOf'] ) ) { 2646 $matching_schema = rest_find_any_matching_schema( $value, $args, $param ); 2647 if ( is_wp_error( $matching_schema ) ) { 2648 return $matching_schema; 2649 } 2650 2651 if ( ! isset( $args['type'] ) ) { 2652 $args['type'] = $matching_schema['type']; 2653 } 2654 2655 $value = rest_sanitize_value_from_schema( $value, $matching_schema, $param ); 2656 } 2657 2658 if ( isset( $args['oneOf'] ) ) { 2659 $matching_schema = rest_find_one_matching_schema( $value, $args, $param ); 2660 if ( is_wp_error( $matching_schema ) ) { 2661 return $matching_schema; 2662 } 2663 2664 if ( ! isset( $args['type'] ) ) { 2665 $args['type'] = $matching_schema['type']; 2666 } 2667 2668 $value = rest_sanitize_value_from_schema( $value, $matching_schema, $param ); 2669 } 2670 2671 $allowed_types = array( 'array', 'object', 'string', 'number', 'integer', 'boolean', 'null' ); 2672 2673 if ( ! isset( $args['type'] ) ) { 2674 /* translators: %s: Parameter. */ 2675 _doing_it_wrong( __FUNCTION__, sprintf( __( 'The "type" schema keyword for %s is required.' ), $param ), '5.5.0' ); 2676 } 2677 2678 if ( is_array( $args['type'] ) ) { 2679 $best_type = rest_handle_multi_type_schema( $value, $args, $param ); 2680 2681 if ( ! $best_type ) { 2682 return null; 2683 } 2684 2685 $args['type'] = $best_type; 2686 } 2687 2688 if ( ! in_array( $args['type'], $allowed_types, true ) ) { 2689 _doing_it_wrong( 2690 __FUNCTION__, 2691 /* translators: 1: Parameter, 2: The list of allowed types. */ 2692 wp_sprintf( __( 'The "type" schema keyword for %1$s can only be one of the built-in types: %2$l.' ), $param, $allowed_types ), 2693 '5.5.0' 2694 ); 2695 } 2696 2697 if ( 'array' === $args['type'] ) { 2698 $value = rest_sanitize_array( $value ); 2699 2700 if ( ! empty( $args['items'] ) ) { 2701 foreach ( $value as $index => $v ) { 2702 $value[ $index ] = rest_sanitize_value_from_schema( $v, $args['items'], $param . '[' . $index . ']' ); 2703 } 2704 } 2705 2706 if ( ! empty( $args['uniqueItems'] ) && ! rest_validate_array_contains_unique_items( $value ) ) { 2707 /* translators: %s: Parameter. */ 2708 return new WP_Error( 'rest_duplicate_items', sprintf( __( '%s has duplicate items.' ), $param ) ); 2709 } 2710 2711 return $value; 2712 } 2713 2714 if ( 'object' === $args['type'] ) { 2715 $value = rest_sanitize_object( $value ); 2716 2717 foreach ( $value as $property => $v ) { 2718 if ( isset( $args['properties'][ $property ] ) ) { 2719 $value[ $property ] = rest_sanitize_value_from_schema( $v, $args['properties'][ $property ], $param . '[' . $property . ']' ); 2720 continue; 2721 } 2722 2723 $pattern_property_schema = rest_find_matching_pattern_property_schema( $property, $args ); 2724 if ( null !== $pattern_property_schema ) { 2725 $value[ $property ] = rest_sanitize_value_from_schema( $v, $pattern_property_schema, $param . '[' . $property . ']' ); 2726 continue; 2727 } 2728 2729 if ( isset( $args['additionalProperties'] ) ) { 2730 if ( false === $args['additionalProperties'] ) { 2731 unset( $value[ $property ] ); 2732 } elseif ( is_array( $args['additionalProperties'] ) ) { 2733 $value[ $property ] = rest_sanitize_value_from_schema( $v, $args['additionalProperties'], $param . '[' . $property . ']' ); 2734 } 2735 } 2736 } 2737 2738 return $value; 2739 } 2740 2741 if ( 'null' === $args['type'] ) { 2742 return null; 2743 } 2744 2745 if ( 'integer' === $args['type'] ) { 2746 return (int) $value; 2747 } 2748 2749 if ( 'number' === $args['type'] ) { 2750 return (float) $value; 2751 } 2752 2753 if ( 'boolean' === $args['type'] ) { 2754 return rest_sanitize_boolean( $value ); 2755 } 2756 2757 // This behavior matches rest_validate_value_from_schema(). 2758 if ( isset( $args['format'] ) 2759 && ( ! isset( $args['type'] ) || 'string' === $args['type'] || ! in_array( $args['type'], $allowed_types, true ) ) 2760 ) { 2761 switch ( $args['format'] ) { 2762 case 'hex-color': 2763 return (string) sanitize_hex_color( $value ); 2764 2765 case 'date-time': 2766 return sanitize_text_field( $value ); 2767 2768 case 'email': 2769 // sanitize_email() validates, which would be unexpected. 2770 return sanitize_text_field( $value ); 2771 2772 case 'uri': 2773 return esc_url_raw( $value ); 2774 2775 case 'ip': 2776 return sanitize_text_field( $value ); 2777 2778 case 'uuid': 2779 return sanitize_text_field( $value ); 2780 } 2781 } 2782 2783 if ( 'string' === $args['type'] ) { 2784 return (string) $value; 2785 } 2786 2787 return $value; 2788 } 2789 2790 /** 2791 * Append result of internal request to REST API for purpose of preloading data to be attached to a page. 2792 * Expected to be called in the context of `array_reduce`. 2793 * 2794 * @since 5.0.0 2795 * 2796 * @param array $memo Reduce accumulator. 2797 * @param string $path REST API path to preload. 2798 * @return array Modified reduce accumulator. 2799 */ 2800 function rest_preload_api_request( $memo, $path ) { 2801 // array_reduce() doesn't support passing an array in PHP 5.2, 2802 // so we need to make sure we start with one. 2803 if ( ! is_array( $memo ) ) { 2804 $memo = array(); 2805 } 2806 2807 if ( empty( $path ) ) { 2808 return $memo; 2809 } 2810 2811 $method = 'GET'; 2812 if ( is_array( $path ) && 2 === count( $path ) ) { 2813 $method = end( $path ); 2814 $path = reset( $path ); 2815 2816 if ( ! in_array( $method, array( 'GET', 'OPTIONS' ), true ) ) { 2817 $method = 'GET'; 2818 } 2819 } 2820 2821 $path_parts = parse_url( $path ); 2822 if ( false === $path_parts ) { 2823 return $memo; 2824 } 2825 2826 $request = new WP_REST_Request( $method, $path_parts['path'] ); 2827 if ( ! empty( $path_parts['query'] ) ) { 2828 parse_str( $path_parts['query'], $query_params ); 2829 $request->set_query_params( $query_params ); 2830 } 2831 2832 $response = rest_do_request( $request ); 2833 if ( 200 === $response->status ) { 2834 $server = rest_get_server(); 2835 $embed = $request->has_param( '_embed' ) ? rest_parse_embed_param( $request['_embed'] ) : false; 2836 $data = (array) $server->response_to_data( $response, $embed ); 2837 2838 if ( 'OPTIONS' === $method ) { 2839 $response = rest_send_allow_header( $response, $server, $request ); 2840 2841 $memo[ $method ][ $path ] = array( 2842 'body' => $data, 2843 'headers' => $response->headers, 2844 ); 2845 } else { 2846 $memo[ $path ] = array( 2847 'body' => $data, 2848 'headers' => $response->headers, 2849 ); 2850 } 2851 } 2852 2853 return $memo; 2854 } 2855 2856 /** 2857 * Parses the "_embed" parameter into the list of resources to embed. 2858 * 2859 * @since 5.4.0 2860 * 2861 * @param string|array $embed Raw "_embed" parameter value. 2862 * @return true|string[] Either true to embed all embeds, or a list of relations to embed. 2863 */ 2864 function rest_parse_embed_param( $embed ) { 2865 if ( ! $embed || 'true' === $embed || '1' === $embed ) { 2866 return true; 2867 } 2868 2869 $rels = wp_parse_list( $embed ); 2870 2871 if ( ! $rels ) { 2872 return true; 2873 } 2874 2875 return $rels; 2876 } 2877 2878 /** 2879 * Filters the response to remove any fields not available in the given context. 2880 * 2881 * @since 5.5.0 2882 * @since 5.6.0 Support the "patternProperties" keyword for objects. 2883 * Support the "anyOf" and "oneOf" keywords. 2884 * 2885 * @param array|object $data The response data to modify. 2886 * @param array $schema The schema for the endpoint used to filter the response. 2887 * @param string $context The requested context. 2888 * @return array|object The filtered response data. 2889 */ 2890 function rest_filter_response_by_context( $data, $schema, $context ) { 2891 if ( isset( $schema['anyOf'] ) ) { 2892 $matching_schema = rest_find_any_matching_schema( $data, $schema, '' ); 2893 if ( ! is_wp_error( $matching_schema ) ) { 2894 if ( ! isset( $schema['type'] ) ) { 2895 $schema['type'] = $matching_schema['type']; 2896 } 2897 2898 $data = rest_filter_response_by_context( $data, $matching_schema, $context ); 2899 } 2900 } 2901 2902 if ( isset( $schema['oneOf'] ) ) { 2903 $matching_schema = rest_find_one_matching_schema( $data, $schema, '', true ); 2904 if ( ! is_wp_error( $matching_schema ) ) { 2905 if ( ! isset( $schema['type'] ) ) { 2906 $schema['type'] = $matching_schema['type']; 2907 } 2908 2909 $data = rest_filter_response_by_context( $data, $matching_schema, $context ); 2910 } 2911 } 2912 2913 if ( ! is_array( $data ) && ! is_object( $data ) ) { 2914 return $data; 2915 } 2916 2917 if ( isset( $schema['type'] ) ) { 2918 $type = $schema['type']; 2919 } elseif ( isset( $schema['properties'] ) ) { 2920 $type = 'object'; // Back compat if a developer accidentally omitted the type. 2921 } else { 2922 return $data; 2923 } 2924 2925 $is_array_type = 'array' === $type || ( is_array( $type ) && in_array( 'array', $type, true ) ); 2926 $is_object_type = 'object' === $type || ( is_array( $type ) && in_array( 'object', $type, true ) ); 2927 2928 if ( $is_array_type && $is_object_type ) { 2929 if ( rest_is_array( $data ) ) { 2930 $is_object_type = false; 2931 } else { 2932 $is_array_type = false; 2933 } 2934 } 2935 2936 $has_additional_properties = $is_object_type && isset( $schema['additionalProperties'] ) && is_array( $schema['additionalProperties'] ); 2937 2938 foreach ( $data as $key => $value ) { 2939 $check = array(); 2940 2941 if ( $is_array_type ) { 2942 $check = isset( $schema['items'] ) ? $schema['items'] : array(); 2943 } elseif ( $is_object_type ) { 2944 if ( isset( $schema['properties'][ $key ] ) ) { 2945 $check = $schema['properties'][ $key ]; 2946 } else { 2947 $pattern_property_schema = rest_find_matching_pattern_property_schema( $key, $schema ); 2948 if ( null !== $pattern_property_schema ) { 2949 $check = $pattern_property_schema; 2950 } elseif ( $has_additional_properties ) { 2951 $check = $schema['additionalProperties']; 2952 } 2953 } 2954 } 2955 2956 if ( ! isset( $check['context'] ) ) { 2957 continue; 2958 } 2959 2960 if ( ! in_array( $context, $check['context'], true ) ) { 2961 if ( $is_array_type ) { 2962 // All array items share schema, so there's no need to check each one. 2963 $data = array(); 2964 break; 2965 } 2966 2967 if ( is_object( $data ) ) { 2968 unset( $data->$key ); 2969 } else { 2970 unset( $data[ $key ] ); 2971 } 2972 } elseif ( is_array( $value ) || is_object( $value ) ) { 2973 $new_value = rest_filter_response_by_context( $value, $check, $context ); 2974 2975 if ( is_object( $data ) ) { 2976 $data->$key = $new_value; 2977 } else { 2978 $data[ $key ] = $new_value; 2979 } 2980 } 2981 } 2982 2983 return $data; 2984 } 2985 2986 /** 2987 * Sets the "additionalProperties" to false by default for all object definitions in the schema. 2988 * 2989 * @since 5.5.0 2990 * @since 5.6.0 Support the "patternProperties" keyword. 2991 * 2992 * @param array $schema The schema to modify. 2993 * @return array The modified schema. 2994 */ 2995 function rest_default_additional_properties_to_false( $schema ) { 2996 $type = (array) $schema['type']; 2997 2998 if ( in_array( 'object', $type, true ) ) { 2999 if ( isset( $schema['properties'] ) ) { 3000 foreach ( $schema['properties'] as $key => $child_schema ) { 3001 $schema['properties'][ $key ] = rest_default_additional_properties_to_false( $child_schema ); 3002 } 3003 } 3004 3005 if ( isset( $schema['patternProperties'] ) ) { 3006 foreach ( $schema['patternProperties'] as $key => $child_schema ) { 3007 $schema['patternProperties'][ $key ] = rest_default_additional_properties_to_false( $child_schema ); 3008 } 3009 } 3010 3011 if ( ! isset( $schema['additionalProperties'] ) ) { 3012 $schema['additionalProperties'] = false; 3013 } 3014 } 3015 3016 if ( in_array( 'array', $type, true ) ) { 3017 if ( isset( $schema['items'] ) ) { 3018 $schema['items'] = rest_default_additional_properties_to_false( $schema['items'] ); 3019 } 3020 } 3021 3022 return $schema; 3023 } 3024 3025 /** 3026 * Gets the REST API route for a post. 3027 * 3028 * @since 5.5.0 3029 * 3030 * @param int|WP_Post $post Post ID or post object. 3031 * @return string The route path with a leading slash for the given post, or an empty string if there is not a route. 3032 */ 3033 function rest_get_route_for_post( $post ) { 3034 $post = get_post( $post ); 3035 3036 if ( ! $post instanceof WP_Post ) { 3037 return ''; 3038 } 3039 3040 $post_type = get_post_type_object( $post->post_type ); 3041 if ( ! $post_type ) { 3042 return ''; 3043 } 3044 3045 $controller = $post_type->get_rest_controller(); 3046 if ( ! $controller ) { 3047 return ''; 3048 } 3049 3050 $route = ''; 3051 3052 // The only two controllers that we can detect are the Attachments and Posts controllers. 3053 if ( in_array( get_class( $controller ), array( 'WP_REST_Attachments_Controller', 'WP_REST_Posts_Controller' ), true ) ) { 3054 $namespace = 'wp/v2'; 3055 $rest_base = ! empty( $post_type->rest_base ) ? $post_type->rest_base : $post_type->name; 3056 $route = sprintf( '/%s/%s/%d', $namespace, $rest_base, $post->ID ); 3057 } 3058 3059 /** 3060 * Filters the REST API route for a post. 3061 * 3062 * @since 5.5.0 3063 * 3064 * @param string $route The route path. 3065 * @param WP_Post $post The post object. 3066 */ 3067 return apply_filters( 'rest_route_for_post', $route, $post ); 3068 } 3069 3070 /** 3071 * Gets the REST API route for a term. 3072 * 3073 * @since 5.5.0 3074 * 3075 * @param int|WP_Term $term Term ID or term object. 3076 * @return string The route path with a leading slash for the given term, or an empty string if there is not a route. 3077 */ 3078 function rest_get_route_for_term( $term ) { 3079 $term = get_term( $term ); 3080 3081 if ( ! $term instanceof WP_Term ) { 3082 return ''; 3083 } 3084 3085 $taxonomy = get_taxonomy( $term->taxonomy ); 3086 if ( ! $taxonomy ) { 3087 return ''; 3088 } 3089 3090 $controller = $taxonomy->get_rest_controller(); 3091 if ( ! $controller ) { 3092 return ''; 3093 } 3094 3095 $route = ''; 3096 3097 // The only controller that works is the Terms controller. 3098 if ( $controller instanceof WP_REST_Terms_Controller ) { 3099 $namespace = 'wp/v2'; 3100 $rest_base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; 3101 $route = sprintf( '/%s/%s/%d', $namespace, $rest_base, $term->term_id ); 3102 } 3103 3104 /** 3105 * Filters the REST API route for a term. 3106 * 3107 * @since 5.5.0 3108 * 3109 * @param string $route The route path. 3110 * @param WP_Term $term The term object. 3111 */ 3112 return apply_filters( 'rest_route_for_term', $route, $term ); 3113 } 3114 3115 /** 3116 * Gets the REST route for the currently queried object. 3117 * 3118 * @since 5.5.0 3119 * 3120 * @return string The REST route of the resource, or an empty string if no resource identified. 3121 */ 3122 function rest_get_queried_resource_route() { 3123 if ( is_singular() ) { 3124 $route = rest_get_route_for_post( get_queried_object() ); 3125 } elseif ( is_category() || is_tag() || is_tax() ) { 3126 $route = rest_get_route_for_term( get_queried_object() ); 3127 } elseif ( is_author() ) { 3128 $route = '/wp/v2/users/' . get_queried_object_id(); 3129 } else { 3130 $route = ''; 3131 } 3132 3133 /** 3134 * Filters the REST route for the currently queried object. 3135 * 3136 * @since 5.5.0 3137 * 3138 * @param string $link The route with a leading slash, or an empty string. 3139 */ 3140 return apply_filters( 'rest_queried_resource_route', $route ); 3141 } 3142 3143 /** 3144 * Retrieves an array of endpoint arguments from the item schema and endpoint method. 3145 * 3146 * @since 5.6.0 3147 * 3148 * @param array $schema The full JSON schema for the endpoint. 3149 * @param string $method Optional. HTTP method of the endpoint. The arguments for `CREATABLE` endpoints are 3150 * checked for required values and may fall-back to a given default, this is not done 3151 * on `EDITABLE` endpoints. Default WP_REST_Server::CREATABLE. 3152 * @return array The endpoint arguments. 3153 */ 3154 function rest_get_endpoint_args_for_schema( $schema, $method = WP_REST_Server::CREATABLE ) { 3155 3156 $schema_properties = ! empty( $schema['properties'] ) ? $schema['properties'] : array(); 3157 $endpoint_args = array(); 3158 $valid_schema_properties = rest_get_allowed_schema_keywords(); 3159 $valid_schema_properties = array_diff( $valid_schema_properties, array( 'default', 'required' ) ); 3160 3161 foreach ( $schema_properties as $field_id => $params ) { 3162 3163 // Arguments specified as `readonly` are not allowed to be set. 3164 if ( ! empty( $params['readonly'] ) ) { 3165 continue; 3166 } 3167 3168 $endpoint_args[ $field_id ] = array( 3169 'validate_callback' => 'rest_validate_request_arg', 3170 'sanitize_callback' => 'rest_sanitize_request_arg', 3171 ); 3172 3173 if ( WP_REST_Server::CREATABLE === $method && isset( $params['default'] ) ) { 3174 $endpoint_args[ $field_id ]['default'] = $params['default']; 3175 } 3176 3177 if ( WP_REST_Server::CREATABLE === $method && ! empty( $params['required'] ) ) { 3178 $endpoint_args[ $field_id ]['required'] = true; 3179 } 3180 3181 foreach ( $valid_schema_properties as $schema_prop ) { 3182 if ( isset( $params[ $schema_prop ] ) ) { 3183 $endpoint_args[ $field_id ][ $schema_prop ] = $params[ $schema_prop ]; 3184 } 3185 } 3186 3187 // Merge in any options provided by the schema property. 3188 if ( isset( $params['arg_options'] ) ) { 3189 3190 // Only use required / default from arg_options on CREATABLE endpoints. 3191 if ( WP_REST_Server::CREATABLE !== $method ) { 3192 $params['arg_options'] = array_diff_key( 3193 $params['arg_options'], 3194 array( 3195 'required' => '', 3196 'default' => '', 3197 ) 3198 ); 3199 } 3200 3201 $endpoint_args[ $field_id ] = array_merge( $endpoint_args[ $field_id ], $params['arg_options'] ); 3202 } 3203 } 3204 3205 return $endpoint_args; 3206 } 3207 3208 3209 /** 3210 * Converts an error to a response object. 3211 * 3212 * This iterates over all error codes and messages to change it into a flat 3213 * array. This enables simpler client behaviour, as it is represented as a 3214 * list in JSON rather than an object/map. 3215 * 3216 * @since 5.7.0 3217 * 3218 * @param WP_Error $error WP_Error instance. 3219 * 3220 * @return WP_REST_Response List of associative arrays with code and message keys. 3221 */ 3222 function rest_convert_error_to_response( $error ) { 3223 $status = array_reduce( 3224 $error->get_all_error_data(), 3225 function ( $status, $error_data ) { 3226 return is_array( $error_data ) && isset( $error_data['status'] ) ? $error_data['status'] : $status; 3227 }, 3228 500 3229 ); 3230 3231 $errors = array(); 3232 3233 foreach ( (array) $error->errors as $code => $messages ) { 3234 $all_data = $error->get_all_error_data( $code ); 3235 $last_data = array_pop( $all_data ); 3236 3237 foreach ( (array) $messages as $message ) { 3238 $formatted = array( 3239 'code' => $code, 3240 'message' => $message, 3241 'data' => $last_data, 3242 ); 3243 3244 if ( $all_data ) { 3245 $formatted['additional_data'] = $all_data; 3246 } 3247 3248 $errors[] = $formatted; 3249 } 3250 } 3251 3252 $data = $errors[0]; 3253 if ( count( $errors ) > 1 ) { 3254 // Remove the primary error. 3255 array_shift( $errors ); 3256 $data['additional_errors'] = $errors; 3257 } 3258 3259 return new WP_REST_Response( $data, $status ); 3260 }