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