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