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