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