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