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