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