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();
363 if ( 'POST' !== $this->method && ! empty( $body ) ) {
364 $this->parse_body_params();
367 $accepts_body_data = array( 'POST', 'PUT', 'PATCH' );
368 if ( in_array( $this->method, $accepts_body_data ) ) {
374 $order[] = 'defaults';
377 * Filters the parameter order.
379 * The order affects which parameters are checked when using get_param() and family.
380 * This acts similarly to PHP's `request_order` setting.
384 * @param array $order {
385 * An array of types to check, in order of priority.
387 * @param string $type The type to check.
389 * @param WP_REST_Request $this The request object.
391 return apply_filters( 'rest_request_parameter_order', $order, $this );
395 * Retrieves a parameter from the request.
400 * @param string $key Parameter name.
401 * @return mixed|null Value if set, null otherwise.
403 public function get_param( $key ) {
404 $order = $this->get_parameter_order();
406 foreach ( $order as $type ) {
407 // Determine if we have the parameter for this type.
408 if ( isset( $this->params[ $type ][ $key ] ) ) {
409 return $this->params[ $type ][ $key ];
417 * Sets a parameter on the request.
422 * @param string $key Parameter name.
423 * @param mixed $value Parameter value.
425 public function set_param( $key, $value ) {
426 switch ( $this->method ) {
428 $this->params['POST'][ $key ] = $value;
432 $this->params['GET'][ $key ] = $value;
438 * Retrieves merged parameters from the request.
440 * The equivalent of get_param(), but returns all parameters for the request.
441 * Handles merging all the available values into a single array.
446 * @return array Map of key to value.
448 public function get_params() {
449 $order = $this->get_parameter_order();
450 $order = array_reverse( $order, true );
453 foreach ( $order as $type ) {
454 // array_merge / the "+" operator will mess up
455 // numeric keys, so instead do a manual foreach.
456 foreach ( (array) $this->params[ $type ] as $key => $value ) {
457 $params[ $key ] = $value;
465 * Retrieves parameters from the route itself.
467 * These are parsed from the URL using the regex.
472 * @return array Parameter map of key to value.
474 public function get_url_params() {
475 return $this->params['URL'];
479 * Sets parameters from the route.
481 * Typically, this is set after parsing the URL.
486 * @param array $params Parameter map of key to value.
488 public function set_url_params( $params ) {
489 $this->params['URL'] = $params;
493 * Retrieves parameters from the query string.
495 * These are the parameters you'd typically find in `$_GET`.
500 * @return array Parameter map of key to value
502 public function get_query_params() {
503 return $this->params['GET'];
507 * Sets parameters from the query string.
509 * Typically, this is set from `$_GET`.
514 * @param array $params Parameter map of key to value.
516 public function set_query_params( $params ) {
517 $this->params['GET'] = $params;
521 * Retrieves parameters from the body.
523 * These are the parameters you'd typically find in `$_POST`.
528 * @return array Parameter map of key to value.
530 public function get_body_params() {
531 return $this->params['POST'];
535 * Sets parameters from the body.
537 * Typically, this is set from `$_POST`.
542 * @param array $params Parameter map of key to value.
544 public function set_body_params( $params ) {
545 $this->params['POST'] = $params;
549 * Retrieves multipart file parameters from the body.
551 * These are the parameters you'd typically find in `$_FILES`.
556 * @return array Parameter map of key to value
558 public function get_file_params() {
559 return $this->params['FILES'];
563 * Sets multipart file parameters from the body.
565 * Typically, this is set from `$_FILES`.
570 * @param array $params Parameter map of key to value.
572 public function set_file_params( $params ) {
573 $this->params['FILES'] = $params;
577 * Retrieves the default parameters.
579 * These are the parameters set in the route registration.
584 * @return array Parameter map of key to value
586 public function get_default_params() {
587 return $this->params['defaults'];
591 * Sets default parameters.
593 * These are the parameters set in the route registration.
598 * @param array $params Parameter map of key to value.
600 public function set_default_params( $params ) {
601 $this->params['defaults'] = $params;
605 * Retrieves the request body content.
610 * @return string Binary data from the request body.
612 public function get_body() {
622 * @param string $data Binary data from the request body.
624 public function set_body( $data ) {
627 // Enable lazy parsing.
628 $this->parsed_json = false;
629 $this->parsed_body = false;
630 $this->params['JSON'] = null;
634 * Retrieves the parameters from a JSON-formatted body.
639 * @return array Parameter map of key to value.
641 public function get_json_params() {
642 // Ensure the parameters have been parsed out.
643 $this->parse_json_params();
645 return $this->params['JSON'];
649 * Parses the JSON parameters.
651 * Avoids parsing the JSON data until we need to access it.
654 * @since 4.7.0 Returns error instance if value cannot be decoded.
656 * @return true|WP_Error True if the JSON data was passed or no JSON data was provided, WP_Error if invalid JSON was passed.
658 protected function parse_json_params() {
659 if ( $this->parsed_json ) {
663 $this->parsed_json = true;
665 // Check that we actually got JSON.
666 $content_type = $this->get_content_type();
668 if ( empty( $content_type ) || 'application/json' !== $content_type['value'] ) {
672 $params = json_decode( $this->get_body(), true );
675 * Check for a parsing error.
677 * Note that due to WP's JSON compatibility functions, json_last_error
678 * might not be defined: https://core.trac.wordpress.org/ticket/27799
680 if ( null === $params && ( ! function_exists( 'json_last_error' ) || JSON_ERROR_NONE !== json_last_error() ) ) {
681 // Ensure subsequent calls receive error instance.
682 $this->parsed_json = false;
685 'status' => WP_Http::BAD_REQUEST,
687 if ( function_exists( 'json_last_error' ) ) {
688 $error_data['json_error_code'] = json_last_error();
689 $error_data['json_error_message'] = json_last_error_msg();
692 return new WP_Error( 'rest_invalid_json', __( 'Invalid JSON body passed.' ), $error_data );
695 $this->params['JSON'] = $params;
700 * Parses the request body parameters.
702 * Parses out URL-encoded bodies for request methods that aren't supported
703 * natively by PHP. In PHP 5.x, only POST has these parsed automatically.
708 protected function parse_body_params() {
709 if ( $this->parsed_body ) {
713 $this->parsed_body = true;
716 * Check that we got URL-encoded. Treat a missing content-type as
717 * URL-encoded for maximum compatibility.
719 $content_type = $this->get_content_type();
721 if ( ! empty( $content_type ) && 'application/x-www-form-urlencoded' !== $content_type['value'] ) {
725 parse_str( $this->get_body(), $params );
728 * Amazingly, parse_str follows magic quote rules. Sigh.
730 * NOTE: Do not refactor to use `wp_unslash`.
732 if ( get_magic_quotes_gpc() ) {
733 $params = stripslashes_deep( $params );
737 * Add to the POST parameters stored internally. If a user has already
738 * set these manually (via `set_body_params`), don't override them.
740 $this->params['POST'] = array_merge( $params, $this->params['POST'] );
744 * Retrieves the route that matched the request.
749 * @return string Route matching regex.
751 public function get_route() {
756 * Sets the route that matched the request.
761 * @param string $route Route matching regex.
763 public function set_route( $route ) {
764 $this->route = $route;
768 * Retrieves the attributes for the request.
770 * These are the options for the route that was matched.
775 * @return array Attributes for the request.
777 public function get_attributes() {
778 return $this->attributes;
782 * Sets the attributes for the request.
787 * @param array $attributes Attributes for the request.
789 public function set_attributes( $attributes ) {
790 $this->attributes = $attributes;
794 * Sanitizes (where possible) the params on the request.
796 * This is primarily based off the sanitize_callback param on each registered
802 * @return true|WP_Error True if parameters were sanitized, WP_Error if an error occurred during sanitization.
804 public function sanitize_params() {
805 $attributes = $this->get_attributes();
807 // No arguments set, skip sanitizing.
808 if ( empty( $attributes['args'] ) ) {
812 $order = $this->get_parameter_order();
814 $invalid_params = array();
816 foreach ( $order as $type ) {
817 if ( empty( $this->params[ $type ] ) ) {
820 foreach ( $this->params[ $type ] as $key => $value ) {
821 // if no sanitize_callback was specified, default to rest_parse_request_arg
822 // if a type was specified in the args.
823 if ( ! isset( $attributes['args'][ $key ]['sanitize_callback'] ) && ! empty( $attributes['args'][ $key ]['type'] ) ) {
824 $attributes['args'][ $key ]['sanitize_callback'] = 'rest_parse_request_arg';
826 // Check if this param has a sanitize_callback added.
827 if ( ! isset( $attributes['args'][ $key ] ) || empty( $attributes['args'][ $key ]['sanitize_callback'] ) ) {
831 $sanitized_value = call_user_func( $attributes['args'][ $key ]['sanitize_callback'], $value, $this, $key );
833 if ( is_wp_error( $sanitized_value ) ) {
834 $invalid_params[ $key ] = $sanitized_value->get_error_message();
836 $this->params[ $type ][ $key ] = $sanitized_value;
841 if ( $invalid_params ) {
842 return new WP_Error( 'rest_invalid_param', sprintf( __( 'Invalid parameter(s): %s' ), implode( ', ', array_keys( $invalid_params ) ) ), array( 'status' => 400, 'params' => $invalid_params ) );
849 * Checks whether this request is valid according to its attributes.
854 * @return bool|WP_Error True if there are no parameters to validate or if all pass validation,
855 * WP_Error if required parameters are missing.
857 public function has_valid_params() {
858 // If JSON data was passed, check for errors.
859 $json_error = $this->parse_json_params();
860 if ( is_wp_error( $json_error ) ) {
864 $attributes = $this->get_attributes();
867 // No arguments set, skip validation.
868 if ( empty( $attributes['args'] ) ) {
872 foreach ( $attributes['args'] as $key => $arg ) {
874 $param = $this->get_param( $key );
875 if ( isset( $arg['required'] ) && true === $arg['required'] && null === $param ) {
880 if ( ! empty( $required ) ) {
881 return new WP_Error( 'rest_missing_callback_param', sprintf( __( 'Missing parameter(s): %s' ), implode( ', ', $required ) ), array( 'status' => 400, 'params' => $required ) );
885 * Check the validation callbacks for each registered arg.
887 * This is done after required checking as required checking is cheaper.
889 $invalid_params = array();
891 foreach ( $attributes['args'] as $key => $arg ) {
893 $param = $this->get_param( $key );
895 if ( null !== $param && ! empty( $arg['validate_callback'] ) ) {
896 $valid_check = call_user_func( $arg['validate_callback'], $param, $this, $key );
898 if ( false === $valid_check ) {
899 $invalid_params[ $key ] = __( 'Invalid parameter.' );
902 if ( is_wp_error( $valid_check ) ) {
903 $invalid_params[ $key ] = $valid_check->get_error_message();
908 if ( $invalid_params ) {
909 return new WP_Error( 'rest_invalid_param', sprintf( __( 'Invalid parameter(s): %s' ), implode( ', ', array_keys( $invalid_params ) ) ), array( 'status' => 400, 'params' => $invalid_params ) );
917 * Checks if a parameter is set.
922 * @param string $offset Parameter name.
923 * @return bool Whether the parameter is set.
925 public function offsetExists( $offset ) {
926 $order = $this->get_parameter_order();
928 foreach ( $order as $type ) {
929 if ( isset( $this->params[ $type ][ $offset ] ) ) {
938 * Retrieves a parameter from the request.
943 * @param string $offset Parameter name.
944 * @return mixed|null Value if set, null otherwise.
946 public function offsetGet( $offset ) {
947 return $this->get_param( $offset );
951 * Sets a parameter on the request.
956 * @param string $offset Parameter name.
957 * @param mixed $value Parameter value.
959 public function offsetSet( $offset, $value ) {
960 $this->set_param( $offset, $value );
964 * Removes a parameter from the request.
969 * @param string $offset Parameter name.
971 public function offsetUnset( $offset ) {
972 $order = $this->get_parameter_order();
974 // Remove the offset from every group.
975 foreach ( $order as $type ) {
976 unset( $this->params[ $type ][ $offset ] );
981 * Retrieves a WP_REST_Request object from a full URL.
987 * @param string $url URL with protocol, domain, path and query args.
988 * @return WP_REST_Request|false WP_REST_Request object on success, false on failure.
990 public static function from_url( $url ) {
991 $bits = parse_url( $url );
992 $query_params = array();
994 if ( ! empty( $bits['query'] ) ) {
995 wp_parse_str( $bits['query'], $query_params );
998 $api_root = rest_url();
999 if ( get_option( 'permalink_structure' ) && 0 === strpos( $url, $api_root ) ) {
1000 // Pretty permalinks on, and URL is under the API root.
1001 $api_url_part = substr( $url, strlen( untrailingslashit( $api_root ) ) );
1002 $route = parse_url( $api_url_part, PHP_URL_PATH );
1003 } elseif ( ! empty( $query_params['rest_route'] ) ) {
1004 // ?rest_route=... set directly
1005 $route = $query_params['rest_route'];
1006 unset( $query_params['rest_route'] );
1010 if ( ! empty( $route ) ) {
1011 $request = new WP_REST_Request( 'GET', $route );
1012 $request->set_query_params( $query_params );
1016 * Filters the request generated from a URL.
1020 * @param WP_REST_Request|false $request Generated request object, or false if URL
1021 * could not be parsed.
1022 * @param string $url URL the request was generated from.
1024 return apply_filters( 'rest_request_from_url', $request, $url );