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