3 * REST API: WP_REST_Request class
11 * Core class used to implement a REST request object.
13 * Contains data from the request, to be passed to the callback.
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.
19 * Note: When using features provided by ArrayAccess, be aware that WordPress deliberately
20 * does not distinguish between arguments of the same name for different request methods.
21 * For instance, in a request with `GET id=1` and `POST id=2`, `$request['id']` will equal
22 * 2 (`POST`) not 1 (`GET`). For more precision between request methods, use
23 * WP_REST_Request::get_body_params(), WP_REST_Request::get_url_params(), etc.
29 class WP_REST_Request implements ArrayAccess {
38 protected $method = '';
41 * Parameters passed to the request.
43 * These typically come from the `$_GET`, `$_POST` and `$_FILES`
44 * superglobals when being created from the global scope.
48 * @var array Contains GET, POST and FILES keys mapping to arrays of data.
53 * HTTP headers for the request.
57 * @var array Map of key to value. Key is always lowercase, as per HTTP specification.
59 protected $headers = array();
66 * @var string Binary data from the request.
68 protected $body = null;
71 * Route matched for the request.
80 * Attributes (options) for the route that was matched.
82 * This is the options array used when the route was registered, typically
83 * containing the callback as well as the valid methods for the route.
87 * @var array Attributes for the request.
89 protected $attributes = array();
92 * Used to determine if the JSON data has been parsed yet.
94 * Allows lazy-parsing of JSON data where possible.
100 protected $parsed_json = false;
103 * Used to determine if the body data has been parsed yet.
109 protected $parsed_body = false;
117 * @param string $method Optional. Request method. Default empty.
118 * @param string $route Optional. Request route. Default empty.
119 * @param array $attributes Optional. Request attributes. Default empty array.
121 public function __construct( $method = '', $route = '', $attributes = array() ) {
122 $this->params = array(
128 // See parse_json_params.
131 'defaults' => array(),
134 $this->set_method( $method );
135 $this->set_route( $route );
136 $this->set_attributes( $attributes );
140 * Retrieves the HTTP method for the request.
145 * @return string HTTP method.
147 public function get_method() {
148 return $this->method;
152 * Sets HTTP method for the request.
157 * @param string $method HTTP method.
159 public function set_method( $method ) {
160 $this->method = strtoupper( $method );
164 * Retrieves all headers from the request.
169 * @return array Map of key to value. Key is always lowercase, as per HTTP specification.
171 public function get_headers() {
172 return $this->headers;
176 * Canonicalizes the header name.
178 * Ensures that header names are always treated the same regardless of
179 * source. Header names are always case insensitive.
181 * Note that we treat `-` (dashes) and `_` (underscores) as the same
182 * character, as per header parsing rules in both Apache and nginx.
184 * @link http://stackoverflow.com/q/18185366
185 * @link http://wiki.nginx.org/Pitfalls#Missing_.28disappearing.29_HTTP_headers
186 * @link https://nginx.org/en/docs/http/ngx_http_core_module.html#underscores_in_headers
192 * @param string $key Header name.
193 * @return string Canonicalized name.
195 public static function canonicalize_header_name( $key ) {
196 $key = strtolower( $key );
197 $key = str_replace( '-', '_', $key );
203 * Retrieves the given header from the request.
205 * If the header has multiple values, they will be concatenated with a comma
206 * as per the HTTP specification. Be aware that some non-compliant headers
207 * (notably cookie headers) cannot be joined this way.
212 * @param string $key Header name, will be canonicalized to lowercase.
213 * @return string|null String value if set, null otherwise.
215 public function get_header( $key ) {
216 $key = $this->canonicalize_header_name( $key );
218 if ( ! isset( $this->headers[ $key ] ) ) {
222 return implode( ',', $this->headers[ $key ] );
226 * Retrieves header values from the request.
231 * @param string $key Header name, will be canonicalized to lowercase.
232 * @return array|null List of string values if set, null otherwise.
234 public function get_header_as_array( $key ) {
235 $key = $this->canonicalize_header_name( $key );
237 if ( ! isset( $this->headers[ $key ] ) ) {
241 return $this->headers[ $key ];
245 * Sets the header on request.
250 * @param string $key Header name.
251 * @param string $value Header value, or list of values.
253 public function set_header( $key, $value ) {
254 $key = $this->canonicalize_header_name( $key );
255 $value = (array) $value;
257 $this->headers[ $key ] = $value;
261 * Appends a header value for the given header.
266 * @param string $key Header name.
267 * @param string $value Header value, or list of values.
269 public function add_header( $key, $value ) {
270 $key = $this->canonicalize_header_name( $key );
271 $value = (array) $value;
273 if ( ! isset( $this->headers[ $key ] ) ) {
274 $this->headers[ $key ] = array();
277 $this->headers[ $key ] = array_merge( $this->headers[ $key ], $value );
281 * Removes all values for a header.
286 * @param string $key Header name.
288 public function remove_header( $key ) {
289 unset( $this->headers[ $key ] );
293 * Sets headers on the request.
298 * @param array $headers Map of header name to value.
299 * @param bool $override If true, replace the request's headers. Otherwise, merge with existing.
301 public function set_headers( $headers, $override = true ) {
302 if ( true === $override ) {
303 $this->headers = array();
306 foreach ( $headers as $key => $value ) {
307 $this->set_header( $key, $value );
312 * Retrieves the content-type of the request.
317 * @return array Map containing 'value' and 'parameters' keys.
319 public function get_content_type() {
320 $value = $this->get_header( 'content-type' );
321 if ( empty( $value ) ) {
326 if ( strpos( $value, ';' ) ) {
327 list( $value, $parameters ) = explode( ';', $value, 2 );
330 $value = strtolower( $value );
331 if ( strpos( $value, '/' ) === false ) {
335 // Parse type and subtype out.
336 list( $type, $subtype ) = explode( '/', $value, 2 );
338 $data = compact( 'value', 'type', 'subtype', 'parameters' );
339 $data = array_map( 'trim', $data );
345 * Retrieves the parameter priority order.
347 * Used when checking parameters in get_param().
352 * @return array List of types to check, in order of priority.
354 protected function get_parameter_order() {
358 $this->parse_json_params();
360 // Ensure we parse the body data.
361 $body = $this->get_body();
362 if ( $this->method !== 'POST' && ! empty( $body ) ) {
363 $this->parse_body_params();
366 $accepts_body_data = array( 'POST', 'PUT', 'PATCH' );
367 if ( in_array( $this->method, $accepts_body_data ) ) {
373 $order[] = 'defaults';
376 * Filters the parameter order.
378 * The order affects which parameters are checked when using get_param() and family.
379 * This acts similarly to PHP's `request_order` setting.
383 * @param array $order {
384 * An array of types to check, in order of priority.
386 * @param string $type The type to check.
388 * @param WP_REST_Request $this The request object.
390 return apply_filters( 'rest_request_parameter_order', $order, $this );
394 * Retrieves a parameter from the request.
399 * @param string $key Parameter name.
400 * @return mixed|null Value if set, null otherwise.
402 public function get_param( $key ) {
403 $order = $this->get_parameter_order();
405 foreach ( $order as $type ) {
406 // Determine if we have the parameter for this type.
407 if ( isset( $this->params[ $type ][ $key ] ) ) {
408 return $this->params[ $type ][ $key ];
416 * Sets a parameter on the request.
421 * @param string $key Parameter name.
422 * @param mixed $value Parameter value.
424 public function set_param( $key, $value ) {
425 switch ( $this->method ) {
427 $this->params['POST'][ $key ] = $value;
431 $this->params['GET'][ $key ] = $value;
437 * Retrieves merged parameters from the request.
439 * The equivalent of get_param(), but returns all parameters for the request.
440 * Handles merging all the available values into a single array.
445 * @return array Map of key to value.
447 public function get_params() {
448 $order = $this->get_parameter_order();
449 $order = array_reverse( $order, true );
452 foreach ( $order as $type ) {
453 $params = array_merge( $params, (array) $this->params[ $type ] );
460 * Retrieves parameters from the route itself.
462 * These are parsed from the URL using the regex.
467 * @return array Parameter map of key to value.
469 public function get_url_params() {
470 return $this->params['URL'];
474 * Sets parameters from the route.
476 * Typically, this is set after parsing the URL.
481 * @param array $params Parameter map of key to value.
483 public function set_url_params( $params ) {
484 $this->params['URL'] = $params;
488 * Retrieves parameters from the query string.
490 * These are the parameters you'd typically find in `$_GET`.
495 * @return array Parameter map of key to value
497 public function get_query_params() {
498 return $this->params['GET'];
502 * Sets parameters from the query string.
504 * Typically, this is set from `$_GET`.
509 * @param array $params Parameter map of key to value.
511 public function set_query_params( $params ) {
512 $this->params['GET'] = $params;
516 * Retrieves parameters from the body.
518 * These are the parameters you'd typically find in `$_POST`.
523 * @return array Parameter map of key to value.
525 public function get_body_params() {
526 return $this->params['POST'];
530 * Sets parameters from the body.
532 * Typically, this is set from `$_POST`.
537 * @param array $params Parameter map of key to value.
539 public function set_body_params( $params ) {
540 $this->params['POST'] = $params;
544 * Retrieves multipart file parameters from the body.
546 * These are the parameters you'd typically find in `$_FILES`.
551 * @return array Parameter map of key to value
553 public function get_file_params() {
554 return $this->params['FILES'];
558 * Sets multipart file parameters from the body.
560 * Typically, this is set from `$_FILES`.
565 * @param array $params Parameter map of key to value.
567 public function set_file_params( $params ) {
568 $this->params['FILES'] = $params;
572 * Retrieves the default parameters.
574 * These are the parameters set in the route registration.
579 * @return array Parameter map of key to value
581 public function get_default_params() {
582 return $this->params['defaults'];
586 * Sets default parameters.
588 * These are the parameters set in the route registration.
593 * @param array $params Parameter map of key to value.
595 public function set_default_params( $params ) {
596 $this->params['defaults'] = $params;
600 * Retrieves the request body content.
605 * @return string Binary data from the request body.
607 public function get_body() {
617 * @param string $data Binary data from the request body.
619 public function set_body( $data ) {
622 // Enable lazy parsing.
623 $this->parsed_json = false;
624 $this->parsed_body = false;
625 $this->params['JSON'] = null;
629 * Retrieves the parameters from a JSON-formatted body.
634 * @return array Parameter map of key to value.
636 public function get_json_params() {
637 // Ensure the parameters have been parsed out.
638 $this->parse_json_params();
640 return $this->params['JSON'];
644 * Parses the JSON parameters.
646 * Avoids parsing the JSON data until we need to access it.
651 protected function parse_json_params() {
652 if ( $this->parsed_json ) {
656 $this->parsed_json = true;
658 // Check that we actually got JSON.
659 $content_type = $this->get_content_type();
661 if ( empty( $content_type ) || 'application/json' !== $content_type['value'] ) {
665 $params = json_decode( $this->get_body(), true );
668 * Check for a parsing error.
670 * Note that due to WP's JSON compatibility functions, json_last_error
671 * might not be defined: https://core.trac.wordpress.org/ticket/27799
673 if ( null === $params && ( ! function_exists( 'json_last_error' ) || JSON_ERROR_NONE !== json_last_error() ) ) {
677 $this->params['JSON'] = $params;
681 * Parses the request body parameters.
683 * Parses out URL-encoded bodies for request methods that aren't supported
684 * natively by PHP. In PHP 5.x, only POST has these parsed automatically.
689 protected function parse_body_params() {
690 if ( $this->parsed_body ) {
694 $this->parsed_body = true;
697 * Check that we got URL-encoded. Treat a missing content-type as
698 * URL-encoded for maximum compatibility.
700 $content_type = $this->get_content_type();
702 if ( ! empty( $content_type ) && 'application/x-www-form-urlencoded' !== $content_type['value'] ) {
706 parse_str( $this->get_body(), $params );
709 * Amazingly, parse_str follows magic quote rules. Sigh.
711 * NOTE: Do not refactor to use `wp_unslash`.
713 if ( get_magic_quotes_gpc() ) {
714 $params = stripslashes_deep( $params );
718 * Add to the POST parameters stored internally. If a user has already
719 * set these manually (via `set_body_params`), don't override them.
721 $this->params['POST'] = array_merge( $params, $this->params['POST'] );
725 * Retrieves the route that matched the request.
730 * @return string Route matching regex.
732 public function get_route() {
737 * Sets the route that matched the request.
742 * @param string $route Route matching regex.
744 public function set_route( $route ) {
745 $this->route = $route;
749 * Retrieves the attributes for the request.
751 * These are the options for the route that was matched.
756 * @return array Attributes for the request.
758 public function get_attributes() {
759 return $this->attributes;
763 * Sets the attributes for the request.
768 * @param array $attributes Attributes for the request.
770 public function set_attributes( $attributes ) {
771 $this->attributes = $attributes;
775 * Sanitizes (where possible) the params on the request.
777 * This is primarily based off the sanitize_callback param on each registered
783 * @return true|null True if there are no parameters to sanitize, null otherwise.
785 public function sanitize_params() {
787 $attributes = $this->get_attributes();
789 // No arguments set, skip sanitizing.
790 if ( empty( $attributes['args'] ) ) {
794 $order = $this->get_parameter_order();
796 foreach ( $order as $type ) {
797 if ( empty( $this->params[ $type ] ) ) {
800 foreach ( $this->params[ $type ] as $key => $value ) {
801 // Check if this param has a sanitize_callback added.
802 if ( isset( $attributes['args'][ $key ] ) && ! empty( $attributes['args'][ $key ]['sanitize_callback'] ) ) {
803 $this->params[ $type ][ $key ] = call_user_func( $attributes['args'][ $key ]['sanitize_callback'], $value, $this, $key );
811 * Checks whether this request is valid according to its attributes.
816 * @return bool|WP_Error True if there are no parameters to validate or if all pass validation,
817 * WP_Error if required parameters are missing.
819 public function has_valid_params() {
821 $attributes = $this->get_attributes();
824 // No arguments set, skip validation.
825 if ( empty( $attributes['args'] ) ) {
829 foreach ( $attributes['args'] as $key => $arg ) {
831 $param = $this->get_param( $key );
832 if ( isset( $arg['required'] ) && true === $arg['required'] && null === $param ) {
837 if ( ! empty( $required ) ) {
838 return new WP_Error( 'rest_missing_callback_param', sprintf( __( 'Missing parameter(s): %s' ), implode( ', ', $required ) ), array( 'status' => 400, 'params' => $required ) );
842 * Check the validation callbacks for each registered arg.
844 * This is done after required checking as required checking is cheaper.
846 $invalid_params = array();
848 foreach ( $attributes['args'] as $key => $arg ) {
850 $param = $this->get_param( $key );
852 if ( null !== $param && ! empty( $arg['validate_callback'] ) ) {
853 $valid_check = call_user_func( $arg['validate_callback'], $param, $this, $key );
855 if ( false === $valid_check ) {
856 $invalid_params[ $key ] = __( 'Invalid parameter.' );
859 if ( is_wp_error( $valid_check ) ) {
860 $invalid_params[ $key ] = $valid_check->get_error_message();
865 if ( $invalid_params ) {
866 return new WP_Error( 'rest_invalid_param', sprintf( __( 'Invalid parameter(s): %s' ), implode( ', ', array_keys( $invalid_params ) ) ), array( 'status' => 400, 'params' => $invalid_params ) );
874 * Checks if a parameter is set.
879 * @param string $offset Parameter name.
880 * @return bool Whether the parameter is set.
882 public function offsetExists( $offset ) {
883 $order = $this->get_parameter_order();
885 foreach ( $order as $type ) {
886 if ( isset( $this->params[ $type ][ $offset ] ) ) {
895 * Retrieves a parameter from the request.
900 * @param string $offset Parameter name.
901 * @return mixed|null Value if set, null otherwise.
903 public function offsetGet( $offset ) {
904 return $this->get_param( $offset );
908 * Sets a parameter on the request.
913 * @param string $offset Parameter name.
914 * @param mixed $value Parameter value.
916 public function offsetSet( $offset, $value ) {
917 $this->set_param( $offset, $value );
921 * Removes a parameter from the request.
926 * @param string $offset Parameter name.
928 public function offsetUnset( $offset ) {
929 $order = $this->get_parameter_order();
931 // Remove the offset from every group.
932 foreach ( $order as $type ) {
933 unset( $this->params[ $type ][ $offset ] );
938 * Retrieves a WP_REST_Request object from a full URL.
944 * @param string $url URL with protocol, domain, path and query args.
945 * @return WP_REST_Request|false WP_REST_Request object on success, false on failure.
947 public static function from_url( $url ) {
948 $bits = parse_url( $url );
949 $query_params = array();
951 if ( ! empty( $bits['query'] ) ) {
952 wp_parse_str( $bits['query'], $query_params );
955 $api_root = rest_url();
956 if ( get_option( 'permalink_structure' ) && 0 === strpos( $url, $api_root ) ) {
957 // Pretty permalinks on, and URL is under the API root
958 $api_url_part = substr( $url, strlen( untrailingslashit( $api_root ) ) );
959 $route = parse_url( $api_url_part, PHP_URL_PATH );
960 } elseif ( ! empty( $query_params['rest_route'] ) ) {
961 // ?rest_route=... set directly
962 $route = $query_params['rest_route'];
963 unset( $query_params['rest_route'] );
967 if ( ! empty( $route ) ) {
968 $request = new WP_REST_Request( 'GET', $route );
969 $request->set_query_params( $query_params );
973 * Filters the request generated from a URL.
977 * @param WP_REST_Request|false $request Generated request object, or false if URL
978 * could not be parsed.
979 * @param string $url URL the request was generated from.
981 return apply_filters( 'rest_request_from_url', $request, $url );