class-wp-rest-controller.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672
  1. <?php
  2. /**
  3. * REST API: WP_REST_Controller class
  4. *
  5. * @package WordPress
  6. * @subpackage REST_API
  7. * @since 4.7.0
  8. */
  9. /**
  10. * Core base controller for managing and interacting with REST API items.
  11. *
  12. * @since 4.7.0
  13. */
  14. abstract class WP_REST_Controller {
  15. /**
  16. * The namespace of this controller's route.
  17. *
  18. * @since 4.7.0
  19. * @var string
  20. */
  21. protected $namespace;
  22. /**
  23. * The base of this controller's route.
  24. *
  25. * @since 4.7.0
  26. * @var string
  27. */
  28. protected $rest_base;
  29. /**
  30. * Cached results of get_item_schema.
  31. *
  32. * @since 5.3.0
  33. * @var array
  34. */
  35. protected $schema;
  36. /**
  37. * Registers the routes for the objects of the controller.
  38. *
  39. * @since 4.7.0
  40. */
  41. public function register_routes() {
  42. /* translators: %s: register_routes() */
  43. _doing_it_wrong( 'WP_REST_Controller::register_routes', sprintf( __( "Method '%s' must be overridden." ), __METHOD__ ), '4.7' );
  44. }
  45. /**
  46. * Checks if a given request has access to get items.
  47. *
  48. * @since 4.7.0
  49. *
  50. * @param WP_REST_Request $request Full data about the request.
  51. * @return WP_Error|bool True if the request has read access, WP_Error object otherwise.
  52. */
  53. public function get_items_permissions_check( $request ) {
  54. /* translators: %s: Method name. */
  55. return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
  56. }
  57. /**
  58. * Retrieves a collection of items.
  59. *
  60. * @since 4.7.0
  61. *
  62. * @param WP_REST_Request $request Full data about the request.
  63. * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
  64. */
  65. public function get_items( $request ) {
  66. /* translators: %s: Method name. */
  67. return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
  68. }
  69. /**
  70. * Checks if a given request has access to get a specific item.
  71. *
  72. * @since 4.7.0
  73. *
  74. * @param WP_REST_Request $request Full data about the request.
  75. * @return WP_Error|bool True if the request has read access for the item, WP_Error object otherwise.
  76. */
  77. public function get_item_permissions_check( $request ) {
  78. /* translators: %s: Method name. */
  79. return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
  80. }
  81. /**
  82. * Retrieves one item from the collection.
  83. *
  84. * @since 4.7.0
  85. *
  86. * @param WP_REST_Request $request Full data about the request.
  87. * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
  88. */
  89. public function get_item( $request ) {
  90. /* translators: %s: Method name. */
  91. return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
  92. }
  93. /**
  94. * Checks if a given request has access to create items.
  95. *
  96. * @since 4.7.0
  97. *
  98. * @param WP_REST_Request $request Full data about the request.
  99. * @return WP_Error|bool True if the request has access to create items, WP_Error object otherwise.
  100. */
  101. public function create_item_permissions_check( $request ) {
  102. /* translators: %s: Method name. */
  103. return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
  104. }
  105. /**
  106. * Creates one item from the collection.
  107. *
  108. * @since 4.7.0
  109. *
  110. * @param WP_REST_Request $request Full data about the request.
  111. * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
  112. */
  113. public function create_item( $request ) {
  114. /* translators: %s: Method name. */
  115. return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
  116. }
  117. /**
  118. * Checks if a given request has access to update a specific item.
  119. *
  120. * @since 4.7.0
  121. *
  122. * @param WP_REST_Request $request Full data about the request.
  123. * @return WP_Error|bool True if the request has access to update the item, WP_Error object otherwise.
  124. */
  125. public function update_item_permissions_check( $request ) {
  126. /* translators: %s: Method name. */
  127. return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
  128. }
  129. /**
  130. * Updates one item from the collection.
  131. *
  132. * @since 4.7.0
  133. *
  134. * @param WP_REST_Request $request Full data about the request.
  135. * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
  136. */
  137. public function update_item( $request ) {
  138. /* translators: %s: Method name. */
  139. return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
  140. }
  141. /**
  142. * Checks if a given request has access to delete a specific item.
  143. *
  144. * @since 4.7.0
  145. *
  146. * @param WP_REST_Request $request Full data about the request.
  147. * @return WP_Error|bool True if the request has access to delete the item, WP_Error object otherwise.
  148. */
  149. public function delete_item_permissions_check( $request ) {
  150. /* translators: %s: Method name. */
  151. return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
  152. }
  153. /**
  154. * Deletes one item from the collection.
  155. *
  156. * @since 4.7.0
  157. *
  158. * @param WP_REST_Request $request Full data about the request.
  159. * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
  160. */
  161. public function delete_item( $request ) {
  162. /* translators: %s: Method name. */
  163. return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
  164. }
  165. /**
  166. * Prepares one item for create or update operation.
  167. *
  168. * @since 4.7.0
  169. *
  170. * @param WP_REST_Request $request Request object.
  171. * @return WP_Error|object The prepared item, or WP_Error object on failure.
  172. */
  173. protected function prepare_item_for_database( $request ) {
  174. /* translators: %s: Method name. */
  175. return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
  176. }
  177. /**
  178. * Prepares the item for the REST response.
  179. *
  180. * @since 4.7.0
  181. *
  182. * @param mixed $item WordPress representation of the item.
  183. * @param WP_REST_Request $request Request object.
  184. * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
  185. */
  186. public function prepare_item_for_response( $item, $request ) {
  187. /* translators: %s: Method name. */
  188. return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
  189. }
  190. /**
  191. * Prepares a response for insertion into a collection.
  192. *
  193. * @since 4.7.0
  194. *
  195. * @param WP_REST_Response $response Response object.
  196. * @return array|mixed Response data, ready for insertion into collection data.
  197. */
  198. public function prepare_response_for_collection( $response ) {
  199. if ( ! ( $response instanceof WP_REST_Response ) ) {
  200. return $response;
  201. }
  202. $data = (array) $response->get_data();
  203. $server = rest_get_server();
  204. $links = $server::get_compact_response_links( $response );
  205. if ( ! empty( $links ) ) {
  206. $data['_links'] = $links;
  207. }
  208. return $data;
  209. }
  210. /**
  211. * Filters a response based on the context defined in the schema.
  212. *
  213. * @since 4.7.0
  214. *
  215. * @param array $data Response data to fiter.
  216. * @param string $context Context defined in the schema.
  217. * @return array Filtered response.
  218. */
  219. public function filter_response_by_context( $data, $context ) {
  220. $schema = $this->get_item_schema();
  221. foreach ( $data as $key => $value ) {
  222. if ( empty( $schema['properties'][ $key ] ) || empty( $schema['properties'][ $key ]['context'] ) ) {
  223. continue;
  224. }
  225. if ( ! in_array( $context, $schema['properties'][ $key ]['context'], true ) ) {
  226. unset( $data[ $key ] );
  227. continue;
  228. }
  229. if ( 'object' === $schema['properties'][ $key ]['type'] && ! empty( $schema['properties'][ $key ]['properties'] ) ) {
  230. foreach ( $schema['properties'][ $key ]['properties'] as $attribute => $details ) {
  231. if ( empty( $details['context'] ) ) {
  232. continue;
  233. }
  234. if ( ! in_array( $context, $details['context'], true ) ) {
  235. if ( isset( $data[ $key ][ $attribute ] ) ) {
  236. unset( $data[ $key ][ $attribute ] );
  237. }
  238. }
  239. }
  240. }
  241. }
  242. return $data;
  243. }
  244. /**
  245. * Retrieves the item's schema, conforming to JSON Schema.
  246. *
  247. * @since 4.7.0
  248. *
  249. * @return array Item schema data.
  250. */
  251. public function get_item_schema() {
  252. return $this->add_additional_fields_schema( array() );
  253. }
  254. /**
  255. * Retrieves the item's schema for display / public consumption purposes.
  256. *
  257. * @since 4.7.0
  258. *
  259. * @return array Public item schema data.
  260. */
  261. public function get_public_item_schema() {
  262. $schema = $this->get_item_schema();
  263. foreach ( $schema['properties'] as &$property ) {
  264. unset( $property['arg_options'] );
  265. }
  266. return $schema;
  267. }
  268. /**
  269. * Retrieves the query params for the collections.
  270. *
  271. * @since 4.7.0
  272. *
  273. * @return array Query parameters for the collection.
  274. */
  275. public function get_collection_params() {
  276. return array(
  277. 'context' => $this->get_context_param(),
  278. 'page' => array(
  279. 'description' => __( 'Current page of the collection.' ),
  280. 'type' => 'integer',
  281. 'default' => 1,
  282. 'sanitize_callback' => 'absint',
  283. 'validate_callback' => 'rest_validate_request_arg',
  284. 'minimum' => 1,
  285. ),
  286. 'per_page' => array(
  287. 'description' => __( 'Maximum number of items to be returned in result set.' ),
  288. 'type' => 'integer',
  289. 'default' => 10,
  290. 'minimum' => 1,
  291. 'maximum' => 100,
  292. 'sanitize_callback' => 'absint',
  293. 'validate_callback' => 'rest_validate_request_arg',
  294. ),
  295. 'search' => array(
  296. 'description' => __( 'Limit results to those matching a string.' ),
  297. 'type' => 'string',
  298. 'sanitize_callback' => 'sanitize_text_field',
  299. 'validate_callback' => 'rest_validate_request_arg',
  300. ),
  301. );
  302. }
  303. /**
  304. * Retrieves the magical context param.
  305. *
  306. * Ensures consistent descriptions between endpoints, and populates enum from schema.
  307. *
  308. * @since 4.7.0
  309. *
  310. * @param array $args Optional. Additional arguments for context parameter. Default empty array.
  311. * @return array Context parameter details.
  312. */
  313. public function get_context_param( $args = array() ) {
  314. $param_details = array(
  315. 'description' => __( 'Scope under which the request is made; determines fields present in response.' ),
  316. 'type' => 'string',
  317. 'sanitize_callback' => 'sanitize_key',
  318. 'validate_callback' => 'rest_validate_request_arg',
  319. );
  320. $schema = $this->get_item_schema();
  321. if ( empty( $schema['properties'] ) ) {
  322. return array_merge( $param_details, $args );
  323. }
  324. $contexts = array();
  325. foreach ( $schema['properties'] as $attributes ) {
  326. if ( ! empty( $attributes['context'] ) ) {
  327. $contexts = array_merge( $contexts, $attributes['context'] );
  328. }
  329. }
  330. if ( ! empty( $contexts ) ) {
  331. $param_details['enum'] = array_unique( $contexts );
  332. rsort( $param_details['enum'] );
  333. }
  334. return array_merge( $param_details, $args );
  335. }
  336. /**
  337. * Adds the values from additional fields to a data object.
  338. *
  339. * @since 4.7.0
  340. *
  341. * @param array $prepared Prepared response array.
  342. * @param WP_REST_Request $request Full details about the request.
  343. * @return array Modified data object with additional fields.
  344. */
  345. protected function add_additional_fields_to_object( $prepared, $request ) {
  346. $additional_fields = $this->get_additional_fields();
  347. $requested_fields = $this->get_fields_for_response( $request );
  348. foreach ( $additional_fields as $field_name => $field_options ) {
  349. if ( ! $field_options['get_callback'] ) {
  350. continue;
  351. }
  352. if ( ! in_array( $field_name, $requested_fields, true ) ) {
  353. continue;
  354. }
  355. $prepared[ $field_name ] = call_user_func( $field_options['get_callback'], $prepared, $field_name, $request, $this->get_object_type() );
  356. }
  357. return $prepared;
  358. }
  359. /**
  360. * Updates the values of additional fields added to a data object.
  361. *
  362. * @since 4.7.0
  363. *
  364. * @param object $object Data model like WP_Term or WP_Post.
  365. * @param WP_REST_Request $request Full details about the request.
  366. * @return bool|WP_Error True on success, WP_Error object if a field cannot be updated.
  367. */
  368. protected function update_additional_fields_for_object( $object, $request ) {
  369. $additional_fields = $this->get_additional_fields();
  370. foreach ( $additional_fields as $field_name => $field_options ) {
  371. if ( ! $field_options['update_callback'] ) {
  372. continue;
  373. }
  374. // Don't run the update callbacks if the data wasn't passed in the request.
  375. if ( ! isset( $request[ $field_name ] ) ) {
  376. continue;
  377. }
  378. $result = call_user_func( $field_options['update_callback'], $request[ $field_name ], $object, $field_name, $request, $this->get_object_type() );
  379. if ( is_wp_error( $result ) ) {
  380. return $result;
  381. }
  382. }
  383. return true;
  384. }
  385. /**
  386. * Adds the schema from additional fields to a schema array.
  387. *
  388. * The type of object is inferred from the passed schema.
  389. *
  390. * @since 4.7.0
  391. *
  392. * @param array $schema Schema array.
  393. * @return array Modified Schema array.
  394. */
  395. protected function add_additional_fields_schema( $schema ) {
  396. if ( empty( $schema['title'] ) ) {
  397. return $schema;
  398. }
  399. // Can't use $this->get_object_type otherwise we cause an inf loop.
  400. $object_type = $schema['title'];
  401. $additional_fields = $this->get_additional_fields( $object_type );
  402. foreach ( $additional_fields as $field_name => $field_options ) {
  403. if ( ! $field_options['schema'] ) {
  404. continue;
  405. }
  406. $schema['properties'][ $field_name ] = $field_options['schema'];
  407. }
  408. return $schema;
  409. }
  410. /**
  411. * Retrieves all of the registered additional fields for a given object-type.
  412. *
  413. * @since 4.7.0
  414. *
  415. * @param string $object_type Optional. The object type.
  416. * @return array Registered additional fields (if any), empty array if none or if the object type could
  417. * not be inferred.
  418. */
  419. protected function get_additional_fields( $object_type = null ) {
  420. if ( ! $object_type ) {
  421. $object_type = $this->get_object_type();
  422. }
  423. if ( ! $object_type ) {
  424. return array();
  425. }
  426. global $wp_rest_additional_fields;
  427. if ( ! $wp_rest_additional_fields || ! isset( $wp_rest_additional_fields[ $object_type ] ) ) {
  428. return array();
  429. }
  430. return $wp_rest_additional_fields[ $object_type ];
  431. }
  432. /**
  433. * Retrieves the object type this controller is responsible for managing.
  434. *
  435. * @since 4.7.0
  436. *
  437. * @return string Object type for the controller.
  438. */
  439. protected function get_object_type() {
  440. $schema = $this->get_item_schema();
  441. if ( ! $schema || ! isset( $schema['title'] ) ) {
  442. return null;
  443. }
  444. return $schema['title'];
  445. }
  446. /**
  447. * Gets an array of fields to be included on the response.
  448. *
  449. * Included fields are based on item schema and `_fields=` request argument.
  450. *
  451. * @since 4.9.6
  452. *
  453. * @param WP_REST_Request $request Full details about the request.
  454. * @return array Fields to be included in the response.
  455. */
  456. public function get_fields_for_response( $request ) {
  457. $schema = $this->get_item_schema();
  458. $properties = isset( $schema['properties'] ) ? $schema['properties'] : array();
  459. $additional_fields = $this->get_additional_fields();
  460. foreach ( $additional_fields as $field_name => $field_options ) {
  461. // For back-compat, include any field with an empty schema
  462. // because it won't be present in $this->get_item_schema().
  463. if ( is_null( $field_options['schema'] ) ) {
  464. $properties[ $field_name ] = $field_options;
  465. }
  466. }
  467. // Exclude fields that specify a different context than the request context.
  468. $context = $request['context'];
  469. if ( $context ) {
  470. foreach ( $properties as $name => $options ) {
  471. if ( ! empty( $options['context'] ) && ! in_array( $context, $options['context'], true ) ) {
  472. unset( $properties[ $name ] );
  473. }
  474. }
  475. }
  476. $fields = array_keys( $properties );
  477. if ( ! isset( $request['_fields'] ) ) {
  478. return $fields;
  479. }
  480. $requested_fields = wp_parse_list( $request['_fields'] );
  481. if ( 0 === count( $requested_fields ) ) {
  482. return $fields;
  483. }
  484. // Trim off outside whitespace from the comma delimited list.
  485. $requested_fields = array_map( 'trim', $requested_fields );
  486. // Always persist 'id', because it can be needed for add_additional_fields_to_object().
  487. if ( in_array( 'id', $fields, true ) ) {
  488. $requested_fields[] = 'id';
  489. }
  490. // Return the list of all requested fields which appear in the schema.
  491. return array_reduce(
  492. $requested_fields,
  493. function( $response_fields, $field ) use ( $fields ) {
  494. if ( in_array( $field, $fields, true ) ) {
  495. $response_fields[] = $field;
  496. return $response_fields;
  497. }
  498. // Check for nested fields if $field is not a direct match.
  499. $nested_fields = explode( '.', $field );
  500. // A nested field is included so long as its top-level property is
  501. // present in the schema.
  502. if ( in_array( $nested_fields[0], $fields, true ) ) {
  503. $response_fields[] = $field;
  504. }
  505. return $response_fields;
  506. },
  507. array()
  508. );
  509. }
  510. /**
  511. * Retrieves an array of endpoint arguments from the item schema for the controller.
  512. *
  513. * @since 4.7.0
  514. *
  515. * @param string $method Optional. HTTP method of the request. The arguments for `CREATABLE` requests are
  516. * checked for required values and may fall-back to a given default, this is not done
  517. * on `EDITABLE` requests. Default WP_REST_Server::CREATABLE.
  518. * @return array Endpoint arguments.
  519. */
  520. public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) {
  521. $schema = $this->get_item_schema();
  522. $schema_properties = ! empty( $schema['properties'] ) ? $schema['properties'] : array();
  523. $endpoint_args = array();
  524. foreach ( $schema_properties as $field_id => $params ) {
  525. // Arguments specified as `readonly` are not allowed to be set.
  526. if ( ! empty( $params['readonly'] ) ) {
  527. continue;
  528. }
  529. $endpoint_args[ $field_id ] = array(
  530. 'validate_callback' => 'rest_validate_request_arg',
  531. 'sanitize_callback' => 'rest_sanitize_request_arg',
  532. );
  533. if ( isset( $params['description'] ) ) {
  534. $endpoint_args[ $field_id ]['description'] = $params['description'];
  535. }
  536. if ( WP_REST_Server::CREATABLE === $method && isset( $params['default'] ) ) {
  537. $endpoint_args[ $field_id ]['default'] = $params['default'];
  538. }
  539. if ( WP_REST_Server::CREATABLE === $method && ! empty( $params['required'] ) ) {
  540. $endpoint_args[ $field_id ]['required'] = true;
  541. }
  542. foreach ( array( 'type', 'format', 'enum', 'items', 'properties', 'additionalProperties' ) as $schema_prop ) {
  543. if ( isset( $params[ $schema_prop ] ) ) {
  544. $endpoint_args[ $field_id ][ $schema_prop ] = $params[ $schema_prop ];
  545. }
  546. }
  547. // Merge in any options provided by the schema property.
  548. if ( isset( $params['arg_options'] ) ) {
  549. // Only use required / default from arg_options on CREATABLE endpoints.
  550. if ( WP_REST_Server::CREATABLE !== $method ) {
  551. $params['arg_options'] = array_diff_key(
  552. $params['arg_options'],
  553. array(
  554. 'required' => '',
  555. 'default' => '',
  556. )
  557. );
  558. }
  559. $endpoint_args[ $field_id ] = array_merge( $endpoint_args[ $field_id ], $params['arg_options'] );
  560. }
  561. }
  562. return $endpoint_args;
  563. }
  564. /**
  565. * Sanitizes the slug value.
  566. *
  567. * @since 4.7.0
  568. *
  569. * @internal We can't use sanitize_title() directly, as the second
  570. * parameter is the fallback title, which would end up being set to the
  571. * request object.
  572. *
  573. * @see https://github.com/WP-API/WP-API/issues/1585
  574. *
  575. * @todo Remove this in favour of https://core.trac.wordpress.org/ticket/34659
  576. *
  577. * @param string $slug Slug value passed in request.
  578. * @return string Sanitized value for the slug.
  579. */
  580. public function sanitize_slug( $slug ) {
  581. return sanitize_title( $slug );
  582. }
  583. }