Brak opisu

class-wp-rest-controller.php 18KB

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