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