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