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