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