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