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