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