class-wp-rest-request.php 26 KB

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