Ticket #33982: 33982.3.2.patch
| File 33982.3.2.patch, 126.8 KB (added by , 10 years ago) |
|---|
-
src/wp-includes/default-filters.php
204 204 205 205 add_filter( 'http_request_host_is_external', 'allowed_http_request_hosts', 10, 2 ); 206 206 207 // REST API filters. 208 add_action( 'xmlrpc_rsd_apis', 'rest_output_rsd' ); 209 add_action( 'wp_head', 'rest_output_link_wp_head', 10, 0 ); 210 add_action( 'template_redirect', 'rest_output_link_header', 11, 0 ); 211 add_action( 'auth_cookie_malformed', 'rest_cookie_collect_status' ); 212 add_action( 'auth_cookie_expired', 'rest_cookie_collect_status' ); 213 add_action( 'auth_cookie_bad_username', 'rest_cookie_collect_status' ); 214 add_action( 'auth_cookie_bad_hash', 'rest_cookie_collect_status' ); 215 add_action( 'auth_cookie_valid', 'rest_cookie_collect_status' ); 216 add_filter( 'rest_authentication_errors', 'rest_cookie_check_errors', 100 ); 217 207 218 // Actions 208 219 add_action( 'wp_head', '_wp_render_title_tag', 1 ); 209 220 add_action( 'wp_head', 'wp_enqueue_scripts', 1 ); … … 347 358 add_action( 'register_new_user', 'wp_send_new_user_notifications' ); 348 359 add_action( 'edit_user_created_user', 'wp_send_new_user_notifications' ); 349 360 361 // REST API actions. 362 add_action( 'init', 'rest_api_init' ); 363 add_action( 'rest_api_init', 'rest_api_default_filters', 10, 1 ); 364 add_action( 'parse_request', 'rest_api_loaded' ); 365 350 366 /** 351 367 * Filters formerly mixed into wp-includes 352 368 */ -
src/wp-includes/rest-api/lib/class-wp-rest-request.php
1 <?php 2 /** 3 * REST API: WP_REST_Request class 4 * 5 * @package WordPress 6 * @subpackage REST_API 7 * @since 4.4.0 8 */ 9 10 /** 11 * Core class used to implement a REST request object. 12 * 13 * Contains data from the request, to be passed to the callback. 14 * 15 * Note: This implements ArrayAccess, and acts as an array of parameters when 16 * used in that manner. It does not use ArrayObject (as we cannot rely on SPL), 17 * so be aware it may have non-array behaviour in some cases. 18 * 19 * @since 4.4.0 20 * 21 * @see ArrayAccess 22 */ 23 class WP_REST_Request implements ArrayAccess { 24 25 /** 26 * HTTP method. 27 * 28 * @since 4.4.0 29 * @access protected 30 * @var string 31 */ 32 protected $method = ''; 33 34 /** 35 * Parameters passed to the request. 36 * 37 * These typically come from the `$_GET`, `$_POST` and `$_FILES` 38 * superglobals when being created from the global scope. 39 * 40 * @since 4.4.0 41 * @access protected 42 * @var array Contains GET, POST and FILES keys mapping to arrays of data. 43 */ 44 protected $params; 45 46 /** 47 * HTTP headers for the request. 48 * 49 * @since 4.4.0 50 * @access protected 51 * @var array Map of key to value. Key is always lowercase, as per HTTP specification. 52 */ 53 protected $headers = array(); 54 55 /** 56 * Body data. 57 * 58 * @since 4.4.0 59 * @access protected 60 * @var string Binary data from the request. 61 */ 62 protected $body = null; 63 64 /** 65 * Route matched for the request. 66 * 67 * @since 4.4.0 68 * @access protected 69 * @var string 70 */ 71 protected $route; 72 73 /** 74 * Attributes (options) for the route that was matched. 75 * 76 * This is the options array used when the route was registered, typically 77 * containing the callback as well as the valid methods for the route. 78 * 79 * @since 4.4.0 80 * @access protected 81 * @var array Attributes for the request. 82 */ 83 protected $attributes = array(); 84 85 /** 86 * Used to determine if the JSON data has been parsed yet. 87 * 88 * Allows lazy-parsing of JSON data where possible. 89 * 90 * @since 4.4.0 91 * @access protected 92 * @var bool 93 */ 94 protected $parsed_json = false; 95 96 /** 97 * Used to determine if the body data has been parsed yet. 98 * 99 * @since 4.4.0 100 * @access protected 101 * @var bool 102 */ 103 protected $parsed_body = false; 104 105 /** 106 * Constructor. 107 * 108 * @since 4.4.0 109 * @access public 110 * 111 * @param string $method Optional. Request method. Default empty. 112 * @param string $route Optional. Request route. Default empty. 113 * @param array $attributes Optional. Request attributes. Default empty array. 114 */ 115 public function __construct( $method = '', $route = '', $attributes = array() ) { 116 $this->params = array( 117 'URL' => array(), 118 'GET' => array(), 119 'POST' => array(), 120 'FILES' => array(), 121 122 // See parse_json_params. 123 'JSON' => null, 124 125 'defaults' => array(), 126 ); 127 128 $this->set_method( $method ); 129 $this->set_route( $route ); 130 $this->set_attributes( $attributes ); 131 } 132 133 /** 134 * Retrieves the HTTP method for the request. 135 * 136 * @since 4.4.0 137 * @access public 138 * 139 * @return string HTTP method. 140 */ 141 public function get_method() { 142 return $this->method; 143 } 144 145 /** 146 * Sets HTTP method for the request. 147 * 148 * @since 4.4.0 149 * @access public 150 * 151 * @param string $method HTTP method. 152 */ 153 public function set_method( $method ) { 154 $this->method = strtoupper( $method ); 155 } 156 157 /** 158 * Retrieves all headers from the request. 159 * 160 * @since 4.4.0 161 * @access public 162 * 163 * @return array Map of key to value. Key is always lowercase, as per HTTP specification. 164 */ 165 public function get_headers() { 166 return $this->headers; 167 } 168 169 /** 170 * Canonicalizes the header name. 171 * 172 * Ensures that header names are always treated the same regardless of 173 * source. Header names are always case insensitive. 174 * 175 * Note that we treat `-` (dashes) and `_` (underscores) as the same 176 * character, as per header parsing rules in both Apache and nginx. 177 * 178 * @link http://stackoverflow.com/q/18185366 179 * @link http://wiki.nginx.org/Pitfalls#Missing_.28disappearing.29_HTTP_headers 180 * @link http://nginx.org/en/docs/http/ngx_http_core_module.html#underscores_in_headers 181 * 182 * @since 4.4.0 183 * @access public 184 * @static 185 * 186 * @param string $key Header name. 187 * @return string Canonicalized name. 188 */ 189 public static function canonicalize_header_name( $key ) { 190 $key = strtolower( $key ); 191 $key = str_replace( '-', '_', $key ); 192 193 return $key; 194 } 195 196 /** 197 * Retrieves the given header from the request. 198 * 199 * If the header has multiple values, they will be concatenated with a comma 200 * as per the HTTP specification. Be aware that some non-compliant headers 201 * (notably cookie headers) cannot be joined this way. 202 * 203 * @since 4.4.0 204 * @access public 205 * 206 * @param string $key Header name, will be canonicalized to lowercase. 207 * @return string|null String value if set, null otherwise. 208 */ 209 public function get_header( $key ) { 210 $key = $this->canonicalize_header_name( $key ); 211 212 if ( ! isset( $this->headers[ $key ] ) ) { 213 return null; 214 } 215 216 return implode( ',', $this->headers[ $key ] ); 217 } 218 219 /** 220 * Retrieves header values from the request. 221 * 222 * @since 4.4.0 223 * @access public 224 * 225 * @param string $key Header name, will be canonicalized to lowercase. 226 * @return array|null List of string values if set, null otherwise. 227 */ 228 public function get_header_as_array( $key ) { 229 $key = $this->canonicalize_header_name( $key ); 230 231 if ( ! isset( $this->headers[ $key ] ) ) { 232 return null; 233 } 234 235 return $this->headers[ $key ]; 236 } 237 238 /** 239 * Sets the header on request. 240 * 241 * @since 4.4.0 242 * @access public 243 * 244 * @param string $key Header name. 245 * @param string $value Header value, or list of values. 246 */ 247 public function set_header( $key, $value ) { 248 $key = $this->canonicalize_header_name( $key ); 249 $value = (array) $value; 250 251 $this->headers[ $key ] = $value; 252 } 253 254 /** 255 * Appends a header value for the given header. 256 * 257 * @since 4.4.0 258 * @access public 259 * 260 * @param string $key Header name. 261 * @param string $value Header value, or list of values. 262 */ 263 public function add_header( $key, $value ) { 264 $key = $this->canonicalize_header_name( $key ); 265 $value = (array) $value; 266 267 if ( ! isset( $this->headers[ $key ] ) ) { 268 $this->headers[ $key ] = array(); 269 } 270 271 $this->headers[ $key ] = array_merge( $this->headers[ $key ], $value ); 272 } 273 274 /** 275 * Removes all values for a header. 276 * 277 * @since 4.4.0 278 * @access public 279 * 280 * @param string $key Header name. 281 */ 282 public function remove_header( $key ) { 283 unset( $this->headers[ $key ] ); 284 } 285 286 /** 287 * Sets headers on the request. 288 * 289 * @since 4.4.0 290 * @access public 291 * 292 * @param array $headers Map of header name to value. 293 * @param bool $override If true, replace the request's headers. Otherwise, merge with existing. 294 */ 295 public function set_headers( $headers, $override = true ) { 296 if ( true === $override ) { 297 $this->headers = array(); 298 } 299 300 foreach ( $headers as $key => $value ) { 301 $this->set_header( $key, $value ); 302 } 303 } 304 305 /** 306 * Retrieves the content-type of the request. 307 * 308 * @since 4.4.0 309 * @access public 310 * 311 * @return array Map containing 'value' and 'parameters' keys. 312 */ 313 public function get_content_type() { 314 $value = $this->get_header( 'content-type' ); 315 if ( empty( $value ) ) { 316 return null; 317 } 318 319 $parameters = ''; 320 if ( strpos( $value, ';' ) ) { 321 list( $value, $parameters ) = explode( ';', $value, 2 ); 322 } 323 324 $value = strtolower( $value ); 325 if ( strpos( $value, '/' ) === false ) { 326 return null; 327 } 328 329 // Parse type and subtype out. 330 list( $type, $subtype ) = explode( '/', $value, 2 ); 331 332 $data = compact( 'value', 'type', 'subtype', 'parameters' ); 333 $data = array_map( 'trim', $data ); 334 335 return $data; 336 } 337 338 /** 339 * Retrieves the parameter priority order. 340 * 341 * Used when checking parameters in get_param(). 342 * 343 * @since 4.4.0 344 * @access public 345 * 346 * @return array List of types to check, in order of priority. 347 */ 348 protected function get_parameter_order() { 349 $order = array(); 350 $order[] = 'JSON'; 351 352 $this->parse_json_params(); 353 354 // Ensure we parse the body data. 355 $body = $this->get_body(); 356 if ( $this->method !== 'POST' && ! empty( $body ) ) { 357 $this->parse_body_params(); 358 } 359 360 $accepts_body_data = array( 'POST', 'PUT', 'PATCH' ); 361 if ( in_array( $this->method, $accepts_body_data ) ) { 362 $order[] = 'POST'; 363 } 364 365 $order[] = 'GET'; 366 $order[] = 'URL'; 367 $order[] = 'defaults'; 368 369 /** 370 * Filter the parameter order. 371 * 372 * The order affects which parameters are checked when using get_param() and family. 373 * This acts similarly to PHP's `request_order` setting. 374 * 375 * @since 4.4.0 376 * 377 * @param array $order { 378 * An array of types to check, in order of priority. 379 * 380 * @param string $type The type to check. 381 * } 382 * @param WP_REST_Request $this The request object. 383 */ 384 return apply_filters( 'rest_request_parameter_order', $order, $this ); 385 } 386 387 /** 388 * Retrieves a parameter from the request. 389 * 390 * @since 4.4.0 391 * @access public 392 * 393 * @param string $key Parameter name. 394 * @return mixed|null Value if set, null otherwise. 395 */ 396 public function get_param( $key ) { 397 $order = $this->get_parameter_order(); 398 399 foreach ( $order as $type ) { 400 // Determine if we have the parameter for this type. 401 if ( isset( $this->params[ $type ][ $key ] ) ) { 402 return $this->params[ $type ][ $key ]; 403 } 404 } 405 406 return null; 407 } 408 409 /** 410 * Sets a parameter on the request. 411 * 412 * @since 4.4.0 413 * @access public 414 * 415 * @param string $key Parameter name. 416 * @param mixed $value Parameter value. 417 */ 418 public function set_param( $key, $value ) { 419 switch ( $this->method ) { 420 case 'POST': 421 $this->params['POST'][ $key ] = $value; 422 break; 423 424 default: 425 $this->params['GET'][ $key ] = $value; 426 break; 427 } 428 } 429 430 /** 431 * Retrieves merged parameters from the request. 432 * 433 * The equivalent of get_param(), but returns all parameters for the request. 434 * Handles merging all the available values into a single array. 435 * 436 * @since 4.4.0 437 * @access public 438 * 439 * @return array Map of key to value. 440 */ 441 public function get_params() { 442 $order = $this->get_parameter_order(); 443 $order = array_reverse( $order, true ); 444 445 $params = array(); 446 foreach ( $order as $type ) { 447 $params = array_merge( $params, (array) $this->params[ $type ] ); 448 } 449 450 return $params; 451 } 452 453 /** 454 * Retrieves parameters from the route itself. 455 * 456 * These are parsed from the URL using the regex. 457 * 458 * @since 4.4.0 459 * @access public 460 * 461 * @return array Parameter map of key to value. 462 */ 463 public function get_url_params() { 464 return $this->params['URL']; 465 } 466 467 /** 468 * Sets parameters from the route. 469 * 470 * Typically, this is set after parsing the URL. 471 * 472 * @since 4.4.0 473 * @access public 474 * 475 * @param array $params Parameter map of key to value. 476 */ 477 public function set_url_params( $params ) { 478 $this->params['URL'] = $params; 479 } 480 481 /** 482 * Retrieves parameters from the query string. 483 * 484 * These are the parameters you'd typically find in `$_GET`. 485 * 486 * @since 4.4.0 487 * @access public 488 * 489 * @return array Parameter map of key to value 490 */ 491 public function get_query_params() { 492 return $this->params['GET']; 493 } 494 495 /** 496 * Sets parameters from the query string. 497 * 498 * Typically, this is set from `$_GET`. 499 * 500 * @since 4.4.0 501 * @access public 502 * 503 * @param array $params Parameter map of key to value. 504 */ 505 public function set_query_params( $params ) { 506 $this->params['GET'] = $params; 507 } 508 509 /** 510 * Retrieves parameters from the body. 511 * 512 * These are the parameters you'd typically find in `$_POST`. 513 * 514 * @since 4.4.0 515 * @access public 516 * 517 * @return array Parameter map of key to value. 518 */ 519 public function get_body_params() { 520 return $this->params['POST']; 521 } 522 523 /** 524 * Sets parameters from the body. 525 * 526 * Typically, this is set from `$_POST`. 527 * 528 * @since 4.4.0 529 * @access public 530 * 531 * @param array $params Parameter map of key to value. 532 */ 533 public function set_body_params( $params ) { 534 $this->params['POST'] = $params; 535 } 536 537 /** 538 * Retrieves multipart file parameters from the body. 539 * 540 * These are the parameters you'd typically find in `$_FILES`. 541 * 542 * @since 4.4.0 543 * @access public 544 * 545 * @return array Parameter map of key to value 546 */ 547 public function get_file_params() { 548 return $this->params['FILES']; 549 } 550 551 /** 552 * Sets multipart file parameters from the body. 553 * 554 * Typically, this is set from `$_FILES`. 555 * 556 * @since 4.4.0 557 * @access public 558 * 559 * @param array $params Parameter map of key to value. 560 */ 561 public function set_file_params( $params ) { 562 $this->params['FILES'] = $params; 563 } 564 565 /** 566 * Retrieves the default parameters. 567 * 568 * These are the parameters set in the route registration. 569 * 570 * @since 4.4.0 571 * @access public 572 * 573 * @return array Parameter map of key to value 574 */ 575 public function get_default_params() { 576 return $this->params['defaults']; 577 } 578 579 /** 580 * Sets default parameters. 581 * 582 * These are the parameters set in the route registration. 583 * 584 * @since 4.4.0 585 * @access public 586 * 587 * @param array $params Parameter map of key to value. 588 */ 589 public function set_default_params( $params ) { 590 $this->params['defaults'] = $params; 591 } 592 593 /** 594 * Retrieves the request body content. 595 * 596 * @since 4.4.0 597 * @access public 598 * 599 * @return string Binary data from the request body. 600 */ 601 public function get_body() { 602 return $this->body; 603 } 604 605 /** 606 * Sets body content. 607 * 608 * @since 4.4.0 609 * @access public 610 * 611 * @param string $data Binary data from the request body. 612 */ 613 public function set_body( $data ) { 614 $this->body = $data; 615 616 // Enable lazy parsing. 617 $this->parsed_json = false; 618 $this->parsed_body = false; 619 $this->params['JSON'] = null; 620 } 621 622 /** 623 * Retrieves the parameters from a JSON-formatted body. 624 * 625 * @since 4.4.0 626 * @access public 627 * 628 * @return array Parameter map of key to value. 629 */ 630 public function get_json_params() { 631 // Ensure the parameters have been parsed out. 632 $this->parse_json_params(); 633 634 return $this->params['JSON']; 635 } 636 637 /** 638 * Parses the JSON parameters. 639 * 640 * Avoids parsing the JSON data until we need to access it. 641 * 642 * @since 4.4.0 643 * @access protected 644 */ 645 protected function parse_json_params() { 646 if ( $this->parsed_json ) { 647 return; 648 } 649 650 $this->parsed_json = true; 651 652 // Check that we actually got JSON. 653 $content_type = $this->get_content_type(); 654 655 if ( empty( $content_type ) || 'application/json' !== $content_type['value'] ) { 656 return; 657 } 658 659 $params = json_decode( $this->get_body(), true ); 660 661 /* 662 * Check for a parsing error. 663 * 664 * Note that due to WP's JSON compatibility functions, json_last_error 665 * might not be defined: https://core.trac.wordpress.org/ticket/27799 666 */ 667 if ( null === $params && ( ! function_exists( 'json_last_error' ) || JSON_ERROR_NONE !== json_last_error() ) ) { 668 return; 669 } 670 671 $this->params['JSON'] = $params; 672 } 673 674 /** 675 * Parses the request body parameters. 676 * 677 * Parses out URL-encoded bodies for request methods that aren't supported 678 * natively by PHP. In PHP 5.x, only POST has these parsed automatically. 679 * 680 * @since 4.4.0 681 * @access protected 682 */ 683 protected function parse_body_params() { 684 if ( $this->parsed_body ) { 685 return; 686 } 687 688 $this->parsed_body = true; 689 690 /* 691 * Check that we got URL-encoded. Treat a missing content-type as 692 * URL-encoded for maximum compatibility. 693 */ 694 $content_type = $this->get_content_type(); 695 696 if ( ! empty( $content_type ) && 'application/x-www-form-urlencoded' !== $content_type['value'] ) { 697 return; 698 } 699 700 parse_str( $this->get_body(), $params ); 701 702 /* 703 * Amazingly, parse_str follows magic quote rules. Sigh. 704 * 705 * NOTE: Do not refactor to use `wp_unslash`. 706 */ 707 if ( get_magic_quotes_gpc() ) { 708 $params = stripslashes_deep( $params ); 709 } 710 711 /* 712 * Add to the POST parameters stored internally. If a user has already 713 * set these manually (via `set_body_params`), don't override them. 714 */ 715 $this->params['POST'] = array_merge( $params, $this->params['POST'] ); 716 } 717 718 /** 719 * Retrieves the route that matched the request. 720 * 721 * @since 4.4.0 722 * @access public 723 * 724 * @return string Route matching regex. 725 */ 726 public function get_route() { 727 return $this->route; 728 } 729 730 /** 731 * Sets the route that matched the request. 732 * 733 * @since 4.4.0 734 * @access public 735 * 736 * @param string $route Route matching regex. 737 */ 738 public function set_route( $route ) { 739 $this->route = $route; 740 } 741 742 /** 743 * Retrieves the attributes for the request. 744 * 745 * These are the options for the route that was matched. 746 * 747 * @since 4.4.0 748 * @access public 749 * 750 * @return array Attributes for the request. 751 */ 752 public function get_attributes() { 753 return $this->attributes; 754 } 755 756 /** 757 * Sets the attributes for the request. 758 * 759 * @since 4.4.0 760 * @access public 761 * 762 * @param array $attributes Attributes for the request. 763 */ 764 public function set_attributes( $attributes ) { 765 $this->attributes = $attributes; 766 } 767 768 /** 769 * Sanitizes (where possible) the params on the request. 770 * 771 * This is primarily based off the sanitize_callback param on each registered 772 * argument. 773 * 774 * @since 4.4.0 775 * @access public 776 * 777 * @return true|null True if there are no parameters to sanitize, null otherwise. 778 */ 779 public function sanitize_params() { 780 781 $attributes = $this->get_attributes(); 782 783 // No arguments set, skip sanitizing. 784 if ( empty( $attributes['args'] ) ) { 785 return true; 786 } 787 788 $order = $this->get_parameter_order(); 789 790 foreach ( $order as $type ) { 791 if ( empty( $this->params[ $type ] ) ) { 792 continue; 793 } 794 foreach ( $this->params[ $type ] as $key => $value ) { 795 // Check if this param has a sanitize_callback added. 796 if ( isset( $attributes['args'][ $key ] ) && ! empty( $attributes['args'][ $key ]['sanitize_callback'] ) ) { 797 $this->params[ $type ][ $key ] = call_user_func( $attributes['args'][ $key ]['sanitize_callback'], $value, $this, $key ); 798 } 799 } 800 } 801 return null; 802 } 803 804 /** 805 * Checks whether this request is valid according to its attributes. 806 * 807 * @since 4.4.0 808 * @access public 809 * 810 * @return bool|WP_Error True if there are no parameters to validate or if all pass validation, 811 * WP_Error if required parameters are missing. 812 */ 813 public function has_valid_params() { 814 815 $attributes = $this->get_attributes(); 816 $required = array(); 817 818 // No arguments set, skip validation. 819 if ( empty( $attributes['args'] ) ) { 820 return true; 821 } 822 823 foreach ( $attributes['args'] as $key => $arg ) { 824 825 $param = $this->get_param( $key ); 826 if ( isset( $arg['required'] ) && true === $arg['required'] && null === $param ) { 827 $required[] = $key; 828 } 829 } 830 831 if ( ! empty( $required ) ) { 832 return new WP_Error( 'rest_missing_callback_param', sprintf( __( 'Missing parameter(s): %s' ), implode( ', ', $required ) ), array( 'status' => 400, 'params' => $required ) ); 833 } 834 835 /* 836 * Check the validation callbacks for each registered arg. 837 * 838 * This is done after required checking as required checking is cheaper. 839 */ 840 $invalid_params = array(); 841 842 foreach ( $attributes['args'] as $key => $arg ) { 843 844 $param = $this->get_param( $key ); 845 846 if ( null !== $param && ! empty( $arg['validate_callback'] ) ) { 847 $valid_check = call_user_func( $arg['validate_callback'], $param, $this, $key ); 848 849 if ( false === $valid_check ) { 850 $invalid_params[ $key ] = __( 'Invalid param.' ); 851 } 852 853 if ( is_wp_error( $valid_check ) ) { 854 $invalid_params[] = sprintf( '%s (%s)', $key, $valid_check->get_error_message() ); 855 } 856 } 857 } 858 859 if ( $invalid_params ) { 860 return new WP_Error( 'rest_invalid_param', sprintf( __( 'Invalid parameter(s): %s' ), implode( ', ', $invalid_params ) ), array( 'status' => 400, 'params' => $invalid_params ) ); 861 } 862 863 return true; 864 865 } 866 867 /** 868 * Checks if a parameter is set. 869 * 870 * @since 4.4.0 871 * @access public 872 * 873 * @param string $offset Parameter name. 874 * @return bool Whether the parameter is set. 875 */ 876 public function offsetExists( $offset ) { 877 $order = $this->get_parameter_order(); 878 879 foreach ( $order as $type ) { 880 if ( isset( $this->params[ $type ][ $offset ] ) ) { 881 return true; 882 } 883 } 884 885 return false; 886 } 887 888 /** 889 * Retrieves a parameter from the request. 890 * 891 * @since 4.4.0 892 * @access public 893 * 894 * @param string $offset Parameter name. 895 * @return mixed|null Value if set, null otherwise. 896 */ 897 public function offsetGet( $offset ) { 898 return $this->get_param( $offset ); 899 } 900 901 /** 902 * Sets a parameter on the request. 903 * 904 * @since 4.4.0 905 * @access public 906 * 907 * @param string $offset Parameter name. 908 * @param mixed $value Parameter value. 909 */ 910 public function offsetSet( $offset, $value ) { 911 $this->set_param( $offset, $value ); 912 } 913 914 /** 915 * Removes a parameter from the request. 916 * 917 * @since 4.4.0 918 * @access public 919 * 920 * @param string $offset Parameter name. 921 */ 922 public function offsetUnset( $offset ) { 923 $order = $this->get_parameter_order(); 924 925 // Remove the offset from every group. 926 foreach ( $order as $type ) { 927 unset( $this->params[ $type ][ $offset ] ); 928 } 929 } 930 } -
src/wp-includes/rest-api/lib/class-wp-rest-response.php
1 <?php 2 /** 3 * REST API: WP_REST_Response class 4 * 5 * @package WordPress 6 * @subpackage REST_API 7 * @since 4.4.0 8 */ 9 10 /** 11 * Core class used to implement a REST response object. 12 * 13 * @since 4.4.0 14 * 15 * @see WP_HTTP_Response 16 */ 17 class WP_REST_Response extends WP_HTTP_Response { 18 19 /** 20 * Links related to the response. 21 * 22 * @since 4.4.0 23 * @access protected 24 * @var array 25 */ 26 protected $links = array(); 27 28 /** 29 * The route that was to create the response. 30 * 31 * @since 4.4.0 32 * @access protected 33 * @var string 34 */ 35 protected $matched_route = ''; 36 37 /** 38 * The handler that was used to create the response. 39 * 40 * @since 4.4.0 41 * @access protected 42 * @var null|array 43 */ 44 protected $matched_handler = null; 45 46 /** 47 * Adds a link to the response. 48 * 49 * @internal The $rel parameter is first, as this looks nicer when sending multiple. 50 * 51 * @since 4.4.0 52 * @access public 53 * 54 * @link http://tools.ietf.org/html/rfc5988 55 * @link http://www.iana.org/assignments/link-relations/link-relations.xml 56 * 57 * @param string $rel Link relation. Either an IANA registered type, 58 * or an absolute URL. 59 * @param string $href Target URI for the link. 60 * @param array $attributes Optional. Link parameters to send along with the URL. Default empty array. 61 */ 62 public function add_link( $rel, $href, $attributes = array() ) { 63 if ( empty( $this->links[ $rel ] ) ) { 64 $this->links[ $rel ] = array(); 65 } 66 67 if ( isset( $attributes['href'] ) ) { 68 // Remove the href attribute, as it's used for the main URL. 69 unset( $attributes['href'] ); 70 } 71 72 $this->links[ $rel ][] = array( 73 'href' => $href, 74 'attributes' => $attributes, 75 ); 76 } 77 78 /** 79 * Removes a link from the response. 80 * 81 * @since 4.4.0 82 * @access public 83 * 84 * @param string $rel Link relation. Either an IANA registered type, or an absolute URL. 85 * @param string $href Optional. Only remove links for the relation matching the given href. 86 * Default null. 87 */ 88 public function remove_link( $rel, $href = null ) { 89 if ( ! isset( $this->links[ $rel ] ) ) { 90 return; 91 } 92 93 if ( $href ) { 94 $this->links[ $rel ] = wp_list_filter( $this->links[ $rel ], array( 'href' => $href ), 'NOT' ); 95 } else { 96 $this->links[ $rel ] = array(); 97 } 98 99 if ( ! $this->links[ $rel ] ) { 100 unset( $this->links[ $rel ] ); 101 } 102 } 103 104 /** 105 * Adds multiple links to the response. 106 * 107 * Link data should be an associative array with link relation as the key. 108 * The value can either be an associative array of link attributes 109 * (including `href` with the URL for the response), or a list of these 110 * associative arrays. 111 * 112 * @since 4.4.0 113 * @access public 114 * 115 * @param array $links Map of link relation to list of links. 116 */ 117 public function add_links( $links ) { 118 foreach ( $links as $rel => $set ) { 119 // If it's a single link, wrap with an array for consistent handling. 120 if ( isset( $set['href'] ) ) { 121 $set = array( $set ); 122 } 123 124 foreach ( $set as $attributes ) { 125 $this->add_link( $rel, $attributes['href'], $attributes ); 126 } 127 } 128 } 129 130 /** 131 * Retrieves links for the response. 132 * 133 * @since 4.4.0 134 * @access public 135 * 136 * @return array List of links. 137 */ 138 public function get_links() { 139 return $this->links; 140 } 141 142 /** 143 * Sets a single link header. 144 * 145 * @internal The $rel parameter is first, as this looks nicer when sending multiple. 146 * 147 * @since 4.4.0 148 * @access public 149 * 150 * @link http://tools.ietf.org/html/rfc5988 151 * @link http://www.iana.org/assignments/link-relations/link-relations.xml 152 * 153 * @param string $rel Link relation. Either an IANA registered type, or an absolute URL. 154 * @param string $link Target IRI for the link. 155 * @param array $other Optional. Other parameters to send, as an assocative array. 156 * Default empty array. 157 */ 158 public function link_header( $rel, $link, $other = array() ) { 159 $header = '<' . $link . '>; rel="' . $rel . '"'; 160 161 foreach ( $other as $key => $value ) { 162 if ( 'title' === $key ) { 163 $value = '"' . $value . '"'; 164 } 165 $header .= '; ' . $key . '=' . $value; 166 } 167 $this->header( 'Link', $header, false ); 168 } 169 170 /** 171 * Retrieves the route that was used. 172 * 173 * @since 4.4.0 174 * @access public 175 * 176 * @return string The matched route. 177 */ 178 public function get_matched_route() { 179 return $this->matched_route; 180 } 181 182 /** 183 * Sets the route (regex for path) that caused the response. 184 * 185 * @since 4.4.0 186 * @access public 187 * 188 * @param string $route Route name. 189 */ 190 public function set_matched_route( $route ) { 191 $this->matched_route = $route; 192 } 193 194 /** 195 * Retrieves the handler that was used to generate the response. 196 * 197 * @since 4.4.0 198 * @access public 199 * 200 * @return null|array The handler that was used to create the response. 201 */ 202 public function get_matched_handler() { 203 return $this->matched_handler; 204 } 205 206 /** 207 * Retrieves the handler that was responsible for generating the response. 208 * 209 * @since 4.4.0 210 * @access public 211 * 212 * @param array $handler The matched handler. 213 */ 214 public function set_matched_handler( $handler ) { 215 $this->matched_handler = $handler; 216 } 217 218 /** 219 * Checks if the response is an error, i.e. >= 400 response code. 220 * 221 * @since 4.4.0 222 * @access public 223 * 224 * @return bool Whether the response is an error. 225 */ 226 public function is_error() { 227 return $this->get_status() >= 400; 228 } 229 230 /** 231 * Retrieves a WP_Error object from the response. 232 * 233 * @since 4.4.0 234 * @access public 235 * 236 * @return WP_Error|null WP_Error or null on not an errored response. 237 */ 238 public function as_error() { 239 if ( ! $this->is_error() ) { 240 return null; 241 } 242 243 $error = new WP_Error; 244 245 if ( is_array( $this->get_data() ) ) { 246 foreach ( $this->get_data() as $err ) { 247 $error->add( $err['code'], $err['message'], $err['data'] ); 248 } 249 } else { 250 $error->add( $this->get_status(), '', array( 'status' => $this->get_status() ) ); 251 } 252 253 return $error; 254 } 255 } -
src/wp-includes/rest-api/lib/class-wp-rest-server.php
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 /** Admin bootstrap */ 11 require_once( ABSPATH . 'wp-admin/includes/admin.php' ); 12 13 /** 14 * Core class used to implement the WordPress REST API server. 15 * 16 * @since 4.4.0 17 */ 18 class WP_REST_Server { 19 20 /** 21 * GET transport method. 22 * 23 * @since 4.4.0 24 * @var string 25 */ 26 const METHOD_GET = 'GET'; 27 28 /** 29 * POST transport method. 30 * 31 * @since 4.4.0 32 * @var string 33 */ 34 const METHOD_POST = 'POST'; 35 36 /** 37 * PUT transport method. 38 * 39 * @since 4.4.0 40 * @var string 41 */ 42 const METHOD_PUT = 'PUT'; 43 44 /** 45 * PATCH transport method. 46 * 47 * @since 4.4.0 48 * @var string 49 */ 50 const METHOD_PATCH = 'PATCH'; 51 52 /** 53 * DELETE transport method. 54 * 55 * @since 4.4.0 56 * @var string 57 */ 58 const METHOD_DELETE = 'DELETE'; 59 60 /** 61 * Alias for GET transport method. 62 * 63 * @since 4.4.0 64 * @var string 65 */ 66 const READABLE = 'GET'; 67 68 /** 69 * Alias for POST transport method. 70 * 71 * @since 4.4.0 72 * @var string 73 */ 74 const CREATABLE = 'POST'; 75 76 /** 77 * Alias for POST, PUT, PATCH transport methods together. 78 * 79 * @since 4.4.0 80 * @var string 81 */ 82 const EDITABLE = 'POST, PUT, PATCH'; 83 84 /** 85 * Alias for DELETE transport method. 86 * 87 * @since 4.4.0 88 * @var string 89 */ 90 const DELETABLE = 'DELETE'; 91 92 /** 93 * Alias for GET, POST, PUT, PATCH & DELETE transport methods together. 94 * 95 * @since 4.4.0 96 * @var string 97 */ 98 const ALLMETHODS = 'GET, POST, PUT, PATCH, DELETE'; 99 100 /** 101 * Does the endpoint accept raw JSON entities? 102 * 103 * @since 4.4.0 104 * @var int 105 */ 106 const ACCEPT_RAW = 64; 107 108 /** 109 * Does the endpoint accept encoded JSON? 110 * 111 * @since 4.4.0 112 * @var int 113 */ 114 const ACCEPT_JSON = 128; 115 116 /** 117 * Should we hide this endpoint from the index? 118 * 119 * @since 4.4.0 120 * @var int 121 */ 122 const HIDDEN_ENDPOINT = 256; 123 124 /** 125 * Maps HTTP verbs to constants. 126 * 127 * @since 4.4.0 128 * @access public 129 * @static 130 * @var array 131 */ 132 public static $method_map = array( 133 'HEAD' => self::METHOD_GET, 134 'GET' => self::METHOD_GET, 135 'POST' => self::METHOD_POST, 136 'PUT' => self::METHOD_PUT, 137 'PATCH' => self::METHOD_PATCH, 138 'DELETE' => self::METHOD_DELETE, 139 ); 140 141 /** 142 * Namespaces registered to the server. 143 * 144 * @since 4.4.0 145 * @access protected 146 * @var array 147 */ 148 protected $namespaces = array(); 149 150 /** 151 * Endpoints registered to the server. 152 * 153 * @since 4.4.0 154 * @access protected 155 * @var array 156 */ 157 protected $endpoints = array(); 158 159 /** 160 * Options defined for the routes. 161 * 162 * @since 4.4.0 163 * @access protected 164 * @var array 165 */ 166 protected $route_options = array(); 167 168 /** 169 * Instantiates the REST server. 170 * 171 * @since 4.4.0 172 * @access public 173 */ 174 public function __construct() { 175 $this->endpoints = array( 176 // Meta endpoints. 177 '/' => array( 178 'callback' => array( $this, 'get_index' ), 179 'methods' => 'GET', 180 'args' => array( 181 'context' => array( 182 'default' => 'view', 183 ), 184 ), 185 ), 186 ); 187 } 188 189 190 /** 191 * Checks the authentication headers if supplied. 192 * 193 * @since 4.4.0 194 * @access public 195 * 196 * @return WP_Error|null WP_Error indicates unsuccessful login, null indicates successful 197 * or no authentication provided 198 */ 199 public function check_authentication() { 200 /** 201 * Pass an authentication error to the API 202 * 203 * This is used to pass a WP_Error from an authentication method back to 204 * the API. 205 * 206 * Authentication methods should check first if they're being used, as 207 * multiple authentication methods can be enabled on a site (cookies, 208 * HTTP basic auth, OAuth). If the authentication method hooked in is 209 * not actually being attempted, null should be returned to indicate 210 * another authentication method should check instead. Similarly, 211 * callbacks should ensure the value is `null` before checking for 212 * errors. 213 * 214 * A WP_Error instance can be returned if an error occurs, and this should 215 * match the format used by API methods internally (that is, the `status` 216 * data should be used). A callback can return `true` to indicate that 217 * the authentication method was used, and it succeeded. 218 * 219 * @since 4.4.0 220 * 221 * @param WP_Error|null|bool WP_Error if authentication error, null if authentication 222 * method wasn't used, true if authentication succeeded. 223 */ 224 return apply_filters( 'rest_authentication_errors', null ); 225 } 226 227 /** 228 * Converts an error to a response object. 229 * 230 * This iterates over all error codes and messages to change it into a flat 231 * array. This enables simpler client behaviour, as it is represented as a 232 * list in JSON rather than an object/map. 233 * 234 * @since 4.4.0 235 * @access protected 236 * 237 * @param WP_Error $error WP_Error instance. 238 * @return WP_REST_Response List of associative arrays with code and message keys. 239 */ 240 protected function error_to_response( $error ) { 241 $error_data = $error->get_error_data(); 242 243 if ( is_array( $error_data ) && isset( $error_data['status'] ) ) { 244 $status = $error_data['status']; 245 } else { 246 $status = 500; 247 } 248 249 $data = array(); 250 251 foreach ( (array) $error->errors as $code => $messages ) { 252 foreach ( (array) $messages as $message ) { 253 $data[] = array( 'code' => $code, 'message' => $message, 'data' => $error->get_error_data( $code ) ); 254 } 255 } 256 257 $response = new WP_REST_Response( $data, $status ); 258 259 return $response; 260 } 261 262 /** 263 * Retrieves an appropriate error representation in JSON. 264 * 265 * Note: This should only be used in WP_REST_Server::serve_request(), as it 266 * cannot handle WP_Error internally. All callbacks and other internal methods 267 * should instead return a WP_Error with the data set to an array that includes 268 * a 'status' key, with the value being the HTTP status to send. 269 * 270 * @since 4.4.0 271 * @access protected 272 * 273 * @param string $code WP_Error-style code. 274 * @param string $message Human-readable message. 275 * @param int $status Optional. HTTP status code to send. Default null. 276 * @return string JSON representation of the error 277 */ 278 protected function json_error( $code, $message, $status = null ) { 279 if ( $status ) { 280 $this->set_status( $status ); 281 } 282 283 $error = compact( 'code', 'message' ); 284 285 return wp_json_encode( array( $error ) ); 286 } 287 288 /** 289 * Handles serving an API request. 290 * 291 * Matches the current server URI to a route and runs the first matching 292 * callback then outputs a JSON representation of the returned value. 293 * 294 * @since 4.4.0 295 * @access public 296 * 297 * @see WP_REST_Server::dispatch() 298 * 299 * @param string $path Optional. The request route. If not set, `$_SERVER['PATH_INFO']` will be used. 300 * Default null. 301 * @return false|null Null if not served and a HEAD request, false otherwise. 302 */ 303 public function serve_request( $path = null ) { 304 $content_type = isset( $_GET['_jsonp'] ) ? 'application/javascript' : 'application/json'; 305 $this->send_header( 'Content-Type', $content_type . '; charset=' . get_option( 'blog_charset' ) ); 306 307 /* 308 * Mitigate possible JSONP Flash attacks. 309 * 310 * http://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/ 311 */ 312 $this->send_header( 'X-Content-Type-Options', 'nosniff' ); 313 $this->send_header( 'Access-Control-Expose-Headers', 'X-WP-Total, X-WP-TotalPages' ); 314 $this->send_header( 'Access-Control-Allow-Headers', 'Authorization' ); 315 316 /** 317 * Filter whether the REST API is enabled. 318 * 319 * @since 4.4.0 320 * 321 * @param bool $rest_enabled Whether the REST API is enabled. Default true. 322 */ 323 $enabled = apply_filters( 'rest_enabled', true ); 324 325 /** 326 * Filter whether jsonp is enabled. 327 * 328 * @since 4.4.0 329 * 330 * @param bool $jsonp_enabled Whether jsonp is enabled. Default true. 331 */ 332 $jsonp_enabled = apply_filters( 'rest_jsonp_enabled', true ); 333 334 $jsonp_callback = null; 335 336 if ( ! $enabled ) { 337 echo $this->json_error( 'rest_disabled', __( 'The REST API is disabled on this site.' ), 404 ); 338 return false; 339 } 340 if ( isset( $_GET['_jsonp'] ) ) { 341 if ( ! $jsonp_enabled ) { 342 echo $this->json_error( 'rest_callback_disabled', __( 'JSONP support is disabled on this site.' ), 400 ); 343 return false; 344 } 345 346 // Check for invalid characters (only alphanumeric allowed). 347 if ( is_string( $_GET['_jsonp'] ) ) { 348 $jsonp_callback = preg_replace( '/[^\w\.]/', '', wp_unslash( $_GET['_jsonp'] ), -1, $illegal_char_count ); 349 if ( 0 !== $illegal_char_count ) { 350 $jsonp_callback = null; 351 } 352 } 353 if ( null === $jsonp_callback ) { 354 echo $this->json_error( 'rest_callback_invalid', __( 'The JSONP callback function is invalid.' ), 400 ); 355 return false; 356 } 357 } 358 359 if ( empty( $path ) ) { 360 if ( isset( $_SERVER['PATH_INFO'] ) ) { 361 $path = $_SERVER['PATH_INFO']; 362 } else { 363 $path = '/'; 364 } 365 } 366 367 $request = new WP_REST_Request( $_SERVER['REQUEST_METHOD'], $path ); 368 369 $request->set_query_params( $_GET ); 370 $request->set_body_params( $_POST ); 371 $request->set_file_params( $_FILES ); 372 $request->set_headers( $this->get_headers( $_SERVER ) ); 373 $request->set_body( $this->get_raw_data() ); 374 375 /* 376 * HTTP method override for clients that can't use PUT/PATCH/DELETE. First, we check 377 * $_GET['_method']. If that is not set, we check for the HTTP_X_HTTP_METHOD_OVERRIDE 378 * header. 379 */ 380 if ( isset( $_GET['_method'] ) ) { 381 $request->set_method( $_GET['_method'] ); 382 } elseif ( isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ) { 383 $request->set_method( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ); 384 } 385 386 $result = $this->check_authentication(); 387 388 if ( ! is_wp_error( $result ) ) { 389 $result = $this->dispatch( $request ); 390 } 391 392 // Normalize to either WP_Error or WP_REST_Response... 393 $result = rest_ensure_response( $result ); 394 395 // ...then convert WP_Error across. 396 if ( is_wp_error( $result ) ) { 397 $result = $this->error_to_response( $result ); 398 } 399 400 /** 401 * Filter the API response. 402 * 403 * Allows modification of the response before returning. 404 * 405 * @since 4.4.0 406 * 407 * @param WP_HTTP_Response $result Result to send to the client. Usually a WP_REST_Response. 408 * @param WP_REST_Server $this Server instance. 409 * @param WP_REST_Request $request Request used to generate the response. 410 */ 411 $result = apply_filters( 'rest_post_dispatch', rest_ensure_response( $result ), $this, $request ); 412 413 // Wrap the response in an envelope if asked for. 414 if ( isset( $_GET['_envelope'] ) ) { 415 $result = $this->envelope_response( $result, isset( $_GET['_embed'] ) ); 416 } 417 418 // Send extra data from response objects. 419 $headers = $result->get_headers(); 420 $this->send_headers( $headers ); 421 422 $code = $result->get_status(); 423 $this->set_status( $code ); 424 425 /** 426 * Filter whether the request has already been served. 427 * 428 * Allow sending the request manually - by returning true, the API result 429 * will not be sent to the client. 430 * 431 * @since 4.4.0 432 * 433 * @param bool $served Whether the request has already been served. 434 * Default false. 435 * @param WP_HTTP_Response $result Result to send to the client. Usually a WP_REST_Response. 436 * @param WP_REST_Request $request Request used to generate the response. 437 * @param WP_REST_Server $this Server instance. 438 */ 439 $served = apply_filters( 'rest_pre_serve_request', false, $result, $request, $this ); 440 441 if ( ! $served ) { 442 if ( 'HEAD' === $request->get_method() ) { 443 return null; 444 } 445 446 // Embed links inside the request. 447 $result = $this->response_to_data( $result, isset( $_GET['_embed'] ) ); 448 449 $result = wp_json_encode( $result ); 450 451 $json_error_message = $this->get_json_last_error(); 452 if ( $json_error_message ) { 453 $json_error_obj = new WP_Error( 'rest_encode_error', $json_error_message, array( 'status' => 500 ) ); 454 $result = $this->error_to_response( $json_error_obj ); 455 $result = wp_json_encode( $result->data[0] ); 456 } 457 458 if ( $jsonp_callback ) { 459 // Prepend '/**/' to mitigate possible JSONP Flash attacks 460 // http://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/ 461 echo '/**/' . $jsonp_callback . '(' . $result . ')'; 462 } else { 463 echo $result; 464 } 465 } 466 return null; 467 } 468 469 /** 470 * Converts a response to data to send. 471 * 472 * @since 4.4.0 473 * @access public 474 * 475 * @param WP_REST_Response $response Response object. 476 * @param bool $embed Whether links should be embedded. 477 * @return array { 478 * Data with sub-requests embedded. 479 * 480 * @type array [$_links] Links. 481 * @type array [$_embedded] Embeddeds. 482 * } 483 */ 484 public function response_to_data( $response, $embed ) { 485 $data = $this->prepare_response( $response->get_data() ); 486 $links = $this->get_response_links( $response ); 487 488 if ( ! empty( $links ) ) { 489 // Convert links to part of the data. 490 $data['_links'] = $links; 491 } 492 if ( $embed ) { 493 // Determine if this is a numeric array. 494 if ( rest_is_list( $data ) ) { 495 $data = array_map( array( $this, 'embed_links' ), $data ); 496 } else { 497 $data = $this->embed_links( $data ); 498 } 499 } 500 501 return $data; 502 } 503 504 /** 505 * Retrieves links from a response. 506 * 507 * Extracts the links from a response into a structured hash, suitable for 508 * direct output. 509 * 510 * @since 4.4.0 511 * @access public 512 * @static 513 * 514 * @param WP_REST_Response $response Response to extract links from. 515 * @return array Map of link relation to list of link hashes. 516 */ 517 public static function get_response_links( $response ) { 518 $links = $response->get_links(); 519 520 if ( empty( $links ) ) { 521 return array(); 522 } 523 524 // Convert links to part of the data. 525 $data = array(); 526 foreach ( $links as $rel => $items ) { 527 $data[ $rel ] = array(); 528 529 foreach ( $items as $item ) { 530 $attributes = $item['attributes']; 531 $attributes['href'] = $item['href']; 532 $data[ $rel ][] = $attributes; 533 } 534 } 535 536 return $data; 537 } 538 539 /** 540 * Embeds the links from the data into the request. 541 * 542 * @since 4.4.0 543 * @access protected 544 * 545 * @param array $data Data from the request. 546 * @return array { 547 * Data with sub-requests embedded. 548 * 549 * @type array [$_links] Links. 550 * @type array [$_embedded] Embeddeds. 551 * } 552 */ 553 protected function embed_links( $data ) { 554 if ( empty( $data['_links'] ) ) { 555 return $data; 556 } 557 558 $embedded = array(); 559 $api_root = rest_url(); 560 561 foreach ( $data['_links'] as $rel => $links ) { 562 // Ignore links to self, for obvious reasons. 563 if ( 'self' === $rel ) { 564 continue; 565 } 566 567 $embeds = array(); 568 569 foreach ( $links as $item ) { 570 // Determine if the link is embeddable. 571 if ( empty( $item['embeddable'] ) || strpos( $item['href'], $api_root ) !== 0 ) { 572 // Ensure we keep the same order. 573 $embeds[] = array(); 574 continue; 575 } 576 577 // Run through our internal routing and serve. 578 $route = substr( $item['href'], strlen( untrailingslashit( $api_root ) ) ); 579 $query_params = array(); 580 581 // Parse out URL query parameters. 582 $parsed = parse_url( $route ); 583 if ( empty( $parsed['path'] ) ) { 584 $embeds[] = array(); 585 continue; 586 } 587 588 if ( ! empty( $parsed['query'] ) ) { 589 parse_str( $parsed['query'], $query_params ); 590 591 // Ensure magic quotes are stripped. 592 if ( get_magic_quotes_gpc() ) { 593 $query_params = stripslashes_deep( $query_params ); 594 } 595 } 596 597 // Embedded resources get passed context=embed. 598 if ( empty( $query_params['context'] ) ) { 599 $query_params['context'] = 'embed'; 600 } 601 602 $request = new WP_REST_Request( 'GET', $parsed['path'] ); 603 604 $request->set_query_params( $query_params ); 605 $response = $this->dispatch( $request ); 606 607 $embeds[] = $this->response_to_data( $response, false ); 608 } 609 610 // Determine if any real links were found. 611 $has_links = count( array_filter( $embeds ) ); 612 if ( $has_links ) { 613 $embedded[ $rel ] = $embeds; 614 } 615 } 616 617 if ( ! empty( $embedded ) ) { 618 $data['_embedded'] = $embedded; 619 } 620 621 return $data; 622 } 623 624 /** 625 * Wraps the response in an envelope. 626 * 627 * The enveloping technique is used to work around browser/client 628 * compatibility issues. Essentially, it converts the full HTTP response to 629 * data instead. 630 * 631 * @since 4.4.0 632 * @access public 633 * 634 * @param WP_REST_Response $response Response object. 635 * @param bool $embed Whether links should be embedded. 636 * @return WP_REST_Response New response with wrapped data 637 */ 638 public function envelope_response( $response, $embed ) { 639 $envelope = array( 640 'body' => $this->response_to_data( $response, $embed ), 641 'status' => $response->get_status(), 642 'headers' => $response->get_headers(), 643 ); 644 645 /** 646 * Filter the enveloped form of a response. 647 * 648 * @since 4.4.0 649 * 650 * @param array $envelope Envelope data. 651 * @param WP_REST_Response $response Original response data. 652 */ 653 $envelope = apply_filters( 'rest_envelope_response', $envelope, $response ); 654 655 // Ensure it's still a response and return. 656 return rest_ensure_response( $envelope ); 657 } 658 659 /** 660 * Registers a route to the server. 661 * 662 * @since 4.4.0 663 * @access public 664 * 665 * @param string $namespace Namespace. 666 * @param string $route The REST route. 667 * @param array $route_args Route arguments. 668 * @param bool $override Optional. Whether the route should be overriden if it already exists. 669 * Default false. 670 */ 671 public function register_route( $namespace, $route, $route_args, $override = false ) { 672 if ( ! isset( $this->namespaces[ $namespace ] ) ) { 673 $this->namespaces[ $namespace ] = array(); 674 675 $this->register_route( $namespace, '/' . $namespace, array( 676 array( 677 'methods' => self::READABLE, 678 'callback' => array( $this, 'get_namespace_index' ), 679 'args' => array( 680 'namespace' => array( 681 'default' => $namespace, 682 ), 683 'context' => array( 684 'default' => 'view', 685 ), 686 ), 687 ), 688 ) ); 689 } 690 691 // Associative to avoid double-registration. 692 $this->namespaces[ $namespace ][ $route ] = true; 693 $route_args['namespace'] = $namespace; 694 695 if ( $override || empty( $this->endpoints[ $route ] ) ) { 696 $this->endpoints[ $route ] = $route_args; 697 } else { 698 $this->endpoints[ $route ] = array_merge( $this->endpoints[ $route ], $route_args ); 699 } 700 } 701 702 /** 703 * Retrieves the route map. 704 * 705 * The route map is an associative array with path regexes as the keys. The 706 * value is an indexed array with the callback function/method as the first 707 * item, and a bitmask of HTTP methods as the second item (see the class 708 * constants). 709 * 710 * Each route can be mapped to more than one callback by using an array of 711 * the indexed arrays. This allows mapping e.g. GET requests to one callback 712 * and POST requests to another. 713 * 714 * Note that the path regexes (array keys) must have @ escaped, as this is 715 * used as the delimiter with preg_match() 716 * 717 * @since 4.4.0 718 * @access public 719 * 720 * @return array `'/path/regex' => array( $callback, $bitmask )` or 721 * `'/path/regex' => array( array( $callback, $bitmask ), ...)`. 722 */ 723 public function get_routes() { 724 725 /** 726 * Filter the array of available endpoints. 727 * 728 * @since 4.4.0 729 * 730 * @param array $endpoints The available endpoints. An array of matching regex patterns, each mapped 731 * to an array of callbacks for the endpoint. These take the format 732 * `'/path/regex' => array( $callback, $bitmask )` or 733 * `'/path/regex' => array( array( $callback, $bitmask ). 734 */ 735 $endpoints = apply_filters( 'rest_endpoints', $this->endpoints ); 736 737 // Normalise the endpoints. 738 $defaults = array( 739 'methods' => '', 740 'accept_json' => false, 741 'accept_raw' => false, 742 'show_in_index' => true, 743 'args' => array(), 744 ); 745 746 foreach ( $endpoints as $route => &$handlers ) { 747 748 if ( isset( $handlers['callback'] ) ) { 749 // Single endpoint, add one deeper. 750 $handlers = array( $handlers ); 751 } 752 753 if ( ! isset( $this->route_options[ $route ] ) ) { 754 $this->route_options[ $route ] = array(); 755 } 756 757 foreach ( $handlers as $key => &$handler ) { 758 759 if ( ! is_numeric( $key ) ) { 760 // Route option, move it to the options. 761 $this->route_options[ $route ][ $key ] = $handler; 762 unset( $handlers[ $key ] ); 763 continue; 764 } 765 766 $handler = wp_parse_args( $handler, $defaults ); 767 768 // Allow comma-separated HTTP methods. 769 if ( is_string( $handler['methods'] ) ) { 770 $methods = explode( ',', $handler['methods'] ); 771 } else if ( is_array( $handler['methods'] ) ) { 772 $methods = $handler['methods']; 773 } else { 774 $methods = array(); 775 } 776 777 $handler['methods'] = array(); 778 779 foreach ( $methods as $method ) { 780 $method = strtoupper( trim( $method ) ); 781 $handler['methods'][ $method ] = true; 782 } 783 } 784 } 785 return $endpoints; 786 } 787 788 /** 789 * Retrieves namespaces registered on the server. 790 * 791 * @since 4.4.0 792 * @access public 793 * 794 * @return array List of registered namespaces. 795 */ 796 public function get_namespaces() { 797 return array_keys( $this->namespaces ); 798 } 799 800 /** 801 * Retrieves specified options for a route. 802 * 803 * @since 4.4.0 804 * @access public 805 * 806 * @param string $route Route pattern to fetch options for. 807 * @return array|null Data as an associative array if found, or null if not found. 808 */ 809 public function get_route_options( $route ) { 810 if ( ! isset( $this->route_options[ $route ] ) ) { 811 return null; 812 } 813 814 return $this->route_options[ $route ]; 815 } 816 817 /** 818 * Matches the request to a callback and call it. 819 * 820 * @since 4.4.0 821 * @access public 822 * 823 * @param WP_REST_Request $request Request to attempt dispatching. 824 * @return WP_REST_Response Response returned by the callback. 825 */ 826 public function dispatch( $request ) { 827 /** 828 * Filter the pre-calculated result of a REST dispatch request. 829 * 830 * Allow hijacking the request before dispatching by returning a non-empty. The returned value 831 * will be used to serve the request instead. 832 * 833 * @since 4.4.0 834 * 835 * @param mixed $result Response to replace the requested version with. Can be anything 836 * a normal endpoint can return, or null to not hijack the request. 837 * @param WP_REST_Server $this Server instance. 838 * @param WP_REST_Request $request Request used to generate the response. 839 */ 840 $result = apply_filters( 'rest_pre_dispatch', null, $this, $request ); 841 842 if ( ! empty( $result ) ) { 843 return $result; 844 } 845 846 $method = $request->get_method(); 847 $path = $request->get_route(); 848 849 foreach ( $this->get_routes() as $route => $handlers ) { 850 foreach ( $handlers as $handler ) { 851 $callback = $handler['callback']; 852 $response = null; 853 854 if ( empty( $handler['methods'][ $method ] ) ) { 855 continue; 856 } 857 858 $match = preg_match( '@^' . $route . '$@i', $path, $args ); 859 860 if ( ! $match ) { 861 continue; 862 } 863 864 if ( ! is_callable( $callback ) ) { 865 $response = new WP_Error( 'rest_invalid_handler', __( 'The handler for the route is invalid' ), array( 'status' => 500 ) ); 866 } 867 868 if ( ! is_wp_error( $response ) ) { 869 870 $request->set_url_params( $args ); 871 $request->set_attributes( $handler ); 872 873 $request->sanitize_params(); 874 875 $defaults = array(); 876 877 foreach ( $handler['args'] as $arg => $options ) { 878 if ( isset( $options['default'] ) ) { 879 $defaults[ $arg ] = $options['default']; 880 } 881 } 882 883 $request->set_default_params( $defaults ); 884 885 $check_required = $request->has_valid_params(); 886 if ( is_wp_error( $check_required ) ) { 887 $response = $check_required; 888 } 889 } 890 891 if ( ! is_wp_error( $response ) ) { 892 // Check permission specified on the route. 893 if ( ! empty( $handler['permission_callback'] ) ) { 894 $permission = call_user_func( $handler['permission_callback'], $request ); 895 896 if ( is_wp_error( $permission ) ) { 897 $response = $permission; 898 } else if ( false === $permission || null === $permission ) { 899 $response = new WP_Error( 'rest_forbidden', __( "You don't have permission to do this." ), array( 'status' => 403 ) ); 900 } 901 } 902 } 903 904 if ( ! is_wp_error( $response ) ) { 905 /** 906 * Filter the REST dispatch request result. 907 * 908 * Allow plugins to override dispatching the request. 909 * 910 * @since 4.4.0 911 * 912 * @param bool $dispatch_result Dispatch result, will be used if not empty. 913 * @param WP_REST_Request $request Request used to generate the response. 914 */ 915 $dispatch_result = apply_filters( 'rest_dispatch_request', null, $request ); 916 917 // Allow plugins to halt the request via this filter. 918 if ( null !== $dispatch_result ) { 919 $response = $dispatch_result; 920 } else { 921 $response = call_user_func( $callback, $request ); 922 } 923 } 924 925 if ( is_wp_error( $response ) ) { 926 $response = $this->error_to_response( $response ); 927 } else { 928 $response = rest_ensure_response( $response ); 929 } 930 931 $response->set_matched_route( $route ); 932 $response->set_matched_handler( $handler ); 933 934 return $response; 935 } 936 } 937 938 return $this->error_to_response( new WP_Error( 'rest_no_route', __( 'No route was found matching the URL and request method' ), array( 'status' => 404 ) ) ); 939 } 940 941 /** 942 * Returns if an error occurred during most recent JSON encode/decode. 943 * 944 * Strings to be translated will be in format like 945 * "Encoding error: Maximum stack depth exceeded". 946 * 947 * @since 4.4.0 948 * @access protected 949 * 950 * @return bool|string Boolean false or string error message. 951 */ 952 protected function get_json_last_error() { 953 // See https://core.trac.wordpress.org/ticket/27799. 954 if ( ! function_exists( 'json_last_error' ) ) { 955 return false; 956 } 957 958 $last_error_code = json_last_error(); 959 960 if ( ( defined( 'JSON_ERROR_NONE' ) && JSON_ERROR_NONE === $last_error_code ) || empty( $last_error_code ) ) { 961 return false; 962 } 963 964 return json_last_error_msg(); 965 } 966 967 /** 968 * Retrieves the site index. 969 * 970 * This endpoint describes the capabilities of the site. 971 * 972 * @since 4.4.0 973 * @access public 974 * 975 * @param array $request { 976 * Request. 977 * 978 * @type string $context Context. 979 * } 980 * @return array Index entity 981 */ 982 public function get_index( $request ) { 983 // General site data. 984 $available = array( 985 'name' => get_option( 'blogname' ), 986 'description' => get_option( 'blogdescription' ), 987 'url' => get_option( 'siteurl' ), 988 'namespaces' => array_keys( $this->namespaces ), 989 'authentication' => array(), 990 'routes' => $this->get_data_for_routes( $this->get_routes(), $request['context'] ), 991 ); 992 993 $response = new WP_REST_Response( $available ); 994 995 $response->add_link( 'help', 'http://v2.wp-api.org/' ); 996 997 /** 998 * Filter the API root index data. 999 * 1000 * This contains the data describing the API. This includes information 1001 * about supported authentication schemes, supported namespaces, routes 1002 * available on the API, and a small amount of data about the site. 1003 * 1004 * @since 4.4.0 1005 * 1006 * @param WP_REST_Response $response Response data. 1007 */ 1008 return apply_filters( 'rest_index', $response ); 1009 } 1010 1011 /** 1012 * Retrieves the index for a namespace. 1013 * 1014 * @since 4.4.0 1015 * @access public 1016 * 1017 * @param WP_REST_Request $request REST request instance. 1018 * @return WP_REST_Response|WP_Error WP_REST_Response instance if the index was found, 1019 * WP_Error if the namespace isn't set. 1020 */ 1021 public function get_namespace_index( $request ) { 1022 $namespace = $request['namespace']; 1023 1024 if ( ! isset( $this->namespaces[ $namespace ] ) ) { 1025 return new WP_Error( 'rest_invalid_namespace', __( 'The specified namespace could not be found.' ), array( 'status' => 404 ) ); 1026 } 1027 1028 $routes = $this->namespaces[ $namespace ]; 1029 $endpoints = array_intersect_key( $this->get_routes(), $routes ); 1030 1031 $data = array( 1032 'namespace' => $namespace, 1033 'routes' => $this->get_data_for_routes( $endpoints, $request['context'] ), 1034 ); 1035 $response = rest_ensure_response( $data ); 1036 1037 // Link to the root index. 1038 $response->add_link( 'up', rest_url( '/' ) ); 1039 1040 /** 1041 * Filter the namespace index data. 1042 * 1043 * This typically is just the route data for the namespace, but you can 1044 * add any data you'd like here. 1045 * 1046 * @since 4.4.0 1047 * 1048 * @param WP_REST_Response $response Response data. 1049 * @param WP_REST_Request $request Request data. The namespace is passed as the 'namespace' parameter. 1050 */ 1051 return apply_filters( 'rest_namespace_index', $response, $request ); 1052 } 1053 1054 /** 1055 * Retrieves the publicly-visible data for routes. 1056 * 1057 * @since 4.4.0 1058 * @access public 1059 * 1060 * @param array $routes Routes to get data for. 1061 * @param string $context Optional. Context for data. Accepts 'view' or 'help'. Default 'view'. 1062 * @return array Route data to expose in indexes. 1063 */ 1064 public function get_data_for_routes( $routes, $context = 'view' ) { 1065 $available = array(); 1066 1067 // Find the available routes. 1068 foreach ( $routes as $route => $callbacks ) { 1069 $data = $this->get_data_for_route( $route, $callbacks, $context ); 1070 if ( empty( $data ) ) { 1071 continue; 1072 } 1073 1074 /** 1075 * Filter the REST endpoint data. 1076 * 1077 * @since 4.4.0 1078 * 1079 * @param WP_REST_Request $request Request data. The namespace is passed as the 'namespace' parameter. 1080 */ 1081 $available[ $route ] = apply_filters( 'rest_endpoints_description', $data ); 1082 } 1083 1084 /** 1085 * Filter the publicly-visible data for routes. 1086 * 1087 * This data is exposed on indexes and can be used by clients or 1088 * developers to investigate the site and find out how to use it. It 1089 * acts as a form of self-documentation. 1090 * 1091 * @since 4.4.0 1092 * 1093 * @param array $available Map of route to route data. 1094 * @param array $routes Internal route data as an associative array. 1095 */ 1096 return apply_filters( 'rest_route_data', $available, $routes ); 1097 } 1098 1099 /** 1100 * Retrieves publicly-visible data for the route. 1101 * 1102 * @since 4.4.0 1103 * @access public 1104 * 1105 * @param string $route Route to get data for. 1106 * @param array $callbacks Callbacks to convert to data. 1107 * @param string $context Optional. Context for the data. Accepts 'view' or 'help'. Default 'view'. 1108 * @return array|null Data for the route, or null if no publicly-visible data. 1109 */ 1110 public function get_data_for_route( $route, $callbacks, $context = 'view' ) { 1111 $data = array( 1112 'namespace' => '', 1113 'methods' => array(), 1114 'endpoints' => array(), 1115 ); 1116 1117 if ( isset( $this->route_options[ $route ] ) ) { 1118 $options = $this->route_options[ $route ]; 1119 1120 if ( isset( $options['namespace'] ) ) { 1121 $data['namespace'] = $options['namespace']; 1122 } 1123 1124 if ( isset( $options['schema'] ) && 'help' === $context ) { 1125 $data['schema'] = call_user_func( $options['schema'] ); 1126 } 1127 } 1128 1129 $route = preg_replace( '#\(\?P<(\w+?)>.*?\)#', '{$1}', $route ); 1130 1131 foreach ( $callbacks as $callback ) { 1132 // Skip to the next route if any callback is hidden. 1133 if ( empty( $callback['show_in_index'] ) ) { 1134 continue; 1135 } 1136 1137 $data['methods'] = array_merge( $data['methods'], array_keys( $callback['methods'] ) ); 1138 $endpoint_data = array( 1139 'methods' => array_keys( $callback['methods'] ), 1140 ); 1141 1142 if ( isset( $callback['args'] ) ) { 1143 $endpoint_data['args'] = array(); 1144 foreach ( $callback['args'] as $key => $opts ) { 1145 $arg_data = array( 1146 'required' => ! empty( $opts['required'] ), 1147 ); 1148 if ( isset( $opts['default'] ) ) { 1149 $arg_data['default'] = $opts['default']; 1150 } 1151 $endpoint_data['args'][ $key ] = $arg_data; 1152 } 1153 } 1154 1155 $data['endpoints'][] = $endpoint_data; 1156 1157 // For non-variable routes, generate links. 1158 if ( strpos( $route, '{' ) === false ) { 1159 $data['_links'] = array( 1160 'self' => rest_url( $route ), 1161 ); 1162 } 1163 } 1164 1165 if ( empty( $data['methods'] ) ) { 1166 // No methods supported, hide the route. 1167 return null; 1168 } 1169 1170 return $data; 1171 } 1172 1173 /** 1174 * Sends an HTTP status code. 1175 * 1176 * @since 4.4.0 1177 * @access protected 1178 * 1179 * @param int $code HTTP status. 1180 */ 1181 protected function set_status( $code ) { 1182 status_header( $code ); 1183 } 1184 1185 /** 1186 * Sends an HTTP header. 1187 * 1188 * @since 4.4.0 1189 * @access public 1190 * 1191 * @param string $key Header key. 1192 * @param string $value Header value. 1193 */ 1194 public function send_header( $key, $value ) { 1195 /* 1196 * Sanitize as per RFC2616 (Section 4.2): 1197 * 1198 * Any LWS that occurs between field-content MAY be replaced with a 1199 * single SP before interpreting the field value or forwarding the 1200 * message downstream. 1201 */ 1202 $value = preg_replace( '/\s+/', ' ', $value ); 1203 header( sprintf( '%s: %s', $key, $value ) ); 1204 } 1205 1206 /** 1207 * Sends multiple HTTP headers. 1208 * 1209 * @since 4.4.0 1210 * @access public 1211 * 1212 * @param array $headers Map of header name to header value. 1213 */ 1214 public function send_headers( $headers ) { 1215 foreach ( $headers as $key => $value ) { 1216 $this->send_header( $key, $value ); 1217 } 1218 } 1219 1220 /** 1221 * Retrieves the raw request entity (body). 1222 * 1223 * @since 4.4.0 1224 * @access public 1225 * 1226 * @global string $HTTP_RAW_POST_DATA Raw post data. 1227 * 1228 * @return string Raw request data. 1229 */ 1230 public function get_raw_data() { 1231 global $HTTP_RAW_POST_DATA; 1232 1233 /* 1234 * A bug in PHP < 5.2.2 makes $HTTP_RAW_POST_DATA not set by default, 1235 * but we can do it ourself. 1236 */ 1237 if ( ! isset( $HTTP_RAW_POST_DATA ) ) { 1238 $HTTP_RAW_POST_DATA = file_get_contents( 'php://input' ); 1239 } 1240 1241 return $HTTP_RAW_POST_DATA; 1242 } 1243 1244 /** 1245 * Prepares response data to be serialized to JSON. 1246 * 1247 * This supports the JsonSerializable interface for PHP 5.2-5.3 as well. 1248 * 1249 * @since 4.4.0 1250 * @access public 1251 * 1252 * @codeCoverageIgnore This is a compatibility shim. 1253 * 1254 * @param mixed $data Native representation. 1255 * @return array|string Data ready for `json_encode()`. 1256 */ 1257 public function prepare_response( $data ) { 1258 if ( ! defined( 'WP_REST_SERIALIZE_COMPATIBLE' ) || WP_REST_SERIALIZE_COMPATIBLE === false ) { 1259 return $data; 1260 } 1261 1262 switch ( gettype( $data ) ) { 1263 case 'boolean': 1264 case 'integer': 1265 case 'double': 1266 case 'string': 1267 case 'NULL': 1268 // These values can be passed through. 1269 return $data; 1270 1271 case 'array': 1272 // Arrays must be mapped in case they also return objects. 1273 return array_map( array( $this, 'prepare_response' ), $data ); 1274 1275 case 'object': 1276 if ( $data instanceof JsonSerializable ) { 1277 $data = $data->jsonSerialize(); 1278 } else { 1279 $data = get_object_vars( $data ); 1280 } 1281 1282 // Now, pass the array (or whatever was returned from jsonSerialize through.). 1283 return $this->prepare_response( $data ); 1284 1285 default: 1286 return null; 1287 } 1288 } 1289 1290 /** 1291 * Extracts headers from a PHP-style $_SERVER array. 1292 * 1293 * @since 4.4.0 1294 * @access public 1295 * 1296 * @param array $server Associative array similar to `$_SERVER`. 1297 * @return array Headers extracted from the input. 1298 */ 1299 public function get_headers( $server ) { 1300 $headers = array(); 1301 1302 // CONTENT_* headers are not prefixed with HTTP_. 1303 $additional = array( 'CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true ); 1304 1305 foreach ( $server as $key => $value ) { 1306 if ( strpos( $key, 'HTTP_' ) === 0 ) { 1307 $headers[ substr( $key, 5 ) ] = $value; 1308 } elseif ( isset( $additional[ $key ] ) ) { 1309 $headers[ $key ] = $value; 1310 } 1311 } 1312 1313 return $headers; 1314 } 1315 } -
src/wp-includes/rest-api/rest-functions.php
1 <?php 2 /** 3 * REST API functions. 4 * 5 * @package WordPress 6 * @subpackage REST_API 7 */ 8 9 /** 10 * Registers a REST API route. 11 * 12 * @since 4.4.0 13 * 14 * @param string $namespace The first URL segment after core prefix. Should be unique to your package/plugin. 15 * @param string $route The base URL for route you are adding. 16 * @param array $args Optional. Either an array of options for the endpoint, or an array of arrays for 17 * multiple methods. Default empty array. 18 * @param bool $override Optional. If the route already exists, should we override it? True overrides, 19 * false merges (with newer overriding if duplicate keys exist). Default false. 20 */ 21 function register_rest_route( $namespace, $route, $args = array(), $override = false ) { 22 23 /** @var WP_REST_Server $wp_rest_server */ 24 global $wp_rest_server; 25 26 if ( isset( $args['callback'] ) ) { 27 // Upgrade a single set to multiple. 28 $args = array( $args ); 29 } 30 31 $defaults = array( 32 'methods' => 'GET', 33 'callback' => null, 34 'args' => array(), 35 ); 36 foreach ( $args as $key => &$arg_group ) { 37 if ( ! is_numeric( $arg_group ) ) { 38 // Route option, skip here. 39 continue; 40 } 41 42 $arg_group = array_merge( $defaults, $arg_group ); 43 } 44 45 if ( $namespace ) { 46 $full_route = '/' . trim( $namespace, '/' ) . '/' . trim( $route, '/' ); 47 } else { 48 /* 49 * Non-namespaced routes are not allowed, with the exception of the main 50 * and namespace indexes. If you really need to register a 51 * non-namespaced route, call `WP_REST_Server::register_route` directly. 52 */ 53 _doing_it_wrong( 'register_rest_route', 'Routes must be namespaced with plugin name and version', 'WPAPI-2.0' ); 54 55 $full_route = '/' . trim( $route, '/' ); 56 } 57 58 $wp_rest_server->register_route( $namespace, $full_route, $args, $override ); 59 } 60 61 /** 62 * Registers a new field on an existing WordPress object type. 63 * 64 * @since 4.4.0 65 * 66 * @global array $wp_rest_additional_fields Holds registered fields, organized 67 * by object type. 68 * 69 * @param string|array $object_type Object(s) the field is being registered 70 * to, "post"|"term"|"comment" etc. 71 * @param string $attribute The attribute name. 72 * @param array $args { 73 * Optional. An array of arguments used to handle the registered field. 74 * 75 * @type string|array|null $get_callback Optional. The callback function used to retrieve the field 76 * value. Default is 'null', the field will not be returned in 77 * the response. 78 * @type string|array|null $update_callback Optional. The callback function used to set and update the 79 * field value. Default is 'null', the value cannot be set or 80 * updated. 81 * @type string|array|null schema Optional. The callback function used to create the schema for 82 * this field. Default is 'null', no schema entry will be returned. 83 * } 84 */ 85 function register_api_field( $object_type, $attribute, $args = array() ) { 86 87 $defaults = array( 88 'get_callback' => null, 89 'update_callback' => null, 90 'schema' => null, 91 ); 92 93 $args = wp_parse_args( $args, $defaults ); 94 95 global $wp_rest_additional_fields; 96 97 $object_types = (array) $object_type; 98 99 foreach ( $object_types as $object_type ) { 100 $wp_rest_additional_fields[ $object_type ][ $attribute ] = $args; 101 } 102 } 103 104 /** 105 * Registers rewrite rules for the API. 106 * 107 * @since 4.4.0 108 * 109 * @see rest_api_register_rewrites() 110 * @global WP $wp Current WordPress environment instance. 111 */ 112 function rest_api_init() { 113 rest_api_register_rewrites(); 114 115 global $wp; 116 $wp->add_query_var( 'rest_route' ); 117 } 118 119 /** 120 * Adds REST rewrite rules. 121 * 122 * @since 4.4.0 123 * 124 * @see add_rewrite_rule() 125 */ 126 function rest_api_register_rewrites() { 127 add_rewrite_rule( '^' . rest_get_url_prefix() . '/?$','index.php?rest_route=/','top' ); 128 add_rewrite_rule( '^' . rest_get_url_prefix() . '/(.*)?','index.php?rest_route=/$matches[1]','top' ); 129 } 130 131 /** 132 * Registers the default REST API filters. 133 * 134 * @since 4.4.0 135 * 136 * @internal This will live in default-filters.php 137 */ 138 function rest_api_default_filters() { 139 // Deprecated reporting. 140 add_action( 'deprecated_function_run', 'rest_handle_deprecated_function', 10, 3 ); 141 add_filter( 'deprecated_function_trigger_error', '__return_false' ); 142 add_action( 'deprecated_argument_run', 'rest_handle_deprecated_argument', 10, 3 ); 143 add_filter( 'deprecated_argument_trigger_error', '__return_false' ); 144 145 // Default serving. 146 add_filter( 'rest_pre_serve_request', 'rest_send_cors_headers' ); 147 add_filter( 'rest_post_dispatch', 'rest_send_allow_header', 10, 3 ); 148 149 add_filter( 'rest_pre_dispatch', 'rest_handle_options_request', 10, 3 ); 150 } 151 152 /** 153 * Loads the REST API. 154 * 155 * @since 4.4.0 156 */ 157 function rest_api_loaded() { 158 if ( empty( $GLOBALS['wp']->query_vars['rest_route'] ) ) { 159 return; 160 } 161 162 /** 163 * Whether this is a REST Request. 164 * 165 * @var bool 166 */ 167 define( 'REST_REQUEST', true ); 168 169 /** @var WP_REST_Server $wp_rest_server */ 170 global $wp_rest_server; 171 172 /** 173 * Filter the REST Server Class. 174 * 175 * This filter allows you to adjust the server class used by the API, using a 176 * different class to handle requests. 177 * 178 * @since 4.4.0 179 * 180 * @param string $class_name The name of the server class. Default 'WP_REST_Server'. 181 */ 182 $wp_rest_server_class = apply_filters( 'wp_rest_server_class', 'WP_REST_Server' ); 183 $wp_rest_server = new $wp_rest_server_class; 184 185 /** 186 * Fires when preparing to serve an API request. 187 * 188 * Endpoint objects should be created and register their hooks on this action rather 189 * than another action to ensure they're only loaded when needed. 190 * 191 * @since 4.4.0 192 * 193 * @param WP_REST_Server $wp_rest_server Server object. 194 */ 195 do_action( 'rest_api_init', $wp_rest_server ); 196 197 // Fire off the request. 198 $wp_rest_server->serve_request( $GLOBALS['wp']->query_vars['rest_route'] ); 199 200 // We're done. 201 die(); 202 } 203 204 /** 205 * Retrieves the URL prefix for any API resource. 206 * 207 * @since 4.4.0 208 * 209 * @return string Prefix. 210 */ 211 function rest_get_url_prefix() { 212 /** 213 * Filter the REST URL prefix. 214 * 215 * @since 4.4.0 216 * 217 * @param string $prefix URL prefix. Default 'wp-json'. 218 */ 219 return apply_filters( 'rest_url_prefix', 'wp-json' ); 220 } 221 222 /** 223 * Retrieves the URL to a REST endpoint on a site. 224 * 225 * Note: The returned URL is NOT escaped. 226 * 227 * @since 4.4.0 228 * 229 * @todo Check if this is even necessary 230 * 231 * @param int $blog_id Optional. Blog ID. Default of null returns URL for current blog. 232 * @param string $path Optional. REST route. Default '/'. 233 * @param string $scheme Optional. Sanitization scheme. Default 'json'. 234 * @return string Full URL to the endpoint. 235 */ 236 function get_rest_url( $blog_id = null, $path = '/', $scheme = 'json' ) { 237 if ( empty( $path ) ) { 238 $path = '/'; 239 } 240 241 if ( is_multisite() && get_blog_option( $blog_id, 'permalink_structure' ) || get_option( 'permalink_structure' ) ) { 242 $url = get_home_url( $blog_id, rest_get_url_prefix(), $scheme ); 243 $url .= '/' . ltrim( $path, '/' ); 244 } else { 245 $url = trailingslashit( get_home_url( $blog_id, '', $scheme ) ); 246 247 $path = '/' . ltrim( $path, '/' ); 248 249 $url = add_query_arg( 'rest_route', $path, $url ); 250 } 251 252 /** 253 * Filter the REST URL. 254 * 255 * Use this filter to adjust the url returned by the `get_rest_url` function. 256 * 257 * @since 4.4.0 258 * 259 * @param string $url REST URL. 260 * @param string $path REST route. 261 * @param int $blod_ig Blog ID. 262 * @param string $scheme Sanitization scheme. 263 */ 264 return apply_filters( 'rest_url', $url, $path, $blog_id, $scheme ); 265 } 266 267 /** 268 * Retrieves the URL to a REST endpoint. 269 * 270 * Note: The returned URL is NOT escaped. 271 * 272 * @since 4.4.0 273 * 274 * @param string $path Optional. REST route. Default empty. 275 * @param string $scheme Optional. Sanitization scheme. Default 'json'. 276 * @return string Full URL to the endpoint. 277 */ 278 function rest_url( $path = '', $scheme = 'json' ) { 279 return get_rest_url( null, $path, $scheme ); 280 } 281 282 /** 283 * Do a REST request. 284 * 285 * Used primarily to route internal requests through WP_REST_Server. 286 * 287 * @since 4.4.0 288 * 289 * @global WP_REST_Server $wp_rest_server 290 * 291 * @param WP_REST_Request|string $request Request. 292 * @return WP_REST_Response REST response. 293 */ 294 function rest_do_request( $request ) { 295 global $wp_rest_server; 296 $request = rest_ensure_request( $request ); 297 return $wp_rest_server->dispatch( $request ); 298 } 299 300 /** 301 * Ensures request arguments are a request object (for consistency). 302 * 303 * @since 4.4.0 304 * 305 * @param array|WP_REST_Request $request Request to check. 306 * @return WP_REST_Request REST request instance. 307 */ 308 function rest_ensure_request( $request ) { 309 if ( $request instanceof WP_REST_Request ) { 310 return $request; 311 } 312 313 return new WP_REST_Request( 'GET', '', $request ); 314 } 315 316 /** 317 * Ensures a REST response is a response object (for consistency). 318 * 319 * This implements WP_HTTP_Response, allowing usage of `set_status`/`header`/etc 320 * without needing to double-check the object. Will also allow WP_Error to indicate error 321 * responses, so users should immediately check for this value. 322 * 323 * @since 4.4.0 324 * 325 * @param WP_Error|WP_HTTP_Response|mixed $response Response to check. 326 * @return mixed WP_Error if response generated an error, WP_HTTP_Response if response 327 * is a already an instance, otherwise returns a new WP_REST_Response instance. 328 */ 329 function rest_ensure_response( $response ) { 330 if ( is_wp_error( $response ) ) { 331 return $response; 332 } 333 334 if ( $response instanceof WP_HTTP_Response ) { 335 return $response; 336 } 337 338 return new WP_REST_Response( $response ); 339 } 340 341 /** 342 * Handles _deprecated_function() errors. 343 * 344 * @since 4.4.0 345 * 346 * @param string $function Function name. 347 * @param string $replacement Replacement function name. 348 * @param string $version Version. 349 */ 350 function rest_handle_deprecated_function( $function, $replacement, $version ) { 351 if ( ! empty( $replacement ) ) { 352 $string = sprintf( __( '%1$s (since %2$s; use %3$s instead)' ), $function, $version, $replacement ); 353 } else { 354 $string = sprintf( __( '%1$s (since %2$s; no alternative available)' ), $function, $version ); 355 } 356 357 header( sprintf( 'X-WP-DeprecatedFunction: %s', $string ) ); 358 } 359 360 /** 361 * Handles _deprecated_argument() errors. 362 * 363 * @since 4.4.0 364 * 365 * @param string $function Function name. 366 * @param string $replacement Replacement function name. 367 * @param string $version Version. 368 */ 369 function rest_handle_deprecated_argument( $function, $replacement, $version ) { 370 if ( ! empty( $replacement ) ) { 371 $string = sprintf( __( '%1$s (since %2$s; %3$s)' ), $function, $version, $replacement ); 372 } else { 373 $string = sprintf( __( '%1$s (since %2$s; no alternative available)' ), $function, $version ); 374 } 375 376 header( sprintf( 'X-WP-DeprecatedParam: %s', $string ) ); 377 } 378 379 /** 380 * Sends Cross-Origin Resource Sharing headers with API requests. 381 * 382 * @since 4.4.0 383 * 384 * @param mixed $value Response data. 385 * @return mixed Response data. 386 */ 387 function rest_send_cors_headers( $value ) { 388 $origin = get_http_origin(); 389 390 if ( $origin ) { 391 header( 'Access-Control-Allow-Origin: ' . esc_url_raw( $origin ) ); 392 header( 'Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE' ); 393 header( 'Access-Control-Allow-Credentials: true' ); 394 } 395 396 return $value; 397 } 398 399 /** 400 * Handles OPTIONS requests for the server. 401 * 402 * This is handled outside of the server code, as it doesn't obey normal route 403 * mapping. 404 * 405 * @since 4.4.0 406 * 407 * @param mixed $response Current response, either response or `null` to indicate pass-through. 408 * @param WP_REST_Server $handler ResponseHandler instance (usually WP_REST_Server). 409 * @param WP_REST_Request $request The request that was used to make current response. 410 * @return WP_REST_Response Modified response, either response or `null` to indicate pass-through. 411 */ 412 function rest_handle_options_request( $response, $handler, $request ) { 413 if ( ! empty( $response ) || $request->get_method() !== 'OPTIONS' ) { 414 return $response; 415 } 416 417 $response = new WP_REST_Response(); 418 $data = array(); 419 420 $accept = array(); 421 422 foreach ( $handler->get_routes() as $route => $endpoints ) { 423 $match = preg_match( '@^' . $route . '$@i', $request->get_route(), $args ); 424 425 if ( ! $match ) { 426 continue; 427 } 428 429 $data = $handler->get_data_for_route( $route, $endpoints, 'help' ); 430 $accept = array_merge( $accept, $data['methods'] ); 431 break; 432 } 433 $response->header( 'Accept', implode( ', ', $accept ) ); 434 435 $response->set_data( $data ); 436 return $response; 437 } 438 439 /** 440 * Sends the "Allow" header to state all methods that can be sent to the current route. 441 * 442 * @since 4.4.0 443 * 444 * @param WP_REST_Response $response Current response being served. 445 * @param WP_REST_Server $server ResponseHandler instance (usually WP_REST_Server). 446 * @param WP_REST_Request $request The request that was used to make current response. 447 * @return WP_REST_Response Current response being served. 448 */ 449 function rest_send_allow_header( $response, $server, $request ) { 450 451 $matched_route = $response->get_matched_route(); 452 453 if ( ! $matched_route ) { 454 return $response; 455 } 456 457 $routes = $server->get_routes(); 458 459 $allowed_methods = array(); 460 461 // Get the allowed methods across the routes. 462 foreach ( $routes[ $matched_route ] as $_handler ) { 463 foreach ( $_handler['methods'] as $handler_method => $value ) { 464 465 if ( ! empty( $_handler['permission_callback'] ) ) { 466 467 $permission = call_user_func( $_handler['permission_callback'], $request ); 468 469 $allowed_methods[ $handler_method ] = true === $permission; 470 } else { 471 $allowed_methods[ $handler_method ] = true; 472 } 473 } 474 } 475 476 // Strip out all the methods that are not allowed (false values). 477 $allowed_methods = array_filter( $allowed_methods ); 478 479 if ( $allowed_methods ) { 480 $response->header( 'Allow', implode( ', ', array_map( 'strtoupper', array_keys( $allowed_methods ) ) ) ); 481 } 482 483 return $response; 484 } 485 486 /** 487 * Determines if the variable a list. 488 * 489 * A list would be defined as a numeric-indexed array. 490 * 491 * @since 4.4.0 492 * 493 * @param mixed $data Variable to check. 494 * @return bool Whether the variable is a list. 495 */ 496 function rest_is_list( $data ) { 497 if ( ! is_array( $data ) ) { 498 return false; 499 } 500 501 $keys = array_keys( $data ); 502 $string_keys = array_filter( $keys, 'is_string' ); 503 return count( $string_keys ) === 0; 504 } 505 506 /** 507 * Adds the REST API URL to the WP RSD endpoint. 508 * 509 * @since 4.4.0 510 * 511 * @see get_rest_url() 512 */ 513 function rest_output_rsd() { 514 $api_root = get_rest_url(); 515 516 if ( empty( $api_root ) ) { 517 return; 518 } 519 ?> 520 <api name="WP-API" blogID="1" preferred="false" apiLink="<?php echo esc_url( $api_root ); ?>" /> 521 <?php 522 } 523 524 /** 525 * Outputs the REST API link tag into page header. 526 * 527 * @since 4.4.0 528 * 529 * @see get_rest_url() 530 */ 531 function rest_output_link_wp_head() { 532 $api_root = get_rest_url(); 533 534 if ( empty( $api_root ) ) { 535 return; 536 } 537 538 echo "<link rel='https://github.com/WP-API/WP-API' href='" . esc_url( $api_root ) . "' />\n"; 539 } 540 541 /** 542 * Sends a Link header for the REST API. 543 * 544 * @since 4.4.0 545 */ 546 function rest_output_link_header() { 547 if ( headers_sent() ) { 548 return; 549 } 550 551 $api_root = get_rest_url(); 552 553 if ( empty( $api_root ) ) { 554 return; 555 } 556 557 header( 'Link: <' . esc_url_raw( $api_root ) . '>; rel="https://github.com/WP-API/WP-API"', false ); 558 } 559 560 /** 561 * Checks for errors when using cookie-based authentication. 562 * 563 * WordPress' built-in cookie authentication is always active 564 * for logged in users. However, the API has to check nonces 565 * for each request to ensure users are not vulnerable to CSRF. 566 * 567 * @since 4.4.0 568 * 569 * @global mixed $wp_rest_auth_cookie 570 * 571 * @param WP_Error|mixed $result Error from another authentication handler, null if we should handle it, 572 * or another value if not. 573 * @return WP_Error|mixed|bool WP_Error if the cookie is invalid, the $result, otherwise true. 574 */ 575 function rest_cookie_check_errors( $result ) { 576 if ( ! empty( $result ) ) { 577 return $result; 578 } 579 580 global $wp_rest_auth_cookie; 581 582 /* 583 * Is cookie authentication being used? (If we get an auth 584 * error, but we're still logged in, another authentication 585 * must have been used). 586 */ 587 if ( true !== $wp_rest_auth_cookie && is_user_logged_in() ) { 588 return $result; 589 } 590 591 // Determine if there is a nonce. 592 $nonce = null; 593 594 if ( isset( $_REQUEST['_wp_rest_nonce'] ) ) { 595 $nonce = $_REQUEST['_wp_rest_nonce']; 596 } elseif ( isset( $_SERVER['HTTP_X_WP_NONCE'] ) ) { 597 $nonce = $_SERVER['HTTP_X_WP_NONCE']; 598 } 599 600 if ( null === $nonce ) { 601 // No nonce at all, so act as if it's an unauthenticated request. 602 wp_set_current_user( 0 ); 603 return true; 604 } 605 606 // Check the nonce. 607 $result = wp_verify_nonce( $nonce, 'wp_rest' ); 608 609 if ( ! $result ) { 610 return new WP_Error( 'rest_cookie_invalid_nonce', __( 'Cookie nonce is invalid' ), array( 'status' => 403 ) ); 611 } 612 613 return true; 614 } 615 616 /** 617 * Collects cookie authentication status. 618 * 619 * Collects errors from wp_validate_auth_cookie for use by rest_cookie_check_errors. 620 * 621 * @since 4.4.0 622 * 623 * @see current_action() 624 * @global mixed $wp_rest_auth_cookie 625 */ 626 function rest_cookie_collect_status() { 627 global $wp_rest_auth_cookie; 628 629 $status_type = current_action(); 630 631 if ( 'auth_cookie_valid' !== $status_type ) { 632 $wp_rest_auth_cookie = substr( $status_type, 12 ); 633 return; 634 } 635 636 $wp_rest_auth_cookie = true; 637 } 638 639 /** 640 * Parses an RFC3339 timestamp into a DateTime. 641 * 642 * @since 4.4.0 643 * 644 * @param string $date RFC3339 timestamp. 645 * @param bool $force_utc Optional. Whether to force UTC timezone instead of using 646 * the timestamp's timezone. Default false. 647 * @return DateTime DateTime instance. 648 */ 649 function rest_parse_date( $date, $force_utc = false ) { 650 if ( $force_utc ) { 651 $date = preg_replace( '/[+-]\d+:?\d+$/', '+00:00', $date ); 652 } 653 654 $regex = '#^\d{4}-\d{2}-\d{2}[Tt ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}(?::\d{2})?)?$#'; 655 656 if ( ! preg_match( $regex, $date, $matches ) ) { 657 return false; 658 } 659 660 return strtotime( $date ); 661 } 662 663 /** 664 * Retrieves a local date with its GMT equivalent, in MySQL datetime format. 665 * 666 * @since 4.4.0 667 * 668 * @see rest_parse_date() 669 * 670 * @param string $date RFC3339 timestamp. 671 * @param bool $force_utc Whether a UTC timestamp should be forced. Default false. 672 * @return array|null Local and UTC datetime strings, in MySQL datetime format (Y-m-d H:i:s), 673 * null on failure. 674 */ 675 function rest_get_date_with_gmt( $date, $force_utc = false ) { 676 $date = rest_parse_date( $date, $force_utc ); 677 678 if ( empty( $date ) ) { 679 return null; 680 } 681 682 $utc = date( 'Y-m-d H:i:s', $date ); 683 $local = get_date_from_gmt( $utc ); 684 685 return array( $local, $utc ); 686 } -
src/wp-includes/rest-api.php
1 <?php 2 /** 3 * REST API functions. 4 * 5 * @package WordPress 6 * @subpackage REST_API 7 */ 8 9 /** 10 * Version number for our API. 11 * 12 * @var string 13 */ 14 define( 'REST_API_VERSION', '2.0' ); 15 16 /** WP_REST_Server class */ 17 require_once( ABSPATH . WPINC . '/rest-api/lib/class-wp-rest-server.php' ); 18 19 /** WP_REST_Response class */ 20 require_once( ABSPATH . WPINC . '/rest-api/lib/class-wp-rest-response.php' ); 21 22 /** WP_REST_Request class */ 23 require_once( ABSPATH . WPINC . '/rest-api/lib/class-wp-rest-request.php' ); 24 25 /** REST functions */ 26 require_once( ABSPATH . WPINC . '/rest-api/rest-functions.php' ); -
src/wp-settings.php
153 153 require( ABSPATH . WPINC . '/nav-menu.php' ); 154 154 require( ABSPATH . WPINC . '/nav-menu-template.php' ); 155 155 require( ABSPATH . WPINC . '/admin-bar.php' ); 156 require( ABSPATH . WPINC . '/rest-api.php' ); 156 157 157 158 // Load multisite-specific files. 158 159 if ( is_multisite() ) { -
tests/phpunit/includes/bootstrap.php
90 90 _delete_all_posts(); 91 91 92 92 require dirname( __FILE__ ) . '/testcase.php'; 93 require dirname( __FILE__ ) . '/testcase-rest-api.php'; 93 94 require dirname( __FILE__ ) . '/testcase-xmlrpc.php'; 94 95 require dirname( __FILE__ ) . '/testcase-ajax.php'; 95 96 require dirname( __FILE__ ) . '/testcase-canonical.php'; 96 97 require dirname( __FILE__ ) . '/exceptions.php'; 97 98 require dirname( __FILE__ ) . '/utils.php'; 99 require dirname( __FILE__ ) . '/spy-rest-server.php'; 98 100 99 101 /** 100 102 * A child class of the PHP test runner. -
tests/phpunit/includes/spy-rest-server.php
1 <?php 2 3 class Spy_REST_Server extends WP_REST_Server { 4 /** 5 * Get the raw $endpoints data from the server 6 * 7 * @return array 8 */ 9 public function get_raw_endpoint_data() { 10 return $this->endpoints; 11 } 12 13 /** 14 * Allow calling protected methods from tests 15 * 16 * @param string $method Method to call 17 * @param array $args Arguments to pass to the method 18 * @return mixed 19 */ 20 public function __call( $method, $args ) { 21 return call_user_func_array( array( $this, $method ), $args ); 22 } 23 } -
tests/phpunit/includes/testcase-rest-api.php
1 <?php 2 3 abstract class WP_Test_REST_TestCase extends WP_UnitTestCase { 4 protected function assertErrorResponse( $code, $response, $status = null ) { 5 6 if ( is_a( $response, 'WP_REST_Response' ) ) { 7 $response = $response->as_error(); 8 } 9 10 $this->assertInstanceOf( 'WP_Error', $response ); 11 $this->assertEquals( $code, $response->get_error_code() ); 12 13 if ( null !== $status ) { 14 $data = $response->get_error_data(); 15 $this->assertArrayHasKey( 'status', $data ); 16 $this->assertEquals( $status, $data['status'] ); 17 } 18 } 19 } -
tests/phpunit/tests/rest-api/rest-request.php
1 <?php 2 /** 3 * Unit tests covering WP_REST_Request functionality. 4 * 5 * @package WordPress 6 * @subpackage REST API 7 */ 8 9 /** 10 * @group restapi 11 */ 12 class Tests_REST_Request extends WP_UnitTestCase { 13 public function setUp() { 14 $this->request = new WP_REST_Request(); 15 } 16 17 public function test_header() { 18 $value = 'application/x-wp-example'; 19 20 $this->request->set_header( 'Content-Type', $value ); 21 22 $this->assertEquals( $value, $this->request->get_header( 'Content-Type' ) ); 23 } 24 25 public function test_header_missing() { 26 $this->assertNull( $this->request->get_header( 'missing' ) ); 27 $this->assertNull( $this->request->get_header_as_array( 'missing' ) ); 28 } 29 30 public function test_header_multiple() { 31 $value1 = 'application/x-wp-example-1'; 32 $value2 = 'application/x-wp-example-2'; 33 $this->request->add_header( 'Accept', $value1 ); 34 $this->request->add_header( 'Accept', $value2 ); 35 36 $this->assertEquals( $value1 . ',' . $value2, $this->request->get_header( 'Accept' ) ); 37 $this->assertEquals( array( $value1, $value2 ), $this->request->get_header_as_array( 'Accept' ) ); 38 } 39 40 public static function header_provider() { 41 return array( 42 array( 'Test', 'test' ), 43 array( 'TEST', 'test' ), 44 array( 'Test-Header', 'test_header' ), 45 array( 'test-header', 'test_header' ), 46 array( 'Test_Header', 'test_header' ), 47 array( 'test_header', 'test_header' ), 48 ); 49 } 50 51 /** 52 * @dataProvider header_provider 53 * @param string $original Original header key. 54 * @param string $expected Expected canonicalized version. 55 */ 56 public function test_header_canonicalization( $original, $expected ) { 57 $this->assertEquals( $expected, $this->request->canonicalize_header_name( $original ) ); 58 } 59 60 public static function content_type_provider() { 61 return array( 62 // Check basic parsing. 63 array( 'application/x-wp-example', 'application/x-wp-example', 'application', 'x-wp-example', '' ), 64 array( 'application/x-wp-example; charset=utf-8', 'application/x-wp-example', 'application', 'x-wp-example', 'charset=utf-8' ), 65 66 // Check case insensitivity. 67 array( 'APPLICATION/x-WP-Example', 'application/x-wp-example', 'application', 'x-wp-example', '' ), 68 ); 69 } 70 71 /** 72 * @dataProvider content_type_provider 73 * 74 * @param string $header Header value. 75 * @param string $value Full type value. 76 * @param string $type Main type (application, text, etc). 77 * @param string $subtype Subtype (json, etc). 78 * @param string $parameters Parameters (charset=utf-8, etc). 79 */ 80 public function test_content_type_parsing( $header, $value, $type, $subtype, $parameters ) { 81 // Check we start with nothing. 82 $this->assertEmpty( $this->request->get_content_type() ); 83 84 $this->request->set_header( 'Content-Type', $header ); 85 $parsed = $this->request->get_content_type(); 86 87 $this->assertEquals( $value, $parsed['value'] ); 88 $this->assertEquals( $type, $parsed['type'] ); 89 $this->assertEquals( $subtype, $parsed['subtype'] ); 90 $this->assertEquals( $parameters, $parsed['parameters'] ); 91 } 92 93 protected function request_with_parameters() { 94 $this->request->set_url_params( array( 95 'source' => 'url', 96 'has_url_params' => true, 97 ) ); 98 $this->request->set_query_params( array( 99 'source' => 'query', 100 'has_query_params' => true, 101 ) ); 102 $this->request->set_body_params( array( 103 'source' => 'body', 104 'has_body_params' => true, 105 ) ); 106 107 $json_data = wp_json_encode( array( 108 'source' => 'json', 109 'has_json_params' => true, 110 ) ); 111 $this->request->set_body( $json_data ); 112 113 $this->request->set_default_params( array( 114 'source' => 'defaults', 115 'has_default_params' => true, 116 ) ); 117 } 118 119 public function test_parameter_order() { 120 $this->request_with_parameters(); 121 122 $this->request->set_method( 'GET' ); 123 124 // Check that query takes precedence. 125 $this->assertEquals( 'query', $this->request->get_param( 'source' ) ); 126 127 // Check that the correct arguments are parsed (and that falling through 128 // the stack works). 129 $this->assertTrue( $this->request->get_param( 'has_url_params' ) ); 130 $this->assertTrue( $this->request->get_param( 'has_query_params' ) ); 131 $this->assertTrue( $this->request->get_param( 'has_default_params' ) ); 132 133 // POST and JSON parameters shouldn't be parsed. 134 $this->assertEmpty( $this->request->get_param( 'has_body_params' ) ); 135 $this->assertEmpty( $this->request->get_param( 'has_json_params' ) ); 136 } 137 138 public function test_parameter_order_post() { 139 $this->request_with_parameters(); 140 141 $this->request->set_method( 'POST' ); 142 $this->request->set_header( 'Content-Type', 'application/x-www-form-urlencoded' ); 143 $this->request->set_attributes( array( 'accept_json' => true ) ); 144 145 // Check that POST takes precedence. 146 $this->assertEquals( 'body', $this->request->get_param( 'source' ) ); 147 148 // Check that the correct arguments are parsed (and that falling through 149 // the stack works). 150 $this->assertTrue( $this->request->get_param( 'has_url_params' ) ); 151 $this->assertTrue( $this->request->get_param( 'has_query_params' ) ); 152 $this->assertTrue( $this->request->get_param( 'has_body_params' ) ); 153 $this->assertTrue( $this->request->get_param( 'has_default_params' ) ); 154 155 // JSON shouldn't be parsed. 156 $this->assertEmpty( $this->request->get_param( 'has_json_params' ) ); 157 } 158 159 public function test_parameter_order_json() { 160 $this->request_with_parameters(); 161 162 $this->request->set_method( 'POST' ); 163 $this->request->set_header( 'Content-Type', 'application/json' ); 164 $this->request->set_attributes( array( 'accept_json' => true ) ); 165 166 // Check that JSON takes precedence. 167 $this->assertEquals( 'json', $this->request->get_param( 'source' ) ); 168 169 // Check that the correct arguments are parsed (and that falling through 170 // the stack works). 171 $this->assertTrue( $this->request->get_param( 'has_url_params' ) ); 172 $this->assertTrue( $this->request->get_param( 'has_query_params' ) ); 173 $this->assertTrue( $this->request->get_param( 'has_body_params' ) ); 174 $this->assertTrue( $this->request->get_param( 'has_json_params' ) ); 175 $this->assertTrue( $this->request->get_param( 'has_default_params' ) ); 176 } 177 178 public function test_parameter_order_json_invalid() { 179 $this->request_with_parameters(); 180 181 $this->request->set_method( 'POST' ); 182 $this->request->set_header( 'Content-Type', 'application/json' ); 183 $this->request->set_attributes( array( 'accept_json' => true ) ); 184 185 // Use invalid JSON data. 186 $this->request->set_body( '{ this is not json }' ); 187 188 // Check that JSON is ignored. 189 $this->assertEquals( 'body', $this->request->get_param( 'source' ) ); 190 191 // Check that the correct arguments are parsed (and that falling through 192 // the stack works). 193 $this->assertTrue( $this->request->get_param( 'has_url_params' ) ); 194 $this->assertTrue( $this->request->get_param( 'has_query_params' ) ); 195 $this->assertTrue( $this->request->get_param( 'has_body_params' ) ); 196 $this->assertTrue( $this->request->get_param( 'has_default_params' ) ); 197 198 // JSON should be ignored. 199 $this->assertEmpty( $this->request->get_param( 'has_json_params' ) ); 200 } 201 202 /** 203 * PUT requests don't get $_POST automatically parsed, so ensure that 204 * WP_REST_Request does it for us. 205 */ 206 public function test_parameters_for_put() { 207 $data = array( 208 'foo' => 'bar', 209 'alot' => array( 210 'of' => 'parameters', 211 ), 212 'list' => array( 213 'of', 214 'cool', 215 'stuff', 216 ), 217 ); 218 219 $this->request->set_method( 'PUT' ); 220 $this->request->set_body_params( array() ); 221 $this->request->set_body( http_build_query( $data ) ); 222 223 foreach ( $data as $key => $expected_value ) { 224 $this->assertEquals( $expected_value, $this->request->get_param( $key ) ); 225 } 226 } 227 228 public function test_parameters_for_json_put() { 229 $data = array( 230 'foo' => 'bar', 231 'alot' => array( 232 'of' => 'parameters', 233 ), 234 'list' => array( 235 'of', 236 'cool', 237 'stuff', 238 ), 239 ); 240 241 $this->request->set_method( 'PUT' ); 242 $this->request->add_header( 'content-type', 'application/json' ); 243 $this->request->set_body( wp_json_encode( $data ) ); 244 245 foreach ( $data as $key => $expected_value ) { 246 $this->assertEquals( $expected_value, $this->request->get_param( $key ) ); 247 } 248 } 249 250 public function test_parameters_for_json_post() { 251 $data = array( 252 'foo' => 'bar', 253 'alot' => array( 254 'of' => 'parameters', 255 ), 256 'list' => array( 257 'of', 258 'cool', 259 'stuff', 260 ), 261 ); 262 263 $this->request->set_method( 'POST' ); 264 $this->request->add_header( 'content-type', 'application/json' ); 265 $this->request->set_body( wp_json_encode( $data ) ); 266 267 foreach ( $data as $key => $expected_value ) { 268 $this->assertEquals( $expected_value, $this->request->get_param( $key ) ); 269 } 270 } 271 272 public function test_parameter_merging() { 273 $this->request_with_parameters(); 274 275 $this->request->set_method( 'POST' ); 276 277 $expected = array( 278 'source' => 'body', 279 'has_url_params' => true, 280 'has_query_params' => true, 281 'has_body_params' => true, 282 'has_default_params' => true, 283 ); 284 $this->assertEquals( $expected, $this->request->get_params() ); 285 } 286 287 public function test_sanitize_params() { 288 $this->request->set_url_params( array( 289 'someinteger' => '123', 290 'somestring' => 'hello', 291 )); 292 293 $this->request->set_attributes( array( 294 'args' => array( 295 'someinteger' => array( 296 'sanitize_callback' => 'absint', 297 ), 298 'somestring' => array( 299 'sanitize_callback' => 'absint', 300 ), 301 ), 302 ) ); 303 304 $this->request->sanitize_params(); 305 306 $this->assertEquals( 123, $this->request->get_param( 'someinteger' ) ); 307 $this->assertEquals( 0, $this->request->get_param( 'somestring' ) ); 308 } 309 310 public function test_has_valid_params_required_flag() { 311 $this->request->set_attributes( array( 312 'args' => array( 313 'someinteger' => array( 314 'required' => true, 315 ), 316 ), 317 ) ); 318 319 $valid = $this->request->has_valid_params(); 320 321 $this->assertWPError( $valid ); 322 $this->assertEquals( 'rest_missing_callback_param', $valid->get_error_code() ); 323 } 324 325 public function test_has_valid_params_required_flag_multiple() { 326 $this->request->set_attributes( array( 327 'args' => array( 328 'someinteger' => array( 329 'required' => true, 330 ), 331 'someotherinteger' => array( 332 'required' => true, 333 ), 334 ), 335 )); 336 337 $valid = $this->request->has_valid_params(); 338 339 $this->assertWPError( $valid ); 340 $this->assertEquals( 'rest_missing_callback_param', $valid->get_error_code() ); 341 342 $data = $valid->get_error_data( 'rest_missing_callback_param' ); 343 344 $this->assertTrue( in_array( 'someinteger', $data['params'] ) ); 345 $this->assertTrue( in_array( 'someotherinteger', $data['params'] ) ); 346 } 347 348 public function test_has_valid_params_validate_callback() { 349 $this->request->set_url_params( array( 350 'someinteger' => '123', 351 )); 352 353 $this->request->set_attributes( array( 354 'args' => array( 355 'someinteger' => array( 356 'validate_callback' => '__return_false', 357 ), 358 ), 359 )); 360 361 $valid = $this->request->has_valid_params(); 362 363 $this->assertWPError( $valid ); 364 $this->assertEquals( 'rest_invalid_param', $valid->get_error_code() ); 365 } 366 367 public function test_has_multiple_invalid_params_validate_callback() { 368 $this->request->set_url_params( array( 369 'someinteger' => '123', 370 'someotherinteger' => '123', 371 )); 372 373 $this->request->set_attributes( array( 374 'args' => array( 375 'someinteger' => array( 376 'validate_callback' => '__return_false', 377 ), 378 'someotherinteger' => array( 379 'validate_callback' => '__return_false', 380 ), 381 ), 382 )); 383 384 $valid = $this->request->has_valid_params(); 385 386 $this->assertWPError( $valid ); 387 $this->assertEquals( 'rest_invalid_param', $valid->get_error_code() ); 388 389 $data = $valid->get_error_data( 'rest_invalid_param' ); 390 391 $this->assertArrayHasKey( 'someinteger', $data['params'] ); 392 $this->assertArrayHasKey( 'someotherinteger', $data['params'] ); 393 } 394 } -
tests/phpunit/tests/rest-api/rest-server.php
1 <?php 2 /** 3 * Unit tests covering WP_REST_Server functionality. 4 * 5 * @package WordPress 6 * @subpackage REST API 7 */ 8 9 /** 10 * @group restapi 11 */ 12 class Tests_REST_Server extends WP_Test_REST_TestCase { 13 public function setUp() { 14 parent::setUp(); 15 16 /** @var WP_REST_Server $wp_rest_server */ 17 global $wp_rest_server; 18 $this->server = $wp_rest_server = new Spy_REST_Server(); 19 20 do_action( 'rest_api_init', $this->server ); 21 } 22 23 public function test_envelope() { 24 $data = array( 25 'amount of arbitrary data' => 'alot', 26 ); 27 $status = 987; 28 $headers = array( 29 'Arbitrary-Header' => 'value', 30 'Multiple' => 'maybe, yes', 31 ); 32 33 $response = new WP_REST_Response( $data, $status ); 34 $response->header( 'Arbitrary-Header', 'value' ); 35 36 // Check header concatenation as well. 37 $response->header( 'Multiple', 'maybe' ); 38 $response->header( 'Multiple', 'yes', false ); 39 40 $envelope_response = $this->server->envelope_response( $response, false ); 41 42 // The envelope should still be a response, but with defaults. 43 $this->assertInstanceOf( 'WP_REST_Response', $envelope_response ); 44 $this->assertEquals( 200, $envelope_response->get_status() ); 45 $this->assertEmpty( $envelope_response->get_headers() ); 46 $this->assertEmpty( $envelope_response->get_links() ); 47 48 $enveloped = $envelope_response->get_data(); 49 50 $this->assertEquals( $data, $enveloped['body'] ); 51 $this->assertEquals( $status, $enveloped['status'] ); 52 $this->assertEquals( $headers, $enveloped['headers'] ); 53 } 54 55 public function test_default_param() { 56 57 register_rest_route( 'test-ns', '/test', array( 58 'methods' => array( 'GET' ), 59 'callback' => '__return_null', 60 'args' => array( 61 'foo' => array( 62 'default' => 'bar', 63 ), 64 ), 65 ) ); 66 67 $request = new WP_REST_Request( 'GET', '/test-ns/test' ); 68 $response = $this->server->dispatch( $request ); 69 70 $this->assertEquals( 'bar', $request['foo'] ); 71 } 72 73 public function test_default_param_is_overridden() { 74 75 register_rest_route( 'test-ns', '/test', array( 76 'methods' => array( 'GET' ), 77 'callback' => '__return_null', 78 'args' => array( 79 'foo' => array( 80 'default' => 'bar', 81 ), 82 ), 83 ) ); 84 85 $request = new WP_REST_Request( 'GET', '/test-ns/test' ); 86 $request->set_query_params( array( 'foo' => 123 ) ); 87 $response = $this->server->dispatch( $request ); 88 89 $this->assertEquals( '123', $request['foo'] ); 90 } 91 92 public function test_optional_param() { 93 register_rest_route( 'optional', '/test', array( 94 'methods' => array( 'GET' ), 95 'callback' => '__return_null', 96 'args' => array( 97 'foo' => array(), 98 ), 99 ) ); 100 101 $request = new WP_REST_Request( 'GET', '/optional/test' ); 102 $request->set_query_params( array() ); 103 $response = $this->server->dispatch( $request ); 104 $this->assertInstanceOf( 'WP_REST_Response', $response ); 105 $this->assertEquals( 200, $response->get_status() ); 106 $this->assertArrayNotHasKey( 'foo', (array) $request ); 107 } 108 109 /** 110 * Pass a capability which the user does not have, this should 111 * result in a 403 error. 112 */ 113 function test_rest_route_capability_authorization_fails() { 114 register_rest_route( 'test-ns', '/test', array( 115 'methods' => 'GET', 116 'callback' => '__return_null', 117 'should_exist' => false, 118 'permission_callback' => array( $this, 'permission_denied' ), 119 ) ); 120 121 $request = new WP_REST_Request( 'GET', '/test-ns/test', array() ); 122 $result = $this->server->dispatch( $request ); 123 124 $this->assertEquals( 403, $result->get_status() ); 125 } 126 127 /** 128 * An editor should be able to get access to an route with the 129 * edit_posts capability. 130 */ 131 function test_rest_route_capability_authorization() { 132 register_rest_route( 'test-ns', '/test', array( 133 'methods' => 'GET', 134 'callback' => '__return_null', 135 'should_exist' => false, 136 'permission_callback' => '__return_true', 137 ) ); 138 139 $editor = $this->factory->user->create( array( 'role' => 'editor' ) ); 140 141 $request = new WP_REST_Request( 'GET', '/test-ns/test', array() ); 142 143 wp_set_current_user( $editor ); 144 145 $result = $this->server->dispatch( $request ); 146 147 $this->assertEquals( 200, $result->get_status() ); 148 } 149 150 /** 151 * An "Allow" HTTP header should be sent with a request 152 * for all available methods on that route. 153 */ 154 function test_allow_header_sent() { 155 156 register_rest_route( 'test-ns', '/test', array( 157 'methods' => 'GET', 158 'callback' => '__return_null', 159 'should_exist' => false, 160 ) ); 161 162 $request = new WP_REST_Request( 'GET', '/test-ns/test', array() ); 163 164 $result = $this->server->dispatch( $request ); 165 $result = apply_filters( 'rest_post_dispatch', $result, $this->server, $request ); 166 167 $this->assertFalse( $result->get_status() !== 200 ); 168 169 $sent_headers = $result->get_headers(); 170 $this->assertEquals( $sent_headers['Allow'], 'GET' ); 171 } 172 173 /** 174 * The "Allow" HTTP header should include all available 175 * methods that can be sent to a route. 176 */ 177 function test_allow_header_sent_with_multiple_methods() { 178 179 register_rest_route( 'test-ns', '/test', array( 180 'methods' => 'GET', 181 'callback' => '__return_null', 182 'should_exist' => false, 183 ) ); 184 185 register_rest_route( 'test-ns', '/test', array( 186 'methods' => 'POST', 187 'callback' => '__return_null', 188 'should_exist' => false, 189 ) ); 190 191 $request = new WP_REST_Request( 'GET', '/test-ns/test', array() ); 192 193 $result = $this->server->dispatch( $request ); 194 195 $this->assertFalse( $result->get_status() !== 200 ); 196 197 $result = apply_filters( 'rest_post_dispatch', $result, $this->server, $request ); 198 199 $sent_headers = $result->get_headers(); 200 $this->assertEquals( $sent_headers['Allow'], 'GET, POST' ); 201 } 202 203 /** 204 * The "Allow" HTTP header should NOT include other methods 205 * which the user does not have access to. 206 */ 207 function test_allow_header_send_only_permitted_methods() { 208 209 register_rest_route( 'test-ns', '/test', array( 210 'methods' => 'GET', 211 'callback' => '__return_null', 212 'should_exist' => false, 213 'permission_callback' => array( $this, 'permission_denied' ), 214 ) ); 215 216 register_rest_route( 'test-ns', '/test', array( 217 'methods' => 'POST', 218 'callback' => '__return_null', 219 'should_exist' => false, 220 ) ); 221 222 $request = new WP_REST_Request( 'GET', '/test-ns/test', array() ); 223 224 $result = $this->server->dispatch( $request ); 225 $result = apply_filters( 'rest_post_dispatch', $result, $this->server, $request ); 226 227 $this->assertEquals( $result->get_status(), 403 ); 228 229 $sent_headers = $result->get_headers(); 230 $this->assertEquals( $sent_headers['Allow'], 'POST' ); 231 } 232 233 public function permission_denied() { 234 return new WP_Error( 'forbidden', 'You are not allowed to do this', array( 'status' => 403 ) ); 235 } 236 237 public function test_error_to_response() { 238 $code = 'wp-api-test-error'; 239 $message = 'Test error message for the API'; 240 $error = new WP_Error( $code, $message ); 241 242 $response = $this->server->error_to_response( $error ); 243 $this->assertInstanceOf( 'WP_REST_Response', $response ); 244 245 // Make sure we default to a 500 error. 246 $this->assertEquals( 500, $response->get_status() ); 247 248 $data = $response->get_data(); 249 $this->assertCount( 1, $data ); 250 251 $this->assertEquals( $code, $data[0]['code'] ); 252 $this->assertEquals( $message, $data[0]['message'] ); 253 } 254 255 public function test_error_to_response_with_status() { 256 $code = 'wp-api-test-error'; 257 $message = 'Test error message for the API'; 258 $error = new WP_Error( $code, $message, array( 'status' => 400 ) ); 259 260 $response = $this->server->error_to_response( $error ); 261 $this->assertInstanceOf( 'WP_REST_Response', $response ); 262 263 $this->assertEquals( 400, $response->get_status() ); 264 265 $data = $response->get_data(); 266 $this->assertCount( 1, $data ); 267 268 $this->assertEquals( $code, $data[0]['code'] ); 269 $this->assertEquals( $message, $data[0]['message'] ); 270 } 271 272 public function test_rest_error() { 273 $data = array( 274 array( 275 'code' => 'wp-api-test-error', 276 'message' => 'Message text', 277 ), 278 ); 279 $expected = wp_json_encode( $data ); 280 $response = $this->server->json_error( 'wp-api-test-error', 'Message text' ); 281 282 $this->assertEquals( $expected, $response ); 283 } 284 285 public function test_json_error_with_status() { 286 $stub = $this->getMockBuilder( 'Spy_REST_Server' ) 287 ->setMethods( array( 'set_status' ) ) 288 ->getMock(); 289 290 $stub->expects( $this->once() ) 291 ->method( 'set_status' ) 292 ->with( $this->equalTo( 400 ) ); 293 294 $data = array( 295 array( 296 'code' => 'wp-api-test-error', 297 'message' => 'Message text', 298 ), 299 ); 300 $expected = wp_json_encode( $data ); 301 302 $response = $stub->json_error( 'wp-api-test-error', 'Message text', 400 ); 303 304 $this->assertEquals( $expected, $response ); 305 } 306 307 public function test_response_to_data_links() { 308 $response = new WP_REST_Response(); 309 $response->add_link( 'self', 'http://example.com/' ); 310 $response->add_link( 'alternate', 'http://example.org/', array( 'type' => 'application/xml' ) ); 311 312 $data = $this->server->response_to_data( $response, false ); 313 $this->assertArrayHasKey( '_links', $data ); 314 315 $self = array( 316 'href' => 'http://example.com/', 317 ); 318 $this->assertEquals( $self, $data['_links']['self'][0] ); 319 320 $alternate = array( 321 'href' => 'http://example.org/', 322 'type' => 'application/xml', 323 ); 324 $this->assertEquals( $alternate, $data['_links']['alternate'][0] ); 325 } 326 327 public function test_link_embedding() { 328 // Register our testing route. 329 $this->server->register_route( 'test', '/test/embeddable', array( 330 'methods' => 'GET', 331 'callback' => array( $this, 'embedded_response_callback' ), 332 ) ); 333 $response = new WP_REST_Response(); 334 335 // External links should be ignored. 336 $response->add_link( 'alternate', 'http://not-api.example.com/', array( 'embeddable' => true ) ); 337 338 // All others should be embedded. 339 $response->add_link( 'alternate', rest_url( '/test/embeddable' ), array( 'embeddable' => true ) ); 340 341 $data = $this->server->response_to_data( $response, true ); 342 $this->assertArrayHasKey( '_embedded', $data ); 343 344 $alternate = $data['_embedded']['alternate']; 345 $this->assertCount( 2, $alternate ); 346 $this->assertEmpty( $alternate[0] ); 347 348 $this->assertInternalType( 'array', $alternate[1] ); 349 $this->assertArrayNotHasKey( 'code', $alternate[1] ); 350 $this->assertTrue( $alternate[1]['hello'] ); 351 352 // Ensure the context is set to embed when requesting. 353 $this->assertEquals( 'embed', $alternate[1]['parameters']['context'] ); 354 } 355 356 /** 357 * @depends test_link_embedding 358 */ 359 public function test_link_embedding_self() { 360 // Register our testing route. 361 $this->server->register_route( 'test', '/test/embeddable', array( 362 'methods' => 'GET', 363 'callback' => array( $this, 'embedded_response_callback' ), 364 ) ); 365 $response = new WP_REST_Response(); 366 367 // 'self' should be ignored. 368 $response->add_link( 'self', rest_url( '/test/notembeddable' ), array( 'embeddable' => true ) ); 369 370 $data = $this->server->response_to_data( $response, true ); 371 372 $this->assertArrayNotHasKey( '_embedded', $data ); 373 } 374 375 /** 376 * @depends test_link_embedding 377 */ 378 public function test_link_embedding_params() { 379 // Register our testing route. 380 $this->server->register_route( 'test', '/test/embeddable', array( 381 'methods' => 'GET', 382 'callback' => array( $this, 'embedded_response_callback' ), 383 ) ); 384 385 $response = new WP_REST_Response(); 386 $response->add_link( 'alternate', rest_url( '/test/embeddable?parsed_params=yes' ), array( 'embeddable' => true ) ); 387 388 $data = $this->server->response_to_data( $response, true ); 389 390 $this->assertArrayHasKey( '_embedded', $data ); 391 $this->assertArrayHasKey( 'alternate', $data['_embedded'] ); 392 $data = $data['_embedded']['alternate'][0]; 393 394 $this->assertEquals( 'yes', $data['parameters']['parsed_params'] ); 395 } 396 397 /** 398 * @depends test_link_embedding_params 399 */ 400 public function test_link_embedding_error() { 401 // Register our testing route. 402 $this->server->register_route( 'test', '/test/embeddable', array( 403 'methods' => 'GET', 404 'callback' => array( $this, 'embedded_response_callback' ), 405 ) ); 406 407 $response = new WP_REST_Response(); 408 $response->add_link( 'up', rest_url( '/test/embeddable?error=1' ), array( 'embeddable' => true ) ); 409 410 $data = $this->server->response_to_data( $response, true ); 411 412 $this->assertArrayHasKey( '_embedded', $data ); 413 $this->assertArrayHasKey( 'up', $data['_embedded'] ); 414 415 // Check that errors are embedded correctly. 416 $up = $data['_embedded']['up']; 417 $this->assertCount( 1, $up ); 418 419 $up_data = $up[0]; 420 $this->assertEquals( 'wp-api-test-error', $up_data[0]['code'] ); 421 $this->assertEquals( 'Test message', $up_data[0]['message'] ); 422 $this->assertEquals( 403, $up_data[0]['data']['status'] ); 423 } 424 425 /** 426 * Ensure embedding is a no-op without links in the data. 427 */ 428 public function test_link_embedding_without_links() { 429 $data = array( 430 'untouched' => 'data', 431 ); 432 $result = $this->server->embed_links( $data ); 433 434 $this->assertArrayNotHasKey( '_links', $data ); 435 $this->assertArrayNotHasKey( '_embedded', $data ); 436 $this->assertEquals( 'data', $data['untouched'] ); 437 } 438 439 public function embedded_response_callback( $request ) { 440 $params = $request->get_params(); 441 442 if ( isset( $params['error'] ) ) { 443 return new WP_Error( 'wp-api-test-error', 'Test message', array( 'status' => 403 ) ); 444 } 445 446 $data = array( 447 'hello' => true, 448 'parameters' => $params, 449 ); 450 451 return $data; 452 } 453 454 public function test_removing_links() { 455 $response = new WP_REST_Response(); 456 $response->add_link( 'self', 'http://example.com/' ); 457 $response->add_link( 'alternate', 'http://example.org/', array( 'type' => 'application/xml' ) ); 458 459 $response->remove_link( 'self' ); 460 461 $data = $this->server->response_to_data( $response, false ); 462 $this->assertArrayHasKey( '_links', $data ); 463 464 $this->assertArrayNotHasKey( 'self', $data['_links'] ); 465 466 $alternate = array( 467 'href' => 'http://example.org/', 468 'type' => 'application/xml', 469 ); 470 $this->assertEquals( $alternate, $data['_links']['alternate'][0] ); 471 } 472 473 public function test_removing_links_for_href() { 474 $response = new WP_REST_Response(); 475 $response->add_link( 'self', 'http://example.com/' ); 476 $response->add_link( 'self', 'https://example.com/' ); 477 478 $response->remove_link( 'self', 'https://example.com/' ); 479 480 $data = $this->server->response_to_data( $response, false ); 481 $this->assertArrayHasKey( '_links', $data ); 482 483 $this->assertArrayHasKey( 'self', $data['_links'] ); 484 485 $self_not_filtered = array( 486 'href' => 'http://example.com/', 487 ); 488 $this->assertEquals( $self_not_filtered, $data['_links']['self'][0] ); 489 } 490 491 public function test_get_index() { 492 $server = new WP_REST_Server(); 493 $server->register_route( 'test/example', '/test/example/some-route', array( 494 array( 495 'methods' => WP_REST_Server::READABLE, 496 'callback' => '__return_true', 497 ), 498 array( 499 'methods' => WP_REST_Server::DELETABLE, 500 'callback' => '__return_true', 501 ), 502 ) ); 503 504 $request = new WP_REST_Request( 'GET', '/' ); 505 $index = $server->dispatch( $request ); 506 $data = $index->get_data(); 507 508 $this->assertArrayHasKey( 'name', $data ); 509 $this->assertArrayHasKey( 'description', $data ); 510 $this->assertArrayHasKey( 'url', $data ); 511 $this->assertArrayHasKey( 'namespaces', $data ); 512 $this->assertArrayHasKey( 'authentication', $data ); 513 $this->assertArrayHasKey( 'routes', $data ); 514 515 // Check namespace data. 516 $this->assertContains( 'test/example', $data['namespaces'] ); 517 518 // Check the route. 519 $this->assertArrayHasKey( '/test/example/some-route', $data['routes'] ); 520 $route = $data['routes']['/test/example/some-route']; 521 $this->assertEquals( 'test/example', $route['namespace'] ); 522 $this->assertArrayHasKey( 'methods', $route ); 523 $this->assertContains( 'GET', $route['methods'] ); 524 $this->assertContains( 'DELETE', $route['methods'] ); 525 $this->assertArrayHasKey( '_links', $route ); 526 } 527 528 public function test_get_namespace_index() { 529 $server = new WP_REST_Server(); 530 $server->register_route( 'test/example', '/test/example/some-route', array( 531 array( 532 'methods' => WP_REST_Server::READABLE, 533 'callback' => '__return_true', 534 ), 535 array( 536 'methods' => WP_REST_Server::DELETABLE, 537 'callback' => '__return_true', 538 ), 539 ) ); 540 $server->register_route( 'test/another', '/test/another/route', array( 541 array( 542 'methods' => WP_REST_Server::READABLE, 543 'callback' => '__return_false', 544 ), 545 ) ); 546 547 $request = new WP_REST_Request(); 548 $request->set_param( 'namespace', 'test/example' ); 549 $index = rest_ensure_response( $server->get_namespace_index( $request ) ); 550 $data = $index->get_data(); 551 552 // Check top-level. 553 $this->assertEquals( 'test/example', $data['namespace'] ); 554 $this->assertArrayHasKey( 'routes', $data ); 555 556 // Check we have the route we expect... 557 $this->assertArrayHasKey( '/test/example/some-route', $data['routes'] ); 558 559 // ...and none we don't. 560 $this->assertArrayNotHasKey( '/test/another/route', $data['routes'] ); 561 } 562 563 public function test_get_namespaces() { 564 $server = new WP_REST_Server(); 565 $server->register_route( 'test/example', '/test/example/some-route', array( 566 array( 567 'methods' => WP_REST_Server::READABLE, 568 'callback' => '__return_true', 569 ), 570 ) ); 571 $server->register_route( 'test/another', '/test/another/route', array( 572 array( 573 'methods' => WP_REST_Server::READABLE, 574 'callback' => '__return_false', 575 ), 576 ) ); 577 578 $namespaces = $server->get_namespaces(); 579 $this->assertContains( 'test/example', $namespaces ); 580 $this->assertContains( 'test/another', $namespaces ); 581 } 582 } -
tests/phpunit/tests/rest-api.php
1 <?php 2 /** 3 * REST API functions. 4 * 5 * @package WordPress 6 * @subpackage REST API 7 */ 8 9 require_once ABSPATH . 'wp-admin/includes/admin.php'; 10 require_once ABSPATH . WPINC . '/rest-api.php'; 11 12 /** 13 * @group restapi 14 */ 15 class Tests_REST_API extends WP_UnitTestCase { 16 public function setUp() { 17 // Override the normal server with our spying server. 18 $GLOBALS['wp_rest_server'] = new Spy_REST_Server(); 19 parent::setup(); 20 } 21 22 /** 23 * The plugin should be installed and activated. 24 */ 25 function test_rest_api_activated() { 26 $this->assertTrue( class_exists( 'WP_REST_Server' ) ); 27 } 28 29 /** 30 * The rest_api_init hook should have been registered with init, and should 31 * have a default priority of 10. 32 */ 33 function test_init_action_added() { 34 $this->assertEquals( 10, has_action( 'init', 'rest_api_init' ) ); 35 } 36 37 /** 38 * Check that a single route is canonicalized. 39 * 40 * Ensures that single and multiple routes are handled correctly. 41 */ 42 public function test_route_canonicalized() { 43 register_rest_route( 'test-ns', '/test', array( 44 'methods' => array( 'GET' ), 45 'callback' => '__return_null', 46 ) ); 47 48 // Check the route was registered correctly. 49 $endpoints = $GLOBALS['wp_rest_server']->get_raw_endpoint_data(); 50 $this->assertArrayHasKey( '/test-ns/test', $endpoints ); 51 52 // Check the route was wrapped in an array. 53 $endpoint = $endpoints['/test-ns/test']; 54 $this->assertArrayNotHasKey( 'callback', $endpoint ); 55 $this->assertArrayHasKey( 'namespace', $endpoint ); 56 $this->assertEquals( 'test-ns', $endpoint['namespace'] ); 57 58 // Grab the filtered data. 59 $filtered_endpoints = $GLOBALS['wp_rest_server']->get_routes(); 60 $this->assertArrayHasKey( '/test-ns/test', $filtered_endpoints ); 61 $endpoint = $filtered_endpoints['/test-ns/test']; 62 $this->assertCount( 1, $endpoint ); 63 $this->assertArrayHasKey( 'callback', $endpoint[0] ); 64 $this->assertArrayHasKey( 'methods', $endpoint[0] ); 65 $this->assertArrayHasKey( 'args', $endpoint[0] ); 66 } 67 68 /** 69 * Check that a single route is canonicalized. 70 * 71 * Ensures that single and multiple routes are handled correctly. 72 */ 73 public function test_route_canonicalized_multiple() { 74 register_rest_route( 'test-ns', '/test', array( 75 array( 76 'methods' => array( 'GET' ), 77 'callback' => '__return_null', 78 ), 79 array( 80 'methods' => array( 'POST' ), 81 'callback' => '__return_null', 82 ), 83 ) ); 84 85 // Check the route was registered correctly. 86 $endpoints = $GLOBALS['wp_rest_server']->get_raw_endpoint_data(); 87 $this->assertArrayHasKey( '/test-ns/test', $endpoints ); 88 89 // Check the route was wrapped in an array. 90 $endpoint = $endpoints['/test-ns/test']; 91 $this->assertArrayNotHasKey( 'callback', $endpoint ); 92 $this->assertArrayHasKey( 'namespace', $endpoint ); 93 $this->assertEquals( 'test-ns', $endpoint['namespace'] ); 94 95 $filtered_endpoints = $GLOBALS['wp_rest_server']->get_routes(); 96 $endpoint = $filtered_endpoints['/test-ns/test']; 97 $this->assertCount( 2, $endpoint ); 98 99 // Check for both methods. 100 foreach ( array( 0, 1 ) as $key ) { 101 $this->assertArrayHasKey( 'callback', $endpoint[ $key ] ); 102 $this->assertArrayHasKey( 'methods', $endpoint[ $key ] ); 103 $this->assertArrayHasKey( 'args', $endpoint[ $key ] ); 104 } 105 } 106 107 /** 108 * Check that routes are merged by default. 109 */ 110 public function test_route_merge() { 111 register_rest_route( 'test-ns', '/test', array( 112 'methods' => array( 'GET' ), 113 'callback' => '__return_null', 114 ) ); 115 register_rest_route( 'test-ns', '/test', array( 116 'methods' => array( 'POST' ), 117 'callback' => '__return_null', 118 ) ); 119 120 // Check both routes exist. 121 $endpoints = $GLOBALS['wp_rest_server']->get_routes(); 122 $endpoint = $endpoints['/test-ns/test']; 123 $this->assertCount( 2, $endpoint ); 124 } 125 126 /** 127 * Check that we can override routes. 128 */ 129 public function test_route_override() { 130 register_rest_route( 'test-ns', '/test', array( 131 'methods' => array( 'GET' ), 132 'callback' => '__return_null', 133 'should_exist' => false, 134 ) ); 135 register_rest_route( 'test-ns', '/test', array( 136 'methods' => array( 'POST' ), 137 'callback' => '__return_null', 138 'should_exist' => true, 139 ), true ); 140 141 // Check we only have one route. 142 $endpoints = $GLOBALS['wp_rest_server']->get_routes(); 143 $endpoint = $endpoints['/test-ns/test']; 144 $this->assertCount( 1, $endpoint ); 145 146 // Check it's the right one. 147 $this->assertArrayHasKey( 'should_exist', $endpoint[0] ); 148 $this->assertTrue( $endpoint[0]['should_exist'] ); 149 } 150 151 /** 152 * The rest_route query variable should be registered. 153 */ 154 function test_rest_route_query_var() { 155 rest_api_init(); 156 $this->assertTrue( in_array( 'rest_route', $GLOBALS['wp']->public_query_vars ) ); 157 } 158 159 public function test_route_method() { 160 register_rest_route( 'test-ns', '/test', array( 161 'methods' => array( 'GET' ), 162 'callback' => '__return_null', 163 ) ); 164 165 $routes = $GLOBALS['wp_rest_server']->get_routes(); 166 167 $this->assertEquals( $routes['/test-ns/test'][0]['methods'], array( 'GET' => true ) ); 168 } 169 170 /** 171 * The 'methods' arg should accept a single value as well as array. 172 */ 173 public function test_route_method_string() { 174 register_rest_route( 'test-ns', '/test', array( 175 'methods' => 'GET', 176 'callback' => '__return_null', 177 ) ); 178 179 $routes = $GLOBALS['wp_rest_server']->get_routes(); 180 181 $this->assertEquals( $routes['/test-ns/test'][0]['methods'], array( 'GET' => true ) ); 182 } 183 184 /** 185 * The 'methods' arg should accept a single value as well as array. 186 */ 187 public function test_route_method_array() { 188 register_rest_route( 'test-ns', '/test', array( 189 'methods' => array( 'GET', 'POST' ), 190 'callback' => '__return_null', 191 ) ); 192 193 $routes = $GLOBALS['wp_rest_server']->get_routes(); 194 195 $this->assertEquals( $routes['/test-ns/test'][0]['methods'], array( 'GET' => true, 'POST' => true ) ); 196 } 197 198 /** 199 * The 'methods' arg should a comma seperated string. 200 */ 201 public function test_route_method_comma_seperated() { 202 register_rest_route( 'test-ns', '/test', array( 203 'methods' => 'GET,POST', 204 'callback' => '__return_null', 205 ) ); 206 207 $routes = $GLOBALS['wp_rest_server']->get_routes(); 208 209 $this->assertEquals( $routes['/test-ns/test'][0]['methods'], array( 'GET' => true, 'POST' => true ) ); 210 } 211 212 public function test_options_request() { 213 register_rest_route( 'test-ns', '/test', array( 214 'methods' => 'GET,POST', 215 'callback' => '__return_null', 216 ) ); 217 218 $request = new WP_REST_Request( 'OPTIONS', '/test-ns/test' ); 219 $response = rest_handle_options_request( null, $GLOBALS['wp_rest_server'], $request ); 220 221 $headers = $response->get_headers(); 222 $this->assertArrayHasKey( 'Accept', $headers ); 223 224 $this->assertEquals( 'GET, POST', $headers['Accept'] ); 225 } 226 227 /** 228 * Ensure that the OPTIONS handler doesn't kick in for non-OPTIONS requests. 229 */ 230 public function test_options_request_not_options() { 231 register_rest_route( 'test-ns', '/test', array( 232 'methods' => 'GET,POST', 233 'callback' => '__return_true', 234 ) ); 235 236 $request = new WP_REST_Request( 'GET', '/test-ns/test' ); 237 $response = rest_handle_options_request( null, $GLOBALS['wp_rest_server'], $request ); 238 239 $this->assertNull( $response ); 240 } 241 242 /** 243 * The get_rest_url function should return a URL consistently terminated with a "/", 244 * whether the blog is configured with pretty permalink support or not. 245 */ 246 public function test_rest_url_generation() { 247 // In pretty permalinks case, we expect a path of wp-json/ with no query. 248 update_option( 'permalink_structure', '/%year%/%monthnum%/%day%/%postname%/' ); 249 $this->assertEquals( 'http://' . WP_TESTS_DOMAIN . '/wp-json/', get_rest_url() ); 250 251 update_option( 'permalink_structure', '' ); 252 // In non-pretty case, we get a query string to invoke the rest router. 253 $this->assertEquals( 'http://' . WP_TESTS_DOMAIN . '/?rest_route=/', get_rest_url() ); 254 } 255 }