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