class-wp-rest-controller.php 18 KB

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