class-wc-rest-product-attributes-controller.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  1. <?php
  2. /**
  3. * REST API Product Attributes controller
  4. *
  5. * Handles requests to the products/attributes endpoint.
  6. *
  7. * @author WooThemes
  8. * @category API
  9. * @package WooCommerce/API
  10. * @since 3.0.0
  11. */
  12. if ( ! defined( 'ABSPATH' ) ) {
  13. exit;
  14. }
  15. /**
  16. * REST API Product Attributes controller class.
  17. *
  18. * @package WooCommerce/API
  19. * @extends WC_REST_Controller
  20. */
  21. class WC_REST_Product_Attributes_V1_Controller extends WC_REST_Controller {
  22. /**
  23. * Endpoint namespace.
  24. *
  25. * @var string
  26. */
  27. protected $namespace = 'wc/v1';
  28. /**
  29. * Route base.
  30. *
  31. * @var string
  32. */
  33. protected $rest_base = 'products/attributes';
  34. /**
  35. * Attribute name.
  36. *
  37. * @var string
  38. */
  39. protected $attribute = '';
  40. /**
  41. * Register the routes for product attributes.
  42. */
  43. public function register_routes() {
  44. register_rest_route( $this->namespace, '/' . $this->rest_base, array(
  45. array(
  46. 'methods' => WP_REST_Server::READABLE,
  47. 'callback' => array( $this, 'get_items' ),
  48. 'permission_callback' => array( $this, 'get_items_permissions_check' ),
  49. 'args' => $this->get_collection_params(),
  50. ),
  51. array(
  52. 'methods' => WP_REST_Server::CREATABLE,
  53. 'callback' => array( $this, 'create_item' ),
  54. 'permission_callback' => array( $this, 'create_item_permissions_check' ),
  55. 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array(
  56. 'name' => array(
  57. 'description' => __( 'Name for the resource.', 'woocommerce' ),
  58. 'type' => 'string',
  59. 'required' => true,
  60. ),
  61. ) ),
  62. ),
  63. 'schema' => array( $this, 'get_public_item_schema' ),
  64. ));
  65. register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
  66. 'args' => array(
  67. 'id' => array(
  68. 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
  69. 'type' => 'integer',
  70. ),
  71. ),
  72. array(
  73. 'methods' => WP_REST_Server::READABLE,
  74. 'callback' => array( $this, 'get_item' ),
  75. 'permission_callback' => array( $this, 'get_item_permissions_check' ),
  76. 'args' => array(
  77. 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
  78. ),
  79. ),
  80. array(
  81. 'methods' => WP_REST_Server::EDITABLE,
  82. 'callback' => array( $this, 'update_item' ),
  83. 'permission_callback' => array( $this, 'update_item_permissions_check' ),
  84. 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
  85. ),
  86. array(
  87. 'methods' => WP_REST_Server::DELETABLE,
  88. 'callback' => array( $this, 'delete_item' ),
  89. 'permission_callback' => array( $this, 'delete_item_permissions_check' ),
  90. 'args' => array(
  91. 'force' => array(
  92. 'default' => true,
  93. 'type' => 'boolean',
  94. 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ),
  95. ),
  96. ),
  97. ),
  98. 'schema' => array( $this, 'get_public_item_schema' ),
  99. ) );
  100. register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array(
  101. array(
  102. 'methods' => WP_REST_Server::EDITABLE,
  103. 'callback' => array( $this, 'batch_items' ),
  104. 'permission_callback' => array( $this, 'batch_items_permissions_check' ),
  105. 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
  106. ),
  107. 'schema' => array( $this, 'get_public_batch_schema' ),
  108. ) );
  109. }
  110. /**
  111. * Check if a given request has access to read the attributes.
  112. *
  113. * @param WP_REST_Request $request Full details about the request.
  114. * @return WP_Error|boolean
  115. */
  116. public function get_items_permissions_check( $request ) {
  117. if ( ! wc_rest_check_manager_permissions( 'attributes', 'read' ) ) {
  118. return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
  119. }
  120. return true;
  121. }
  122. /**
  123. * Check if a given request has access to create a attribute.
  124. *
  125. * @param WP_REST_Request $request Full details about the request.
  126. * @return WP_Error|boolean
  127. */
  128. public function create_item_permissions_check( $request ) {
  129. if ( ! wc_rest_check_manager_permissions( 'attributes', 'create' ) ) {
  130. return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you cannot create new resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
  131. }
  132. return true;
  133. }
  134. /**
  135. * Check if a given request has access to read a attribute.
  136. *
  137. * @param WP_REST_Request $request Full details about the request.
  138. * @return WP_Error|boolean
  139. */
  140. public function get_item_permissions_check( $request ) {
  141. if ( ! $this->get_taxonomy( $request ) ) {
  142. return new WP_Error( 'woocommerce_rest_taxonomy_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) );
  143. }
  144. if ( ! wc_rest_check_manager_permissions( 'attributes', 'read' ) ) {
  145. return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
  146. }
  147. return true;
  148. }
  149. /**
  150. * Check if a given request has access to update a attribute.
  151. *
  152. * @param WP_REST_Request $request Full details about the request.
  153. * @return WP_Error|boolean
  154. */
  155. public function update_item_permissions_check( $request ) {
  156. if ( ! $this->get_taxonomy( $request ) ) {
  157. return new WP_Error( 'woocommerce_rest_taxonomy_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) );
  158. }
  159. if ( ! wc_rest_check_manager_permissions( 'attributes', 'edit' ) ) {
  160. return new WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot update resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
  161. }
  162. return true;
  163. }
  164. /**
  165. * Check if a given request has access to delete a attribute.
  166. *
  167. * @param WP_REST_Request $request Full details about the request.
  168. * @return WP_Error|boolean
  169. */
  170. public function delete_item_permissions_check( $request ) {
  171. if ( ! $this->get_taxonomy( $request ) ) {
  172. return new WP_Error( 'woocommerce_rest_taxonomy_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) );
  173. }
  174. if ( ! wc_rest_check_manager_permissions( 'attributes', 'delete' ) ) {
  175. return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
  176. }
  177. return true;
  178. }
  179. /**
  180. * Check if a given request has access batch create, update and delete items.
  181. *
  182. * @param WP_REST_Request $request Full details about the request.
  183. *
  184. * @return bool|WP_Error
  185. */
  186. public function batch_items_permissions_check( $request ) {
  187. if ( ! wc_rest_check_manager_permissions( 'attributes', 'batch' ) ) {
  188. return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
  189. }
  190. return true;
  191. }
  192. /**
  193. * Get all attributes.
  194. *
  195. * @param WP_REST_Request $request
  196. * @return array
  197. */
  198. public function get_items( $request ) {
  199. $attributes = wc_get_attribute_taxonomies();
  200. $data = array();
  201. foreach ( $attributes as $attribute_obj ) {
  202. $attribute = $this->prepare_item_for_response( $attribute_obj, $request );
  203. $attribute = $this->prepare_response_for_collection( $attribute );
  204. $data[] = $attribute;
  205. }
  206. return rest_ensure_response( $data );
  207. }
  208. /**
  209. * Create a single attribute.
  210. *
  211. * @param WP_REST_Request $request Full details about the request.
  212. * @return WP_REST_Request|WP_Error
  213. */
  214. public function create_item( $request ) {
  215. global $wpdb;
  216. $id = wc_create_attribute( array(
  217. 'name' => $request['name'],
  218. 'slug' => wc_sanitize_taxonomy_name( stripslashes( $request['slug'] ) ),
  219. 'type' => ! empty( $request['type'] ) ? $request['type'] : 'select',
  220. 'order_by' => ! empty( $request['order_by'] ) ? $request['order_by'] : 'menu_order',
  221. 'has_archives' => true === $request['has_archives'],
  222. ) );
  223. // Checks for errors.
  224. if ( is_wp_error( $id ) ) {
  225. return new WP_Error( 'woocommerce_rest_cannot_create', $id->get_error_message(), array( 'status' => 400 ) );
  226. }
  227. $attribute = $this->get_attribute( $id );
  228. if ( is_wp_error( $attribute ) ) {
  229. return $attribute;
  230. }
  231. $this->update_additional_fields_for_object( $attribute, $request );
  232. /**
  233. * Fires after a single product attribute is created or updated via the REST API.
  234. *
  235. * @param stdObject $attribute Inserted attribute object.
  236. * @param WP_REST_Request $request Request object.
  237. * @param boolean $creating True when creating attribute, false when updating.
  238. */
  239. do_action( 'woocommerce_rest_insert_product_attribute', $attribute, $request, true );
  240. $request->set_param( 'context', 'edit' );
  241. $response = $this->prepare_item_for_response( $attribute, $request );
  242. $response = rest_ensure_response( $response );
  243. $response->set_status( 201 );
  244. $response->header( 'Location', rest_url( '/' . $this->namespace . '/' . $this->rest_base . '/' . $attribute->attribute_id ) );
  245. return $response;
  246. }
  247. /**
  248. * Get a single attribute.
  249. *
  250. * @param WP_REST_Request $request Full details about the request.
  251. * @return WP_REST_Request|WP_Error
  252. */
  253. public function get_item( $request ) {
  254. $attribute = $this->get_attribute( (int) $request['id'] );
  255. if ( is_wp_error( $attribute ) ) {
  256. return $attribute;
  257. }
  258. $response = $this->prepare_item_for_response( $attribute, $request );
  259. return rest_ensure_response( $response );
  260. }
  261. /**
  262. * Update a single term from a taxonomy.
  263. *
  264. * @param WP_REST_Request $request Full details about the request.
  265. * @return WP_REST_Request|WP_Error
  266. */
  267. public function update_item( $request ) {
  268. global $wpdb;
  269. $id = (int) $request['id'];
  270. $edited = wc_update_attribute( $id, array(
  271. 'name' => $request['name'],
  272. 'slug' => wc_sanitize_taxonomy_name( stripslashes( $request['slug'] ) ),
  273. 'type' => $request['type'],
  274. 'order_by' => $request['order_by'],
  275. 'has_archives' => $request['has_archives'],
  276. ) );
  277. // Checks for errors.
  278. if ( is_wp_error( $edited ) ) {
  279. return new WP_Error( 'woocommerce_rest_cannot_edit', $edited->get_error_message(), array( 'status' => 400 ) );
  280. }
  281. $attribute = $this->get_attribute( $id );
  282. if ( is_wp_error( $attribute ) ) {
  283. return $attribute;
  284. }
  285. $this->update_additional_fields_for_object( $attribute, $request );
  286. /**
  287. * Fires after a single product attribute is created or updated via the REST API.
  288. *
  289. * @param stdObject $attribute Inserted attribute object.
  290. * @param WP_REST_Request $request Request object.
  291. * @param boolean $creating True when creating attribute, false when updating.
  292. */
  293. do_action( 'woocommerce_rest_insert_product_attribute', $attribute, $request, false );
  294. $request->set_param( 'context', 'edit' );
  295. $response = $this->prepare_item_for_response( $attribute, $request );
  296. return rest_ensure_response( $response );
  297. }
  298. /**
  299. * Delete a single attribute.
  300. *
  301. * @param WP_REST_Request $request Full details about the request.
  302. * @return WP_REST_Response|WP_Error
  303. */
  304. public function delete_item( $request ) {
  305. $force = isset( $request['force'] ) ? (bool) $request['force'] : false;
  306. // We don't support trashing for this type, error out.
  307. if ( ! $force ) {
  308. return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Resource does not support trashing.', 'woocommerce' ), array( 'status' => 501 ) );
  309. }
  310. $attribute = $this->get_attribute( (int) $request['id'] );
  311. if ( is_wp_error( $attribute ) ) {
  312. return $attribute;
  313. }
  314. $request->set_param( 'context', 'edit' );
  315. $response = $this->prepare_item_for_response( $attribute, $request );
  316. $deleted = wc_delete_attribute( $attribute->attribute_id );
  317. if ( false === $deleted ) {
  318. return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) );
  319. }
  320. /**
  321. * Fires after a single attribute is deleted via the REST API.
  322. *
  323. * @param stdObject $attribute The deleted attribute.
  324. * @param WP_REST_Response $response The response data.
  325. * @param WP_REST_Request $request The request sent to the API.
  326. */
  327. do_action( 'woocommerce_rest_delete_product_attribute', $attribute, $response, $request );
  328. return $response;
  329. }
  330. /**
  331. * Prepare a single product attribute output for response.
  332. *
  333. * @param obj $item Term object.
  334. * @param WP_REST_Request $request
  335. * @return WP_REST_Response $response
  336. */
  337. public function prepare_item_for_response( $item, $request ) {
  338. $data = array(
  339. 'id' => (int) $item->attribute_id,
  340. 'name' => $item->attribute_label,
  341. 'slug' => wc_attribute_taxonomy_name( $item->attribute_name ),
  342. 'type' => $item->attribute_type,
  343. 'order_by' => $item->attribute_orderby,
  344. 'has_archives' => (bool) $item->attribute_public,
  345. );
  346. $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
  347. $data = $this->add_additional_fields_to_object( $data, $request );
  348. $data = $this->filter_response_by_context( $data, $context );
  349. $response = rest_ensure_response( $data );
  350. $response->add_links( $this->prepare_links( $item ) );
  351. /**
  352. * Filter a attribute item returned from the API.
  353. *
  354. * Allows modification of the product attribute data right before it is returned.
  355. *
  356. * @param WP_REST_Response $response The response object.
  357. * @param object $item The original attribute object.
  358. * @param WP_REST_Request $request Request used to generate the response.
  359. */
  360. return apply_filters( 'woocommerce_rest_prepare_product_attribute', $response, $item, $request );
  361. }
  362. /**
  363. * Prepare links for the request.
  364. *
  365. * @param object $attribute Attribute object.
  366. * @return array Links for the given attribute.
  367. */
  368. protected function prepare_links( $attribute ) {
  369. $base = '/' . $this->namespace . '/' . $this->rest_base;
  370. $links = array(
  371. 'self' => array(
  372. 'href' => rest_url( trailingslashit( $base ) . $attribute->attribute_id ),
  373. ),
  374. 'collection' => array(
  375. 'href' => rest_url( $base ),
  376. ),
  377. );
  378. return $links;
  379. }
  380. /**
  381. * Get the Attribute's schema, conforming to JSON Schema.
  382. *
  383. * @return array
  384. */
  385. public function get_item_schema() {
  386. $schema = array(
  387. '$schema' => 'http://json-schema.org/draft-04/schema#',
  388. 'title' => 'product_attribute',
  389. 'type' => 'object',
  390. 'properties' => array(
  391. 'id' => array(
  392. 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
  393. 'type' => 'integer',
  394. 'context' => array( 'view', 'edit' ),
  395. 'readonly' => true,
  396. ),
  397. 'name' => array(
  398. 'description' => __( 'Attribute name.', 'woocommerce' ),
  399. 'type' => 'string',
  400. 'context' => array( 'view', 'edit' ),
  401. 'arg_options' => array(
  402. 'sanitize_callback' => 'sanitize_text_field',
  403. ),
  404. ),
  405. 'slug' => array(
  406. 'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ),
  407. 'type' => 'string',
  408. 'context' => array( 'view', 'edit' ),
  409. 'arg_options' => array(
  410. 'sanitize_callback' => 'sanitize_title',
  411. ),
  412. ),
  413. 'type' => array(
  414. 'description' => __( 'Type of attribute.', 'woocommerce' ),
  415. 'type' => 'string',
  416. 'default' => 'select',
  417. 'enum' => array_keys( wc_get_attribute_types() ),
  418. 'context' => array( 'view', 'edit' ),
  419. ),
  420. 'order_by' => array(
  421. 'description' => __( 'Default sort order.', 'woocommerce' ),
  422. 'type' => 'string',
  423. 'default' => 'menu_order',
  424. 'enum' => array( 'menu_order', 'name', 'name_num', 'id' ),
  425. 'context' => array( 'view', 'edit' ),
  426. ),
  427. 'has_archives' => array(
  428. 'description' => __( 'Enable/Disable attribute archives.', 'woocommerce' ),
  429. 'type' => 'boolean',
  430. 'default' => false,
  431. 'context' => array( 'view', 'edit' ),
  432. ),
  433. ),
  434. );
  435. return $this->add_additional_fields_schema( $schema );
  436. }
  437. /**
  438. * Get the query params for collections
  439. *
  440. * @return array
  441. */
  442. public function get_collection_params() {
  443. $params = array();
  444. $params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
  445. return $params;
  446. }
  447. /**
  448. * Get attribute name.
  449. *
  450. * @param WP_REST_Request $request Full details about the request.
  451. * @return string
  452. */
  453. protected function get_taxonomy( $request ) {
  454. if ( '' !== $this->attribute ) {
  455. return $this->attribute;
  456. }
  457. if ( $request['id'] ) {
  458. $name = wc_attribute_taxonomy_name_by_id( (int) $request['id'] );
  459. $this->attribute = $name;
  460. }
  461. return $this->attribute;
  462. }
  463. /**
  464. * Get attribute data.
  465. *
  466. * @param int $id Attribute ID.
  467. * @return stdClass|WP_Error
  468. */
  469. protected function get_attribute( $id ) {
  470. global $wpdb;
  471. $attribute = $wpdb->get_row( $wpdb->prepare( "
  472. SELECT *
  473. FROM {$wpdb->prefix}woocommerce_attribute_taxonomies
  474. WHERE attribute_id = %d
  475. ", $id ) );
  476. if ( is_wp_error( $attribute ) || is_null( $attribute ) ) {
  477. return new WP_Error( 'woocommerce_rest_attribute_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) );
  478. }
  479. return $attribute;
  480. }
  481. /**
  482. * Validate attribute slug.
  483. *
  484. * @deprecated 3.2.0
  485. * @param string $slug
  486. * @param bool $new_data
  487. * @return bool|WP_Error
  488. */
  489. protected function validate_attribute_slug( $slug, $new_data = true ) {
  490. if ( strlen( $slug ) >= 28 ) {
  491. return new WP_Error( 'woocommerce_rest_invalid_product_attribute_slug_too_long', sprintf( __( 'Slug "%s" is too long (28 characters max). Shorten it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) );
  492. } elseif ( wc_check_if_attribute_name_is_reserved( $slug ) ) {
  493. return new WP_Error( 'woocommerce_rest_invalid_product_attribute_slug_reserved_name', sprintf( __( 'Slug "%s" is not allowed because it is a reserved term. Change it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) );
  494. } elseif ( $new_data && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) {
  495. return new WP_Error( 'woocommerce_rest_invalid_product_attribute_slug_already_exists', sprintf( __( 'Slug "%s" is already in use. Change it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) );
  496. }
  497. return true;
  498. }
  499. /**
  500. * Schedule to flush rewrite rules.
  501. *
  502. * @deprecated 3.2.0
  503. * @since 3.0.0
  504. */
  505. protected function flush_rewrite_rules() {
  506. wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' );
  507. }
  508. }