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