| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * REST API: WP_REST_Server class 4 * 5 * @package WordPress 6 * @subpackage REST_API 7 * @since 4.4.0 8 */ 9 10 /** 11 * Core class used to implement the WordPress REST API server. 12 * 13 * The aliases below describe a route handler as {@see WP_REST_Server::get_routes()} 14 * returns it, once the defaults have been filled in and the methods normalized. 15 * 16 * An endpoint argument is a JSON Schema fragment, so the keys named here are only the 17 * ones WordPress reads itself. The rest are schema keywords, left to the open end of 18 * the shape and validated by {@see rest_validate_value_from_schema()}. See 19 * {@see rest_get_allowed_schema_keywords()} for the vocabulary the REST API exposes. 20 * 21 * @since 4.4.0 22 * 23 * @phpstan-import-type Target_Hints from WP_REST_Response 24 * 25 * @phpstan-type Endpoint_Arg array{ 26 * required?: bool, 27 * default?: mixed, 28 * type?: string|list<string>, 29 * validate_callback?: callable|false|null, 30 * sanitize_callback?: callable|false|null, 31 * ... 32 * } 33 * @phpstan-type Route_Handler array{ 34 * methods: array<uppercase-string, true>, 35 * callback?: callable|null, 36 * permission_callback?: callable|null, 37 * validate_callback?: callable|null, 38 * args: array<non-empty-string, Endpoint_Arg>, 39 * accept_json: bool, 40 * accept_raw: bool, 41 * show_in_index: bool, 42 * allow_batch?: array{v1?: bool}|false, 43 * data_processing?: bool|array{fields?: bool, embed?: bool, links?: bool}, 44 * ... 45 * } 46 * @phpstan-type Data_Processing array{fields: bool, embed: bool, links: bool} 47 */ 48 #[AllowDynamicProperties] 49 class WP_REST_Server { 50 51 /** 52 * Alias for GET transport method. 53 * 54 * @since 4.4.0 55 * @var string 56 */ 57 const READABLE = 'GET'; 58 59 /** 60 * Alias for POST transport method. 61 * 62 * @since 4.4.0 63 * @var string 64 */ 65 const CREATABLE = 'POST'; 66 67 /** 68 * Alias for POST, PUT, PATCH transport methods together. 69 * 70 * @since 4.4.0 71 * @var string 72 */ 73 const EDITABLE = 'POST, PUT, PATCH'; 74 75 /** 76 * Alias for DELETE transport method. 77 * 78 * @since 4.4.0 79 * @var string 80 */ 81 const DELETABLE = 'DELETE'; 82 83 /** 84 * Alias for GET, POST, PUT, PATCH & DELETE transport methods together. 85 * 86 * @since 4.4.0 87 * @var string 88 */ 89 const ALLMETHODS = 'GET, POST, PUT, PATCH, DELETE'; 90 91 /** 92 * Namespaces registered to the server. 93 * 94 * Keyed by namespace, each value being the set of path regexes registered in it, 95 * held as keys mapped to true so that a route cannot be registered twice. 96 * 97 * @since 4.4.0 98 * @var array 99 * 100 * @phpstan-var array<string, array<non-empty-string, true>> 101 */ 102 protected $namespaces = array(); 103 104 /** 105 * Endpoints registered to the server. 106 * 107 * Keyed by path regex. Each value is a route exactly as it was registered, before 108 * {@see WP_REST_Server::get_routes()} normalizes it: either a single endpoint's 109 * arguments, or an array of them under numeric keys alongside the route's options 110 * under their own non-numeric keys. 111 * 112 * @since 4.4.0 113 * @var array 114 * 115 * @phpstan-var array<non-empty-string, array<array-key, mixed>> 116 */ 117 protected $endpoints = array(); 118 119 /** 120 * Options defined for the routes. 121 * 122 * @since 4.4.0 123 * @var array 124 * 125 * @phpstan-var array<non-empty-string, array<string, mixed>> 126 */ 127 protected $route_options = array(); 128 129 /** 130 * Caches embedded requests. 131 * 132 * @since 5.4.0 133 * @var array 134 */ 135 protected $embed_cache = array(); 136 137 /** 138 * Stores request objects that are currently being handled. 139 * 140 * @since 6.5.0 141 * @var array 142 */ 143 protected $dispatching_requests = array(); 144 145 /** 146 * Instantiates the REST server. 147 * 148 * @since 4.4.0 149 */ 150 public function __construct() { 151 $this->endpoints = array( 152 // Meta endpoints. 153 '/' => array( 154 'callback' => array( $this, 'get_index' ), 155 'methods' => 'GET', 156 'args' => array( 157 'context' => array( 158 'default' => 'view', 159 ), 160 ), 161 ), 162 '/batch/v1' => array( 163 'callback' => array( $this, 'serve_batch_request_v1' ), 164 'methods' => 'POST', 165 'args' => array( 166 'validation' => array( 167 'type' => 'string', 168 'enum' => array( 'require-all-validate', 'normal' ), 169 'default' => 'normal', 170 ), 171 'requests' => array( 172 'required' => true, 173 'type' => 'array', 174 'maxItems' => $this->get_max_batch_size(), 175 'items' => array( 176 'type' => 'object', 177 'properties' => array( 178 'method' => array( 179 'type' => 'string', 180 'enum' => array( 'POST', 'PUT', 'PATCH', 'DELETE' ), 181 'default' => 'POST', 182 ), 183 'path' => array( 184 'type' => 'string', 185 'required' => true, 186 ), 187 'body' => array( 188 'type' => 'object', 189 'properties' => array(), 190 'additionalProperties' => true, 191 ), 192 'headers' => array( 193 'type' => 'object', 194 'properties' => array(), 195 'additionalProperties' => array( 196 'type' => array( 'string', 'array' ), 197 'items' => array( 198 'type' => 'string', 199 ), 200 ), 201 ), 202 ), 203 ), 204 ), 205 ), 206 ), 207 ); 208 } 209 210 211 /** 212 * Checks the authentication headers if supplied. 213 * 214 * @since 4.4.0 215 * 216 * @return WP_Error|null|true WP_Error if authentication error occurred, null if authentication 217 * method wasn't used, true if authentication succeeded. 218 */ 219 public function check_authentication() { 220 /** 221 * Filters REST API authentication errors. 222 * 223 * This is used to pass a WP_Error from an authentication method back to 224 * the API. 225 * 226 * Authentication methods should check first if they're being used, as 227 * multiple authentication methods can be enabled on a site (cookies, 228 * HTTP basic auth, OAuth). If the authentication method hooked in is 229 * not actually being attempted, null should be returned to indicate 230 * another authentication method should check instead. Similarly, 231 * callbacks should ensure the value is `null` before checking for 232 * errors. 233 * 234 * A WP_Error instance can be returned if an error occurs, and this should 235 * match the format used by API methods internally (that is, the `status` 236 * data should be used). A callback can return `true` to indicate that 237 * the authentication method was used, and it succeeded. 238 * 239 * @since 4.4.0 240 * 241 * @param WP_Error|null|true $errors WP_Error if authentication error occurred, null if authentication 242 * method wasn't used, true if authentication succeeded. 243 */ 244 return apply_filters( 'rest_authentication_errors', null ); 245 } 246 247 /** 248 * Converts an error to a response object. 249 * 250 * This iterates over all error codes and messages to change it into a flat 251 * array. This enables simpler client behavior, as it is represented as a 252 * list in JSON rather than an object/map. 253 * 254 * @since 4.4.0 255 * @since 5.7.0 Converted to a wrapper of {@see rest_convert_error_to_response()}. 256 * 257 * @param WP_Error $error WP_Error instance. 258 * @return WP_REST_Response List of associative arrays with code and message keys. 259 */ 260 protected function error_to_response( $error ) { 261 return rest_convert_error_to_response( $error ); 262 } 263 264 /** 265 * Retrieves an appropriate error representation in JSON. 266 * 267 * Note: This should only be used in WP_REST_Server::serve_request(), as it 268 * cannot handle WP_Error internally. All callbacks and other internal methods 269 * should instead return a WP_Error with the data set to an array that includes 270 * a 'status' key, with the value being the HTTP status to send. 271 * 272 * @since 4.4.0 273 * 274 * @param string $code WP_Error-style code. 275 * @param string $message Human-readable message. 276 * @param int|null $status Optional. HTTP status code to send. Default null. 277 * @return string JSON representation of the error. 278 * 279 * @phpstan-param int<100, 599>|null $status 280 */ 281 protected function json_error( $code, $message, $status = null ) { 282 if ( $status ) { 283 $this->set_status( $status ); 284 } 285 286 $error = compact( 'code', 'message' ); 287 288 return wp_json_encode( $error ); 289 } 290 291 /** 292 * Gets the encoding options passed to {@see wp_json_encode}. 293 * 294 * @since 6.1.0 295 * 296 * @param \WP_REST_Request $request The current request object. 297 * @return int The JSON encode options. 298 */ 299 protected function get_json_encode_options( WP_REST_Request $request ) { 300 $options = 0; 301 302 if ( $request->has_param( '_pretty' ) ) { 303 $options |= JSON_PRETTY_PRINT; 304 } 305 306 /** 307 * Filters the JSON encoding options used to send the REST API response. 308 * 309 * @since 6.1.0 310 * 311 * @param int $options JSON encoding options {@see json_encode()}. 312 * @param WP_REST_Request $request Current request object. 313 */ 314 return apply_filters( 'rest_json_encode_options', $options, $request ); 315 } 316 317 /** 318 * Handles serving a REST API request. 319 * 320 * Matches the current server URI to a route and runs the first matching 321 * callback then outputs a JSON representation of the returned value. 322 * 323 * @since 4.4.0 324 * 325 * @see WP_REST_Server::dispatch() 326 * 327 * @global WP_User $current_user The currently authenticated user. 328 * 329 * @param string|null $path Optional. The request route. If not set, `$_SERVER['PATH_INFO']` will be used. 330 * Default null. 331 * @return null|false Null if not served and a HEAD request, false otherwise. 332 */ 333 public function serve_request( $path = null ) { 334 // Refuse to start a fresh top-level REST cycle while another dispatch 335 // is already in flight. Internal sub-requests must use dispatch(). 336 if ( $this->is_dispatching() ) { 337 return false; 338 } 339 340 /* @var WP_User|null $current_user */ 341 global $current_user; 342 343 if ( $current_user instanceof WP_User && ! $current_user->exists() ) { 344 /* 345 * If there is no current user authenticated via other means, clear 346 * the cached lack of user, so that an authenticate check can set it 347 * properly. 348 * 349 * This is done because for authentications such as Application 350 * Passwords, we don't want it to be accepted unless the current HTTP 351 * request is a REST API request, which can't always be identified early 352 * enough in evaluation. 353 */ 354 $current_user = null; 355 } 356 357 /** 358 * Filters whether JSONP is enabled for the REST API. 359 * 360 * @since 4.4.0 361 * 362 * @param bool $jsonp_enabled Whether JSONP is enabled. Default true. 363 */ 364 $jsonp_enabled = apply_filters( 'rest_jsonp_enabled', true ); 365 366 $jsonp_callback = false; 367 if ( isset( $_GET['_jsonp'] ) ) { 368 $jsonp_callback = $_GET['_jsonp']; 369 } 370 371 $content_type = ( $jsonp_callback && $jsonp_enabled ) ? 'application/javascript' : 'application/json'; 372 $this->send_header( 'Content-Type', $content_type . '; charset=' . get_option( 'blog_charset' ) ); 373 $this->send_header( 'X-Robots-Tag', 'noindex' ); 374 375 $api_root = get_rest_url(); 376 if ( ! empty( $api_root ) ) { 377 $this->send_header( 'Link', '<' . sanitize_url( $api_root ) . '>; rel="https://api.w.org/"' ); 378 } 379 380 /* 381 * Mitigate possible JSONP Flash attacks. 382 * 383 * https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/ 384 */ 385 $this->send_header( 'X-Content-Type-Options', 'nosniff' ); 386 387 /** 388 * Filters whether the REST API is enabled. 389 * 390 * @since 4.4.0 391 * @deprecated 4.7.0 Use the {@see 'rest_authentication_errors'} filter to 392 * restrict access to the REST API. 393 * 394 * @param bool $rest_enabled Whether the REST API is enabled. Default true. 395 */ 396 apply_filters_deprecated( 397 'rest_enabled', 398 array( true ), 399 '4.7.0', 400 'rest_authentication_errors', 401 sprintf( 402 /* translators: %s: rest_authentication_errors */ 403 __( 'The REST API can no longer be completely disabled, the %s filter can be used to restrict access to the API, instead.' ), 404 'rest_authentication_errors' 405 ) 406 ); 407 408 if ( $jsonp_callback ) { 409 if ( ! $jsonp_enabled ) { 410 echo $this->json_error( 'rest_callback_disabled', __( 'JSONP support is disabled on this site.' ), 400 ); 411 return false; 412 } 413 414 if ( ! wp_check_jsonp_callback( $jsonp_callback ) ) { 415 echo $this->json_error( 'rest_callback_invalid', __( 'Invalid JSONP callback function.' ), 400 ); 416 return false; 417 } 418 } 419 420 if ( empty( $path ) ) { 421 $path = $_SERVER['PATH_INFO'] ?? '/'; 422 } 423 424 $request = new WP_REST_Request( $_SERVER['REQUEST_METHOD'], $path ); 425 426 $request->set_query_params( wp_unslash( $_GET ) ); 427 $request->set_body_params( wp_unslash( $_POST ) ); 428 $request->set_file_params( $_FILES ); 429 $request->set_headers( $this->get_headers( wp_unslash( $_SERVER ) ) ); 430 $request->set_body( self::get_raw_data() ); 431 432 /* 433 * HTTP method override for clients that can't use PUT/PATCH/DELETE. First, we check 434 * $_GET['_method']. If that is not set, we check for the HTTP_X_HTTP_METHOD_OVERRIDE 435 * header. 436 */ 437 $method_overridden = false; 438 if ( isset( $_GET['_method'] ) ) { 439 $request->set_method( $_GET['_method'] ); 440 } elseif ( isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ) { 441 $request->set_method( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ); 442 $method_overridden = true; 443 } 444 445 $expose_headers = array( 'X-WP-Total', 'X-WP-TotalPages', 'Link' ); 446 447 /** 448 * Filters the list of response headers that are exposed to REST API CORS requests. 449 * 450 * @since 5.5.0 451 * @since 6.3.0 The `$request` parameter was added. 452 * 453 * @param string[] $expose_headers The list of response headers to expose. 454 * @param WP_REST_Request $request The request in context. 455 */ 456 $expose_headers = apply_filters( 'rest_exposed_cors_headers', $expose_headers, $request ); 457 458 $this->send_header( 'Access-Control-Expose-Headers', implode( ', ', $expose_headers ) ); 459 460 $allow_headers = array( 461 'Authorization', 462 'X-WP-Nonce', 463 'Content-Disposition', 464 'Content-MD5', 465 'Content-Type', 466 ); 467 468 /** 469 * Filters the list of request headers that are allowed for REST API CORS requests. 470 * 471 * The allowed headers are passed to the browser to specify which 472 * headers can be passed to the REST API. By default, we allow the 473 * Content-* headers needed to upload files to the media endpoints. 474 * As well as the Authorization and Nonce headers for allowing authentication. 475 * 476 * @since 5.5.0 477 * @since 6.3.0 The `$request` parameter was added. 478 * 479 * @param string[] $allow_headers The list of request headers to allow. 480 * @param WP_REST_Request $request The request in context. 481 */ 482 $allow_headers = apply_filters( 'rest_allowed_cors_headers', $allow_headers, $request ); 483 484 $this->send_header( 'Access-Control-Allow-Headers', implode( ', ', $allow_headers ) ); 485 486 $result = $this->check_authentication(); 487 488 if ( ! is_wp_error( $result ) ) { 489 $result = $this->dispatch( $request ); 490 } 491 492 // Normalize to either WP_Error or WP_REST_Response... 493 $result = rest_ensure_response( $result ); 494 495 // ...then convert WP_Error across. 496 if ( is_wp_error( $result ) ) { 497 $result = $this->error_to_response( $result ); 498 } 499 500 /** 501 * Filters the REST API response. 502 * 503 * Allows modification of the response before returning. 504 * 505 * @since 4.4.0 506 * @since 4.5.0 Applied to embedded responses. 507 * 508 * @param WP_HTTP_Response $result Result to send to the client. Usually a `WP_REST_Response`. 509 * @param WP_REST_Server $server Server instance. 510 * @param WP_REST_Request $request Request used to generate the response. 511 */ 512 $result = apply_filters( 'rest_post_dispatch', rest_ensure_response( $result ), $this, $request ); 513 514 // Wrap the response in an envelope if asked for. 515 if ( isset( $_GET['_envelope'] ) ) { 516 $embed = isset( $_GET['_embed'] ) ? rest_parse_embed_param( $_GET['_embed'] ) : false; 517 $result = $this->envelope_response( $result, $embed ); 518 } 519 520 // Send extra data from response objects. 521 $headers = $result->get_headers(); 522 $this->send_headers( $headers ); 523 524 $code = $result->get_status(); 525 $this->set_status( $code ); 526 527 /** 528 * Filters whether to send no-cache headers on a REST API request. 529 * 530 * @since 4.4.0 531 * @since 6.3.2 Moved the block to catch the filter added on rest_cookie_check_errors() from wp-includes/rest-api.php. 532 * 533 * @param bool $rest_send_nocache_headers Whether to send no-cache headers. 534 */ 535 $send_no_cache_headers = apply_filters( 'rest_send_nocache_headers', is_user_logged_in() ); 536 537 /* 538 * Send no-cache headers if $send_no_cache_headers is true, 539 * OR if the HTTP_X_HTTP_METHOD_OVERRIDE is used but resulted a 4xx response code. 540 */ 541 if ( $send_no_cache_headers || ( true === $method_overridden && $code >= 400 && $code < 500 ) ) { 542 foreach ( wp_get_nocache_headers() as $header => $header_value ) { 543 if ( empty( $header_value ) ) { 544 $this->remove_header( $header ); 545 } else { 546 $this->send_header( $header, $header_value ); 547 } 548 } 549 } 550 551 /** 552 * Filters whether the REST API request has already been served. 553 * 554 * Allow sending the request manually - by returning true, the API result 555 * will not be sent to the client. 556 * 557 * @since 4.4.0 558 * 559 * @param bool $served Whether the request has already been served. Default false. 560 * @param WP_HTTP_Response $result Result to send to the client. Usually a `WP_REST_Response`. 561 * @param WP_REST_Request $request Request used to generate the response. 562 * @param WP_REST_Server $server Server instance. 563 */ 564 $served = apply_filters( 'rest_pre_serve_request', false, $result, $request, $this ); 565 566 if ( ! $served ) { 567 if ( 'HEAD' === $request->get_method() ) { 568 return null; 569 } 570 571 // Embed links inside the request. 572 $embed = isset( $_GET['_embed'] ) ? rest_parse_embed_param( $_GET['_embed'] ) : false; 573 $result = $this->response_to_data( $result, $embed ); 574 575 /** 576 * Filters the REST API response. 577 * 578 * Allows modification of the response data after inserting 579 * embedded data (if any) and before echoing the response data. 580 * 581 * @since 4.8.1 582 * 583 * @param array $result Response data to send to the client. 584 * @param WP_REST_Server $server Server instance. 585 * @param WP_REST_Request $request Request used to generate the response. 586 */ 587 $result = apply_filters( 'rest_pre_echo_response', $result, $this, $request ); 588 589 // The 204 response shouldn't have a body. 590 if ( 204 === $code || null === $result ) { 591 return null; 592 } 593 594 $result = wp_json_encode( $result, $this->get_json_encode_options( $request ) ); 595 596 $json_error_message = $this->get_json_last_error(); 597 598 if ( $json_error_message ) { 599 $this->set_status( 500 ); 600 $json_error_obj = new WP_Error( 601 'rest_encode_error', 602 $json_error_message, 603 array( 'status' => 500 ) 604 ); 605 606 $result = $this->error_to_response( $json_error_obj ); 607 $result = wp_json_encode( $result->data, $this->get_json_encode_options( $request ) ); 608 } 609 610 if ( $jsonp_callback ) { 611 // Prepend '/**/' to mitigate possible JSONP Flash attacks. 612 // https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/ 613 echo '/**/' . $jsonp_callback . '(' . $result . ')'; 614 } else { 615 echo $result; 616 } 617 } 618 619 return null; 620 } 621 622 /** 623 * Converts a response to data to send. 624 * 625 * @since 4.4.0 626 * @since 5.4.0 The `$embed` parameter can now contain a list of link relations to include. 627 * @since 7.2.0 The links are not added or embedded when the response turns off the `links` or 628 * `embed` data processing, scalar and object data is returned as it is unless 629 * the object implements `ArrayAccess`, and list items that are not arrays are left 630 * as they are when embedding. 631 * 632 * @see WP_REST_Response::get_data_processing() 633 * 634 * @param WP_REST_Response $response Response object. 635 * @param bool|string[] $embed Whether to embed all links, a filtered list of link relations, or no links. 636 * @return mixed Data with sub-requests embedded: an array with the links under `_links` and the embedded 637 * resources under `_embedded`, or scalar and object data as it is unless the object 638 * implements `ArrayAccess`. 639 */ 640 public function response_to_data( $response, $embed ) { 641 $data = $response->get_data(); 642 643 // Scalar and object data has no place for links or embedded resources, unless the object acts as an array. 644 if ( null !== $data && ! is_array( $data ) && ! $data instanceof ArrayAccess ) { 645 return $data; 646 } 647 648 $processing = $response->get_data_processing(); 649 $links = $processing['links'] ? self::get_compact_response_links( $response ) : array(); 650 651 if ( ! empty( $links ) ) { 652 // Convert links to part of the data. 653 $data['_links'] = $links; 654 } 655 656 if ( $embed && $processing['embed'] ) { 657 $this->embed_cache = array(); 658 // Determine if this is a numeric array. 659 if ( wp_is_numeric_array( $data ) ) { 660 foreach ( $data as $key => $item ) { 661 $data[ $key ] = is_array( $item ) || $item instanceof ArrayAccess ? $this->embed_links( $item, $embed ) : $item; 662 } 663 } else { 664 $data = $this->embed_links( $data, $embed ); 665 } 666 $this->embed_cache = array(); 667 } 668 669 return $data; 670 } 671 672 /** 673 * Retrieves links from a response. 674 * 675 * Extracts the links from a response into a structured hash, suitable for 676 * direct output. 677 * 678 * @since 4.4.0 679 * 680 * @param WP_REST_Response $response Response to extract links from. 681 * @return array Map of link relation to list of link hashes. 682 */ 683 public static function get_response_links( $response ) { 684 $links = $response->get_links(); 685 686 if ( empty( $links ) ) { 687 return array(); 688 } 689 690 // Convert links to part of the data. 691 $data = array(); 692 foreach ( $links as $rel => $items ) { 693 $data[ $rel ] = array(); 694 695 foreach ( $items as $item ) { 696 $attributes = $item['attributes']; 697 $attributes['href'] = $item['href']; 698 699 if ( 'self' !== $rel ) { 700 $data[ $rel ][] = $attributes; 701 continue; 702 } 703 704 $target_hints = self::get_target_hints_for_link( $attributes ); 705 if ( $target_hints ) { 706 $attributes['targetHints'] = $target_hints; 707 } 708 709 $data[ $rel ][] = $attributes; 710 } 711 } 712 713 return $data; 714 } 715 716 /** 717 * Gets the target hints for a REST API Link. 718 * 719 * @since 6.7.0 720 * 721 * @param array<string, mixed> $link The link to get target hints for. 722 * @return array<string, string[]>|null Map of hint name to list of values, which is empty if the route 723 * allows no methods. Null if the link already has an `allow` hint, 724 * or its URL does not resolve to a valid REST API request. 725 * 726 * @phpstan-param array{ 727 * href: non-falsy-string, 728 * targetHints?: Target_Hints, 729 * ...<string, mixed> 730 * } $link 731 * @phpstan-return array<lowercase-string, list<string>>|null 732 */ 733 protected static function get_target_hints_for_link( $link ) { 734 // Prefer targetHints that were specifically designated by the developer. 735 if ( isset( $link['targetHints']['allow'] ) ) { 736 return null; 737 } 738 739 $request = WP_REST_Request::from_url( $link['href'] ); 740 if ( ! $request ) { 741 return null; 742 } 743 744 $server = rest_get_server(); 745 $match = $server->match_request_to_handler( $request ); 746 747 if ( is_wp_error( $match ) ) { 748 return null; 749 } 750 751 if ( is_wp_error( $request->has_valid_params() ) ) { 752 return null; 753 } 754 755 if ( is_wp_error( $request->sanitize_params() ) ) { 756 return null; 757 } 758 759 $target_hints = array(); 760 761 $response = new WP_REST_Response(); 762 $response->set_matched_route( $match[0] ); 763 $response->set_matched_handler( $match[1] ); 764 $headers = rest_send_allow_header( $response, $server, $request )->get_headers(); 765 766 foreach ( $headers as $name => $value ) { 767 $name = $request::canonicalize_header_name( $name ); 768 769 $target_hints[ $name ] = array_map( 'trim', explode( ',', $value ) ); 770 } 771 772 return $target_hints; 773 } 774 775 /** 776 * Retrieves the CURIEs (compact URIs) used for relations. 777 * 778 * Extracts the links from a response into a structured hash, suitable for 779 * direct output. 780 * 781 * @since 4.5.0 782 * 783 * @param WP_REST_Response $response Response to extract links from. 784 * @return array Map of link relation to list of link hashes. 785 */ 786 public static function get_compact_response_links( $response ) { 787 $links = self::get_response_links( $response ); 788 789 if ( empty( $links ) ) { 790 return array(); 791 } 792 793 $curies = $response->get_curies(); 794 $used_curies = array(); 795 796 foreach ( $links as $rel => $items ) { 797 798 // Convert $rel URIs to their compact versions if they exist. 799 foreach ( $curies as $curie ) { 800 $href_prefix = substr( $curie['href'], 0, strpos( $curie['href'], '{rel}' ) ); 801 if ( ! str_starts_with( $rel, $href_prefix ) ) { 802 continue; 803 } 804 805 // Relation now changes from '$uri' to '$curie:$relation'. 806 $rel_regex = str_replace( '\{rel\}', '(.+)', preg_quote( $curie['href'], '!' ) ); 807 preg_match( '!' . $rel_regex . '!', $rel, $matches ); 808 if ( $matches ) { 809 $new_rel = $curie['name'] . ':' . $matches[1]; 810 $used_curies[ $curie['name'] ] = $curie; 811 $links[ $new_rel ] = $items; 812 unset( $links[ $rel ] ); 813 break; 814 } 815 } 816 } 817 818 // Push the curies onto the start of the links array. 819 if ( $used_curies ) { 820 $links['curies'] = array_values( $used_curies ); 821 } 822 823 return $links; 824 } 825 826 /** 827 * Embeds the links from the data into the request. 828 * 829 * @since 4.4.0 830 * @since 5.4.0 The `$embed` parameter can now contain a list of link relations to include. 831 * 832 * @param array $data Data from the request. 833 * @param bool|string[] $embed Whether to embed all links or a filtered list of link relations. 834 * Default true. 835 * @return array { 836 * Data with sub-requests embedded. 837 * 838 * @type array $_links Links. 839 * @type array $_embedded Embedded objects. 840 * } 841 */ 842 protected function embed_links( $data, $embed = true ) { 843 if ( empty( $data['_links'] ) ) { 844 return $data; 845 } 846 847 $embedded = array(); 848 849 foreach ( $data['_links'] as $rel => $links ) { 850 /* 851 * If a list of relations was specified, and the link relation 852 * is not in the list of allowed relations, don't process the link. 853 */ 854 if ( is_array( $embed ) && ! in_array( $rel, $embed, true ) ) { 855 continue; 856 } 857 858 $embeds = array(); 859 860 foreach ( $links as $item ) { 861 // Determine if the link is embeddable. 862 if ( empty( $item['embeddable'] ) ) { 863 // Ensure we keep the same order. 864 $embeds[] = array(); 865 continue; 866 } 867 868 if ( ! array_key_exists( $item['href'], $this->embed_cache ) ) { 869 // Run through our internal routing and serve. 870 $request = WP_REST_Request::from_url( $item['href'] ); 871 if ( ! $request ) { 872 $embeds[] = array(); 873 continue; 874 } 875 876 // Embedded resources get passed context=embed. 877 if ( empty( $request['context'] ) ) { 878 $request['context'] = 'embed'; 879 } 880 881 if ( empty( $request['per_page'] ) ) { 882 $matched = $this->match_request_to_handler( $request ); 883 if ( ! is_wp_error( $matched ) && isset( $matched[1]['args']['per_page']['maximum'] ) ) { 884 $request['per_page'] = (int) $matched[1]['args']['per_page']['maximum']; 885 } 886 } 887 888 $response = $this->dispatch( $request ); 889 890 /** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */ 891 $response = apply_filters( 'rest_post_dispatch', rest_ensure_response( $response ), $this, $request ); 892 893 $this->embed_cache[ $item['href'] ] = $this->response_to_data( $response, false ); 894 } 895 896 $embeds[] = $this->embed_cache[ $item['href'] ]; 897 } 898 899 // Determine if any real links were found. 900 $has_links = count( array_filter( $embeds ) ); 901 902 if ( $has_links ) { 903 $embedded[ $rel ] = $embeds; 904 } 905 } 906 907 if ( ! empty( $embedded ) ) { 908 $data['_embedded'] = $embedded; 909 } 910 911 return $data; 912 } 913 914 /** 915 * Wraps the response in an envelope. 916 * 917 * The enveloping technique is used to work around browser/client 918 * compatibility issues. Essentially, it converts the full HTTP response to 919 * data instead. 920 * 921 * @since 4.4.0 922 * @since 6.0.0 The `$embed` parameter can now contain a list of link relations to include. 923 * 924 * @param WP_REST_Response $response Response object. 925 * @param bool|string[] $embed Whether to embed all links, a filtered list of link relations, or no links. 926 * @return WP_REST_Response New response with wrapped data 927 */ 928 public function envelope_response( $response, $embed ) { 929 $envelope = array( 930 'body' => $this->response_to_data( $response, $embed ), 931 'status' => $response->get_status(), 932 'headers' => $response->get_headers(), 933 ); 934 935 /** 936 * Filters the enveloped form of a REST API response. 937 * 938 * @since 4.4.0 939 * 940 * @param array $envelope { 941 * Envelope data. 942 * 943 * @type array $body Response data. 944 * @type int $status The 3-digit HTTP status code. 945 * @type array $headers Map of header name to header value. 946 * } 947 * @param WP_REST_Response $response Original response data. 948 */ 949 $envelope = apply_filters( 'rest_envelope_response', $envelope, $response ); 950 951 // Ensure it's still a response and return. 952 return rest_ensure_response( $envelope ); 953 } 954 955 /** 956 * Registers a route to the server. 957 * 958 * @since 4.4.0 959 * 960 * @param string $route_namespace Namespace. 961 * @param string $route The REST route. 962 * @param array $route_args Route arguments. 963 * @param bool $override Optional. Whether the route should be overridden if it already exists. 964 * Default false. 965 * 966 * @phpstan-param non-empty-string $route 967 */ 968 public function register_route( $route_namespace, $route, $route_args, $override = false ) { 969 if ( ! isset( $this->namespaces[ $route_namespace ] ) ) { 970 $this->namespaces[ $route_namespace ] = array(); 971 972 $this->register_route( 973 $route_namespace, 974 '/' . $route_namespace, 975 array( 976 array( 977 'methods' => self::READABLE, 978 'callback' => array( $this, 'get_namespace_index' ), 979 'args' => array( 980 'namespace' => array( 981 'default' => $route_namespace, 982 ), 983 'context' => array( 984 'default' => 'view', 985 ), 986 ), 987 ), 988 ) 989 ); 990 } 991 992 // Associative to avoid double-registration. 993 $this->namespaces[ $route_namespace ][ $route ] = true; 994 995 $route_args['namespace'] = $route_namespace; 996 997 if ( $override || empty( $this->endpoints[ $route ] ) ) { 998 $this->endpoints[ $route ] = $route_args; 999 } else { 1000 $this->endpoints[ $route ] = array_merge( $this->endpoints[ $route ], $route_args ); 1001 } 1002 } 1003 1004 /** 1005 * Retrieves the route map. 1006 * 1007 * The route map is an associative array with path regexes as the keys. The value 1008 * is an indexed array of the handlers registered for that route, each of them an 1009 * associative array of endpoint arguments: the callback, its permission callback, 1010 * the arguments it accepts, and the HTTP methods it responds to as a map of method 1011 * name to true. 1012 * 1013 * Each route can be mapped to more than one handler. This allows mapping e.g. GET 1014 * requests to one callback and POST requests to another. 1015 * 1016 * Route options, the non-numeric keys a route is registered with such as `schema` 1017 * and `namespace`, are not returned here. They are moved to 1018 * {@see WP_REST_Server::get_route_options()}. 1019 * 1020 * Note that the path regexes (array keys) must have @ escaped, as this is 1021 * used as the delimiter with preg_match() 1022 * 1023 * @since 4.4.0 1024 * @since 5.4.0 Added `$route_namespace` parameter. 1025 * 1026 * @param string $route_namespace Optionally, only return routes in the given namespace. 1027 * @return array Route map as `'/path/regex' => array( $handler, ... )`, where each 1028 * `$handler` is an array of endpoint arguments. 1029 * 1030 * @phpstan-return array<non-empty-string, array<int, Route_Handler>> 1031 */ 1032 public function get_routes( $route_namespace = '' ) { 1033 $endpoints = $this->endpoints; 1034 1035 if ( $route_namespace ) { 1036 $endpoints = wp_list_filter( $endpoints, array( 'namespace' => $route_namespace ) ); 1037 } 1038 1039 /** 1040 * Filters the array of available REST API endpoints. 1041 * 1042 * @since 4.4.0 1043 * 1044 * @param array $endpoints The available endpoints, as registered and before normalization. 1045 * An array of matching regex patterns, each mapped either to a single 1046 * endpoint's arguments or to an array of them, alongside any route 1047 * options under their own non-numeric keys. 1048 * 1049 * @phpstan-param array<non-empty-string, array<array-key, mixed>> $endpoints 1050 */ 1051 $endpoints = apply_filters( 'rest_endpoints', $endpoints ); 1052 1053 // Normalize the endpoints. 1054 $defaults = array( 1055 'methods' => '', 1056 'accept_json' => false, 1057 'accept_raw' => false, 1058 'show_in_index' => true, 1059 'args' => array(), 1060 ); 1061 1062 foreach ( $endpoints as $route => &$handlers ) { 1063 1064 if ( isset( $handlers['callback'] ) ) { 1065 // Single endpoint, add one deeper. 1066 $handlers = array( $handlers ); 1067 } 1068 1069 if ( ! isset( $this->route_options[ $route ] ) ) { 1070 $this->route_options[ $route ] = array(); 1071 } 1072 1073 foreach ( $handlers as $key => &$handler ) { 1074 1075 if ( ! is_numeric( $key ) ) { 1076 // Route option, move it to the options. 1077 $this->route_options[ $route ][ $key ] = $handler; 1078 unset( $handlers[ $key ] ); 1079 continue; 1080 } 1081 1082 $handler = wp_parse_args( $handler, $defaults ); 1083 1084 // Allow comma-separated HTTP methods. 1085 if ( is_string( $handler['methods'] ) ) { 1086 $methods = explode( ',', $handler['methods'] ); 1087 } elseif ( is_array( $handler['methods'] ) ) { 1088 $methods = array(); 1089 1090 /* 1091 * Array values may themselves be comma-separated, either written that way or 1092 * because they are a multi-method constant such as WP_REST_Server::EDITABLE. 1093 */ 1094 foreach ( array_filter( $handler['methods'], 'is_string' ) as $method ) { 1095 $methods = array_merge( $methods, explode( ',', $method ) ); 1096 } 1097 } else { 1098 $methods = array(); 1099 } 1100 1101 $handler['methods'] = array(); 1102 1103 foreach ( $methods as $method ) { 1104 $method = strtoupper( trim( $method ) ); 1105 $handler['methods'][ $method ] = true; 1106 } 1107 } 1108 } 1109 1110 /* 1111 * The loop above is what turns each registered handler into the documented shape, 1112 * but it does so by reference, which static analysis cannot follow. 1113 */ 1114 /** @phpstan-var array<non-empty-string, array<int, Route_Handler>> $endpoints */ 1115 1116 return $endpoints; 1117 } 1118 1119 /** 1120 * Retrieves namespaces registered on the server. 1121 * 1122 * @since 4.4.0 1123 * 1124 * @return string[] List of registered namespaces. 1125 */ 1126 public function get_namespaces() { 1127 return array_keys( $this->namespaces ); 1128 } 1129 1130 /** 1131 * Retrieves specified options for a route. 1132 * 1133 * @since 4.4.0 1134 * 1135 * @param string $route Route pattern to fetch options for. 1136 * @return array|null Data as an associative array if found, or null if not found. 1137 */ 1138 public function get_route_options( $route ) { 1139 if ( ! isset( $this->route_options[ $route ] ) ) { 1140 return null; 1141 } 1142 1143 return $this->route_options[ $route ]; 1144 } 1145 1146 /** 1147 * Resolves the data processing settings of a route handler. 1148 * 1149 * A route, or one of its handlers, sets the `data_processing` option to serve its data as returned. 1150 * The option is `false` to turn off every feature, or an array with any of these keys: 1151 * 1152 * - `fields`: Whether the `_fields` parameter filters the response data. 1153 * - `embed`: Whether the `_embed` parameter embeds linked resources in the response data. 1154 * - `links`: Whether the response links are added to the response data as `_links`. 1155 * 1156 * Each feature is resolved on its own: the handler setting applies, then the route setting, and the 1157 * feature stays on when neither sets it. Embedding needs the links, so `embed` is off whenever `links` 1158 * is off. 1159 * 1160 * The option covers these three features only. Authentication, permission checks, status codes, headers 1161 * and JSON encoding apply as usual, and keys of the data itself, such as its own `_links`, stay as returned. 1162 * 1163 * The server resolves the settings when it matches a request, and sets them on the request attributes, 1164 * on the matched handler and on the response, for a response returned by the `rest_pre_dispatch` filter too. 1165 * 1166 * @since 7.2.0 1167 * 1168 * @see WP_REST_Response::get_data_processing() 1169 * 1170 * @param array $handler The route handler. 1171 * @param array|null $route_options Optional. The options of the handler's route. Default null. 1172 * @return array { 1173 * Whether each data processing feature is on. 1174 * 1175 * @type bool $fields Whether the `_fields` parameter filters the response data. 1176 * @type bool $embed Whether the `_embed` parameter embeds linked resources in the response data. 1177 * @type bool $links Whether the response links are added to the response data as `_links`. 1178 * } 1179 * 1180 * @phpstan-return Data_Processing 1181 */ 1182 public static function resolve_data_processing( $handler, $route_options = null ) { 1183 $processing = array( 1184 'fields' => true, 1185 'embed' => true, 1186 'links' => true, 1187 ); 1188 1189 $settings = array( 1190 $route_options['data_processing'] ?? null, 1191 $handler['data_processing'] ?? null, 1192 ); 1193 1194 foreach ( $settings as $setting ) { 1195 if ( is_bool( $setting ) ) { 1196 $processing = array_fill_keys( array_keys( $processing ), $setting ); 1197 continue; 1198 } 1199 1200 if ( ! is_array( $setting ) ) { 1201 continue; 1202 } 1203 1204 foreach ( array_keys( $processing ) as $feature ) { 1205 if ( isset( $setting[ $feature ] ) ) { 1206 $processing[ $feature ] = (bool) $setting[ $feature ]; 1207 } 1208 } 1209 } 1210 1211 if ( ! $processing['links'] ) { 1212 $processing['embed'] = false; 1213 } 1214 1215 return $processing; 1216 } 1217 1218 /** 1219 * Matches the request to a callback and call it. 1220 * 1221 * @since 4.4.0 1222 * 1223 * @param WP_REST_Request $request Request to attempt dispatching. 1224 * @return WP_REST_Response Response returned by the callback. 1225 */ 1226 public function dispatch( $request ) { 1227 $this->dispatching_requests[] = $request; 1228 1229 /** 1230 * Filters the pre-calculated result of a REST API dispatch request. 1231 * 1232 * Allow hijacking the request before dispatching by returning a non-empty. The returned value 1233 * will be used to serve the request instead. 1234 * 1235 * @since 4.4.0 1236 * 1237 * @param mixed $result Response to replace the requested version with. Can be anything 1238 * a normal endpoint can return, or null to not hijack the request. 1239 * @param WP_REST_Server $server Server instance. 1240 * @param WP_REST_Request $request Request used to generate the response. 1241 */ 1242 $result = apply_filters( 'rest_pre_dispatch', null, $this, $request ); 1243 1244 if ( ! empty( $result ) ) { 1245 1246 // Normalize to either WP_Error or WP_REST_Response... 1247 $result = rest_ensure_response( $result ); 1248 1249 // ...then convert WP_Error across. 1250 if ( is_wp_error( $result ) ) { 1251 $result = $this->error_to_response( $result ); 1252 } 1253 1254 // The response matched no handler, so it follows the settings of the handler the request would match. 1255 $match = $this->find_handler_for_request( $request ); 1256 if ( null !== $match ) { 1257 $result->set_data_processing( $match[1]['data_processing'] ?? true ); 1258 } 1259 1260 array_pop( $this->dispatching_requests ); 1261 return $result; 1262 } 1263 1264 $error = null; 1265 $matched = $this->match_request_to_handler( $request ); 1266 1267 if ( is_wp_error( $matched ) ) { 1268 $response = $this->error_to_response( $matched ); 1269 array_pop( $this->dispatching_requests ); 1270 return $response; 1271 } 1272 1273 list( $route, $handler ) = $matched; 1274 1275 if ( ! is_callable( $handler['callback'] ) ) { 1276 $error = new WP_Error( 1277 'rest_invalid_handler', 1278 __( 'The handler for the route is invalid.' ), 1279 array( 'status' => 500 ) 1280 ); 1281 } 1282 1283 if ( ! is_wp_error( $error ) ) { 1284 $check_required = $request->has_valid_params(); 1285 if ( is_wp_error( $check_required ) ) { 1286 $error = $check_required; 1287 } else { 1288 $check_sanitized = $request->sanitize_params(); 1289 if ( is_wp_error( $check_sanitized ) ) { 1290 $error = $check_sanitized; 1291 } 1292 } 1293 } 1294 1295 $response = $this->respond_to_request( $request, $route, $handler, $error ); 1296 array_pop( $this->dispatching_requests ); 1297 return $response; 1298 } 1299 1300 /** 1301 * Returns whether the REST server is currently dispatching / responding to a request. 1302 * 1303 * This may be a standalone REST API request, or an internal request dispatched from within a regular page load. 1304 * 1305 * @since 6.5.0 1306 * 1307 * @return bool Whether the REST server is currently handling a request. 1308 */ 1309 public function is_dispatching() { 1310 return (bool) $this->dispatching_requests; 1311 } 1312 1313 /** 1314 * Matches a request object to its handler. 1315 * 1316 * @access private 1317 * @since 5.6.0 1318 * 1319 * @param WP_REST_Request $request The request object. 1320 * @return array<int, string|array<string, mixed>>|WP_Error The route and request handler on success or a WP_Error instance if no handler was found. 1321 * 1322 * @phpstan-return array{non-empty-string, Route_Handler}|WP_Error 1323 */ 1324 protected function match_request_to_handler( $request ) { 1325 $match = $this->find_handler_for_request( $request ); 1326 1327 if ( null === $match ) { 1328 return new WP_Error( 1329 'rest_no_route', 1330 __( 'No route was found matching the URL and request method.' ), 1331 array( 'status' => 404 ) 1332 ); 1333 } 1334 1335 list( $route, $handler, $args ) = $match; 1336 1337 if ( ! is_callable( $handler['callback'] ) ) { 1338 return array( $route, $handler ); 1339 } 1340 1341 $request->set_url_params( $args ); 1342 $request->set_attributes( $handler ); 1343 1344 $defaults = array(); 1345 1346 foreach ( $handler['args'] as $arg => $options ) { 1347 if ( isset( $options['default'] ) ) { 1348 $defaults[ $arg ] = $options['default']; 1349 } 1350 } 1351 1352 $request->set_default_params( $defaults ); 1353 1354 return array( $route, $handler ); 1355 } 1356 1357 /** 1358 * Finds the route and the handler that a request matches, without changing the request. 1359 * 1360 * The handler carries its resolved data processing settings under `data_processing`. 1361 * 1362 * @access private 1363 * @since 7.2.0 1364 * 1365 * @param WP_REST_Request $request The request object. 1366 * @return array|null The route, the handler with its resolved data processing settings, and the URL 1367 * parameters, or null when no handler matches. 1368 * 1369 * @phpstan-return array{non-empty-string, Route_Handler, array<string, string>}|null 1370 */ 1371 protected function find_handler_for_request( $request ) { 1372 $method = $request->get_method(); 1373 $path = $request->get_route(); 1374 1375 $with_namespace = array(); 1376 1377 foreach ( $this->get_namespaces() as $namespace ) { 1378 if ( str_starts_with( trailingslashit( ltrim( $path, '/' ) ), $namespace ) ) { 1379 $with_namespace[] = $this->get_routes( $namespace ); 1380 } 1381 } 1382 1383 if ( $with_namespace ) { 1384 $routes = array_merge( ...$with_namespace ); 1385 } else { 1386 $routes = $this->get_routes(); 1387 } 1388 1389 foreach ( $routes as $route => $handlers ) { 1390 $match = preg_match( '@^' . $route . '$@i', $path, $matches ); 1391 1392 if ( ! $match ) { 1393 continue; 1394 } 1395 1396 $args = array(); 1397 1398 foreach ( $matches as $param => $value ) { 1399 if ( ! is_int( $param ) ) { 1400 $args[ $param ] = $value; 1401 } 1402 } 1403 1404 foreach ( $handlers as $handler ) { 1405 // Fallback to GET method if no HEAD method is registered. 1406 $checked_method = $method; 1407 if ( 'HEAD' === $method && empty( $handler['methods']['HEAD'] ) ) { 1408 $checked_method = 'GET'; 1409 } 1410 if ( empty( $handler['methods'][ $checked_method ] ) ) { 1411 continue; 1412 } 1413 1414 // Resolve the data processing settings once, for the request attributes, the matched handler and the response. 1415 $handler['data_processing'] = self::resolve_data_processing( $handler, $this->get_route_options( $route ) ); 1416 1417 return array( $route, $handler, $args ); 1418 } 1419 } 1420 1421 return null; 1422 } 1423 1424 /** 1425 * Dispatches the request to the callback handler. 1426 * 1427 * @access private 1428 * @since 5.6.0 1429 * 1430 * @param WP_REST_Request $request The request object. 1431 * @param string $route The matched route regex. 1432 * @param array<string, mixed> $handler The matched route handler. 1433 * @param WP_Error|null $response The current error object if any. 1434 * @return WP_REST_Response 1435 * 1436 * @phpstan-param Route_Handler $handler 1437 */ 1438 protected function respond_to_request( $request, $route, $handler, $response ) { 1439 /** 1440 * Filters the response before executing any REST API callbacks. 1441 * 1442 * Allows plugins to perform additional validation after a 1443 * request is initialized and matched to a registered route, 1444 * but before it is executed. 1445 * 1446 * Note that this filter will not be called for requests that 1447 * fail to authenticate or match to a registered route. 1448 * 1449 * @since 4.7.0 1450 * 1451 * @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client. 1452 * Usually a WP_REST_Response or WP_Error. 1453 * @param array<string, mixed> $handler Route handler used for the request. 1454 * @param WP_REST_Request $request Request used to generate the response. 1455 * 1456 * @phpstan-param Route_Handler $handler 1457 */ 1458 $response = apply_filters( 'rest_request_before_callbacks', $response, $handler, $request ); 1459 1460 // Check permission specified on the route. 1461 if ( ! is_wp_error( $response ) && ! empty( $handler['permission_callback'] ) ) { 1462 $permission = call_user_func( $handler['permission_callback'], $request ); 1463 1464 if ( is_wp_error( $permission ) ) { 1465 $response = $permission; 1466 } elseif ( false === $permission || null === $permission ) { 1467 $response = new WP_Error( 1468 'rest_forbidden', 1469 __( 'Sorry, you are not allowed to do that.' ), 1470 array( 'status' => rest_authorization_required_code() ) 1471 ); 1472 } 1473 } 1474 1475 if ( ! is_wp_error( $response ) ) { 1476 /** 1477 * Filters the REST API dispatch request result. 1478 * 1479 * Allow plugins to override dispatching the request. 1480 * 1481 * @since 4.4.0 1482 * @since 4.5.0 Added `$route` and `$handler` parameters. 1483 * 1484 * @param mixed $dispatch_result Dispatch result, will be used if not empty. 1485 * @param WP_REST_Request $request Request used to generate the response. 1486 * @param string $route Route matched for the request. 1487 * @param array<string, mixed> $handler Route handler used for the request. 1488 * 1489 * @phpstan-param Route_Handler $handler 1490 */ 1491 $dispatch_result = apply_filters( 'rest_dispatch_request', null, $request, $route, $handler ); 1492 1493 // Allow plugins to halt the request via this filter. 1494 if ( null !== $dispatch_result ) { 1495 $response = $dispatch_result; 1496 } else { 1497 $response = call_user_func( $handler['callback'], $request ); 1498 } 1499 } 1500 1501 /** 1502 * Filters the response immediately after executing any REST API 1503 * callbacks. 1504 * 1505 * Allows plugins to perform any needed cleanup, for example, 1506 * to undo changes made during the {@see 'rest_request_before_callbacks'} 1507 * filter. 1508 * 1509 * Note that this filter will not be called for requests that 1510 * fail to authenticate or match to a registered route. 1511 * 1512 * Note that an endpoint's `permission_callback` can still be 1513 * called after this filter - see `rest_send_allow_header()`. 1514 * 1515 * @since 4.7.0 1516 * 1517 * @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client. 1518 * Usually a WP_REST_Response or WP_Error. 1519 * @param array<string, mixed> $handler Route handler used for the request. 1520 * @param WP_REST_Request $request Request used to generate the response. 1521 * 1522 * @phpstan-param Route_Handler $handler 1523 */ 1524 $response = apply_filters( 'rest_request_after_callbacks', $response, $handler, $request ); 1525 1526 if ( is_wp_error( $response ) ) { 1527 $response = $this->error_to_response( $response ); 1528 } else { 1529 $response = rest_ensure_response( $response ); 1530 } 1531 1532 $response->set_matched_route( $route ); 1533 $response->set_matched_handler( $handler ); 1534 $response->set_data_processing( $handler['data_processing'] ?? true ); 1535 1536 return $response; 1537 } 1538 1539 /** 1540 * Returns if an error occurred during most recent JSON encode/decode. 1541 * 1542 * Strings to be translated will be in format like 1543 * "Encoding error: Maximum stack depth exceeded". 1544 * 1545 * @since 4.4.0 1546 * 1547 * @return false|string Boolean false or string error message. 1548 */ 1549 protected function get_json_last_error() { 1550 if ( JSON_ERROR_NONE === json_last_error() ) { 1551 return false; 1552 } 1553 1554 return json_last_error_msg(); 1555 } 1556 1557 /** 1558 * Retrieves the site index. 1559 * 1560 * This endpoint describes the capabilities of the site. 1561 * 1562 * @since 4.4.0 1563 * 1564 * @param WP_REST_Request $request Request data. 1565 * @return WP_REST_Response The API root index data. 1566 */ 1567 public function get_index( $request ) { 1568 // General site data. 1569 $available = array( 1570 'name' => get_option( 'blogname' ), 1571 'description' => get_option( 'blogdescription' ), 1572 'url' => get_option( 'siteurl' ), 1573 'home' => home_url(), 1574 'gmt_offset' => get_option( 'gmt_offset' ), 1575 'timezone_string' => get_option( 'timezone_string' ), 1576 'page_for_posts' => (int) get_option( 'page_for_posts' ), 1577 'page_on_front' => (int) get_option( 'page_on_front' ), 1578 'show_on_front' => get_option( 'show_on_front' ), 1579 'namespaces' => array_keys( $this->namespaces ), 1580 'authentication' => array(), 1581 'routes' => $this->get_data_for_routes( $this->get_routes(), $request['context'] ), 1582 ); 1583 1584 // Add media processing settings for users who can upload files. 1585 if ( wp_is_client_side_media_processing_enabled() && current_user_can( 'upload_files' ) ) { 1586 $available = array_merge( $available, _wp_get_client_side_media_image_settings() ); 1587 } 1588 1589 $response = new WP_REST_Response( $available ); 1590 1591 $fields = $request['_fields'] ?? ''; 1592 $fields = wp_parse_list( $fields ); 1593 if ( empty( $fields ) ) { 1594 $fields[] = '_links'; 1595 } 1596 1597 if ( $request->has_param( '_embed' ) ) { 1598 $fields[] = '_embedded'; 1599 } 1600 1601 if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) { 1602 $response->add_link( 'help', 'https://developer.wordpress.org/rest-api/' ); 1603 $this->add_active_theme_link_to_index( $response ); 1604 $this->add_site_logo_to_index( $response ); 1605 $this->add_site_icon_to_index( $response ); 1606 } else { 1607 if ( rest_is_field_included( 'site_logo', $fields ) ) { 1608 $this->add_site_logo_to_index( $response ); 1609 } 1610 if ( rest_is_field_included( 'site_icon', $fields ) || rest_is_field_included( 'site_icon_url', $fields ) ) { 1611 $this->add_site_icon_to_index( $response ); 1612 } 1613 } 1614 1615 /** 1616 * Filters the REST API root index data. 1617 * 1618 * This contains the data describing the API. This includes information 1619 * about supported authentication schemes, supported namespaces, routes 1620 * available on the API, and a small amount of data about the site. 1621 * 1622 * @since 4.4.0 1623 * @since 6.0.0 Added `$request` parameter. 1624 * 1625 * @param WP_REST_Response $response Response data. 1626 * @param WP_REST_Request $request Request data. 1627 */ 1628 return apply_filters( 'rest_index', $response, $request ); 1629 } 1630 1631 /** 1632 * Adds a link to the active theme for users who have proper permissions. 1633 * 1634 * @since 5.7.0 1635 * 1636 * @param WP_REST_Response $response REST API response. 1637 */ 1638 protected function add_active_theme_link_to_index( WP_REST_Response $response ) { 1639 $should_add = current_user_can( 'switch_themes' ) || current_user_can( 'manage_network_themes' ); 1640 1641 if ( ! $should_add && current_user_can( 'edit_posts' ) ) { 1642 $should_add = true; 1643 } 1644 1645 if ( ! $should_add ) { 1646 foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) { 1647 if ( current_user_can( $post_type->cap->edit_posts ) ) { 1648 $should_add = true; 1649 break; 1650 } 1651 } 1652 } 1653 1654 if ( $should_add ) { 1655 $theme = wp_get_theme(); 1656 $response->add_link( 'https://api.w.org/active-theme', rest_url( 'wp/v2/themes/' . $theme->get_stylesheet() ) ); 1657 } 1658 } 1659 1660 /** 1661 * Exposes the site logo through the WordPress REST API. 1662 * 1663 * This is used for fetching this information when user has no rights 1664 * to update settings. 1665 * 1666 * @since 5.8.0 1667 * 1668 * @param WP_REST_Response $response REST API response. 1669 */ 1670 protected function add_site_logo_to_index( WP_REST_Response $response ) { 1671 $site_logo_id = get_theme_mod( 'custom_logo', 0 ); 1672 1673 $this->add_image_to_index( $response, $site_logo_id, 'site_logo' ); 1674 } 1675 1676 /** 1677 * Exposes the site icon through the WordPress REST API. 1678 * 1679 * This is used for fetching this information when user has no rights 1680 * to update settings. 1681 * 1682 * @since 5.9.0 1683 * 1684 * @param WP_REST_Response $response REST API response. 1685 */ 1686 protected function add_site_icon_to_index( WP_REST_Response $response ) { 1687 $site_icon_id = get_option( 'site_icon', 0 ); 1688 1689 $this->add_image_to_index( $response, $site_icon_id, 'site_icon' ); 1690 1691 $response->data['site_icon_url'] = get_site_icon_url(); 1692 } 1693 1694 /** 1695 * Exposes an image through the WordPress REST API. 1696 * This is used for fetching this information when user has no rights 1697 * to update settings. 1698 * 1699 * @since 5.9.0 1700 * 1701 * @param WP_REST_Response $response REST API response. 1702 * @param int $image_id Image attachment ID. 1703 * @param string $type Type of Image. 1704 */ 1705 protected function add_image_to_index( WP_REST_Response $response, $image_id, $type ) { 1706 $response->data[ $type ] = (int) $image_id; 1707 if ( $image_id ) { 1708 $response->add_link( 1709 'https://api.w.org/featuredmedia', 1710 rest_url( rest_get_route_for_post( $image_id ) ), 1711 array( 1712 'embeddable' => true, 1713 'type' => $type, 1714 ) 1715 ); 1716 } 1717 } 1718 1719 /** 1720 * Retrieves the index for a namespace. 1721 * 1722 * @since 4.4.0 1723 * 1724 * @param WP_REST_Request $request REST request instance. 1725 * @return WP_REST_Response|WP_Error WP_REST_Response instance if the index was found, 1726 * WP_Error if the namespace isn't set. 1727 */ 1728 public function get_namespace_index( $request ) { 1729 $namespace = $request['namespace']; 1730 1731 if ( ! isset( $this->namespaces[ $namespace ] ) ) { 1732 return new WP_Error( 1733 'rest_invalid_namespace', 1734 __( 'The specified namespace could not be found.' ), 1735 array( 'status' => 404 ) 1736 ); 1737 } 1738 1739 $routes = $this->namespaces[ $namespace ]; 1740 $endpoints = array_intersect_key( $this->get_routes(), $routes ); 1741 1742 $data = array( 1743 'namespace' => $namespace, 1744 'routes' => $this->get_data_for_routes( $endpoints, $request['context'] ), 1745 ); 1746 $response = rest_ensure_response( $data ); 1747 1748 // Link to the root index. 1749 $response->add_link( 'up', rest_url( '/' ) ); 1750 1751 /** 1752 * Filters the REST API namespace index data. 1753 * 1754 * This typically is just the route data for the namespace, but you can 1755 * add any data you'd like here. 1756 * 1757 * @since 4.4.0 1758 * 1759 * @param WP_REST_Response $response Response data. 1760 * @param WP_REST_Request $request Request data. The namespace is passed as the 'namespace' parameter. 1761 */ 1762 return apply_filters( 'rest_namespace_index', $response, $request ); 1763 } 1764 1765 /** 1766 * Retrieves the publicly-visible data for routes. 1767 * 1768 * @since 4.4.0 1769 * 1770 * @param array $routes Routes to get data for. 1771 * @param string $context Optional. Context for data. Accepts 'view' or 'help'. Default 'view'. 1772 * @return array[] Route data to expose in indexes, keyed by route. 1773 */ 1774 public function get_data_for_routes( $routes, $context = 'view' ) { 1775 $available = array(); 1776 1777 // Find the available routes. 1778 foreach ( $routes as $route => $callbacks ) { 1779 $data = $this->get_data_for_route( $route, $callbacks, $context ); 1780 if ( empty( $data ) ) { 1781 continue; 1782 } 1783 1784 /** 1785 * Filters the publicly-visible data for a single REST API route. 1786 * 1787 * @since 4.4.0 1788 * 1789 * @param array $data Publicly-visible data for the route. 1790 */ 1791 $available[ $route ] = apply_filters( 'rest_endpoints_description', $data ); 1792 } 1793 1794 /** 1795 * Filters the publicly-visible data for REST API routes. 1796 * 1797 * This data is exposed on indexes and can be used by clients or 1798 * developers to investigate the site and find out how to use it. It 1799 * acts as a form of self-documentation. 1800 * 1801 * @since 4.4.0 1802 * 1803 * @param array[] $available Route data to expose in indexes, keyed by route. 1804 * @param array $routes Internal route data as an associative array. 1805 */ 1806 return apply_filters( 'rest_route_data', $available, $routes ); 1807 } 1808 1809 /** 1810 * Retrieves publicly-visible data for the route. 1811 * 1812 * @since 4.4.0 1813 * 1814 * @param string $route Route to get data for. 1815 * @param array $callbacks Callbacks to convert to data. 1816 * @param string $context Optional. Context for the data. Accepts 'view' or 'help'. Default 'view'. 1817 * @return array|null Data for the route, or null if no publicly-visible data. 1818 */ 1819 public function get_data_for_route( $route, $callbacks, $context = 'view' ) { 1820 $data = array( 1821 'namespace' => '', 1822 'methods' => array(), 1823 'endpoints' => array(), 1824 ); 1825 1826 $allow_batch = false; 1827 $route_options = $this->get_route_options( $route ); 1828 1829 if ( null !== $route_options ) { 1830 $options = $route_options; 1831 1832 if ( isset( $options['namespace'] ) ) { 1833 $data['namespace'] = $options['namespace']; 1834 } 1835 1836 $allow_batch = $options['allow_batch'] ?? false; 1837 1838 if ( isset( $options['schema'] ) && 'help' === $context ) { 1839 $data['schema'] = call_user_func( $options['schema'] ); 1840 } 1841 } 1842 1843 $allowed_schema_keywords = array_flip( wp_get_json_schema_allowed_keywords( 'rest-api' ) ); 1844 1845 $route = preg_replace( '#\(\?P<(\w+?)>.*?\)#', '{$1}', $route ); 1846 1847 foreach ( $callbacks as $callback ) { 1848 // Skip to the next route if any callback is hidden. 1849 if ( empty( $callback['show_in_index'] ) ) { 1850 continue; 1851 } 1852 1853 $data['methods'] = array_merge( $data['methods'], array_keys( $callback['methods'] ) ); 1854 $endpoint_data = array( 1855 'methods' => array_keys( $callback['methods'] ), 1856 ); 1857 1858 $callback_batch = $callback['allow_batch'] ?? $allow_batch; 1859 1860 if ( $callback_batch ) { 1861 $endpoint_data['allow_batch'] = $callback_batch; 1862 } 1863 1864 $data_processing = self::resolve_data_processing( $callback, $route_options ); 1865 1866 if ( in_array( false, $data_processing, true ) ) { 1867 $endpoint_data['data_processing'] = $data_processing; 1868 } 1869 1870 if ( isset( $callback['args'] ) ) { 1871 $endpoint_data['args'] = array(); 1872 1873 foreach ( $callback['args'] as $key => $opts ) { 1874 if ( is_string( $opts ) ) { 1875 $opts = array( $opts => 0 ); 1876 } elseif ( ! is_array( $opts ) ) { 1877 $opts = array(); 1878 } 1879 $arg_data = array_intersect_key( $opts, $allowed_schema_keywords ); 1880 $arg_data['required'] = ! empty( $opts['required'] ); 1881 1882 $endpoint_data['args'][ $key ] = $arg_data; 1883 } 1884 } 1885 1886 $data['endpoints'][] = $endpoint_data; 1887 1888 // For non-variable routes, generate links. 1889 if ( ! str_contains( $route, '{' ) ) { 1890 $data['_links'] = array( 1891 'self' => array( 1892 array( 1893 'href' => rest_url( $route ), 1894 ), 1895 ), 1896 ); 1897 } 1898 } 1899 1900 if ( empty( $data['methods'] ) ) { 1901 // No methods supported, hide the route. 1902 return null; 1903 } 1904 1905 return $data; 1906 } 1907 1908 /** 1909 * Gets the maximum number of requests that can be included in a batch. 1910 * 1911 * @since 5.6.0 1912 * 1913 * @return int The maximum requests. 1914 */ 1915 protected function get_max_batch_size() { 1916 /** 1917 * Filters the maximum number of REST API requests that can be included in a batch. 1918 * 1919 * @since 5.6.0 1920 * 1921 * @param int $max_size The maximum size. 1922 */ 1923 return apply_filters( 'rest_get_max_batch_size', 25 ); 1924 } 1925 1926 /** 1927 * Serves the batch/v1 request. 1928 * 1929 * @since 5.6.0 1930 * 1931 * @param WP_REST_Request $batch_request The batch request object. 1932 * @return WP_REST_Response The generated response object. 1933 */ 1934 public function serve_batch_request_v1( WP_REST_Request $batch_request ) { 1935 $requests = array(); 1936 1937 foreach ( $batch_request['requests'] as $args ) { 1938 $parsed_url = wp_parse_url( $args['path'] ); 1939 1940 if ( false === $parsed_url ) { 1941 $requests[] = new WP_Error( 'parse_path_failed', __( 'Could not parse the path.' ), array( 'status' => 400 ) ); 1942 1943 continue; 1944 } 1945 1946 $single_request = new WP_REST_Request( $args['method'] ?? 'POST', $parsed_url['path'] ); 1947 1948 if ( ! empty( $parsed_url['query'] ) ) { 1949 $query_args = array(); 1950 wp_parse_str( $parsed_url['query'], $query_args ); 1951 $single_request->set_query_params( $query_args ); 1952 } 1953 1954 if ( ! empty( $args['body'] ) ) { 1955 $single_request->set_body_params( $args['body'] ); 1956 } 1957 1958 if ( ! empty( $args['headers'] ) ) { 1959 $single_request->set_headers( $args['headers'] ); 1960 } 1961 1962 $requests[] = $single_request; 1963 } 1964 1965 $matches = array(); 1966 $validation = array(); 1967 $has_error = false; 1968 1969 foreach ( $requests as $single_request ) { 1970 if ( is_wp_error( $single_request ) ) { 1971 $has_error = true; 1972 $matches[] = $single_request; 1973 $validation[] = $single_request; 1974 continue; 1975 } 1976 1977 $match = $this->match_request_to_handler( $single_request ); 1978 $matches[] = $match; 1979 $error = null; 1980 1981 if ( is_wp_error( $match ) ) { 1982 $error = $match; 1983 } 1984 1985 if ( ! $error ) { 1986 list( $route, $handler ) = $match; 1987 1988 if ( isset( $handler['allow_batch'] ) ) { 1989 $allow_batch = $handler['allow_batch']; 1990 } else { 1991 $route_options = $this->get_route_options( $route ); 1992 $allow_batch = $route_options['allow_batch'] ?? false; 1993 } 1994 1995 if ( ! is_array( $allow_batch ) || empty( $allow_batch['v1'] ) ) { 1996 $error = new WP_Error( 1997 'rest_batch_not_allowed', 1998 __( 'The requested route does not support batch requests.' ), 1999 array( 'status' => 400 ) 2000 ); 2001 } 2002 } 2003 2004 if ( ! $error ) { 2005 $check_required = $single_request->has_valid_params(); 2006 if ( is_wp_error( $check_required ) ) { 2007 $error = $check_required; 2008 } 2009 } 2010 2011 if ( ! $error ) { 2012 $check_sanitized = $single_request->sanitize_params(); 2013 if ( is_wp_error( $check_sanitized ) ) { 2014 $error = $check_sanitized; 2015 } 2016 } 2017 2018 if ( $error ) { 2019 $has_error = true; 2020 $validation[] = $error; 2021 } else { 2022 $validation[] = true; 2023 } 2024 } 2025 2026 $responses = array(); 2027 2028 if ( $has_error && 'require-all-validate' === $batch_request['validation'] ) { 2029 foreach ( $validation as $valid ) { 2030 if ( is_wp_error( $valid ) ) { 2031 $responses[] = $this->envelope_response( $this->error_to_response( $valid ), false )->get_data(); 2032 } else { 2033 $responses[] = null; 2034 } 2035 } 2036 2037 return new WP_REST_Response( 2038 array( 2039 'failed' => 'validation', 2040 'responses' => $responses, 2041 ), 2042 WP_Http::MULTI_STATUS 2043 ); 2044 } 2045 2046 foreach ( $requests as $i => $single_request ) { 2047 if ( is_wp_error( $single_request ) ) { 2048 $result = $this->error_to_response( $single_request ); 2049 $responses[] = $this->envelope_response( $result, false )->get_data(); 2050 continue; 2051 } 2052 2053 $clean_request = clone $single_request; 2054 $clean_request->set_url_params( array() ); 2055 $clean_request->set_attributes( array() ); 2056 $clean_request->set_default_params( array() ); 2057 2058 /** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */ 2059 $result = apply_filters( 'rest_pre_dispatch', null, $this, $clean_request ); 2060 2061 if ( empty( $result ) ) { 2062 $match = $matches[ $i ]; 2063 $error = null; 2064 2065 if ( is_wp_error( $validation[ $i ] ) ) { 2066 $error = $validation[ $i ]; 2067 } 2068 2069 if ( is_wp_error( $match ) ) { 2070 $result = $this->error_to_response( $match ); 2071 } else { 2072 list( $route, $handler ) = $match; 2073 2074 if ( ! $error && ! is_callable( $handler['callback'] ) ) { 2075 $error = new WP_Error( 2076 'rest_invalid_handler', 2077 __( 'The handler for the route is invalid' ), 2078 array( 'status' => 500 ) 2079 ); 2080 } 2081 2082 $result = $this->respond_to_request( $single_request, $route, $handler, $error ); 2083 } 2084 } elseif ( ! is_wp_error( $matches[ $i ] ) ) { 2085 // The response matched no handler, so it follows the settings of the handler the request matched. 2086 $result = rest_ensure_response( $result ); 2087 if ( $result instanceof WP_REST_Response ) { 2088 $result->set_data_processing( $matches[ $i ][1]['data_processing'] ?? true ); 2089 } 2090 } 2091 2092 /** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */ 2093 $result = apply_filters( 'rest_post_dispatch', rest_ensure_response( $result ), $this, $single_request ); 2094 2095 $responses[] = $this->envelope_response( $result, false )->get_data(); 2096 } 2097 2098 return new WP_REST_Response( array( 'responses' => $responses ), WP_Http::MULTI_STATUS ); 2099 } 2100 2101 /** 2102 * Sends an HTTP status code. 2103 * 2104 * @since 4.4.0 2105 * 2106 * @param int $code HTTP status. 2107 * 2108 * @phpstan-param int<100, 599> $code 2109 */ 2110 protected function set_status( $code ) { 2111 status_header( $code ); 2112 } 2113 2114 /** 2115 * Sends an HTTP header. 2116 * 2117 * @since 4.4.0 2118 * 2119 * @param string $key Header key. 2120 * @param string $value Header value. 2121 */ 2122 public function send_header( $key, $value ) { 2123 /* 2124 * Sanitize as per RFC2616 (Section 4.2): 2125 * 2126 * Any LWS that occurs between field-content MAY be replaced with a 2127 * single SP before interpreting the field value or forwarding the 2128 * message downstream. 2129 */ 2130 $value = preg_replace( '/\s+/', ' ', $value ); 2131 header( sprintf( '%s: %s', $key, $value ) ); 2132 } 2133 2134 /** 2135 * Sends multiple HTTP headers. 2136 * 2137 * @since 4.4.0 2138 * 2139 * @param array<string, string> $headers Map of header name to header value. 2140 */ 2141 public function send_headers( $headers ) { 2142 foreach ( $headers as $key => $value ) { 2143 $this->send_header( $key, $value ); 2144 } 2145 } 2146 2147 /** 2148 * Removes an HTTP header from the current response. 2149 * 2150 * @since 4.8.0 2151 * 2152 * @param string $key Header key. 2153 */ 2154 public function remove_header( $key ) { 2155 header_remove( $key ); 2156 } 2157 2158 /** 2159 * Retrieves the raw request entity (body). 2160 * 2161 * @since 4.4.0 2162 * 2163 * @global string $HTTP_RAW_POST_DATA Raw post data. 2164 * 2165 * @return string Raw request data. 2166 */ 2167 public static function get_raw_data() { 2168 // phpcs:disable PHPCompatibility.Variables.RemovedPredefinedGlobalVariables.http_raw_post_dataDeprecatedRemoved 2169 global $HTTP_RAW_POST_DATA; 2170 2171 // $HTTP_RAW_POST_DATA was deprecated in PHP 5.6 and removed in PHP 7.0. 2172 if ( ! isset( $HTTP_RAW_POST_DATA ) ) { 2173 $HTTP_RAW_POST_DATA = file_get_contents( 'php://input' ); 2174 } 2175 2176 return $HTTP_RAW_POST_DATA; 2177 // phpcs:enable 2178 } 2179 2180 /** 2181 * Extracts headers from a PHP-style $_SERVER array. 2182 * 2183 * @since 4.4.0 2184 * 2185 * @param array $server Associative array similar to `$_SERVER`. 2186 * @return array Headers extracted from the input. 2187 */ 2188 public function get_headers( $server ) { 2189 $headers = array(); 2190 2191 // CONTENT_* headers are not prefixed with HTTP_. 2192 $additional = array( 2193 'CONTENT_LENGTH' => true, 2194 'CONTENT_MD5' => true, 2195 'CONTENT_TYPE' => true, 2196 ); 2197 2198 foreach ( $server as $key => $value ) { 2199 if ( str_starts_with( $key, 'HTTP_' ) ) { 2200 $headers[ substr( $key, 5 ) ] = $value; 2201 } elseif ( 'REDIRECT_HTTP_AUTHORIZATION' === $key && empty( $server['HTTP_AUTHORIZATION'] ) ) { 2202 /* 2203 * In some server configurations, the authorization header is passed in this alternate location. 2204 * Since it would not be passed in both places we do not check for both headers and resolve. 2205 */ 2206 $headers['AUTHORIZATION'] = $value; 2207 } elseif ( isset( $additional[ $key ] ) ) { 2208 $headers[ $key ] = $value; 2209 } 2210 } 2211 2212 return $headers; 2213 } 2214 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Mon Oct 26 08:20:34 2026 | Cross-referenced by PHPXref |