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