class-wp-rest-request.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983
  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. * @see ArrayAccess
  27. */
  28. class WP_REST_Request implements ArrayAccess {
  29. /**
  30. * HTTP method.
  31. *
  32. * @since 4.4.0
  33. * @var string
  34. */
  35. protected $method = '';
  36. /**
  37. * Parameters passed to the request.
  38. *
  39. * These typically come from the `$_GET`, `$_POST` and `$_FILES`
  40. * superglobals when being created from the global scope.
  41. *
  42. * @since 4.4.0
  43. * @var array Contains GET, POST and FILES keys mapping to arrays of data.
  44. */
  45. protected $params;
  46. /**
  47. * HTTP headers for the request.
  48. *
  49. * @since 4.4.0
  50. * @var array Map of key to value. Key is always lowercase, as per HTTP specification.
  51. */
  52. protected $headers = array();
  53. /**
  54. * Body data.
  55. *
  56. * @since 4.4.0
  57. * @var string Binary data from the request.
  58. */
  59. protected $body = null;
  60. /**
  61. * Route matched for the request.
  62. *
  63. * @since 4.4.0
  64. * @var string
  65. */
  66. protected $route;
  67. /**
  68. * Attributes (options) for the route that was matched.
  69. *
  70. * This is the options array used when the route was registered, typically
  71. * containing the callback as well as the valid methods for the route.
  72. *
  73. * @since 4.4.0
  74. * @var array Attributes for the request.
  75. */
  76. protected $attributes = array();
  77. /**
  78. * Used to determine if the JSON data has been parsed yet.
  79. *
  80. * Allows lazy-parsing of JSON data where possible.
  81. *
  82. * @since 4.4.0
  83. * @var bool
  84. */
  85. protected $parsed_json = false;
  86. /**
  87. * Used to determine if the body data has been parsed yet.
  88. *
  89. * @since 4.4.0
  90. * @var bool
  91. */
  92. protected $parsed_body = false;
  93. /**
  94. * Constructor.
  95. *
  96. * @since 4.4.0
  97. *
  98. * @param string $method Optional. Request method. Default empty.
  99. * @param string $route Optional. Request route. Default empty.
  100. * @param array $attributes Optional. Request attributes. Default empty array.
  101. */
  102. public function __construct( $method = '', $route = '', $attributes = array() ) {
  103. $this->params = array(
  104. 'URL' => array(),
  105. 'GET' => array(),
  106. 'POST' => array(),
  107. 'FILES' => array(),
  108. // See parse_json_params.
  109. 'JSON' => null,
  110. 'defaults' => array(),
  111. );
  112. $this->set_method( $method );
  113. $this->set_route( $route );
  114. $this->set_attributes( $attributes );
  115. }
  116. /**
  117. * Retrieves the HTTP method for the request.
  118. *
  119. * @since 4.4.0
  120. *
  121. * @return string HTTP method.
  122. */
  123. public function get_method() {
  124. return $this->method;
  125. }
  126. /**
  127. * Sets HTTP method for the request.
  128. *
  129. * @since 4.4.0
  130. *
  131. * @param string $method HTTP method.
  132. */
  133. public function set_method( $method ) {
  134. $this->method = strtoupper( $method );
  135. }
  136. /**
  137. * Retrieves all headers from the request.
  138. *
  139. * @since 4.4.0
  140. *
  141. * @return array Map of key to value. Key is always lowercase, as per HTTP specification.
  142. */
  143. public function get_headers() {
  144. return $this->headers;
  145. }
  146. /**
  147. * Canonicalizes the header name.
  148. *
  149. * Ensures that header names are always treated the same regardless of
  150. * source. Header names are always case insensitive.
  151. *
  152. * Note that we treat `-` (dashes) and `_` (underscores) as the same
  153. * character, as per header parsing rules in both Apache and nginx.
  154. *
  155. * @link https://stackoverflow.com/q/18185366
  156. * @link https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/#missing-disappearing-http-headers
  157. * @link https://nginx.org/en/docs/http/ngx_http_core_module.html#underscores_in_headers
  158. *
  159. * @since 4.4.0
  160. * @static
  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 Map containing 'value' and 'parameters' keys.
  266. */
  267. public function get_content_type() {
  268. $value = $this->get_header( 'content-type' );
  269. if ( empty( $value ) ) {
  270. return null;
  271. }
  272. $parameters = '';
  273. if ( strpos( $value, ';' ) ) {
  274. list( $value, $parameters ) = explode( ';', $value, 2 );
  275. }
  276. $value = strtolower( $value );
  277. if ( strpos( $value, '/' ) === false ) {
  278. return null;
  279. }
  280. // Parse type and subtype out.
  281. list( $type, $subtype ) = explode( '/', $value, 2 );
  282. $data = compact( 'value', 'type', 'subtype', 'parameters' );
  283. $data = array_map( 'trim', $data );
  284. return $data;
  285. }
  286. /**
  287. * Retrieves the parameter priority order.
  288. *
  289. * Used when checking parameters in get_param().
  290. *
  291. * @since 4.4.0
  292. *
  293. * @return array List of types to check, in order of priority.
  294. */
  295. protected function get_parameter_order() {
  296. $order = array();
  297. $content_type = $this->get_content_type();
  298. if ( $content_type['value'] === 'application/json' ) {
  299. $order[] = 'JSON';
  300. }
  301. $this->parse_json_params();
  302. // Ensure we parse the body data.
  303. $body = $this->get_body();
  304. if ( 'POST' !== $this->method && ! empty( $body ) ) {
  305. $this->parse_body_params();
  306. }
  307. $accepts_body_data = array( 'POST', 'PUT', 'PATCH', 'DELETE' );
  308. if ( in_array( $this->method, $accepts_body_data ) ) {
  309. $order[] = 'POST';
  310. }
  311. $order[] = 'GET';
  312. $order[] = 'URL';
  313. $order[] = 'defaults';
  314. /**
  315. * Filters the parameter order.
  316. *
  317. * The order affects which parameters are checked when using get_param() and family.
  318. * This acts similarly to PHP's `request_order` setting.
  319. *
  320. * @since 4.4.0
  321. *
  322. * @param array $order {
  323. * An array of types to check, in order of priority.
  324. *
  325. * @param string $type The type to check.
  326. * }
  327. * @param WP_REST_Request $this The request object.
  328. */
  329. return apply_filters( 'rest_request_parameter_order', $order, $this );
  330. }
  331. /**
  332. * Retrieves a parameter from the request.
  333. *
  334. * @since 4.4.0
  335. *
  336. * @param string $key Parameter name.
  337. * @return mixed|null Value if set, null otherwise.
  338. */
  339. public function get_param( $key ) {
  340. $order = $this->get_parameter_order();
  341. foreach ( $order as $type ) {
  342. // Determine if we have the parameter for this type.
  343. if ( isset( $this->params[ $type ][ $key ] ) ) {
  344. return $this->params[ $type ][ $key ];
  345. }
  346. }
  347. return null;
  348. }
  349. /**
  350. * Sets a parameter on the request.
  351. *
  352. * @since 4.4.0
  353. *
  354. * @param string $key Parameter name.
  355. * @param mixed $value Parameter value.
  356. */
  357. public function set_param( $key, $value ) {
  358. $order = $this->get_parameter_order();
  359. $this->params[ $order[0] ][ $key ] = $value;
  360. }
  361. /**
  362. * Retrieves merged parameters from the request.
  363. *
  364. * The equivalent of get_param(), but returns all parameters for the request.
  365. * Handles merging all the available values into a single array.
  366. *
  367. * @since 4.4.0
  368. *
  369. * @return array Map of key to value.
  370. */
  371. public function get_params() {
  372. $order = $this->get_parameter_order();
  373. $order = array_reverse( $order, true );
  374. $params = array();
  375. foreach ( $order as $type ) {
  376. // array_merge / the "+" operator will mess up
  377. // numeric keys, so instead do a manual foreach.
  378. foreach ( (array) $this->params[ $type ] as $key => $value ) {
  379. $params[ $key ] = $value;
  380. }
  381. }
  382. return $params;
  383. }
  384. /**
  385. * Retrieves parameters from the route itself.
  386. *
  387. * These are parsed from the URL using the regex.
  388. *
  389. * @since 4.4.0
  390. *
  391. * @return array Parameter map of key to value.
  392. */
  393. public function get_url_params() {
  394. return $this->params['URL'];
  395. }
  396. /**
  397. * Sets parameters from the route.
  398. *
  399. * Typically, this is set after parsing the URL.
  400. *
  401. * @since 4.4.0
  402. *
  403. * @param array $params Parameter map of key to value.
  404. */
  405. public function set_url_params( $params ) {
  406. $this->params['URL'] = $params;
  407. }
  408. /**
  409. * Retrieves parameters from the query string.
  410. *
  411. * These are the parameters you'd typically find in `$_GET`.
  412. *
  413. * @since 4.4.0
  414. *
  415. * @return array Parameter map of key to value
  416. */
  417. public function get_query_params() {
  418. return $this->params['GET'];
  419. }
  420. /**
  421. * Sets parameters from the query string.
  422. *
  423. * Typically, this is set from `$_GET`.
  424. *
  425. * @since 4.4.0
  426. *
  427. * @param array $params Parameter map of key to value.
  428. */
  429. public function set_query_params( $params ) {
  430. $this->params['GET'] = $params;
  431. }
  432. /**
  433. * Retrieves parameters from the body.
  434. *
  435. * These are the parameters you'd typically find in `$_POST`.
  436. *
  437. * @since 4.4.0
  438. *
  439. * @return array Parameter map of key to value.
  440. */
  441. public function get_body_params() {
  442. return $this->params['POST'];
  443. }
  444. /**
  445. * Sets parameters from the body.
  446. *
  447. * Typically, this is set from `$_POST`.
  448. *
  449. * @since 4.4.0
  450. *
  451. * @param array $params Parameter map of key to value.
  452. */
  453. public function set_body_params( $params ) {
  454. $this->params['POST'] = $params;
  455. }
  456. /**
  457. * Retrieves multipart file parameters from the body.
  458. *
  459. * These are the parameters you'd typically find in `$_FILES`.
  460. *
  461. * @since 4.4.0
  462. *
  463. * @return array Parameter map of key to value
  464. */
  465. public function get_file_params() {
  466. return $this->params['FILES'];
  467. }
  468. /**
  469. * Sets multipart file parameters from the body.
  470. *
  471. * Typically, this is set from `$_FILES`.
  472. *
  473. * @since 4.4.0
  474. *
  475. * @param array $params Parameter map of key to value.
  476. */
  477. public function set_file_params( $params ) {
  478. $this->params['FILES'] = $params;
  479. }
  480. /**
  481. * Retrieves the default parameters.
  482. *
  483. * These are the parameters set in the route registration.
  484. *
  485. * @since 4.4.0
  486. *
  487. * @return array Parameter map of key to value
  488. */
  489. public function get_default_params() {
  490. return $this->params['defaults'];
  491. }
  492. /**
  493. * Sets default parameters.
  494. *
  495. * These are the parameters set in the route registration.
  496. *
  497. * @since 4.4.0
  498. *
  499. * @param array $params Parameter map of key to value.
  500. */
  501. public function set_default_params( $params ) {
  502. $this->params['defaults'] = $params;
  503. }
  504. /**
  505. * Retrieves the request body content.
  506. *
  507. * @since 4.4.0
  508. *
  509. * @return string Binary data from the request body.
  510. */
  511. public function get_body() {
  512. return $this->body;
  513. }
  514. /**
  515. * Sets body content.
  516. *
  517. * @since 4.4.0
  518. *
  519. * @param string $data Binary data from the request body.
  520. */
  521. public function set_body( $data ) {
  522. $this->body = $data;
  523. // Enable lazy parsing.
  524. $this->parsed_json = false;
  525. $this->parsed_body = false;
  526. $this->params['JSON'] = null;
  527. }
  528. /**
  529. * Retrieves the parameters from a JSON-formatted body.
  530. *
  531. * @since 4.4.0
  532. *
  533. * @return array Parameter map of key to value.
  534. */
  535. public function get_json_params() {
  536. // Ensure the parameters have been parsed out.
  537. $this->parse_json_params();
  538. return $this->params['JSON'];
  539. }
  540. /**
  541. * Parses the JSON parameters.
  542. *
  543. * Avoids parsing the JSON data until we need to access it.
  544. *
  545. * @since 4.4.0
  546. * @since 4.7.0 Returns error instance if value cannot be decoded.
  547. * @return true|WP_Error True if the JSON data was passed or no JSON data was provided, WP_Error if invalid JSON was passed.
  548. */
  549. protected function parse_json_params() {
  550. if ( $this->parsed_json ) {
  551. return true;
  552. }
  553. $this->parsed_json = true;
  554. // Check that we actually got JSON.
  555. $content_type = $this->get_content_type();
  556. if ( empty( $content_type ) || 'application/json' !== $content_type['value'] ) {
  557. return true;
  558. }
  559. $body = $this->get_body();
  560. if ( empty( $body ) ) {
  561. return true;
  562. }
  563. $params = json_decode( $body, true );
  564. /*
  565. * Check for a parsing error.
  566. *
  567. * Note that due to WP's JSON compatibility functions, json_last_error
  568. * might not be defined: https://core.trac.wordpress.org/ticket/27799
  569. */
  570. if ( null === $params && ( ! function_exists( 'json_last_error' ) || JSON_ERROR_NONE !== json_last_error() ) ) {
  571. // Ensure subsequent calls receive error instance.
  572. $this->parsed_json = false;
  573. $error_data = array(
  574. 'status' => WP_Http::BAD_REQUEST,
  575. );
  576. if ( function_exists( 'json_last_error' ) ) {
  577. $error_data['json_error_code'] = json_last_error();
  578. $error_data['json_error_message'] = json_last_error_msg();
  579. }
  580. return new WP_Error( 'rest_invalid_json', __( 'Invalid JSON body passed.' ), $error_data );
  581. }
  582. $this->params['JSON'] = $params;
  583. return true;
  584. }
  585. /**
  586. * Parses the request body parameters.
  587. *
  588. * Parses out URL-encoded bodies for request methods that aren't supported
  589. * natively by PHP. In PHP 5.x, only POST has these parsed automatically.
  590. *
  591. * @since 4.4.0
  592. */
  593. protected function parse_body_params() {
  594. if ( $this->parsed_body ) {
  595. return;
  596. }
  597. $this->parsed_body = true;
  598. /*
  599. * Check that we got URL-encoded. Treat a missing content-type as
  600. * URL-encoded for maximum compatibility.
  601. */
  602. $content_type = $this->get_content_type();
  603. if ( ! empty( $content_type ) && 'application/x-www-form-urlencoded' !== $content_type['value'] ) {
  604. return;
  605. }
  606. parse_str( $this->get_body(), $params );
  607. /*
  608. * Amazingly, parse_str follows magic quote rules. Sigh.
  609. *
  610. * NOTE: Do not refactor to use `wp_unslash`.
  611. */
  612. if ( get_magic_quotes_gpc() ) {
  613. $params = stripslashes_deep( $params );
  614. }
  615. /*
  616. * Add to the POST parameters stored internally. If a user has already
  617. * set these manually (via `set_body_params`), don't override them.
  618. */
  619. $this->params['POST'] = array_merge( $params, $this->params['POST'] );
  620. }
  621. /**
  622. * Retrieves the route that matched the request.
  623. *
  624. * @since 4.4.0
  625. *
  626. * @return string Route matching regex.
  627. */
  628. public function get_route() {
  629. return $this->route;
  630. }
  631. /**
  632. * Sets the route that matched the request.
  633. *
  634. * @since 4.4.0
  635. *
  636. * @param string $route Route matching regex.
  637. */
  638. public function set_route( $route ) {
  639. $this->route = $route;
  640. }
  641. /**
  642. * Retrieves the attributes for the request.
  643. *
  644. * These are the options for the route that was matched.
  645. *
  646. * @since 4.4.0
  647. *
  648. * @return array Attributes for the request.
  649. */
  650. public function get_attributes() {
  651. return $this->attributes;
  652. }
  653. /**
  654. * Sets the attributes for the request.
  655. *
  656. * @since 4.4.0
  657. *
  658. * @param array $attributes Attributes for the request.
  659. */
  660. public function set_attributes( $attributes ) {
  661. $this->attributes = $attributes;
  662. }
  663. /**
  664. * Sanitizes (where possible) the params on the request.
  665. *
  666. * This is primarily based off the sanitize_callback param on each registered
  667. * argument.
  668. *
  669. * @since 4.4.0
  670. *
  671. * @return true|WP_Error True if parameters were sanitized, WP_Error if an error occurred during sanitization.
  672. */
  673. public function sanitize_params() {
  674. $attributes = $this->get_attributes();
  675. // No arguments set, skip sanitizing.
  676. if ( empty( $attributes['args'] ) ) {
  677. return true;
  678. }
  679. $order = $this->get_parameter_order();
  680. $invalid_params = array();
  681. foreach ( $order as $type ) {
  682. if ( empty( $this->params[ $type ] ) ) {
  683. continue;
  684. }
  685. foreach ( $this->params[ $type ] as $key => $value ) {
  686. if ( ! isset( $attributes['args'][ $key ] ) ) {
  687. continue;
  688. }
  689. $param_args = $attributes['args'][ $key ];
  690. // If the arg has a type but no sanitize_callback attribute, default to rest_parse_request_arg.
  691. if ( ! array_key_exists( 'sanitize_callback', $param_args ) && ! empty( $param_args['type'] ) ) {
  692. $param_args['sanitize_callback'] = 'rest_parse_request_arg';
  693. }
  694. // If there's still no sanitize_callback, nothing to do here.
  695. if ( empty( $param_args['sanitize_callback'] ) ) {
  696. continue;
  697. }
  698. $sanitized_value = call_user_func( $param_args['sanitize_callback'], $value, $this, $key );
  699. if ( is_wp_error( $sanitized_value ) ) {
  700. $invalid_params[ $key ] = $sanitized_value->get_error_message();
  701. } else {
  702. $this->params[ $type ][ $key ] = $sanitized_value;
  703. }
  704. }
  705. }
  706. if ( $invalid_params ) {
  707. return new WP_Error( 'rest_invalid_param', sprintf( __( 'Invalid parameter(s): %s' ), implode( ', ', array_keys( $invalid_params ) ) ), array( 'status' => 400, 'params' => $invalid_params ) );
  708. }
  709. return true;
  710. }
  711. /**
  712. * Checks whether this request is valid according to its attributes.
  713. *
  714. * @since 4.4.0
  715. *
  716. * @return bool|WP_Error True if there are no parameters to validate or if all pass validation,
  717. * WP_Error if required parameters are missing.
  718. */
  719. public function has_valid_params() {
  720. // If JSON data was passed, check for errors.
  721. $json_error = $this->parse_json_params();
  722. if ( is_wp_error( $json_error ) ) {
  723. return $json_error;
  724. }
  725. $attributes = $this->get_attributes();
  726. $required = array();
  727. // No arguments set, skip validation.
  728. if ( empty( $attributes['args'] ) ) {
  729. return true;
  730. }
  731. foreach ( $attributes['args'] as $key => $arg ) {
  732. $param = $this->get_param( $key );
  733. if ( isset( $arg['required'] ) && true === $arg['required'] && null === $param ) {
  734. $required[] = $key;
  735. }
  736. }
  737. if ( ! empty( $required ) ) {
  738. return new WP_Error( 'rest_missing_callback_param', sprintf( __( 'Missing parameter(s): %s' ), implode( ', ', $required ) ), array( 'status' => 400, 'params' => $required ) );
  739. }
  740. /*
  741. * Check the validation callbacks for each registered arg.
  742. *
  743. * This is done after required checking as required checking is cheaper.
  744. */
  745. $invalid_params = array();
  746. foreach ( $attributes['args'] as $key => $arg ) {
  747. $param = $this->get_param( $key );
  748. if ( null !== $param && ! empty( $arg['validate_callback'] ) ) {
  749. $valid_check = call_user_func( $arg['validate_callback'], $param, $this, $key );
  750. if ( false === $valid_check ) {
  751. $invalid_params[ $key ] = __( 'Invalid parameter.' );
  752. }
  753. if ( is_wp_error( $valid_check ) ) {
  754. $invalid_params[ $key ] = $valid_check->get_error_message();
  755. }
  756. }
  757. }
  758. if ( $invalid_params ) {
  759. return new WP_Error( 'rest_invalid_param', sprintf( __( 'Invalid parameter(s): %s' ), implode( ', ', array_keys( $invalid_params ) ) ), array( 'status' => 400, 'params' => $invalid_params ) );
  760. }
  761. return true;
  762. }
  763. /**
  764. * Checks if a parameter is set.
  765. *
  766. * @since 4.4.0
  767. *
  768. * @param string $offset Parameter name.
  769. * @return bool Whether the parameter is set.
  770. */
  771. public function offsetExists( $offset ) {
  772. $order = $this->get_parameter_order();
  773. foreach ( $order as $type ) {
  774. if ( isset( $this->params[ $type ][ $offset ] ) ) {
  775. return true;
  776. }
  777. }
  778. return false;
  779. }
  780. /**
  781. * Retrieves a parameter from the request.
  782. *
  783. * @since 4.4.0
  784. *
  785. * @param string $offset Parameter name.
  786. * @return mixed|null Value if set, null otherwise.
  787. */
  788. public function offsetGet( $offset ) {
  789. return $this->get_param( $offset );
  790. }
  791. /**
  792. * Sets a parameter on the request.
  793. *
  794. * @since 4.4.0
  795. *
  796. * @param string $offset Parameter name.
  797. * @param mixed $value Parameter value.
  798. */
  799. public function offsetSet( $offset, $value ) {
  800. $this->set_param( $offset, $value );
  801. }
  802. /**
  803. * Removes a parameter from the request.
  804. *
  805. * @since 4.4.0
  806. *
  807. * @param string $offset Parameter name.
  808. */
  809. public function offsetUnset( $offset ) {
  810. $order = $this->get_parameter_order();
  811. // Remove the offset from every group.
  812. foreach ( $order as $type ) {
  813. unset( $this->params[ $type ][ $offset ] );
  814. }
  815. }
  816. /**
  817. * Retrieves a WP_REST_Request object from a full URL.
  818. *
  819. * @static
  820. * @since 4.5.0
  821. *
  822. * @param string $url URL with protocol, domain, path and query args.
  823. * @return WP_REST_Request|false WP_REST_Request object on success, false on failure.
  824. */
  825. public static function from_url( $url ) {
  826. $bits = parse_url( $url );
  827. $query_params = array();
  828. if ( ! empty( $bits['query'] ) ) {
  829. wp_parse_str( $bits['query'], $query_params );
  830. }
  831. $api_root = rest_url();
  832. if ( get_option( 'permalink_structure' ) && 0 === strpos( $url, $api_root ) ) {
  833. // Pretty permalinks on, and URL is under the API root.
  834. $api_url_part = substr( $url, strlen( untrailingslashit( $api_root ) ) );
  835. $route = parse_url( $api_url_part, PHP_URL_PATH );
  836. } elseif ( ! empty( $query_params['rest_route'] ) ) {
  837. // ?rest_route=... set directly
  838. $route = $query_params['rest_route'];
  839. unset( $query_params['rest_route'] );
  840. }
  841. $request = false;
  842. if ( ! empty( $route ) ) {
  843. $request = new WP_REST_Request( 'GET', $route );
  844. $request->set_query_params( $query_params );
  845. }
  846. /**
  847. * Filters the request generated from a URL.
  848. *
  849. * @since 4.5.0
  850. *
  851. * @param WP_REST_Request|false $request Generated request object, or false if URL
  852. * could not be parsed.
  853. * @param string $url URL the request was generated from.
  854. */
  855. return apply_filters( 'rest_request_from_url', $request, $url );
  856. }
  857. }