ru-se.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

class-wp-rest-request.php (25992B)


      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  * 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.
     24  *
     25  * @since 4.4.0
     26  *
     27  * @link https://www.php.net/manual/en/class.arrayaccess.php
     28  */
     29 class WP_REST_Request implements ArrayAccess {
     30 
     31 	/**
     32 	 * HTTP method.
     33 	 *
     34 	 * @since 4.4.0
     35 	 * @var string
     36 	 */
     37 	protected $method = '';
     38 
     39 	/**
     40 	 * Parameters passed to the request.
     41 	 *
     42 	 * These typically come from the `$_GET`, `$_POST` and `$_FILES`
     43 	 * superglobals when being created from the global scope.
     44 	 *
     45 	 * @since 4.4.0
     46 	 * @var array Contains GET, POST and FILES keys mapping to arrays of data.
     47 	 */
     48 	protected $params;
     49 
     50 	/**
     51 	 * HTTP headers for the request.
     52 	 *
     53 	 * @since 4.4.0
     54 	 * @var array Map of key to value. Key is always lowercase, as per HTTP specification.
     55 	 */
     56 	protected $headers = array();
     57 
     58 	/**
     59 	 * Body data.
     60 	 *
     61 	 * @since 4.4.0
     62 	 * @var string Binary data from the request.
     63 	 */
     64 	protected $body = null;
     65 
     66 	/**
     67 	 * Route matched for the request.
     68 	 *
     69 	 * @since 4.4.0
     70 	 * @var string
     71 	 */
     72 	protected $route;
     73 
     74 	/**
     75 	 * Attributes (options) for the route that was matched.
     76 	 *
     77 	 * This is the options array used when the route was registered, typically
     78 	 * containing the callback as well as the valid methods for the route.
     79 	 *
     80 	 * @since 4.4.0
     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 	 * @var bool
     92 	 */
     93 	protected $parsed_json = false;
     94 
     95 	/**
     96 	 * Used to determine if the body data has been parsed yet.
     97 	 *
     98 	 * @since 4.4.0
     99 	 * @var bool
    100 	 */
    101 	protected $parsed_body = false;
    102 
    103 	/**
    104 	 * Constructor.
    105 	 *
    106 	 * @since 4.4.0
    107 	 *
    108 	 * @param string $method     Optional. Request method. Default empty.
    109 	 * @param string $route      Optional. Request route. Default empty.
    110 	 * @param array  $attributes Optional. Request attributes. Default empty array.
    111 	 */
    112 	public function __construct( $method = '', $route = '', $attributes = array() ) {
    113 		$this->params = array(
    114 			'URL'      => array(),
    115 			'GET'      => array(),
    116 			'POST'     => array(),
    117 			'FILES'    => array(),
    118 
    119 			// See parse_json_params.
    120 			'JSON'     => null,
    121 
    122 			'defaults' => array(),
    123 		);
    124 
    125 		$this->set_method( $method );
    126 		$this->set_route( $route );
    127 		$this->set_attributes( $attributes );
    128 	}
    129 
    130 	/**
    131 	 * Retrieves the HTTP method for the request.
    132 	 *
    133 	 * @since 4.4.0
    134 	 *
    135 	 * @return string HTTP method.
    136 	 */
    137 	public function get_method() {
    138 		return $this->method;
    139 	}
    140 
    141 	/**
    142 	 * Sets HTTP method for the request.
    143 	 *
    144 	 * @since 4.4.0
    145 	 *
    146 	 * @param string $method HTTP method.
    147 	 */
    148 	public function set_method( $method ) {
    149 		$this->method = strtoupper( $method );
    150 	}
    151 
    152 	/**
    153 	 * Retrieves all headers from the request.
    154 	 *
    155 	 * @since 4.4.0
    156 	 *
    157 	 * @return array Map of key to value. Key is always lowercase, as per HTTP specification.
    158 	 */
    159 	public function get_headers() {
    160 		return $this->headers;
    161 	}
    162 
    163 	/**
    164 	 * Canonicalizes the header name.
    165 	 *
    166 	 * Ensures that header names are always treated the same regardless of
    167 	 * source. Header names are always case insensitive.
    168 	 *
    169 	 * Note that we treat `-` (dashes) and `_` (underscores) as the same
    170 	 * character, as per header parsing rules in both Apache and nginx.
    171 	 *
    172 	 * @link https://stackoverflow.com/q/18185366
    173 	 * @link https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/#missing-disappearing-http-headers
    174 	 * @link https://nginx.org/en/docs/http/ngx_http_core_module.html#underscores_in_headers
    175 	 *
    176 	 * @since 4.4.0
    177 	 *
    178 	 * @param string $key Header name.
    179 	 * @return string Canonicalized name.
    180 	 */
    181 	public static function canonicalize_header_name( $key ) {
    182 		$key = strtolower( $key );
    183 		$key = str_replace( '-', '_', $key );
    184 
    185 		return $key;
    186 	}
    187 
    188 	/**
    189 	 * Retrieves the given header from the request.
    190 	 *
    191 	 * If the header has multiple values, they will be concatenated with a comma
    192 	 * as per the HTTP specification. Be aware that some non-compliant headers
    193 	 * (notably cookie headers) cannot be joined this way.
    194 	 *
    195 	 * @since 4.4.0
    196 	 *
    197 	 * @param string $key Header name, will be canonicalized to lowercase.
    198 	 * @return string|null String value if set, null otherwise.
    199 	 */
    200 	public function get_header( $key ) {
    201 		$key = $this->canonicalize_header_name( $key );
    202 
    203 		if ( ! isset( $this->headers[ $key ] ) ) {
    204 			return null;
    205 		}
    206 
    207 		return implode( ',', $this->headers[ $key ] );
    208 	}
    209 
    210 	/**
    211 	 * Retrieves header values from the request.
    212 	 *
    213 	 * @since 4.4.0
    214 	 *
    215 	 * @param string $key Header name, will be canonicalized to lowercase.
    216 	 * @return array|null List of string values if set, null otherwise.
    217 	 */
    218 	public function get_header_as_array( $key ) {
    219 		$key = $this->canonicalize_header_name( $key );
    220 
    221 		if ( ! isset( $this->headers[ $key ] ) ) {
    222 			return null;
    223 		}
    224 
    225 		return $this->headers[ $key ];
    226 	}
    227 
    228 	/**
    229 	 * Sets the header on request.
    230 	 *
    231 	 * @since 4.4.0
    232 	 *
    233 	 * @param string $key   Header name.
    234 	 * @param string $value Header value, or list of values.
    235 	 */
    236 	public function set_header( $key, $value ) {
    237 		$key   = $this->canonicalize_header_name( $key );
    238 		$value = (array) $value;
    239 
    240 		$this->headers[ $key ] = $value;
    241 	}
    242 
    243 	/**
    244 	 * Appends a header value for the given header.
    245 	 *
    246 	 * @since 4.4.0
    247 	 *
    248 	 * @param string $key   Header name.
    249 	 * @param string $value Header value, or list of values.
    250 	 */
    251 	public function add_header( $key, $value ) {
    252 		$key   = $this->canonicalize_header_name( $key );
    253 		$value = (array) $value;
    254 
    255 		if ( ! isset( $this->headers[ $key ] ) ) {
    256 			$this->headers[ $key ] = array();
    257 		}
    258 
    259 		$this->headers[ $key ] = array_merge( $this->headers[ $key ], $value );
    260 	}
    261 
    262 	/**
    263 	 * Removes all values for a header.
    264 	 *
    265 	 * @since 4.4.0
    266 	 *
    267 	 * @param string $key Header name.
    268 	 */
    269 	public function remove_header( $key ) {
    270 		$key = $this->canonicalize_header_name( $key );
    271 		unset( $this->headers[ $key ] );
    272 	}
    273 
    274 	/**
    275 	 * Sets headers on the request.
    276 	 *
    277 	 * @since 4.4.0
    278 	 *
    279 	 * @param array $headers  Map of header name to value.
    280 	 * @param bool  $override If true, replace the request's headers. Otherwise, merge with existing.
    281 	 */
    282 	public function set_headers( $headers, $override = true ) {
    283 		if ( true === $override ) {
    284 			$this->headers = array();
    285 		}
    286 
    287 		foreach ( $headers as $key => $value ) {
    288 			$this->set_header( $key, $value );
    289 		}
    290 	}
    291 
    292 	/**
    293 	 * Retrieves the content-type of the request.
    294 	 *
    295 	 * @since 4.4.0
    296 	 *
    297 	 * @return array|null Map containing 'value' and 'parameters' keys
    298 	 *                    or null when no valid content-type header was
    299 	 *                    available.
    300 	 */
    301 	public function get_content_type() {
    302 		$value = $this->get_header( 'content-type' );
    303 		if ( empty( $value ) ) {
    304 			return null;
    305 		}
    306 
    307 		$parameters = '';
    308 		if ( strpos( $value, ';' ) ) {
    309 			list( $value, $parameters ) = explode( ';', $value, 2 );
    310 		}
    311 
    312 		$value = strtolower( $value );
    313 		if ( false === strpos( $value, '/' ) ) {
    314 			return null;
    315 		}
    316 
    317 		// Parse type and subtype out.
    318 		list( $type, $subtype ) = explode( '/', $value, 2 );
    319 
    320 		$data = compact( 'value', 'type', 'subtype', 'parameters' );
    321 		$data = array_map( 'trim', $data );
    322 
    323 		return $data;
    324 	}
    325 
    326 	/**
    327 	 * Checks if the request has specified a JSON content-type.
    328 	 *
    329 	 * @since 5.6.0
    330 	 *
    331 	 * @return bool True if the content-type header is JSON.
    332 	 */
    333 	public function is_json_content_type() {
    334 		$content_type = $this->get_content_type();
    335 
    336 		return isset( $content_type['value'] ) && wp_is_json_media_type( $content_type['value'] );
    337 	}
    338 
    339 	/**
    340 	 * Retrieves the parameter priority order.
    341 	 *
    342 	 * Used when checking parameters in WP_REST_Request::get_param().
    343 	 *
    344 	 * @since 4.4.0
    345 	 *
    346 	 * @return string[] Array of types to check, in order of priority.
    347 	 */
    348 	protected function get_parameter_order() {
    349 		$order = array();
    350 
    351 		if ( $this->is_json_content_type() ) {
    352 			$order[] = 'JSON';
    353 		}
    354 
    355 		$this->parse_json_params();
    356 
    357 		// Ensure we parse the body data.
    358 		$body = $this->get_body();
    359 
    360 		if ( 'POST' !== $this->method && ! empty( $body ) ) {
    361 			$this->parse_body_params();
    362 		}
    363 
    364 		$accepts_body_data = array( 'POST', 'PUT', 'PATCH', 'DELETE' );
    365 		if ( in_array( $this->method, $accepts_body_data, true ) ) {
    366 			$order[] = 'POST';
    367 		}
    368 
    369 		$order[] = 'GET';
    370 		$order[] = 'URL';
    371 		$order[] = 'defaults';
    372 
    373 		/**
    374 		 * Filters the parameter priority order for a REST API request.
    375 		 *
    376 		 * The order affects which parameters are checked when using WP_REST_Request::get_param()
    377 		 * and family. This acts similarly to PHP's `request_order` setting.
    378 		 *
    379 		 * @since 4.4.0
    380 		 *
    381 		 * @param string[]        $order Array of types to check, in order of priority.
    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 	 *
    392 	 * @param string $key Parameter name.
    393 	 * @return mixed|null Value if set, null otherwise.
    394 	 */
    395 	public function get_param( $key ) {
    396 		$order = $this->get_parameter_order();
    397 
    398 		foreach ( $order as $type ) {
    399 			// Determine if we have the parameter for this type.
    400 			if ( isset( $this->params[ $type ][ $key ] ) ) {
    401 				return $this->params[ $type ][ $key ];
    402 			}
    403 		}
    404 
    405 		return null;
    406 	}
    407 
    408 	/**
    409 	 * Checks if a parameter exists in the request.
    410 	 *
    411 	 * This allows distinguishing between an omitted parameter,
    412 	 * and a parameter specifically set to null.
    413 	 *
    414 	 * @since 5.3.0
    415 	 *
    416 	 * @param string $key Parameter name.
    417 	 * @return bool True if a param exists for the given key.
    418 	 */
    419 	public function has_param( $key ) {
    420 		$order = $this->get_parameter_order();
    421 
    422 		foreach ( $order as $type ) {
    423 			if ( is_array( $this->params[ $type ] ) && array_key_exists( $key, $this->params[ $type ] ) ) {
    424 				return true;
    425 			}
    426 		}
    427 
    428 		return false;
    429 	}
    430 
    431 	/**
    432 	 * Sets a parameter on the request.
    433 	 *
    434 	 * If the given parameter key exists in any parameter type an update will take place,
    435 	 * otherwise a new param will be created in the first parameter type (respecting
    436 	 * get_parameter_order()).
    437 	 *
    438 	 * @since 4.4.0
    439 	 *
    440 	 * @param string $key   Parameter name.
    441 	 * @param mixed  $value Parameter value.
    442 	 */
    443 	public function set_param( $key, $value ) {
    444 		$order     = $this->get_parameter_order();
    445 		$found_key = false;
    446 
    447 		foreach ( $order as $type ) {
    448 			if ( 'defaults' !== $type && is_array( $this->params[ $type ] ) && array_key_exists( $key, $this->params[ $type ] ) ) {
    449 				$this->params[ $type ][ $key ] = $value;
    450 				$found_key                     = true;
    451 			}
    452 		}
    453 
    454 		if ( ! $found_key ) {
    455 			$this->params[ $order[0] ][ $key ] = $value;
    456 		}
    457 	}
    458 
    459 	/**
    460 	 * Retrieves merged parameters from the request.
    461 	 *
    462 	 * The equivalent of get_param(), but returns all parameters for the request.
    463 	 * Handles merging all the available values into a single array.
    464 	 *
    465 	 * @since 4.4.0
    466 	 *
    467 	 * @return array Map of key to value.
    468 	 */
    469 	public function get_params() {
    470 		$order = $this->get_parameter_order();
    471 		$order = array_reverse( $order, true );
    472 
    473 		$params = array();
    474 		foreach ( $order as $type ) {
    475 			// array_merge() / the "+" operator will mess up
    476 			// numeric keys, so instead do a manual foreach.
    477 			foreach ( (array) $this->params[ $type ] as $key => $value ) {
    478 				$params[ $key ] = $value;
    479 			}
    480 		}
    481 
    482 		return $params;
    483 	}
    484 
    485 	/**
    486 	 * Retrieves parameters from the route itself.
    487 	 *
    488 	 * These are parsed from the URL using the regex.
    489 	 *
    490 	 * @since 4.4.0
    491 	 *
    492 	 * @return array Parameter map of key to value.
    493 	 */
    494 	public function get_url_params() {
    495 		return $this->params['URL'];
    496 	}
    497 
    498 	/**
    499 	 * Sets parameters from the route.
    500 	 *
    501 	 * Typically, this is set after parsing the URL.
    502 	 *
    503 	 * @since 4.4.0
    504 	 *
    505 	 * @param array $params Parameter map of key to value.
    506 	 */
    507 	public function set_url_params( $params ) {
    508 		$this->params['URL'] = $params;
    509 	}
    510 
    511 	/**
    512 	 * Retrieves parameters from the query string.
    513 	 *
    514 	 * These are the parameters you'd typically find in `$_GET`.
    515 	 *
    516 	 * @since 4.4.0
    517 	 *
    518 	 * @return array Parameter map of key to value
    519 	 */
    520 	public function get_query_params() {
    521 		return $this->params['GET'];
    522 	}
    523 
    524 	/**
    525 	 * Sets parameters from the query string.
    526 	 *
    527 	 * Typically, this is set from `$_GET`.
    528 	 *
    529 	 * @since 4.4.0
    530 	 *
    531 	 * @param array $params Parameter map of key to value.
    532 	 */
    533 	public function set_query_params( $params ) {
    534 		$this->params['GET'] = $params;
    535 	}
    536 
    537 	/**
    538 	 * Retrieves parameters from the body.
    539 	 *
    540 	 * These are the parameters you'd typically find in `$_POST`.
    541 	 *
    542 	 * @since 4.4.0
    543 	 *
    544 	 * @return array Parameter map of key to value.
    545 	 */
    546 	public function get_body_params() {
    547 		return $this->params['POST'];
    548 	}
    549 
    550 	/**
    551 	 * Sets parameters from the body.
    552 	 *
    553 	 * Typically, this is set from `$_POST`.
    554 	 *
    555 	 * @since 4.4.0
    556 	 *
    557 	 * @param array $params Parameter map of key to value.
    558 	 */
    559 	public function set_body_params( $params ) {
    560 		$this->params['POST'] = $params;
    561 	}
    562 
    563 	/**
    564 	 * Retrieves multipart file parameters from the body.
    565 	 *
    566 	 * These are the parameters you'd typically find in `$_FILES`.
    567 	 *
    568 	 * @since 4.4.0
    569 	 *
    570 	 * @return array Parameter map of key to value
    571 	 */
    572 	public function get_file_params() {
    573 		return $this->params['FILES'];
    574 	}
    575 
    576 	/**
    577 	 * Sets multipart file parameters from the body.
    578 	 *
    579 	 * Typically, this is set from `$_FILES`.
    580 	 *
    581 	 * @since 4.4.0
    582 	 *
    583 	 * @param array $params Parameter map of key to value.
    584 	 */
    585 	public function set_file_params( $params ) {
    586 		$this->params['FILES'] = $params;
    587 	}
    588 
    589 	/**
    590 	 * Retrieves the default parameters.
    591 	 *
    592 	 * These are the parameters set in the route registration.
    593 	 *
    594 	 * @since 4.4.0
    595 	 *
    596 	 * @return array Parameter map of key to value
    597 	 */
    598 	public function get_default_params() {
    599 		return $this->params['defaults'];
    600 	}
    601 
    602 	/**
    603 	 * Sets default parameters.
    604 	 *
    605 	 * These are the parameters set in the route registration.
    606 	 *
    607 	 * @since 4.4.0
    608 	 *
    609 	 * @param array $params Parameter map of key to value.
    610 	 */
    611 	public function set_default_params( $params ) {
    612 		$this->params['defaults'] = $params;
    613 	}
    614 
    615 	/**
    616 	 * Retrieves the request body content.
    617 	 *
    618 	 * @since 4.4.0
    619 	 *
    620 	 * @return string Binary data from the request body.
    621 	 */
    622 	public function get_body() {
    623 		return $this->body;
    624 	}
    625 
    626 	/**
    627 	 * Sets body content.
    628 	 *
    629 	 * @since 4.4.0
    630 	 *
    631 	 * @param string $data Binary data from the request body.
    632 	 */
    633 	public function set_body( $data ) {
    634 		$this->body = $data;
    635 
    636 		// Enable lazy parsing.
    637 		$this->parsed_json    = false;
    638 		$this->parsed_body    = false;
    639 		$this->params['JSON'] = null;
    640 	}
    641 
    642 	/**
    643 	 * Retrieves the parameters from a JSON-formatted body.
    644 	 *
    645 	 * @since 4.4.0
    646 	 *
    647 	 * @return array Parameter map of key to value.
    648 	 */
    649 	public function get_json_params() {
    650 		// Ensure the parameters have been parsed out.
    651 		$this->parse_json_params();
    652 
    653 		return $this->params['JSON'];
    654 	}
    655 
    656 	/**
    657 	 * Parses the JSON parameters.
    658 	 *
    659 	 * Avoids parsing the JSON data until we need to access it.
    660 	 *
    661 	 * @since 4.4.0
    662 	 * @since 4.7.0 Returns error instance if value cannot be decoded.
    663 	 * @return true|WP_Error True if the JSON data was passed or no JSON data was provided, WP_Error if invalid JSON was passed.
    664 	 */
    665 	protected function parse_json_params() {
    666 		if ( $this->parsed_json ) {
    667 			return true;
    668 		}
    669 
    670 		$this->parsed_json = true;
    671 
    672 		// Check that we actually got JSON.
    673 		if ( ! $this->is_json_content_type() ) {
    674 			return true;
    675 		}
    676 
    677 		$body = $this->get_body();
    678 		if ( empty( $body ) ) {
    679 			return true;
    680 		}
    681 
    682 		$params = json_decode( $body, true );
    683 
    684 		/*
    685 		 * Check for a parsing error.
    686 		 */
    687 		if ( null === $params && JSON_ERROR_NONE !== json_last_error() ) {
    688 			// Ensure subsequent calls receive error instance.
    689 			$this->parsed_json = false;
    690 
    691 			$error_data = array(
    692 				'status'             => WP_Http::BAD_REQUEST,
    693 				'json_error_code'    => json_last_error(),
    694 				'json_error_message' => json_last_error_msg(),
    695 			);
    696 
    697 			return new WP_Error( 'rest_invalid_json', __( 'Invalid JSON body passed.' ), $error_data );
    698 		}
    699 
    700 		$this->params['JSON'] = $params;
    701 
    702 		return true;
    703 	}
    704 
    705 	/**
    706 	 * Parses the request body parameters.
    707 	 *
    708 	 * Parses out URL-encoded bodies for request methods that aren't supported
    709 	 * natively by PHP. In PHP 5.x, only POST has these parsed automatically.
    710 	 *
    711 	 * @since 4.4.0
    712 	 */
    713 	protected function parse_body_params() {
    714 		if ( $this->parsed_body ) {
    715 			return;
    716 		}
    717 
    718 		$this->parsed_body = true;
    719 
    720 		/*
    721 		 * Check that we got URL-encoded. Treat a missing content-type as
    722 		 * URL-encoded for maximum compatibility.
    723 		 */
    724 		$content_type = $this->get_content_type();
    725 
    726 		if ( ! empty( $content_type ) && 'application/x-www-form-urlencoded' !== $content_type['value'] ) {
    727 			return;
    728 		}
    729 
    730 		parse_str( $this->get_body(), $params );
    731 
    732 		/*
    733 		 * Add to the POST parameters stored internally. If a user has already
    734 		 * set these manually (via `set_body_params`), don't override them.
    735 		 */
    736 		$this->params['POST'] = array_merge( $params, $this->params['POST'] );
    737 	}
    738 
    739 	/**
    740 	 * Retrieves the route that matched the request.
    741 	 *
    742 	 * @since 4.4.0
    743 	 *
    744 	 * @return string Route matching regex.
    745 	 */
    746 	public function get_route() {
    747 		return $this->route;
    748 	}
    749 
    750 	/**
    751 	 * Sets the route that matched the request.
    752 	 *
    753 	 * @since 4.4.0
    754 	 *
    755 	 * @param string $route Route matching regex.
    756 	 */
    757 	public function set_route( $route ) {
    758 		$this->route = $route;
    759 	}
    760 
    761 	/**
    762 	 * Retrieves the attributes for the request.
    763 	 *
    764 	 * These are the options for the route that was matched.
    765 	 *
    766 	 * @since 4.4.0
    767 	 *
    768 	 * @return array Attributes for the request.
    769 	 */
    770 	public function get_attributes() {
    771 		return $this->attributes;
    772 	}
    773 
    774 	/**
    775 	 * Sets the attributes for the request.
    776 	 *
    777 	 * @since 4.4.0
    778 	 *
    779 	 * @param array $attributes Attributes for the request.
    780 	 */
    781 	public function set_attributes( $attributes ) {
    782 		$this->attributes = $attributes;
    783 	}
    784 
    785 	/**
    786 	 * Sanitizes (where possible) the params on the request.
    787 	 *
    788 	 * This is primarily based off the sanitize_callback param on each registered
    789 	 * argument.
    790 	 *
    791 	 * @since 4.4.0
    792 	 *
    793 	 * @return true|WP_Error True if parameters were sanitized, WP_Error if an error occurred during sanitization.
    794 	 */
    795 	public function sanitize_params() {
    796 		$attributes = $this->get_attributes();
    797 
    798 		// No arguments set, skip sanitizing.
    799 		if ( empty( $attributes['args'] ) ) {
    800 			return true;
    801 		}
    802 
    803 		$order = $this->get_parameter_order();
    804 
    805 		$invalid_params  = array();
    806 		$invalid_details = array();
    807 
    808 		foreach ( $order as $type ) {
    809 			if ( empty( $this->params[ $type ] ) ) {
    810 				continue;
    811 			}
    812 
    813 			foreach ( $this->params[ $type ] as $key => $value ) {
    814 				if ( ! isset( $attributes['args'][ $key ] ) ) {
    815 					continue;
    816 				}
    817 
    818 				$param_args = $attributes['args'][ $key ];
    819 
    820 				// If the arg has a type but no sanitize_callback attribute, default to rest_parse_request_arg.
    821 				if ( ! array_key_exists( 'sanitize_callback', $param_args ) && ! empty( $param_args['type'] ) ) {
    822 					$param_args['sanitize_callback'] = 'rest_parse_request_arg';
    823 				}
    824 				// If there's still no sanitize_callback, nothing to do here.
    825 				if ( empty( $param_args['sanitize_callback'] ) ) {
    826 					continue;
    827 				}
    828 
    829 				/** @var mixed|WP_Error $sanitized_value */
    830 				$sanitized_value = call_user_func( $param_args['sanitize_callback'], $value, $this, $key );
    831 
    832 				if ( is_wp_error( $sanitized_value ) ) {
    833 					$invalid_params[ $key ]  = implode( ' ', $sanitized_value->get_error_messages() );
    834 					$invalid_details[ $key ] = rest_convert_error_to_response( $sanitized_value )->get_data();
    835 				} else {
    836 					$this->params[ $type ][ $key ] = $sanitized_value;
    837 				}
    838 			}
    839 		}
    840 
    841 		if ( $invalid_params ) {
    842 			return new WP_Error(
    843 				'rest_invalid_param',
    844 				/* translators: %s: List of invalid parameters. */
    845 				sprintf( __( 'Invalid parameter(s): %s' ), implode( ', ', array_keys( $invalid_params ) ) ),
    846 				array(
    847 					'status'  => 400,
    848 					'params'  => $invalid_params,
    849 					'details' => $invalid_details,
    850 				)
    851 			);
    852 		}
    853 
    854 		return true;
    855 	}
    856 
    857 	/**
    858 	 * Checks whether this request is valid according to its attributes.
    859 	 *
    860 	 * @since 4.4.0
    861 	 *
    862 	 * @return true|WP_Error True if there are no parameters to validate or if all pass validation,
    863 	 *                       WP_Error if required parameters are missing.
    864 	 */
    865 	public function has_valid_params() {
    866 		// If JSON data was passed, check for errors.
    867 		$json_error = $this->parse_json_params();
    868 		if ( is_wp_error( $json_error ) ) {
    869 			return $json_error;
    870 		}
    871 
    872 		$attributes = $this->get_attributes();
    873 		$required   = array();
    874 
    875 		$args = empty( $attributes['args'] ) ? array() : $attributes['args'];
    876 
    877 		foreach ( $args as $key => $arg ) {
    878 			$param = $this->get_param( $key );
    879 			if ( isset( $arg['required'] ) && true === $arg['required'] && null === $param ) {
    880 				$required[] = $key;
    881 			}
    882 		}
    883 
    884 		if ( ! empty( $required ) ) {
    885 			return new WP_Error(
    886 				'rest_missing_callback_param',
    887 				/* translators: %s: List of required parameters. */
    888 				sprintf( __( 'Missing parameter(s): %s' ), implode( ', ', $required ) ),
    889 				array(
    890 					'status' => 400,
    891 					'params' => $required,
    892 				)
    893 			);
    894 		}
    895 
    896 		/*
    897 		 * Check the validation callbacks for each registered arg.
    898 		 *
    899 		 * This is done after required checking as required checking is cheaper.
    900 		 */
    901 		$invalid_params  = array();
    902 		$invalid_details = array();
    903 
    904 		foreach ( $args as $key => $arg ) {
    905 
    906 			$param = $this->get_param( $key );
    907 
    908 			if ( null !== $param && ! empty( $arg['validate_callback'] ) ) {
    909 				/** @var bool|\WP_Error $valid_check */
    910 				$valid_check = call_user_func( $arg['validate_callback'], $param, $this, $key );
    911 
    912 				if ( false === $valid_check ) {
    913 					$invalid_params[ $key ] = __( 'Invalid parameter.' );
    914 				}
    915 
    916 				if ( is_wp_error( $valid_check ) ) {
    917 					$invalid_params[ $key ]  = implode( ' ', $valid_check->get_error_messages() );
    918 					$invalid_details[ $key ] = rest_convert_error_to_response( $valid_check )->get_data();
    919 				}
    920 			}
    921 		}
    922 
    923 		if ( $invalid_params ) {
    924 			return new WP_Error(
    925 				'rest_invalid_param',
    926 				/* translators: %s: List of invalid parameters. */
    927 				sprintf( __( 'Invalid parameter(s): %s' ), implode( ', ', array_keys( $invalid_params ) ) ),
    928 				array(
    929 					'status'  => 400,
    930 					'params'  => $invalid_params,
    931 					'details' => $invalid_details,
    932 				)
    933 			);
    934 		}
    935 
    936 		if ( isset( $attributes['validate_callback'] ) ) {
    937 			$valid_check = call_user_func( $attributes['validate_callback'], $this );
    938 
    939 			if ( is_wp_error( $valid_check ) ) {
    940 				return $valid_check;
    941 			}
    942 
    943 			if ( false === $valid_check ) {
    944 				// A WP_Error instance is preferred, but false is supported for parity with the per-arg validate_callback.
    945 				return new WP_Error( 'rest_invalid_params', __( 'Invalid parameters.' ), array( 'status' => 400 ) );
    946 			}
    947 		}
    948 
    949 		return true;
    950 	}
    951 
    952 	/**
    953 	 * Checks if a parameter is set.
    954 	 *
    955 	 * @since 4.4.0
    956 	 *
    957 	 * @param string $offset Parameter name.
    958 	 * @return bool Whether the parameter is set.
    959 	 */
    960 	public function offsetExists( $offset ) {
    961 		$order = $this->get_parameter_order();
    962 
    963 		foreach ( $order as $type ) {
    964 			if ( isset( $this->params[ $type ][ $offset ] ) ) {
    965 				return true;
    966 			}
    967 		}
    968 
    969 		return false;
    970 	}
    971 
    972 	/**
    973 	 * Retrieves a parameter from the request.
    974 	 *
    975 	 * @since 4.4.0
    976 	 *
    977 	 * @param string $offset Parameter name.
    978 	 * @return mixed|null Value if set, null otherwise.
    979 	 */
    980 	public function offsetGet( $offset ) {
    981 		return $this->get_param( $offset );
    982 	}
    983 
    984 	/**
    985 	 * Sets a parameter on the request.
    986 	 *
    987 	 * @since 4.4.0
    988 	 *
    989 	 * @param string $offset Parameter name.
    990 	 * @param mixed  $value  Parameter value.
    991 	 */
    992 	public function offsetSet( $offset, $value ) {
    993 		$this->set_param( $offset, $value );
    994 	}
    995 
    996 	/**
    997 	 * Removes a parameter from the request.
    998 	 *
    999 	 * @since 4.4.0
   1000 	 *
   1001 	 * @param string $offset Parameter name.
   1002 	 */
   1003 	public function offsetUnset( $offset ) {
   1004 		$order = $this->get_parameter_order();
   1005 
   1006 		// Remove the offset from every group.
   1007 		foreach ( $order as $type ) {
   1008 			unset( $this->params[ $type ][ $offset ] );
   1009 		}
   1010 	}
   1011 
   1012 	/**
   1013 	 * Retrieves a WP_REST_Request object from a full URL.
   1014 	 *
   1015 	 * @since 4.5.0
   1016 	 *
   1017 	 * @param string $url URL with protocol, domain, path and query args.
   1018 	 * @return WP_REST_Request|false WP_REST_Request object on success, false on failure.
   1019 	 */
   1020 	public static function from_url( $url ) {
   1021 		$bits         = parse_url( $url );
   1022 		$query_params = array();
   1023 
   1024 		if ( ! empty( $bits['query'] ) ) {
   1025 			wp_parse_str( $bits['query'], $query_params );
   1026 		}
   1027 
   1028 		$api_root = rest_url();
   1029 		if ( get_option( 'permalink_structure' ) && 0 === strpos( $url, $api_root ) ) {
   1030 			// Pretty permalinks on, and URL is under the API root.
   1031 			$api_url_part = substr( $url, strlen( untrailingslashit( $api_root ) ) );
   1032 			$route        = parse_url( $api_url_part, PHP_URL_PATH );
   1033 		} elseif ( ! empty( $query_params['rest_route'] ) ) {
   1034 			// ?rest_route=... set directly.
   1035 			$route = $query_params['rest_route'];
   1036 			unset( $query_params['rest_route'] );
   1037 		}
   1038 
   1039 		$request = false;
   1040 		if ( ! empty( $route ) ) {
   1041 			$request = new WP_REST_Request( 'GET', $route );
   1042 			$request->set_query_params( $query_params );
   1043 		}
   1044 
   1045 		/**
   1046 		 * Filters the REST API request generated from a URL.
   1047 		 *
   1048 		 * @since 4.5.0
   1049 		 *
   1050 		 * @param WP_REST_Request|false $request Generated request object, or false if URL
   1051 		 *                                       could not be parsed.
   1052 		 * @param string                $url     URL the request was generated from.
   1053 		 */
   1054 		return apply_filters( 'rest_request_from_url', $request, $url );
   1055 	}
   1056 }