Ticket #33982: 33982.patch
File 33982.patch, 137.7 KB (added by , 8 years ago) |
---|
-
src/wp-includes/rest-api/infrastructure/class-jsonserializable.php
1 <?php 2 /** 3 * Compatibility shim for PHP <5.4 4 * 5 * @link http://php.net/jsonserializable 6 * 7 * @package WordPress 8 * @subpackage REST_API 9 * @since 4.4.0 10 */ 11 12 if ( ! interface_exists( 'JsonSerializable' ) ) { 13 define( 'WP_JSON_SERIALIZE_COMPATIBLE', true ); 14 // @codingStandardsIgnoreStart 15 /** 16 * JsonSerializable interface. 17 * 18 * @since 4.4.0 19 */ 20 interface JsonSerializable { 21 public function jsonSerialize(); 22 } 23 // @codingStandardsIgnoreEnd 24 } -
src/wp-includes/rest-api/infrastructure/class-wp-http-response.php
1 <?php 2 /** 3 * REST API: WP_HTTP_Response class 4 * 5 * @package WordPress 6 * @subpackage REST_API 7 * @since 4.4.0 8 */ 9 10 /** 11 * Core class used to prepare HTTP responses. 12 * 13 * @since 4.4.0 14 * 15 * @see WP_HTTP_ResponseInterface 16 */ 17 class WP_HTTP_Response implements WP_HTTP_ResponseInterface { 18 19 /** 20 * Response data. 21 * 22 * @since 4.4.0 23 * @access public 24 * @var mixed 25 */ 26 public $data; 27 28 /** 29 * Response headers. 30 * 31 * @since 4.4.0 32 * @access public 33 * @var int 34 */ 35 public $headers; 36 37 /** 38 * Response status. 39 * 40 * @since 4.4.0 41 * @access public 42 * @var array 43 */ 44 public $status; 45 46 /** 47 * Constructor. 48 * 49 * @since 4.4.0 50 * @access public 51 * 52 * @param mixed $data Response data. Default null 53 * @param int $status Optional. HTTP status code. Default 200. 54 * @param array $headers Optional. HTTP header map. Default empty array. 55 */ 56 public function __construct( $data = null, $status = 200, $headers = array() ) { 57 $this->data = $data; 58 $this->set_status( $status ); 59 $this->set_headers( $headers ); 60 } 61 62 /** 63 * Retrieves headers associated with the response. 64 * 65 * @since 4.4.0 66 * @access public 67 * 68 * @see WP_HTTP_ResponseInterface::get_headers() 69 * 70 * @return array Map of header name to header value. 71 */ 72 public function get_headers() { 73 return $this->headers; 74 } 75 76 /** 77 * Sets all header values. 78 * 79 * @since 4.4.0 80 * @access public 81 * 82 * @param array $headers Map of header name to header value. 83 */ 84 public function set_headers( $headers ) { 85 $this->headers = $headers; 86 } 87 88 /** 89 * Sets a single HTTP header. 90 * 91 * @since 4.4.0 92 * @access public 93 * 94 * @param string $key Header name. 95 * @param string $value Header value. 96 * @param bool $replace Optional. Whether to replace an existing header of the same name. 97 * Default true. 98 */ 99 public function header( $key, $value, $replace = true ) { 100 if ( $replace || ! isset( $this->headers[ $key ] ) ) { 101 $this->headers[ $key ] = $value; 102 } else { 103 $this->headers[ $key ] .= ', ' . $value; 104 } 105 } 106 107 /** 108 * Retrieves the HTTP return code for the response. 109 * 110 * @since 4.4.0 111 * @access public 112 * 113 * @see WP_HTTP_ResponseInterface::get_status() 114 * 115 * @return int The 3-digit HTTP status code. 116 */ 117 public function get_status() { 118 return $this->status; 119 } 120 121 /** 122 * Sets the 3-digit HTTP status code. 123 * 124 * @since 4.4.0 125 * @access public 126 * 127 * @param int $code HTTP status. 128 */ 129 public function set_status( $code ) { 130 $this->status = absint( $code ); 131 } 132 133 /** 134 * Retrieves the response data. 135 * 136 * @since 4.4.0 137 * @access public 138 * 139 * @see WP_HTTP_ResponseInterface::get_status() 140 * 141 * @return mixed Response data. 142 */ 143 public function get_data() { 144 return $this->data; 145 } 146 147 /** 148 * Sets the response data. 149 * 150 * @since 4.4.0 151 * @access public 152 * 153 * @param mixed $data Response data. 154 */ 155 public function set_data( $data ) { 156 $this->data = $data; 157 } 158 159 /** 160 * Retrieves the response data for JSON serialization. 161 * 162 * It is expected that in most implementations, this will return the same as get_data(), 163 * however this may be different if you want to do custom JSON data handling. 164 * 165 * @since 4.4.0 166 * @access public 167 * 168 * @return mixed Any JSON-serializable value. 169 */ 170 // @codingStandardsIgnoreStart 171 public function jsonSerialize() { 172 // @codingStandardsIgnoreEnd 173 return $this->get_data(); 174 } 175 } -
src/wp-includes/rest-api/infrastructure/class-wp-http-responseinterface.php
1 <?php 2 /** 3 * REST API: WP_HTTP_ResponseInterface interface 4 * 5 * @package WordPress 6 * @subpackage REST_API 7 * @since 4.4.0 8 */ 9 10 /** 11 * Core interface used as a base for preparing HTTP responses. 12 * 13 * @since 4.4.0 14 * 15 * @see JsonSerializable 16 */ 17 interface WP_HTTP_ResponseInterface extends JsonSerializable { 18 19 /** 20 * Retrieves headers associated with the response. 21 * 22 * @since 4.4.0 23 * @access public 24 * 25 * @return array Map of header name to header value. 26 */ 27 public function get_headers(); 28 29 /** 30 * Retrieves the HTTP return code for the response. 31 * 32 * @since 4.4.0 33 * @access public 34 * 35 * @return int 3-digit HTTP status code. 36 */ 37 public function get_status(); 38 39 /** 40 * Retrieves the response data. 41 * 42 * @since 4.4.0 43 * @access public 44 * 45 * @return mixed Response data. 46 */ 47 public function get_data(); 48 49 /** 50 * @todo: Remove since it's commented out? 51 * 52 * Retrieves the response data for JSON serialization. 53 * 54 * It is expected that in most implementations, this will return the same as 55 * {@see get_data()}, however this may be different if you want to do custom 56 * JSON data handling. 57 * 58 * @return mixed Any JSON-serializable value 59 */ 60 // public function jsonSerialize(); 61 } -
src/wp-includes/rest-api/infrastructure/class-wp-rest-request.php
1 <?php 2 /** 3 * REST API: WP_REST_Request class 4 * 5 * @package WordPress 6 * @subpackage REST_API 7 * @since 4.4.0 8 */ 9 10 /** 11 * Core class used to implement a REST request object. 12 * 13 * Contains data from the request, to be passed to the callback. 14 * 15 * Note: This implements ArrayAccess, and acts as an array of parameters when 16 * used in that manner. It does not use ArrayObject (as we cannot rely on SPL), 17 * so be aware it may have non-array behaviour in some cases. 18 * 19 * @since 4.4.0 20 * 21 * @see ArrayAccess 22 */ 23 class WP_REST_Request implements ArrayAccess { 24 25 /** 26 * HTTP method. 27 * 28 * @since 4.4.0 29 * @access protected 30 * @var string 31 */ 32 protected $method = ''; 33 34 /** 35 * Parameters passed to the request. 36 * 37 * These typically come from the `$_GET`, `$_POST` and `$_FILES` 38 * superglobals when being created from the global scope. 39 * 40 * @since 4.4.0 41 * @access protected 42 * @var array Contains GET, POST and FILES keys mapping to arrays of data. 43 */ 44 protected $params; 45 46 /** 47 * HTTP headers for the request. 48 * 49 * @since 4.4.0 50 * @access protected 51 * @var array Map of key to value. Key is always lowercase, as per HTTP specification. 52 */ 53 protected $headers = array(); 54 55 /** 56 * Body data. 57 * 58 * @since 4.4.0 59 * @access protected 60 * @var string Binary data from the request. 61 */ 62 protected $body = null; 63 64 /** 65 * Route matched for the request. 66 * 67 * @since 4.4.0 68 * @access protected 69 * @var string 70 */ 71 protected $route; 72 73 /** 74 * Attributes (options) for the route that was matched. 75 * 76 * This is the options array used when the route was registered, typically 77 * containing the callback as well as the valid methods for the route. 78 * 79 * @since 4.4.0 80 * @access protected 81 * @var array Attributes for the request. 82 */ 83 protected $attributes = array(); 84 85 /** 86 * Used to determine if the JSON data has been parsed yet. 87 * 88 * Allows lazy-parsing of JSON data where possible. 89 * 90 * @since 4.4.0 91 * @access protected 92 * @var bool 93 */ 94 protected $parsed_json = false; 95 96 /** 97 * Used to determine if the body data has been parsed yet. 98 * 99 * @since 4.4.0 100 * @access protected 101 * @var bool 102 */ 103 protected $parsed_body = false; 104 105 /** 106 * Constructor. 107 * 108 * @since 4.4.0 109 * @access public 110 * 111 * @param string $method Optional. Request method. Default empty. 112 * @param string $route Optional. Request route. Default empty. 113 * @param array $attributes Optional. Request attributes. Default empty array. 114 */ 115 public function __construct( $method = '', $route = '', $attributes = array() ) { 116 $this->params = array( 117 'URL' => array(), 118 'GET' => array(), 119 'POST' => array(), 120 'FILES' => array(), 121 122 // See parse_json_params 123 'JSON' => null, 124 125 'defaults' => array(), 126 ); 127 128 $this->set_method( $method ); 129 $this->set_route( $route ); 130 $this->set_attributes( $attributes ); 131 } 132 133 /** 134 * Retrieves the HTTP method for the request. 135 * 136 * @since 4.4.0 137 * @access public 138 * 139 * @return string HTTP method. 140 */ 141 public function get_method() { 142 return $this->method; 143 } 144 145 /** 146 * Sets HTTP method for the request. 147 * 148 * @since 4.4.0 149 * @access public 150 * 151 * @param string $method HTTP method. 152 */ 153 public function set_method( $method ) { 154 $this->method = strtoupper( $method ); 155 } 156 157 /** 158 * Retrieves all headers from the request. 159 * 160 * @since 4.4.0 161 * @access public 162 * 163 * @return array Map of key to value. Key is always lowercase, as per HTTP specification. 164 */ 165 public function get_headers() { 166 return $this->headers; 167 } 168 169 /** 170 * Canonicalizes the header name. 171 * 172 * Ensures that header names are always treated the same regardless of 173 * source. Header names are always case insensitive. 174 * 175 * Note that we treat `-` (dashes) and `_` (underscores) as the same 176 * character, as per header parsing rules in both Apache and nginx. 177 * 178 * @link http://stackoverflow.com/q/18185366 179 * @link http://wiki.nginx.org/Pitfalls#Missing_.28disappearing.29_HTTP_headers 180 * @link http://nginx.org/en/docs/http/ngx_http_core_module.html#underscores_in_headers 181 * 182 * @since 4.4.0 183 * @access public 184 * @static 185 * 186 * @param string $key Header name. 187 * @return string Canonicalized name. 188 */ 189 public static function canonicalize_header_name( $key ) { 190 $key = strtolower( $key ); 191 $key = str_replace( '-', '_', $key ); 192 193 return $key; 194 } 195 196 /** 197 * Retrieves the given header from the request. 198 * 199 * If the header has multiple values, they will be concatenated with a comma 200 * as per the HTTP specification. Be aware that some non-compliant headers 201 * (notably cookie headers) cannot be joined this way. 202 * 203 * @since 4.4.0 204 * @access public 205 * 206 * @param string $key Header name, will be canonicalized to lowercase. 207 * @return string|null String value if set, null otherwise. 208 */ 209 public function get_header( $key ) { 210 $key = $this->canonicalize_header_name( $key ); 211 212 if ( ! isset( $this->headers[ $key ] ) ) { 213 return null; 214 } 215 216 return implode( ',', $this->headers[ $key ] ); 217 } 218 219 /** 220 * Retrieves header values from the request. 221 * 222 * @since 4.4.0 223 * @access public 224 * 225 * @param string $key Header name, will be canonicalized to lowercase. 226 * @return array|null List of string values if set, null otherwise. 227 */ 228 public function get_header_as_array( $key ) { 229 $key = $this->canonicalize_header_name( $key ); 230 231 if ( ! isset( $this->headers[ $key ] ) ) { 232 return null; 233 } 234 235 return $this->headers[ $key ]; 236 } 237 238 /** 239 * Sets the header on request. 240 * 241 * @since 4.4.0 242 * @access public 243 * 244 * @param string $key Header name. 245 * @param string $value Header value, or list of values. 246 */ 247 public function set_header( $key, $value ) { 248 $key = $this->canonicalize_header_name( $key ); 249 $value = (array) $value; 250 251 $this->headers[ $key ] = $value; 252 } 253 254 /** 255 * Appends a header value for the given header. 256 * 257 * @since 4.4.0 258 * @access public 259 * 260 * @param string $key Header name. 261 * @param string $value Header value, or list of values. 262 */ 263 public function add_header( $key, $value ) { 264 $key = $this->canonicalize_header_name( $key ); 265 $value = (array) $value; 266 267 if ( ! isset( $this->headers[ $key ] ) ) { 268 $this->headers[ $key ] = array(); 269 } 270 271 $this->headers[ $key ] = array_merge( $this->headers[ $key ], $value ); 272 } 273 274 /** 275 * Removes all values for a header. 276 * 277 * @since 4.4.0 278 * @access public 279 * 280 * @param string $key Header name. 281 */ 282 public function remove_header( $key ) { 283 unset( $this->headers[ $key ] ); 284 } 285 286 /** 287 * Sets headers on the request. 288 * 289 * @since 4.4.0 290 * @access public 291 * 292 * @param array $headers Map of header name to value. 293 * @param bool $override If true, replace the request's headers. Otherwise, merge with existing. 294 */ 295 public function set_headers( $headers, $override = true ) { 296 if ( true === $override ) { 297 $this->headers = array(); 298 } 299 300 foreach ( $headers as $key => $value ) { 301 $this->set_header( $key, $value ); 302 } 303 } 304 305 /** 306 * Retrieves the content-type of the request. 307 * 308 * @since 4.4.0 309 * @access public 310 * 311 * @return array Map containing 'value' and 'parameters' keys. 312 */ 313 public function get_content_type() { 314 $value = $this->get_header( 'content-type' ); 315 if ( empty( $value ) ) { 316 return null; 317 } 318 319 $parameters = ''; 320 if ( strpos( $value, ';' ) ) { 321 list( $value, $parameters ) = explode( ';', $value, 2 ); 322 } 323 324 $value = strtolower( $value ); 325 if ( strpos( $value, '/' ) === false ) { 326 return null; 327 } 328 329 // Parse type and subtype out 330 list( $type, $subtype ) = explode( '/', $value, 2 ); 331 332 $data = compact( 'value', 'type', 'subtype', 'parameters' ); 333 $data = array_map( 'trim', $data ); 334 335 return $data; 336 } 337 338 /** 339 * Retrieves the parameter priority order. 340 * 341 * Used when checking parameters in get_param(). 342 * 343 * @since 4.4.0 344 * @access public 345 * 346 * @return array List of types to check, in order of priority. 347 */ 348 protected function get_parameter_order() { 349 $order = array(); 350 $order[] = 'JSON'; 351 352 $this->parse_json_params(); 353 354 // Ensure we parse the body data 355 $body = $this->get_body(); 356 if ( $this->method !== 'POST' && ! empty( $body ) ) { 357 $this->parse_body_params(); 358 } 359 360 $accepts_body_data = array( 'POST', 'PUT', 'PATCH' ); 361 if ( in_array( $this->method, $accepts_body_data ) ) { 362 $order[] = 'POST'; 363 } 364 365 $order[] = 'GET'; 366 $order[] = 'URL'; 367 $order[] = 'defaults'; 368 369 /** 370 * Filter the parameter order. 371 * 372 * The order affects which parameters are checked when using get_param() and family. 373 * This acts similarly to PHP's `request_order` setting. 374 * 375 * @since 4.4.0 376 * 377 * @param array $order { 378 * An array of types to check, in order of priority. 379 * 380 * @param string $type The type to check. 381 * } 382 * @param WP_REST_Request $this The request object. 383 */ 384 return apply_filters( 'rest_request_parameter_order', $order, $this ); 385 } 386 387 /** 388 * Retrieves a parameter from the request. 389 * 390 * @since 4.4.0 391 * @access public 392 * 393 * @param string $key Parameter name. 394 * @return mixed|null Value if set, null otherwise. 395 */ 396 public function get_param( $key ) { 397 $order = $this->get_parameter_order(); 398 399 foreach ( $order as $type ) { 400 // Determine if we have the parameter for this type. 401 if ( isset( $this->params[ $type ][ $key ] ) ) { 402 return $this->params[ $type ][ $key ]; 403 } 404 } 405 406 return null; 407 } 408 409 /** 410 * Sets a parameter on the request. 411 * 412 * @since 4.4.0 413 * @access public 414 * 415 * @param string $key Parameter name. 416 * @param mixed $value Parameter value. 417 */ 418 public function set_param( $key, $value ) { 419 switch ( $this->method ) { 420 case 'POST': 421 $this->params['POST'][ $key ] = $value; 422 break; 423 424 default: 425 $this->params['GET'][ $key ] = $value; 426 break; 427 } 428 } 429 430 /** 431 * Retrieves merged parameters from the request. 432 * 433 * The equivalent of get_param(), but returns all parameters for the request. 434 * Handles merging all the available values into a single array. 435 * 436 * @since 4.4.0 437 * @access public 438 * 439 * @return array Map of key to value 440 */ 441 public function get_params() { 442 $order = $this->get_parameter_order(); 443 $order = array_reverse( $order, true ); 444 445 $params = array(); 446 foreach ( $order as $type ) { 447 $params = array_merge( $params, (array) $this->params[ $type ] ); 448 } 449 450 return $params; 451 } 452 453 /** 454 * Retrieves parameters from the route itself. 455 * 456 * These are parsed from the URL using the regex. 457 * 458 * @since 4.4.0 459 * @access public 460 * 461 * @return array Parameter map of key to value 462 */ 463 public function get_url_params() { 464 return $this->params['URL']; 465 } 466 467 /** 468 * Sets parameters from the route. 469 * 470 * Typically, this is set after parsing the URL. 471 * 472 * @since 4.4.0 473 * @access public 474 * 475 * @param array $params Parameter map of key to value. 476 */ 477 public function set_url_params( $params ) { 478 $this->params['URL'] = $params; 479 } 480 481 /** 482 * Retrieves parameters from the query string. 483 * 484 * These are the parameters you'd typically find in `$_GET`. 485 * 486 * @since 4.4.0 487 * @access public 488 * 489 * @return array Parameter map of key to value 490 */ 491 public function get_query_params() { 492 return $this->params['GET']; 493 } 494 495 /** 496 * Sets parameters from the query string. 497 * 498 * Typically, this is set from `$_GET`. 499 * 500 * @since 4.4.0 501 * @access public 502 * 503 * @param array $params Parameter map of key to value. 504 */ 505 public function set_query_params( $params ) { 506 $this->params['GET'] = $params; 507 } 508 509 /** 510 * Retrieves parameters from the body. 511 * 512 * These are the parameters you'd typically find in `$_POST`. 513 * 514 * @since 4.4.0 515 * @access public 516 * 517 * @return array Parameter map of key to value. 518 */ 519 public function get_body_params() { 520 return $this->params['POST']; 521 } 522 523 /** 524 * Sets parameters from the body. 525 * 526 * Typically, this is set from `$_POST`. 527 * 528 * @since 4.4.0 529 * @access public 530 * 531 * @param array $params Parameter map of key to value. 532 */ 533 public function set_body_params( $params ) { 534 $this->params['POST'] = $params; 535 } 536 537 /** 538 * Retrieves multipart file parameters from the body. 539 * 540 * These are the parameters you'd typically find in `$_FILES`. 541 * 542 * @since 4.4.0 543 * @access public 544 * 545 * @return array Parameter map of key to value 546 */ 547 public function get_file_params() { 548 return $this->params['FILES']; 549 } 550 551 /** 552 * Sets multipart file parameters from the body. 553 * 554 * Typically, this is set from `$_FILES`. 555 * 556 * @since 4.4.0 557 * @access public 558 * 559 * @param array $params Parameter map of key to value. 560 */ 561 public function set_file_params( $params ) { 562 $this->params['FILES'] = $params; 563 } 564 565 /** 566 * Retrieves the default parameters. 567 * 568 * These are the parameters set in the route registration. 569 * 570 * @since 4.4.0 571 * @access public 572 * 573 * @return array Parameter map of key to value 574 */ 575 public function get_default_params() { 576 return $this->params['defaults']; 577 } 578 579 /** 580 * Sets default parameters. 581 * 582 * These are the parameters set in the route registration. 583 * 584 * @since 4.4.0 585 * @access public 586 * 587 * @param array $params Parameter map of key to value. 588 */ 589 public function set_default_params( $params ) { 590 $this->params['defaults'] = $params; 591 } 592 593 /** 594 * Retrieves the request body content. 595 * 596 * @since 4.4.0 597 * @access public 598 * 599 * @return string Binary data from the request body. 600 */ 601 public function get_body() { 602 return $this->body; 603 } 604 605 /** 606 * Sets body content. 607 * 608 * @since 4.4.0 609 * @access public 610 * 611 * @param string $data Binary data from the request body. 612 */ 613 public function set_body( $data ) { 614 $this->body = $data; 615 616 // Enable lazy parsing. 617 $this->parsed_json = false; 618 $this->parsed_body = false; 619 $this->params['JSON'] = null; 620 } 621 622 /** 623 * Retrieves the parameters from a JSON-formatted body. 624 * 625 * @since 4.4.0 626 * @access public 627 * 628 * @return array Parameter map of key to value. 629 */ 630 public function get_json_params() { 631 // Ensure the parameters have been parsed out. 632 $this->parse_json_params(); 633 634 return $this->params['JSON']; 635 } 636 637 /** 638 * Parses the JSON parameters. 639 * 640 * Avoids parsing the JSON data until we need to access it. 641 * 642 * @since 4.4.0 643 * @access protected 644 */ 645 protected function parse_json_params() { 646 if ( $this->parsed_json ) { 647 return; 648 } 649 650 $this->parsed_json = true; 651 652 // Check that we actually got JSON. 653 $content_type = $this->get_content_type(); 654 655 if ( empty( $content_type ) || 'application/json' !== $content_type['value'] ) { 656 return; 657 } 658 659 $params = json_decode( $this->get_body(), true ); 660 661 /* 662 * Check for a parsing error. 663 * 664 * Note that due to WP's JSON compatibility functions, json_last_error 665 * might not be defined: https://core.trac.wordpress.org/ticket/27799 666 */ 667 if ( null === $params && ( ! function_exists( 'json_last_error' ) || JSON_ERROR_NONE !== json_last_error() ) ) { 668 return; 669 } 670 671 $this->params['JSON'] = $params; 672 } 673 674 /** 675 * Parses the request body parameters. 676 * 677 * Parses out URL-encoded bodies for request methods that aren't supported 678 * natively by PHP. In PHP 5.x, only POST has these parsed automatically. 679 * 680 * @since 4.4.0 681 * @access protected 682 */ 683 protected function parse_body_params() { 684 if ( $this->parsed_body ) { 685 return; 686 } 687 688 $this->parsed_body = true; 689 690 /* 691 * Check that we got URL-encoded. Treat a missing content-type as 692 * URL-encoded for maximum compatibility 693 */ 694 $content_type = $this->get_content_type(); 695 696 if ( ! empty( $content_type ) && 'application/x-www-form-urlencoded' !== $content_type['value'] ) { 697 return; 698 } 699 700 parse_str( $this->get_body(), $params ); 701 702 /* 703 * Amazingly, parse_str follows magic quote rules. Sigh. 704 * 705 * NOTE: Do not refactor to use `wp_unslash`. 706 */ 707 // @codeCoverageIgnoreStart 708 if ( get_magic_quotes_gpc() ) { 709 $params = stripslashes_deep( $params ); 710 } 711 // @codeCoverageIgnoreEnd 712 713 /* 714 * Add to the POST parameters stored internally. If a user has already 715 * set these manually (via `set_body_params`), don't override them. 716 */ 717 $this->params['POST'] = array_merge( $params, $this->params['POST'] ); 718 } 719 720 /** 721 * Retrieves the route that matched the request. 722 * 723 * @since 4.4.0 724 * @access public 725 * 726 * @return string Route matching regex. 727 */ 728 public function get_route() { 729 return $this->route; 730 } 731 732 /** 733 * Sets the route that matched the request. 734 * 735 * @since 4.4.0 736 * @access public 737 * 738 * @param string $route Route matching regex. 739 */ 740 public function set_route( $route ) { 741 $this->route = $route; 742 } 743 744 /** 745 * Retrieves the attributes for the request. 746 * 747 * These are the options for the route that was matched. 748 * 749 * @since 4.4.0 750 * @access public 751 * 752 * @return array Attributes for the request. 753 */ 754 public function get_attributes() { 755 return $this->attributes; 756 } 757 758 /** 759 * Sets the attributes for the request. 760 * 761 * @since 4.4.0 762 * @access public 763 * 764 * @param array $attributes Attributes for the request. 765 */ 766 public function set_attributes( $attributes ) { 767 $this->attributes = $attributes; 768 } 769 770 /** 771 * Sanitizes (where possible) the params on the request. 772 * 773 * This is primarily based off the sanitize_callback param on each registered 774 * argument. 775 * 776 * @since 4.4.0 777 * @access public 778 * 779 * @return true|null True if there are no parameters to sanitize, null otherwise. 780 */ 781 public function sanitize_params() { 782 783 $attributes = $this->get_attributes(); 784 785 // No arguments set, skip sanitizing 786 if ( empty( $attributes['args'] ) ) { 787 return true; 788 } 789 790 $order = $this->get_parameter_order(); 791 792 foreach ( $order as $type ) { 793 if ( empty( $this->params[ $type ] ) ) { 794 continue; 795 } 796 foreach ( $this->params[ $type ] as $key => $value ) { 797 // check if this param has a sanitize_callback added 798 if ( isset( $attributes['args'][ $key ] ) && ! empty( $attributes['args'][ $key ]['sanitize_callback'] ) ) { 799 $this->params[ $type ][ $key ] = call_user_func( $attributes['args'][ $key ]['sanitize_callback'], $value, $this, $key ); 800 } 801 } 802 } 803 } 804 805 /** 806 * Checks whether this request is valid according to its attributes. 807 * 808 * @since 4.4.0 809 * @access public 810 * 811 * @return bool|WP_Error True if there are no parameters to validate or if all pass validation, 812 * WP_Error if required parameters are missing. 813 */ 814 public function has_valid_params() { 815 816 $attributes = $this->get_attributes(); 817 $required = array(); 818 819 // No arguments set, skip validation. 820 if ( empty( $attributes['args'] ) ) { 821 return true; 822 } 823 824 foreach ( $attributes['args'] as $key => $arg ) { 825 826 $param = $this->get_param( $key ); 827 if ( isset( $arg['required'] ) && true === $arg['required'] && null === $param ) { 828 $required[] = $key; 829 } 830 } 831 832 if ( ! empty( $required ) ) { 833 return new WP_Error( 'rest_missing_callback_param', sprintf( __( 'Missing parameter(s): %s' ), implode( ', ', $required ) ), array( 'status' => 400, 'params' => $required ) ); 834 } 835 836 /* 837 * Check the validation callbacks for each registered arg. 838 * 839 * This is done after required checking as required checking is cheaper. 840 */ 841 $invalid_params = array(); 842 843 foreach ( $attributes['args'] as $key => $arg ) { 844 845 $param = $this->get_param( $key ); 846 847 if ( null !== $param && ! empty( $arg['validate_callback'] ) ) { 848 $valid_check = call_user_func( $arg['validate_callback'], $param, $this, $key ); 849 850 if ( false === $valid_check ) { 851 $invalid_params[ $key ] = __( 'Invalid param.' ); 852 } 853 854 if ( is_wp_error( $valid_check ) ) { 855 $invalid_params[] = sprintf( '%s (%s)', $key, $valid_check->get_error_message() ); 856 } 857 } 858 } 859 860 if ( $invalid_params ) { 861 return new WP_Error( 'rest_invalid_param', sprintf( __( 'Invalid parameter(s): %s' ), implode( ', ', $invalid_params ) ), array( 'status' => 400, 'params' => $invalid_params ) ); 862 } 863 864 return true; 865 866 } 867 868 /** 869 * Checks if a parameter is set. 870 * 871 * @since 4.4.0 872 * @access public 873 * 874 * @param string $key Parameter name. 875 * @return bool Whether the parameter is set. 876 */ 877 // @codingStandardsIgnoreStart 878 public function offsetExists( $offset ) { 879 // @codingStandardsIgnoreEnd 880 $order = $this->get_parameter_order(); 881 882 foreach ( $order as $type ) { 883 if ( isset( $this->params[ $type ][ $offset ] ) ) { 884 return true; 885 } 886 } 887 888 return false; 889 } 890 891 /** 892 * Retrieves a parameter from the request. 893 * 894 * @since 4.4.0 895 * @access public 896 * 897 * @param string $key Parameter name. 898 * @return mixed|null Value if set, null otherwise. 899 */ 900 // @codingStandardsIgnoreStart 901 public function offsetGet( $offset ) { 902 // @codingStandardsIgnoreEnd 903 return $this->get_param( $offset ); 904 } 905 906 /** 907 * Sets a parameter on the request. 908 * 909 * @since 4.4.0 910 * @access public 911 * 912 * @param string $key Parameter name. 913 * @param mixed $value Parameter value. 914 */ 915 // @codingStandardsIgnoreStart 916 public function offsetSet( $offset, $value ) { 917 // @codingStandardsIgnoreEnd 918 return $this->set_param( $offset, $value ); 919 } 920 921 /** 922 * Removes a parameter from the request. 923 * 924 * @since 4.4.0 925 * @access public 926 * 927 * @param string $key Parameter name. 928 * @param mixed $value Parameter value. 929 */ 930 // @codingStandardsIgnoreStart 931 public function offsetUnset( $offset ) { 932 // @codingStandardsIgnoreEnd 933 $order = $this->get_parameter_order(); 934 935 // Remove the offset from every group. 936 foreach ( $order as $type ) { 937 unset( $this->params[ $type ][ $offset ] ); 938 } 939 } 940 } -
src/wp-includes/rest-api/infrastructure/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/infrastructure/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_ResponseInterface $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_ResponseInterface $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-extras.php
1 <?php 2 /** 3 * Extra functions related to the REST API. 4 * 5 * @package WordPress 6 * @subpackage REST_API 7 * 8 */ 9 10 add_action( 'xmlrpc_rsd_apis', 'rest_output_rsd' ); 11 add_action( 'wp_head', 'rest_output_link_wp_head', 10, 0 ); 12 add_action( 'template_redirect', 'rest_output_link_header', 11, 0 ); 13 add_action( 'auth_cookie_malformed', 'rest_cookie_collect_status' ); 14 add_action( 'auth_cookie_expired', 'rest_cookie_collect_status' ); 15 add_action( 'auth_cookie_bad_username', 'rest_cookie_collect_status' ); 16 add_action( 'auth_cookie_bad_hash', 'rest_cookie_collect_status' ); 17 add_action( 'auth_cookie_valid', 'rest_cookie_collect_status' ); 18 add_filter( 'rest_authentication_errors', 'rest_cookie_check_errors', 100 ); 19 20 /** 21 * Adds the REST API URL to the WP RSD endpoint. 22 * 23 * @since 4.4.0 24 * 25 * @see get_rest_url() 26 */ 27 function rest_output_rsd() { 28 $api_root = get_rest_url(); 29 30 if ( empty( $api_root ) ) { 31 return; 32 } 33 ?> 34 <api name="WP-API" blogID="1" preferred="false" apiLink="<?php echo esc_url( $api_root ); ?>" /> 35 <?php 36 } 37 38 /** 39 * Outputs the REST API link tag into page header. 40 * 41 * @since 4.4.0 42 * 43 * @see get_rest_url() 44 */ 45 function rest_output_link_wp_head() { 46 $api_root = get_rest_url(); 47 48 if ( empty( $api_root ) ) { 49 return; 50 } 51 52 echo "<link rel='https://github.com/WP-API/WP-API' href='" . esc_url( $api_root ) . "' />\n"; 53 } 54 55 /** 56 * Sends a Link header for the REST API. 57 * 58 * @since 4.4.0 59 */ 60 function rest_output_link_header() { 61 if ( headers_sent() ) { 62 return; 63 } 64 65 $api_root = get_rest_url(); 66 67 if ( empty( $api_root ) ) { 68 return; 69 } 70 71 header( 'Link: <' . esc_url_raw( $api_root ) . '>; rel="https://github.com/WP-API/WP-API"', false ); 72 } 73 74 /** 75 * Checks for errors when using cookie-based authentication. 76 * 77 * WordPress' built-in cookie authentication is always active 78 * for logged in users. However, the API has to check nonces 79 * for each request to ensure users are not vulnerable to CSRF. 80 * 81 * @since 4.4.0 82 * 83 * @global mixed $wp_rest_auth_cookie 84 * 85 * @param WP_Error|mixed $result Error from another authentication handler, null if we should handle it, 86 * or another value if not. 87 * @return WP_Error|mixed|bool WP_Error if the cookie is invalid, the $result, otherwise true. 88 */ 89 function rest_cookie_check_errors( $result ) { 90 if ( ! empty( $result ) ) { 91 return $result; 92 } 93 94 global $wp_rest_auth_cookie; 95 96 /* 97 * Is cookie authentication being used? (If we get an auth 98 * error, but we're still logged in, another authentication 99 * must have been used). 100 */ 101 if ( true !== $wp_rest_auth_cookie && is_user_logged_in() ) { 102 return $result; 103 } 104 105 // Determine if there is a nonce. 106 $nonce = null; 107 108 if ( isset( $_REQUEST['_wp_rest_nonce'] ) ) { 109 $nonce = $_REQUEST['_wp_rest_nonce']; 110 } elseif ( isset( $_SERVER['HTTP_X_WP_NONCE'] ) ) { 111 $nonce = $_SERVER['HTTP_X_WP_NONCE']; 112 } 113 114 if ( null === $nonce ) { 115 // No nonce at all, so act as if it's an unauthenticated request. 116 wp_set_current_user( 0 ); 117 return true; 118 } 119 120 // Check the nonce. 121 $result = wp_verify_nonce( $nonce, 'wp_rest' ); 122 123 if ( ! $result ) { 124 return new WP_Error( 'rest_cookie_invalid_nonce', __( 'Cookie nonce is invalid' ), array( 'status' => 403 ) ); 125 } 126 127 return true; 128 } 129 130 /** 131 * Collects cookie authentication status. 132 * 133 * Collects errors from wp_validate_auth_cookie for use by rest_cookie_check_errors. 134 * 135 * @since 4.4.0 136 * 137 * @see current_action() 138 * @global mixed $wp_rest_auth_cookie 139 */ 140 function rest_cookie_collect_status() { 141 global $wp_rest_auth_cookie; 142 143 $status_type = current_action(); 144 145 if ( 'auth_cookie_valid' !== $status_type ) { 146 $wp_rest_auth_cookie = substr( $status_type, 12 ); 147 return; 148 } 149 150 $wp_rest_auth_cookie = true; 151 } 152 153 /** 154 * Retrieves the avatar urls in various sizes based on a given email address. 155 * 156 * @since 4.4.0 157 * 158 * @see get_avatar_url() 159 * 160 * @param string $email Email address. 161 * @return array $urls Gravatar url for each size. 162 */ 163 function rest_get_avatar_urls( $email ) { 164 $avatar_sizes = rest_get_avatar_sizes(); 165 166 $urls = array(); 167 foreach ( $avatar_sizes as $size ) { 168 $urls[ $size ] = get_avatar_url( $email, array( 'size' => $size ) ); 169 } 170 171 return $urls; 172 } 173 174 /** 175 * Retrieves the pixel sizes for avatars. 176 * 177 * @since 4.4.0 178 * 179 * @return array List of pixel sizes for avatars. Default `[ 24, 48, 96 ]`. 180 */ 181 function rest_get_avatar_sizes() { 182 /** 183 * Filter the REST avatar sizes. 184 * 185 * Use this filter to adjust the array of sizes returned by the 186 * `rest_get_avatar_sizes` function. 187 * 188 * @since 4.4.0 189 * 190 * @param array $sizes An array of int values that are the pixel sizes for avatars. 191 * Default `[ 24, 48, 96 ]`. 192 */ 193 return apply_filters( 'rest_avatar_sizes', array( 24, 48, 96 ) ); 194 } 195 196 /** 197 * Parses an RFC3339 timestamp into a DateTime. 198 * 199 * @since 4.4.0 200 * 201 * @param string $date RFC3339 timestamp. 202 * @param bool $force_utc Optional. Whether to force UTC timezone instead of using 203 * the timestamp's timezone. Default false. 204 * @return DateTime DateTime instance. 205 */ 206 function rest_parse_date( $date, $force_utc = false ) { 207 if ( $force_utc ) { 208 $date = preg_replace( '/[+-]\d+:?\d+$/', '+00:00', $date ); 209 } 210 211 $regex = '#^\d{4}-\d{2}-\d{2}[Tt ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}(?::\d{2})?)?$#'; 212 213 if ( ! preg_match( $regex, $date, $matches ) ) { 214 return false; 215 } 216 217 return strtotime( $date ); 218 } 219 220 /** 221 * Retrieves a local date with its GMT equivalent, in MySQL datetime format. 222 * 223 * @since 4.4.0 224 * 225 * @see rest_parse_date() 226 * 227 * @param string $date RFC3339 timestamp. 228 * @param bool $force_utc Whether a UTC timestamp should be forced. Default false. 229 * @return array|null Local and UTC datetime strings, in MySQL datetime format (Y-m-d H:i:s), 230 * null on failure. 231 */ 232 function rest_get_date_with_gmt( $date, $force_utc = false ) { 233 $date = rest_parse_date( $date, $force_utc ); 234 235 if ( empty( $date ) ) { 236 return null; 237 } 238 239 $utc = date( 'Y-m-d H:i:s', $date ); 240 $local = get_date_from_gmt( $utc ); 241 242 return array( $local, $utc ); 243 } 244 245 /** 246 * Parses and formats a MySQL datetime (Y-m-d H:i:s) for ISO8601/RFC3339. 247 * 248 * Explicitly strips timezones, as datetimes are not saved with any timezone 249 * information. Including any information on the offset could be misleading. 250 * 251 * @since 4.4.0 252 * 253 * @param string $date Date string to parse and format. 254 * @return string Date formatted for ISO8601/RFC3339. 255 */ 256 function rest_mysql_to_rfc3339( $date_string ) { 257 $formatted = mysql2date( 'c', $date_string, false ); 258 259 // Strip timezone information 260 return preg_replace( '/(?:Z|[+-]\d{2}(?::\d{2})?)$/', '', $formatted ); 261 } 262 263 264 /** 265 * Retrieves the timezone object for the site. 266 * 267 * @since 4.4.0 268 * 269 * @return DateTimeZone DateTimeZone instance. 270 */ 271 function rest_get_timezone() { 272 static $zone = null; 273 274 if ( null !== $zone ) { 275 return $zone; 276 } 277 278 $tzstring = get_option( 'timezone_string' ); 279 280 if ( ! $tzstring ) { 281 // Create a UTC+- zone if no timezone string exists. 282 $current_offset = get_option( 'gmt_offset' ); 283 if ( 0 === $current_offset ) { 284 $tzstring = 'UTC'; 285 } elseif ( $current_offset < 0 ) { 286 $tzstring = 'Etc/GMT' . $current_offset; 287 } else { 288 $tzstring = 'Etc/GMT+' . $current_offset; 289 } 290 } 291 $zone = new DateTimeZone( $tzstring ); 292 293 return $zone; 294 } -
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 /** JsonSerializable interface (Compatibility shim for PHP <5.4) */ 17 require_once( ABSPATH . WPINC . '/rest-api/infrastructure/class-jsonserializable.php' ); 18 19 /** WP_REST_Server class */ 20 require_once( ABSPATH . WPINC . '/rest-api/infrastructure/class-wp-rest-server.php' ); 21 22 /** WP_HTTP_ResponseInterface interface */ 23 require_once( ABSPATH . WPINC . '/rest-api/infrastructure/class-wp-http-responseinterface.php' ); 24 25 /** WP_HTTP_Response class */ 26 require_once( ABSPATH . WPINC . '/rest-api/infrastructure/class-wp-http-response.php' ); 27 28 /** WP_REST_Response class */ 29 require_once( ABSPATH . WPINC . '/rest-api/infrastructure/class-wp-rest-response.php' ); 30 31 /** WP_REST_Request class */ 32 require_once( ABSPATH . WPINC . '/rest-api/infrastructure/class-wp-rest-request.php' ); 33 34 /** REST extras */ 35 require_once( ABSPATH . WPINC . '/rest-api/rest-extras.php' ); 36 37 /** 38 * Registers a REST API route. 39 * 40 * @since 4.4.0 41 * 42 * @param string $namespace The first URL segment after core prefix. Should be unique to your package/plugin. 43 * @param string $route The base URL for route you are adding. 44 * @param array $args Optional. Either an array of options for the endpoint, or an array of arrays for 45 * multiple methods. Default empty array. 46 * @param bool $override Optional. If the route already exists, should we override it? True overrides, 47 * false merges (with newer overriding if duplicate keys exist). Default false. 48 */ 49 function register_rest_route( $namespace, $route, $args = array(), $override = false ) { 50 51 /** @var WP_REST_Server $wp_rest_server */ 52 global $wp_rest_server; 53 54 if ( isset( $args['callback'] ) ) { 55 // Upgrade a single set to multiple 56 $args = array( $args ); 57 } 58 59 $defaults = array( 60 'methods' => 'GET', 61 'callback' => null, 62 'args' => array(), 63 ); 64 foreach ( $args as $key => &$arg_group ) { 65 if ( ! is_numeric( $arg_group ) ) { 66 // Route option, skip here 67 continue; 68 } 69 70 $arg_group = array_merge( $defaults, $arg_group ); 71 } 72 73 if ( $namespace ) { 74 $full_route = '/' . trim( $namespace, '/' ) . '/' . trim( $route, '/' ); 75 } else { 76 /* 77 * Non-namespaced routes are not allowed, with the exception of the main 78 * and namespace indexes. If you really need to register a 79 * non-namespaced route, call `WP_REST_Server::register_route` directly. 80 */ 81 _doing_it_wrong( 'register_rest_route', 'Routes must be namespaced with plugin name and version', 'WPAPI-2.0' ); 82 83 $full_route = '/' . trim( $route, '/' ); 84 } 85 86 $wp_rest_server->register_route( $namespace, $full_route, $args, $override ); 87 } 88 89 /** 90 * Registers a new field on an existing WordPress object type. 91 * 92 * @since 4.4.0 93 * 94 * @global array $wp_rest_additional_fields Holds registered fields, organized 95 * by object type. 96 * 97 * @param string|array $object_type Object(s) the field is being registered 98 * to, "post"|"term"|"comment" etc. 99 * @param string $attribute The attribute name. 100 * @param array $args { 101 * Optional. An array of arguments used to handle the registered field. 102 * 103 * @type string|array|null $get_callback Optional. The callback function used to retrieve the field 104 * value. Default is 'null', the field will not be returned in 105 * the response. 106 * @type string|array|null $update_callback Optional. The callback function used to set and update the 107 * field value. Default is 'null', the value cannot be set or 108 * updated. 109 * @type string|array|null schema Optional. The callback function used to create the schema for 110 * this field. Default is 'null', no schema entry will be returned. 111 * } 112 */ 113 function register_api_field( $object_type, $attribute, $args = array() ) { 114 115 $defaults = array( 116 'get_callback' => null, 117 'update_callback' => null, 118 'schema' => null, 119 ); 120 121 $args = wp_parse_args( $args, $defaults ); 122 123 global $wp_rest_additional_fields; 124 125 $object_types = (array) $object_type; 126 127 foreach ( $object_types as $object_type ) { 128 $wp_rest_additional_fields[ $object_type ][ $attribute ] = $args; 129 } 130 } 131 132 /** 133 * Registers rewrite rules for the API. 134 * 135 * @since 4.4.0 136 * 137 * @see rest_api_register_rewrites() 138 * @global WP $wp Current WordPress environment instance. 139 */ 140 function rest_api_init() { 141 rest_api_register_rewrites(); 142 143 global $wp; 144 $wp->add_query_var( 'rest_route' ); 145 } 146 add_action( 'init', 'rest_api_init' ); 147 148 /** 149 * Adds REST rewrite rules. 150 * 151 * @since 4.4.0 152 * 153 * @see add_rewrite_rule() 154 */ 155 function rest_api_register_rewrites() { 156 add_rewrite_rule( '^' . rest_get_url_prefix() . '/?$','index.php?rest_route=/','top' ); 157 add_rewrite_rule( '^' . rest_get_url_prefix() . '/(.*)?','index.php?rest_route=/$matches[1]','top' ); 158 } 159 160 /** 161 * Determines if the rewrite rules should be flushed. 162 * 163 * @since 4.4.0 164 */ 165 function rest_api_maybe_flush_rewrites() { 166 $version = get_option( 'rest_api_plugin_version', null ); 167 168 if ( empty( $version ) || REST_API_VERSION !== $version ) { 169 flush_rewrite_rules(); 170 update_option( 'rest_api_plugin_version', REST_API_VERSION ); 171 } 172 } 173 add_action( 'init', 'rest_api_maybe_flush_rewrites', 999 ); 174 175 /** 176 * Registers the default REST API filters. 177 * 178 * @since 4.4.0 179 * 180 * @internal This will live in default-filters.php 181 * 182 * @global WP_REST_Posts $WP_REST_posts 183 * @global WP_REST_Pages $WP_REST_pages 184 * @global WP_REST_Media $WP_REST_media 185 * @global WP_REST_Taxonomies $WP_REST_taxonomies 186 * 187 * @param WP_REST_Server $server Server object. 188 */ 189 function rest_api_default_filters( $server ) { 190 // Deprecated reporting. 191 add_action( 'deprecated_function_run', 'rest_handle_deprecated_function', 10, 3 ); 192 add_filter( 'deprecated_function_trigger_error', '__return_false' ); 193 add_action( 'deprecated_argument_run', 'rest_handle_deprecated_argument', 10, 3 ); 194 add_filter( 'deprecated_argument_trigger_error', '__return_false' ); 195 196 // Default serving 197 add_filter( 'rest_pre_serve_request', 'rest_send_cors_headers' ); 198 add_filter( 'rest_post_dispatch', 'rest_send_allow_header', 10, 3 ); 199 200 add_filter( 'rest_pre_dispatch', 'rest_handle_options_request', 10, 3 ); 201 202 } 203 add_action( 'rest_api_init', 'rest_api_default_filters', 10, 1 ); 204 205 /** 206 * Loads the REST API. 207 * 208 * @since 4.4.0 209 * 210 * @todo Extract code that should be unit tested into isolated methods such as 211 * the wp_rest_server_class filter and serving requests. This would also 212 * help for code re-use by `wp-json` endpoint. Note that we can't unit 213 * test any method that calls die(). 214 */ 215 function rest_api_loaded() { 216 if ( empty( $GLOBALS['wp']->query_vars['rest_route'] ) ) { 217 return; 218 } 219 220 /** 221 * Whether this is a REST Request. 222 * 223 * @var bool 224 */ 225 define( 'REST_REQUEST', true ); 226 227 /** @var WP_REST_Server $wp_rest_server */ 228 global $wp_rest_server; 229 230 /** 231 * Filter the REST Server Class. 232 * 233 * This filter allows you to adjust the server class used by the API, using a 234 * different class to handle requests. 235 * 236 * @since 4.4.0 237 * 238 * @param string $class_name The name of the server class. Default 'WP_REST_Server'. 239 */ 240 $wp_rest_server_class = apply_filters( 'wp_rest_server_class', 'WP_REST_Server' ); 241 $wp_rest_server = new $wp_rest_server_class; 242 243 /** 244 * Fires when preparing to serve an API request. 245 * 246 * Endpoint objects should be created and register their hooks on this action rather 247 * than another action to ensure they're only loaded when needed. 248 * 249 * @since 4.4.0 250 * 251 * @param WP_REST_Server $wp_rest_server Server object. 252 */ 253 do_action( 'rest_api_init', $wp_rest_server ); 254 255 // Fire off the request. 256 $wp_rest_server->serve_request( $GLOBALS['wp']->query_vars['rest_route'] ); 257 258 // We're done. 259 die(); 260 } 261 add_action( 'parse_request', 'rest_api_loaded' ); 262 263 /** 264 * Retrieves the URL prefix for any API resource. 265 * 266 * @since 4.4.0 267 * 268 * @return string Prefix. 269 */ 270 function rest_get_url_prefix() { 271 /** 272 * Filter the REST URL prefix. 273 * 274 * @since 4.4.0 275 * 276 * @param string $prefix URL prefix. Default 'wp-json'. 277 */ 278 return apply_filters( 'rest_url_prefix', 'wp-json' ); 279 } 280 281 /** 282 * Retrieves the URL to a REST endpoint on a site. 283 * 284 * Note: The returned URL is NOT escaped. 285 * 286 * @since 4.4.0 287 * 288 * @todo Check if this is even necessary 289 * 290 * @param int $blog_id Optional. Blog ID. Default of null returns URL for current blog. 291 * @param string $path Optional. REST route. Default '/'. 292 * @param string $scheme Optional. Sanitization scheme. Default 'json'. 293 * @return string Full URL to the endpoint. 294 */ 295 function get_rest_url( $blog_id = null, $path = '/', $scheme = 'json' ) { 296 if ( empty( $path ) ) { 297 $path = '/'; 298 } 299 300 if ( is_multisite() && get_blog_option( $blog_id, 'permalink_structure' ) || get_option( 'permalink_structure' ) ) { 301 $url = get_home_url( $blog_id, rest_get_url_prefix(), $scheme ); 302 $url .= '/' . ltrim( $path, '/' ); 303 } else { 304 $url = trailingslashit( get_home_url( $blog_id, '', $scheme ) ); 305 306 $path = '/' . ltrim( $path, '/' ); 307 308 $url = add_query_arg( 'rest_route', $path, $url ); 309 } 310 311 /** 312 * Filter the REST URL. 313 * 314 * Use this filter to adjust the url returned by the `get_rest_url` function. 315 * 316 * @since 4.4.0 317 * 318 * @param string $url REST URL. 319 * @param string $path REST route. 320 * @param int $blod_ig Blog ID. 321 * @param string $scheme Sanitization scheme. 322 */ 323 return apply_filters( 'rest_url', $url, $path, $blog_id, $scheme ); 324 } 325 326 /** 327 * Retrieves the URL to a REST endpoint. 328 * 329 * Note: The returned URL is NOT escaped. 330 * 331 * @since 4.4.0 332 * 333 * @param string $path Optional. REST route. Default empty. 334 * @param string $scheme Optional. Sanitization scheme. Default 'json'. 335 * @return string Full URL to the endpoint. 336 */ 337 function rest_url( $path = '', $scheme = 'json' ) { 338 return get_rest_url( null, $path, $scheme ); 339 } 340 341 /** 342 * Do a REST request. 343 * 344 * Used primarily to route internal requests through WP_REST_Server. 345 * 346 * @since 4.4.0 347 * 348 * @param WP_REST_Request|string $request 349 * @return WP_REST_Response REST response. 350 */ 351 function rest_do_request( $request ) { 352 global $wp_rest_server; 353 $request = rest_ensure_request( $request ); 354 return $wp_rest_server->dispatch( $request ); 355 } 356 357 /** 358 * Ensures request arguments are a request object (for consistency). 359 * 360 * @since 4.4.0 361 * 362 * @param array|WP_REST_Request $request Request to check. 363 * @return WP_REST_Request REST request instance. 364 */ 365 function rest_ensure_request( $request ) { 366 if ( $request instanceof WP_REST_Request ) { 367 return $request; 368 } 369 370 return new WP_REST_Request( 'GET', '', $request ); 371 } 372 373 /** 374 * Ensures a REST response is a response object (for consistency). 375 * 376 * This implements WP_HTTP_ResponseInterface, allowing usage of `set_status`/`header`/etc 377 * without needing to double-check the object. Will also allow WP_Error to indicate error 378 * responses, so users should immediately check for this value. 379 * 380 * @since 4.4.0 381 * 382 * @param WP_Error|WP_HTTP_ResponseInterface|mixed $response Response to check. 383 * @return mixed WP_Error if response generated an error, WP_HTTP_ResponseInterface if response 384 * is a already an instance, otherwise returns a new WP_REST_Response instance. 385 */ 386 function rest_ensure_response( $response ) { 387 if ( is_wp_error( $response ) ) { 388 return $response; 389 } 390 391 if ( $response instanceof WP_HTTP_ResponseInterface ) { 392 return $response; 393 } 394 395 return new WP_REST_Response( $response ); 396 } 397 398 /** 399 * Handles _deprecated_function() errors. 400 * 401 * @since 4.4.0 402 * 403 * @param string $function Function name. 404 * @param string $replacement Replacement function name. 405 * @param string $version Version. 406 */ 407 function rest_handle_deprecated_function( $function, $replacement, $version ) { 408 if ( ! empty( $replacement ) ) { 409 $string = sprintf( __( '%1$s (since %2$s; use %3$s instead)' ), $function, $version, $replacement ); 410 } else { 411 $string = sprintf( __( '%1$s (since %2$s; no alternative available)' ), $function, $version ); 412 } 413 414 header( sprintf( 'X-WP-DeprecatedFunction: %s', $string ) ); 415 } 416 417 /** 418 * Handles _deprecated_argument() errors. 419 * 420 * @since 4.4.0 421 * 422 * @param string $function Function name. 423 * @param string $replacement Replacement function name. 424 * @param string $version Version. 425 */ 426 function rest_handle_deprecated_argument( $function, $replacement, $version ) { 427 if ( ! empty( $replacement ) ) { 428 $string = sprintf( __( '%1$s (since %2$s; %3$s)' ), $function, $version, $replacement ); 429 } else { 430 $string = sprintf( __( '%1$s (since %2$s; no alternative available)' ), $function, $version ); 431 } 432 433 header( sprintf( 'X-WP-DeprecatedParam: %s', $string ) ); 434 } 435 436 /** 437 * Sends Cross-Origin Resource Sharing headers with API requests. 438 * 439 * @since 4.4.0 440 * 441 * @param mixed $value Response data. 442 * @return mixed Response data. 443 */ 444 function rest_send_cors_headers( $value ) { 445 $origin = get_http_origin(); 446 447 if ( $origin ) { 448 header( 'Access-Control-Allow-Origin: ' . esc_url_raw( $origin ) ); 449 header( 'Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE' ); 450 header( 'Access-Control-Allow-Credentials: true' ); 451 } 452 453 return $value; 454 } 455 456 /** 457 * Handles OPTIONS requests for the server. 458 * 459 * This is handled outside of the server code, as it doesn't obey normal route 460 * mapping. 461 * 462 * @since 4.4.0 463 * 464 * @param mixed $response Current response, either response or `null` to indicate pass-through. 465 * @param WP_REST_Server $handler ResponseHandler instance (usually WP_REST_Server). 466 * @param WP_REST_Request $request The request that was used to make current response. 467 * @return WP_REST_Response Modified response, either response or `null` to indicate pass-through. 468 */ 469 function rest_handle_options_request( $response, $handler, $request ) { 470 if ( ! empty( $response ) || $request->get_method() !== 'OPTIONS' ) { 471 return $response; 472 } 473 474 $response = new WP_REST_Response(); 475 $data = array(); 476 477 $accept = array(); 478 479 foreach ( $handler->get_routes() as $route => $endpoints ) { 480 $match = preg_match( '@^' . $route . '$@i', $request->get_route(), $args ); 481 482 if ( ! $match ) { 483 continue; 484 } 485 486 $data = $handler->get_data_for_route( $route, $endpoints, 'help' ); 487 $accept = array_merge( $accept, $data['methods'] ); 488 break; 489 } 490 $response->header( 'Accept', implode( ', ', $accept ) ); 491 492 $response->set_data( $data ); 493 return $response; 494 } 495 496 /** 497 * Sends the "Allow" header to state all methods that can be sent to the current route. 498 * 499 * @since 4.4.0 500 * 501 * @param WP_REST_Response $response Current response being served. 502 * @param WP_REST_Server $server ResponseHandler instance (usually WP_REST_Server). 503 * @param WP_REST_Request $request The request that was used to make current response. 504 */ 505 function rest_send_allow_header( $response, $server, $request ) { 506 507 $matched_route = $response->get_matched_route(); 508 509 if ( ! $matched_route ) { 510 return $response; 511 } 512 513 $routes = $server->get_routes(); 514 515 $allowed_methods = array(); 516 517 // Get the allowed methods across the routes. 518 foreach ( $routes[ $matched_route ] as $_handler ) { 519 foreach ( $_handler['methods'] as $handler_method => $value ) { 520 521 if ( ! empty( $_handler['permission_callback'] ) ) { 522 523 $permission = call_user_func( $_handler['permission_callback'], $request ); 524 525 $allowed_methods[ $handler_method ] = true === $permission; 526 } else { 527 $allowed_methods[ $handler_method ] = true; 528 } 529 } 530 } 531 532 // Strip out all the methods that are not allowed (false values). 533 $allowed_methods = array_filter( $allowed_methods ); 534 535 if ( $allowed_methods ) { 536 $response->header( 'Allow', implode( ', ', array_map( 'strtoupper', array_keys( $allowed_methods ) ) ) ); 537 } 538 539 return $response; 540 } 541 542 if ( ! function_exists( 'json_last_error_msg' ) ) : 543 /** 544 * Retrieves the error string of the last json_encode() or json_decode() call. 545 * 546 * @since 4.4.0 547 * 548 * @internal This is a compatibility function for PHP <5.5 549 * 550 * @return bool|string Returns the error message on success, "No Error" if no error has occurred, 551 * or false on failure. 552 */ 553 function json_last_error_msg() { 554 // See https://core.trac.wordpress.org/ticket/27799. 555 if ( ! function_exists( 'json_last_error' ) ) { 556 return false; 557 } 558 559 $last_error_code = json_last_error(); 560 561 // Just in case JSON_ERROR_NONE is not defined. 562 $error_code_none = defined( 'JSON_ERROR_NONE' ) ? JSON_ERROR_NONE : 0; 563 564 switch ( true ) { 565 case $last_error_code === $error_code_none: 566 return 'No error'; 567 568 case defined( 'JSON_ERROR_DEPTH' ) && JSON_ERROR_DEPTH === $last_error_code: 569 return 'Maximum stack depth exceeded'; 570 571 case defined( 'JSON_ERROR_STATE_MISMATCH' ) && JSON_ERROR_STATE_MISMATCH === $last_error_code: 572 return 'State mismatch (invalid or malformed JSON)'; 573 574 case defined( 'JSON_ERROR_CTRL_CHAR' ) && JSON_ERROR_CTRL_CHAR === $last_error_code: 575 return 'Control character error, possibly incorrectly encoded'; 576 577 case defined( 'JSON_ERROR_SYNTAX' ) && JSON_ERROR_SYNTAX === $last_error_code: 578 return 'Syntax error'; 579 580 case defined( 'JSON_ERROR_UTF8' ) && JSON_ERROR_UTF8 === $last_error_code: 581 return 'Malformed UTF-8 characters, possibly incorrectly encoded'; 582 583 case defined( 'JSON_ERROR_RECURSION' ) && JSON_ERROR_RECURSION === $last_error_code: 584 return 'Recursion detected'; 585 586 case defined( 'JSON_ERROR_INF_OR_NAN' ) && JSON_ERROR_INF_OR_NAN === $last_error_code: 587 return 'Inf and NaN cannot be JSON encoded'; 588 589 case defined( 'JSON_ERROR_UNSUPPORTED_TYPE' ) && JSON_ERROR_UNSUPPORTED_TYPE === $last_error_code: 590 return 'Type is not supported'; 591 592 default: 593 return 'An unknown error occurred'; 594 } 595 } 596 endif; 597 598 /** 599 * Determines if the variable a list. 600 * 601 * A list would be defined as a numeric-indexed array. 602 * 603 * @since 4.4.0 604 * 605 * @param mixed $data Variable to check. 606 * @return bool Whether the variable is a list. 607 */ 608 function rest_is_list( $data ) { 609 if ( ! is_array( $data ) ) { 610 return false; 611 } 612 613 $keys = array_keys( $data ); 614 $string_keys = array_filter( $keys, 'is_string' ); 615 return count( $string_keys ) === 0; 616 } -
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 } 20 21 /** 22 * The plugin should be installed and activated. 23 */ 24 function test_rest_api_activated() { 25 $this->assertTrue( class_exists( 'WP_REST_Server' ) ); 26 } 27 28 /** 29 * The rest_api_init hook should have been registered with init, and should 30 * have a default priority of 10. 31 */ 32 function test_init_action_added() { 33 $this->assertEquals( 10, has_action( 'init', 'rest_api_init' ) ); 34 } 35 36 /** 37 * Check that a single route is canonicalized 38 * 39 * Ensures that single and multiple routes are handled correctly 40 */ 41 public function test_route_canonicalized() { 42 register_rest_route( 'test-ns', '/test', array( 43 'methods' => array( 'GET' ), 44 'callback' => '__return_null', 45 ) ); 46 47 // Check the route was registered correctly 48 $endpoints = $GLOBALS['wp_rest_server']->get_raw_endpoint_data(); 49 $this->assertArrayHasKey( '/test-ns/test', $endpoints ); 50 51 // Check the route was wrapped in an array 52 $endpoint = $endpoints['/test-ns/test']; 53 $this->assertArrayNotHasKey( 'callback', $endpoint ); 54 $this->assertArrayHasKey( 'namespace', $endpoint ); 55 $this->assertEquals( 'test-ns', $endpoint['namespace'] ); 56 57 // Grab the filtered data 58 $filtered_endpoints = $GLOBALS['wp_rest_server']->get_routes(); 59 $this->assertArrayHasKey( '/test-ns/test', $filtered_endpoints ); 60 $endpoint = $filtered_endpoints['/test-ns/test']; 61 $this->assertCount( 1, $endpoint ); 62 $this->assertArrayHasKey( 'callback', $endpoint[0] ); 63 $this->assertArrayHasKey( 'methods', $endpoint[0] ); 64 $this->assertArrayHasKey( 'args', $endpoint[0] ); 65 } 66 67 /** 68 * Check that a single route is canonicalized 69 * 70 * Ensures that single and multiple routes are handled correctly 71 */ 72 public function test_route_canonicalized_multiple() { 73 register_rest_route( 'test-ns', '/test', array( 74 array( 75 'methods' => array( 'GET' ), 76 'callback' => '__return_null', 77 ), 78 array( 79 'methods' => array( 'POST' ), 80 'callback' => '__return_null', 81 ), 82 ) ); 83 84 // Check the route was registered correctly 85 $endpoints = $GLOBALS['wp_rest_server']->get_raw_endpoint_data(); 86 $this->assertArrayHasKey( '/test-ns/test', $endpoints ); 87 88 // Check the route was wrapped in an array 89 $endpoint = $endpoints['/test-ns/test']; 90 $this->assertArrayNotHasKey( 'callback', $endpoint ); 91 $this->assertArrayHasKey( 'namespace', $endpoint ); 92 $this->assertEquals( 'test-ns', $endpoint['namespace'] ); 93 94 $filtered_endpoints = $GLOBALS['wp_rest_server']->get_routes(); 95 $endpoint = $filtered_endpoints['/test-ns/test']; 96 $this->assertCount( 2, $endpoint ); 97 98 // Check for both methods 99 foreach ( array( 0, 1 ) as $key ) { 100 $this->assertArrayHasKey( 'callback', $endpoint[ $key ] ); 101 $this->assertArrayHasKey( 'methods', $endpoint[ $key ] ); 102 $this->assertArrayHasKey( 'args', $endpoint[ $key ] ); 103 } 104 } 105 106 /** 107 * Check that routes are merged by default 108 */ 109 public function test_route_merge() { 110 register_rest_route( 'test-ns', '/test', array( 111 'methods' => array( 'GET' ), 112 'callback' => '__return_null', 113 ) ); 114 register_rest_route( 'test-ns', '/test', array( 115 'methods' => array( 'POST' ), 116 'callback' => '__return_null', 117 ) ); 118 119 // Check both routes exist 120 $endpoints = $GLOBALS['wp_rest_server']->get_routes(); 121 $endpoint = $endpoints['/test-ns/test']; 122 $this->assertCount( 2, $endpoint ); 123 } 124 125 /** 126 * Check that we can override routes 127 */ 128 public function test_route_override() { 129 register_rest_route( 'test-ns', '/test', array( 130 'methods' => array( 'GET' ), 131 'callback' => '__return_null', 132 'should_exist' => false, 133 ) ); 134 register_rest_route( 'test-ns', '/test', array( 135 'methods' => array( 'POST' ), 136 'callback' => '__return_null', 137 'should_exist' => true, 138 ), true ); 139 140 // Check we only have one route 141 $endpoints = $GLOBALS['wp_rest_server']->get_routes(); 142 $endpoint = $endpoints['/test-ns/test']; 143 $this->assertCount( 1, $endpoint ); 144 145 // Check it's the right one 146 $this->assertArrayHasKey( 'should_exist', $endpoint[0] ); 147 $this->assertTrue( $endpoint[0]['should_exist'] ); 148 } 149 150 /** 151 * The rest_route query variable should be registered. 152 */ 153 function test_rest_route_query_var() { 154 global $wp; 155 $this->assertTrue( in_array( 'rest_route', $wp->public_query_vars ) ); 156 } 157 158 public function test_route_method() { 159 register_rest_route( 'test-ns', '/test', array( 160 'methods' => array( 'GET' ), 161 'callback' => '__return_null', 162 ) ); 163 164 $routes = $GLOBALS['wp_rest_server']->get_routes(); 165 166 $this->assertEquals( $routes['/test-ns/test'][0]['methods'], array( 'GET' => true ) ); 167 } 168 169 /** 170 * The 'methods' arg should accept a single value as well as array 171 */ 172 public function test_route_method_string() { 173 register_rest_route( 'test-ns', '/test', array( 174 'methods' => 'GET', 175 'callback' => '__return_null', 176 ) ); 177 178 $routes = $GLOBALS['wp_rest_server']->get_routes(); 179 180 $this->assertEquals( $routes['/test-ns/test'][0]['methods'], array( 'GET' => true ) ); 181 } 182 183 /** 184 * The 'methods' arg should accept a single value as well as array 185 */ 186 public function test_route_method_array() { 187 register_rest_route( 'test-ns', '/test', array( 188 'methods' => array( 'GET', 'POST' ), 189 'callback' => '__return_null', 190 ) ); 191 192 $routes = $GLOBALS['wp_rest_server']->get_routes(); 193 194 $this->assertEquals( $routes['/test-ns/test'][0]['methods'], array( 'GET' => true, 'POST' => true ) ); 195 } 196 197 /** 198 * The 'methods' arg should a comma seperated string 199 */ 200 public function test_route_method_comma_seperated() { 201 register_rest_route( 'test-ns', '/test', array( 202 'methods' => 'GET,POST', 203 'callback' => '__return_null', 204 ) ); 205 206 $routes = $GLOBALS['wp_rest_server']->get_routes(); 207 208 $this->assertEquals( $routes['/test-ns/test'][0]['methods'], array( 'GET' => true, 'POST' => true ) ); 209 } 210 211 public function test_options_request() { 212 register_rest_route( 'test-ns', '/test', array( 213 'methods' => 'GET,POST', 214 'callback' => '__return_null', 215 ) ); 216 217 $request = new WP_REST_Request( 'OPTIONS', '/test-ns/test' ); 218 $response = rest_handle_options_request( null, $GLOBALS['wp_rest_server'], $request ); 219 220 $headers = $response->get_headers(); 221 $this->assertArrayHasKey( 'Accept', $headers ); 222 223 $this->assertEquals( 'GET, POST', $headers['Accept'] ); 224 } 225 226 /** 227 * Ensure that the OPTIONS handler doesn't kick in for non-OPTIONS requests 228 */ 229 public function test_options_request_not_options() { 230 register_rest_route( 'test-ns', '/test', array( 231 'methods' => 'GET,POST', 232 'callback' => '__return_true', 233 ) ); 234 235 $request = new WP_REST_Request( 'GET', '/test-ns/test' ); 236 $response = rest_handle_options_request( null, $GLOBALS['wp_rest_server'], $request ); 237 238 $this->assertNull( $response ); 239 } 240 241 /** 242 * The get_rest_url function should return a URL consistently terminated with a "/", 243 * whether the blog is configured with pretty permalink support or not. 244 */ 245 public function test_rest_url_generation() { 246 global $wp_rewrite; 247 248 // Need rewrite rules in place to use url_to_postid 249 $wp_rewrite->init(); 250 251 $wp_rewrite->set_permalink_structure( '/%year%/%monthnum%/%day%/%postname%/' ); 252 $wp_rewrite->flush_rules(); 253 $this->assertEquals( 'http://' . WP_TESTS_DOMAIN . '/wp-json/', get_rest_url() ); 254 255 $wp_rewrite->set_permalink_structure( '' ); 256 $wp_rewrite->flush_rules(); 257 // In non-pretty case, we get a query string to invoke the rest router 258 $this->assertEquals( 'http://' . WP_TESTS_DOMAIN . '/?rest_route=/', get_rest_url() ); 259 } 260 }