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