/includes/abstracts/abstract-wc-rest-crud-controller.php

  1. <?php 
  2. /** 
  3. * Abstract Rest CRUD Controller Class 
  4. * 
  5. * @author Automattic 
  6. * @category API 
  7. * @package WooCommerce/Abstracts 
  8. * @version 3.0.0 
  9. */ 
  10.  
  11. if ( ! defined( 'ABSPATH' ) ) { 
  12. exit; 
  13.  
  14. /** 
  15. * WC_REST_CRUD_Controller class. 
  16. * 
  17. * @extends WC_REST_Posts_Controller 
  18. */ 
  19. abstract class WC_REST_CRUD_Controller extends WC_REST_Posts_Controller { 
  20.  
  21. /** 
  22. * Endpoint namespace. 
  23. * 
  24. * @var string 
  25. */ 
  26. protected $namespace = 'wc/v2'; 
  27.  
  28. /** 
  29. * If object is hierarchical. 
  30. * 
  31. * @var bool 
  32. */ 
  33. protected $hierarchical = false; 
  34.  
  35. /** 
  36. * Get object. 
  37. * 
  38. * @param int $id Object ID. 
  39. * @return WP_Error|WC_Data 
  40. */ 
  41. protected function get_object( $id ) { 
  42. return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass.", 'woocommerce' ), __METHOD__ ), array( 'status' => 405 ) ); 
  43.  
  44. /** 
  45. * Check if a given request has access to read an item. 
  46. * 
  47. * @param WP_REST_Request $request Full details about the request. 
  48. * @return WP_Error|boolean 
  49. */ 
  50. public function get_item_permissions_check( $request ) { 
  51. $object = $this->get_object( (int) $request['id'] ); 
  52.  
  53. if ( $object && 0 !== $object->get_id() && ! wc_rest_check_post_permissions( $this->post_type, 'read', $object->get_id() ) ) { 
  54. return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); 
  55.  
  56. return true; 
  57.  
  58. /** 
  59. * Check if a given request has access to update an item. 
  60. * 
  61. * @param WP_REST_Request $request Full details about the request. 
  62. * @return WP_Error|boolean 
  63. */ 
  64. public function update_item_permissions_check( $request ) { 
  65. $object = $this->get_object( (int) $request['id'] ); 
  66.  
  67. if ( $object && 0 !== $object->get_id() && ! wc_rest_check_post_permissions( $this->post_type, 'edit', $object->get_id() ) ) { 
  68. return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); 
  69.  
  70. return true; 
  71.  
  72. /** 
  73. * Check if a given request has access to delete an item. 
  74. * 
  75. * @param WP_REST_Request $request Full details about the request. 
  76. * @return bool|WP_Error 
  77. */ 
  78. public function delete_item_permissions_check( $request ) { 
  79. $object = $this->get_object( (int) $request['id'] ); 
  80.  
  81. if ( $object && 0 !== $object->get_id() && ! wc_rest_check_post_permissions( $this->post_type, 'delete', $object->get_id() ) ) { 
  82. return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); 
  83.  
  84. return true; 
  85.  
  86. /** 
  87. * Get object permalink. 
  88. * 
  89. * @param int $id Object ID. 
  90. * @return string 
  91. */ 
  92. protected function get_permalink( $object ) { 
  93. return ''; 
  94.  
  95. /** 
  96. * Prepares the object for the REST response. 
  97. * 
  98. * @since 3.0.0 
  99. * @param WC_Data $object Object data. 
  100. * @param WP_REST_Request $request Request object. 
  101. * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure. 
  102. */ 
  103. protected function prepare_object_for_response( $object, $request ) { 
  104. return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass.", 'woocommerce' ), __METHOD__ ), array( 'status' => 405 ) ); 
  105.  
  106. /** 
  107. * Prepares one object for create or update operation. 
  108. * 
  109. * @since 3.0.0 
  110. * @param WP_REST_Request $request Request object. 
  111. * @param bool $creating If is creating a new object. 
  112. * @return WP_Error|WC_Data The prepared item, or WP_Error object on failure. 
  113. */ 
  114. protected function prepare_object_for_database( $request, $creating = false ) { 
  115. return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass.", 'woocommerce' ), __METHOD__ ), array( 'status' => 405 ) ); 
  116.  
  117. /** 
  118. * Get a single item. 
  119. * 
  120. * @param WP_REST_Request $request Full details about the request. 
  121. * @return WP_Error|WP_REST_Response 
  122. */ 
  123. public function get_item( $request ) { 
  124. $object = $this->get_object( (int) $request['id'] ); 
  125.  
  126. if ( ! $object || 0 === $object->get_id() ) { 
  127. return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) ); 
  128.  
  129. $data = $this->prepare_object_for_response( $object, $request ); 
  130. $response = rest_ensure_response( $data ); 
  131.  
  132. if ( $this->public ) { 
  133. $response->link_header( 'alternate', $this->get_permalink( $object ), array( 'type' => 'text/html' ) ); 
  134.  
  135. return $response; 
  136.  
  137. /** 
  138. * Save an object data. 
  139. * 
  140. * @since 3.0.0 
  141. * @param WP_REST_Request $request Full details about the request. 
  142. * @param bool $creating If is creating a new object. 
  143. * @return WC_Data|WP_Error 
  144. */ 
  145. protected function save_object( $request, $creating = false ) { 
  146. try { 
  147. $object = $this->prepare_object_for_database( $request, $creating ); 
  148.  
  149. if ( is_wp_error( $object ) ) { 
  150. return $object; 
  151.  
  152. $object->save(); 
  153.  
  154. return $this->get_object( $object->get_id() ); 
  155. } catch ( WC_Data_Exception $e ) { 
  156. return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); 
  157. } catch ( WC_REST_Exception $e ) { 
  158. return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); 
  159.  
  160. /** 
  161. * Create a single item. 
  162. * 
  163. * @param WP_REST_Request $request Full details about the request. 
  164. * @return WP_Error|WP_REST_Response 
  165. */ 
  166. public function create_item( $request ) { 
  167. if ( ! empty( $request['id'] ) ) { 
  168. /** translators: %s: post type */ 
  169. return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); 
  170.  
  171. $object = $this->save_object( $request, true ); 
  172.  
  173. if ( is_wp_error( $object ) ) { 
  174. return $object; 
  175.  
  176. $this->update_additional_fields_for_object( $object, $request ); 
  177.  
  178. /** 
  179. * Fires after a single object is created or updated via the REST API. 
  180. * 
  181. * @param WC_Data $object Inserted object. 
  182. * @param WP_REST_Request $request Request object. 
  183. * @param boolean $creating True when creating object, false when updating. 
  184. */ 
  185. do_action( "woocommerce_rest_insert_{$this->post_type}_object", $object, $request, true ); 
  186.  
  187. $request->set_param( 'context', 'edit' ); 
  188. $response = $this->prepare_object_for_response( $object, $request ); 
  189. $response = rest_ensure_response( $response ); 
  190. $response->set_status( 201 ); 
  191. $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ) ); 
  192.  
  193. return $response; 
  194.  
  195. /** 
  196. * Update a single post. 
  197. * 
  198. * @param WP_REST_Request $request Full details about the request. 
  199. * @return WP_Error|WP_REST_Response 
  200. */ 
  201. public function update_item( $request ) { 
  202. $object = $this->get_object( (int) $request['id'] ); 
  203.  
  204. if ( ! $object || 0 === $object->get_id() ) { 
  205. return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 400 ) ); 
  206.  
  207. $object = $this->save_object( $request, false ); 
  208.  
  209. if ( is_wp_error( $object ) ) { 
  210. return $object; 
  211.  
  212. $this->update_additional_fields_for_object( $object, $request ); 
  213.  
  214. /** 
  215. * Fires after a single object is created or updated via the REST API. 
  216. * 
  217. * @param WC_Data $object Inserted object. 
  218. * @param WP_REST_Request $request Request object. 
  219. * @param boolean $creating True when creating object, false when updating. 
  220. */ 
  221. do_action( "woocommerce_rest_insert_{$this->post_type}_object", $object, $request, false ); 
  222.  
  223. $request->set_param( 'context', 'edit' ); 
  224. $response = $this->prepare_object_for_response( $object, $request ); 
  225. return rest_ensure_response( $response ); 
  226.  
  227. /** 
  228. * Prepare objects query. 
  229. * 
  230. * @since 3.0.0 
  231. * @param WP_REST_Request $request Full details about the request. 
  232. * @return array 
  233. */ 
  234. protected function prepare_objects_query( $request ) { 
  235. $args = array(); 
  236. $args['offset'] = $request['offset']; 
  237. $args['order'] = $request['order']; 
  238. $args['orderby'] = $request['orderby']; 
  239. $args['paged'] = $request['page']; 
  240. $args['post__in'] = $request['include']; 
  241. $args['post__not_in'] = $request['exclude']; 
  242. $args['posts_per_page'] = $request['per_page']; 
  243. $args['name'] = $request['slug']; 
  244. $args['post_parent__in'] = $request['parent']; 
  245. $args['post_parent__not_in'] = $request['parent_exclude']; 
  246. $args['s'] = $request['search']; 
  247.  
  248. $args['date_query'] = array(); 
  249. // Set before into date query. Date query must be specified as an array of an array. 
  250. if ( isset( $request['before'] ) ) { 
  251. $args['date_query'][0]['before'] = $request['before']; 
  252.  
  253. // Set after into date query. Date query must be specified as an array of an array. 
  254. if ( isset( $request['after'] ) ) { 
  255. $args['date_query'][0]['after'] = $request['after']; 
  256.  
  257. // Force the post_type argument, since it's not a user input variable. 
  258. $args['post_type'] = $this->post_type; 
  259.  
  260. /** 
  261. * Filter the query arguments for a request. 
  262. * 
  263. * Enables adding extra arguments or setting defaults for a post 
  264. * collection request. 
  265. * 
  266. * @param array $args Key value array of query var to query value. 
  267. * @param WP_REST_Request $request The request used. 
  268. */ 
  269. $args = apply_filters( "woocommerce_rest_{$this->post_type}_object_query", $args, $request ); 
  270.  
  271. return $this->prepare_items_query( $args, $request ); 
  272.  
  273. /** 
  274. * Get objects. 
  275. * 
  276. * @since 3.0.0 
  277. * @param array $query_args Query args. 
  278. * @return array 
  279. */ 
  280. protected function get_objects( $query_args ) { 
  281. $query = new WP_Query(); 
  282. $result = $query->query( $query_args ); 
  283.  
  284. $total_posts = $query->found_posts; 
  285. if ( $total_posts < 1 ) { 
  286. // Out-of-bounds, run the query again without LIMIT for total count. 
  287. unset( $query_args['paged'] ); 
  288. $count_query = new WP_Query(); 
  289. $count_query->query( $query_args ); 
  290. $total_posts = $count_query->found_posts; 
  291.  
  292. return array( 
  293. 'objects' => array_map( array( $this, 'get_object' ), $result ),  
  294. 'total' => (int) $total_posts,  
  295. 'pages' => (int) ceil( $total_posts / (int) $query->query_vars['posts_per_page'] ),  
  296. ); 
  297.  
  298. /** 
  299. * Get a collection of posts. 
  300. * 
  301. * @param WP_REST_Request $request Full details about the request. 
  302. * @return WP_Error|WP_REST_Response 
  303. */ 
  304. public function get_items( $request ) { 
  305. $query_args = $this->prepare_objects_query( $request ); 
  306. $query_results = $this->get_objects( $query_args ); 
  307.  
  308. $objects = array(); 
  309. foreach ( $query_results['objects'] as $object ) { 
  310. if ( ! wc_rest_check_post_permissions( $this->post_type, 'read', $object->get_id() ) ) { 
  311. continue; 
  312.  
  313. $data = $this->prepare_object_for_response( $object, $request ); 
  314. $objects[] = $this->prepare_response_for_collection( $data ); 
  315.  
  316. $page = (int) $query_args['paged']; 
  317. $max_pages = $query_results['pages']; 
  318.  
  319. $response = rest_ensure_response( $objects ); 
  320. $response->header( 'X-WP-Total', $query_results['total'] ); 
  321. $response->header( 'X-WP-TotalPages', (int) $max_pages ); 
  322.  
  323. $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) ); 
  324.  
  325. if ( $page > 1 ) { 
  326. $prev_page = $page - 1; 
  327. if ( $prev_page > $max_pages ) { 
  328. $prev_page = $max_pages; 
  329. $prev_link = add_query_arg( 'page', $prev_page, $base ); 
  330. $response->link_header( 'prev', $prev_link ); 
  331. if ( $max_pages > $page ) { 
  332. $next_page = $page + 1; 
  333. $next_link = add_query_arg( 'page', $next_page, $base ); 
  334. $response->link_header( 'next', $next_link ); 
  335.  
  336. return $response; 
  337.  
  338. /** 
  339. * Delete a single item. 
  340. * 
  341. * @param WP_REST_Request $request Full details about the request. 
  342. * @return WP_REST_Response|WP_Error 
  343. */ 
  344. public function delete_item( $request ) { 
  345. $id = (int) $request['id']; 
  346. $force = (bool) $request['force']; 
  347. $object = $this->get_object( (int) $request['id'] ); 
  348. $result = false; 
  349.  
  350. if ( ! $object || 0 === $object->get_id() ) { 
  351. return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) ); 
  352.  
  353. $supports_trash = EMPTY_TRASH_DAYS > 0 && is_callable( array( $object, 'get_status' ) ); 
  354.  
  355. /** 
  356. * Filter whether an object is trashable. 
  357. * 
  358. * Return false to disable trash support for the object. 
  359. * 
  360. * @param boolean $supports_trash Whether the object type support trashing. 
  361. * @param WC_Data $object The object being considered for trashing support. 
  362. */ 
  363. $supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_object_trashable", $supports_trash, $object ); 
  364.  
  365. if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $object->get_id() ) ) { 
  366. /** translators: %s: post type */ 
  367. return new WP_Error( "woocommerce_rest_user_cannot_delete_{$this->post_type}", sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code() ) ); 
  368.  
  369. $request->set_param( 'context', 'edit' ); 
  370. $response = $this->prepare_object_for_response( $object, $request ); 
  371.  
  372. // If we're forcing, then delete permanently. 
  373. if ( $force ) { 
  374. $object->delete( true ); 
  375. $result = 0 === $object->get_id(); 
  376. } else { 
  377. // If we don't support trashing for this type, error out. 
  378. if ( ! $supports_trash ) { 
  379. /** translators: %s: post type */ 
  380. return new WP_Error( 'woocommerce_rest_trash_not_supported', sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), array( 'status' => 501 ) ); 
  381.  
  382. // Otherwise, only trash if we haven't already. 
  383. if ( is_callable( array( $object, 'get_status' ) ) ) { 
  384. if ( 'trash' === $object->get_status() ) { 
  385. /** translators: %s: post type */ 
  386. return new WP_Error( 'woocommerce_rest_already_trashed', sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 410 ) ); 
  387.  
  388. $object->delete(); 
  389. $result = 'trash' === $object->get_status(); 
  390.  
  391. if ( ! $result ) { 
  392. /** translators: %s: post type */ 
  393. return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 500 ) ); 
  394.  
  395. /** 
  396. * Fires after a single object is deleted or trashed via the REST API. 
  397. * 
  398. * @param WC_Data $object The deleted or trashed object. 
  399. * @param WP_REST_Response $response The response data. 
  400. * @param WP_REST_Request $request The request sent to the API. 
  401. */ 
  402. do_action( "woocommerce_rest_delete_{$this->post_type}_object", $object, $response, $request ); 
  403.  
  404. return $response; 
  405.  
  406. /** 
  407. * Prepare links for the request. 
  408. * 
  409. * @param WC_Data $object Object data. 
  410. * @param WP_REST_Request $request Request object. 
  411. * @return array Links for the given post. 
  412. */ 
  413. protected function prepare_links( $object, $request ) { 
  414. $links = array( 
  415. 'self' => array( 
  416. 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ),  
  417. ),  
  418. 'collection' => array( 
  419. 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),  
  420. ),  
  421. ); 
  422.  
  423. return $links; 
  424.  
  425. /** 
  426. * Get the query params for collections of attachments. 
  427. * 
  428. * @return array 
  429. */ 
  430. public function get_collection_params() { 
  431. $params = array(); 
  432. $params['context'] = $this->get_context_param(); 
  433. $params['context']['default'] = 'view'; 
  434.  
  435. $params['page'] = array( 
  436. 'description' => __( 'Current page of the collection.', 'woocommerce' ),  
  437. 'type' => 'integer',  
  438. 'default' => 1,  
  439. 'sanitize_callback' => 'absint',  
  440. 'validate_callback' => 'rest_validate_request_arg',  
  441. 'minimum' => 1,  
  442. ); 
  443. $params['per_page'] = array( 
  444. 'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),  
  445. 'type' => 'integer',  
  446. 'default' => 10,  
  447. 'minimum' => 1,  
  448. 'maximum' => 100,  
  449. 'sanitize_callback' => 'absint',  
  450. 'validate_callback' => 'rest_validate_request_arg',  
  451. ); 
  452. $params['search'] = array( 
  453. 'description' => __( 'Limit results to those matching a string.', 'woocommerce' ),  
  454. 'type' => 'string',  
  455. 'sanitize_callback' => 'sanitize_text_field',  
  456. 'validate_callback' => 'rest_validate_request_arg',  
  457. ); 
  458. $params['after'] = array( 
  459. 'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),  
  460. 'type' => 'string',  
  461. 'format' => 'date-time',  
  462. 'validate_callback' => 'rest_validate_request_arg',  
  463. ); 
  464. $params['before'] = array( 
  465. 'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ),  
  466. 'type' => 'string',  
  467. 'format' => 'date-time',  
  468. 'validate_callback' => 'rest_validate_request_arg',  
  469. ); 
  470. $params['exclude'] = array( 
  471. 'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ),  
  472. 'type' => 'array',  
  473. 'items' => array( 
  474. 'type' => 'integer',  
  475. ),  
  476. 'default' => array(),  
  477. 'sanitize_callback' => 'wp_parse_id_list',  
  478. ); 
  479. $params['include'] = array( 
  480. 'description' => __( 'Limit result set to specific ids.', 'woocommerce' ),  
  481. 'type' => 'array',  
  482. 'items' => array( 
  483. 'type' => 'integer',  
  484. ),  
  485. 'default' => array(),  
  486. 'sanitize_callback' => 'wp_parse_id_list',  
  487. ); 
  488. $params['offset'] = array( 
  489. 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ),  
  490. 'type' => 'integer',  
  491. 'sanitize_callback' => 'absint',  
  492. 'validate_callback' => 'rest_validate_request_arg',  
  493. ); 
  494. $params['order'] = array( 
  495. 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),  
  496. 'type' => 'string',  
  497. 'default' => 'desc',  
  498. 'enum' => array( 'asc', 'desc' ),  
  499. 'validate_callback' => 'rest_validate_request_arg',  
  500. ); 
  501. $params['orderby'] = array( 
  502. 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ),  
  503. 'type' => 'string',  
  504. 'default' => 'date',  
  505. 'enum' => array( 
  506. 'date',  
  507. 'id',  
  508. 'include',  
  509. 'title',  
  510. 'slug',  
  511. ),  
  512. 'validate_callback' => 'rest_validate_request_arg',  
  513. ); 
  514.  
  515. if ( $this->hierarchical ) { 
  516. $params['parent'] = array( 
  517. 'description' => __( 'Limit result set to those of particular parent IDs.', 'woocommerce' ),  
  518. 'type' => 'array',  
  519. 'items' => array( 
  520. 'type' => 'integer',  
  521. ),  
  522. 'sanitize_callback' => 'wp_parse_id_list',  
  523. 'default' => array(),  
  524. ); 
  525. $params['parent_exclude'] = array( 
  526. 'description' => __( 'Limit result set to all items except those of a particular parent ID.', 'woocommerce' ),  
  527. 'type' => 'array',  
  528. 'items' => array( 
  529. 'type' => 'integer',  
  530. ),  
  531. 'sanitize_callback' => 'wp_parse_id_list',  
  532. 'default' => array(),  
  533. ); 
  534.  
  535. return $params; 
.