You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

604 rivejä
18 KiB

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