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