class-wc-rest-product-variations-controller.php 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013
  1. <?php
  2. /**
  3. * REST API variations controller
  4. *
  5. * Handles requests to the /products/<product_id>/variations endpoints.
  6. *
  7. * @package WooCommerce\API
  8. * @since 3.0.0
  9. */
  10. defined( 'ABSPATH' ) || exit;
  11. /**
  12. * REST API variations controller class.
  13. *
  14. * @package WooCommerce/API
  15. * @extends WC_REST_Products_Controller
  16. */
  17. class WC_REST_Product_Variations_Controller extends WC_REST_Products_Controller {
  18. /**
  19. * Endpoint namespace.
  20. *
  21. * @var string
  22. */
  23. protected $namespace = 'wc/v2';
  24. /**
  25. * Route base.
  26. *
  27. * @var string
  28. */
  29. protected $rest_base = 'products/(?P<product_id>[\d]+)/variations';
  30. /**
  31. * Post type.
  32. *
  33. * @var string
  34. */
  35. protected $post_type = 'product_variation';
  36. /**
  37. * Initialize product actions (parent).
  38. */
  39. public function __construct() {
  40. add_filter( "woocommerce_rest_{$this->post_type}_query", array( $this, 'add_product_id' ), 9, 2 );
  41. parent::__construct();
  42. }
  43. /**
  44. * Register the routes for products.
  45. */
  46. public function register_routes() {
  47. register_rest_route(
  48. $this->namespace, '/' . $this->rest_base, array(
  49. 'args' => array(
  50. 'product_id' => array(
  51. 'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ),
  52. 'type' => 'integer',
  53. ),
  54. ),
  55. array(
  56. 'methods' => WP_REST_Server::READABLE,
  57. 'callback' => array( $this, 'get_items' ),
  58. 'permission_callback' => array( $this, 'get_items_permissions_check' ),
  59. 'args' => $this->get_collection_params(),
  60. ),
  61. array(
  62. 'methods' => WP_REST_Server::CREATABLE,
  63. 'callback' => array( $this, 'create_item' ),
  64. 'permission_callback' => array( $this, 'create_item_permissions_check' ),
  65. 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
  66. ),
  67. 'schema' => array( $this, 'get_public_item_schema' ),
  68. )
  69. );
  70. register_rest_route(
  71. $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
  72. 'args' => array(
  73. 'product_id' => array(
  74. 'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ),
  75. 'type' => 'integer',
  76. ),
  77. 'id' => array(
  78. 'description' => __( 'Unique identifier for the variation.', 'woocommerce' ),
  79. 'type' => 'integer',
  80. ),
  81. ),
  82. array(
  83. 'methods' => WP_REST_Server::READABLE,
  84. 'callback' => array( $this, 'get_item' ),
  85. 'permission_callback' => array( $this, 'get_item_permissions_check' ),
  86. 'args' => array(
  87. 'context' => $this->get_context_param(
  88. array(
  89. 'default' => 'view',
  90. )
  91. ),
  92. ),
  93. ),
  94. array(
  95. 'methods' => WP_REST_Server::EDITABLE,
  96. 'callback' => array( $this, 'update_item' ),
  97. 'permission_callback' => array( $this, 'update_item_permissions_check' ),
  98. 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
  99. ),
  100. array(
  101. 'methods' => WP_REST_Server::DELETABLE,
  102. 'callback' => array( $this, 'delete_item' ),
  103. 'permission_callback' => array( $this, 'delete_item_permissions_check' ),
  104. 'args' => array(
  105. 'force' => array(
  106. 'default' => false,
  107. 'type' => 'boolean',
  108. 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ),
  109. ),
  110. ),
  111. ),
  112. 'schema' => array( $this, 'get_public_item_schema' ),
  113. )
  114. );
  115. register_rest_route(
  116. $this->namespace, '/' . $this->rest_base . '/batch', array(
  117. 'args' => array(
  118. 'product_id' => array(
  119. 'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ),
  120. 'type' => 'integer',
  121. ),
  122. ),
  123. array(
  124. 'methods' => WP_REST_Server::EDITABLE,
  125. 'callback' => array( $this, 'batch_items' ),
  126. 'permission_callback' => array( $this, 'batch_items_permissions_check' ),
  127. 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
  128. ),
  129. 'schema' => array( $this, 'get_public_batch_schema' ),
  130. )
  131. );
  132. }
  133. /**
  134. * Get object.
  135. *
  136. * @since 3.0.0
  137. * @param int $id Object ID.
  138. * @return WC_Data
  139. */
  140. protected function get_object( $id ) {
  141. return wc_get_product( $id );
  142. }
  143. /**
  144. * Check if a given request has access to update an item.
  145. *
  146. * @param WP_REST_Request $request Full details about the request.
  147. * @return WP_Error|boolean
  148. */
  149. public function update_item_permissions_check( $request ) {
  150. $object = $this->get_object( (int) $request['id'] );
  151. if ( $object && 0 !== $object->get_id() && ! wc_rest_check_post_permissions( $this->post_type, 'edit', $object->get_id() ) ) {
  152. return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
  153. }
  154. // Check if variation belongs to the correct parent product.
  155. if ( $object && 0 !== $object->get_parent_id() && absint( $request['product_id'] ) !== $object->get_parent_id() ) {
  156. return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Parent product does not match current variation.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
  157. }
  158. return true;
  159. }
  160. /**
  161. * Prepare a single variation output for response.
  162. *
  163. * @since 3.0.0
  164. * @param WC_Data $object Object data.
  165. * @param WP_REST_Request $request Request object.
  166. * @return WP_REST_Response
  167. */
  168. public function prepare_object_for_response( $object, $request ) {
  169. $data = array(
  170. 'id' => $object->get_id(),
  171. 'date_created' => wc_rest_prepare_date_response( $object->get_date_created(), false ),
  172. 'date_created_gmt' => wc_rest_prepare_date_response( $object->get_date_created() ),
  173. 'date_modified' => wc_rest_prepare_date_response( $object->get_date_modified(), false ),
  174. 'date_modified_gmt' => wc_rest_prepare_date_response( $object->get_date_modified() ),
  175. 'description' => wc_format_content( $object->get_description() ),
  176. 'permalink' => $object->get_permalink(),
  177. 'sku' => $object->get_sku(),
  178. 'price' => $object->get_price(),
  179. 'regular_price' => $object->get_regular_price(),
  180. 'sale_price' => $object->get_sale_price(),
  181. 'date_on_sale_from' => wc_rest_prepare_date_response( $object->get_date_on_sale_from(), false ),
  182. 'date_on_sale_from_gmt' => wc_rest_prepare_date_response( $object->get_date_on_sale_from() ),
  183. 'date_on_sale_to' => wc_rest_prepare_date_response( $object->get_date_on_sale_to(), false ),
  184. 'date_on_sale_to_gmt' => wc_rest_prepare_date_response( $object->get_date_on_sale_to() ),
  185. 'on_sale' => $object->is_on_sale(),
  186. 'visible' => $object->is_visible(),
  187. 'purchasable' => $object->is_purchasable(),
  188. 'virtual' => $object->is_virtual(),
  189. 'downloadable' => $object->is_downloadable(),
  190. 'downloads' => $this->get_downloads( $object ),
  191. 'download_limit' => '' !== $object->get_download_limit() ? (int) $object->get_download_limit() : -1,
  192. 'download_expiry' => '' !== $object->get_download_expiry() ? (int) $object->get_download_expiry() : -1,
  193. 'tax_status' => $object->get_tax_status(),
  194. 'tax_class' => $object->get_tax_class(),
  195. 'manage_stock' => $object->managing_stock(),
  196. 'stock_quantity' => $object->get_stock_quantity(),
  197. 'in_stock' => $object->is_in_stock(),
  198. 'backorders' => $object->get_backorders(),
  199. 'backorders_allowed' => $object->backorders_allowed(),
  200. 'backordered' => $object->is_on_backorder(),
  201. 'weight' => $object->get_weight(),
  202. 'dimensions' => array(
  203. 'length' => $object->get_length(),
  204. 'width' => $object->get_width(),
  205. 'height' => $object->get_height(),
  206. ),
  207. 'shipping_class' => $object->get_shipping_class(),
  208. 'shipping_class_id' => $object->get_shipping_class_id(),
  209. 'image' => current( $this->get_images( $object ) ),
  210. 'attributes' => $this->get_attributes( $object ),
  211. 'menu_order' => $object->get_menu_order(),
  212. 'meta_data' => $object->get_meta_data(),
  213. );
  214. $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
  215. $data = $this->add_additional_fields_to_object( $data, $request );
  216. $data = $this->filter_response_by_context( $data, $context );
  217. $response = rest_ensure_response( $data );
  218. $response->add_links( $this->prepare_links( $object, $request ) );
  219. /**
  220. * Filter the data for a response.
  221. *
  222. * The dynamic portion of the hook name, $this->post_type,
  223. * refers to object type being prepared for the response.
  224. *
  225. * @param WP_REST_Response $response The response object.
  226. * @param WC_Data $object Object data.
  227. * @param WP_REST_Request $request Request object.
  228. */
  229. return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request );
  230. }
  231. /**
  232. * Prepare objects query.
  233. *
  234. * @since 3.0.0
  235. * @param WP_REST_Request $request Full details about the request.
  236. * @return array
  237. */
  238. protected function prepare_objects_query( $request ) {
  239. $args = parent::prepare_objects_query( $request );
  240. $args['post_parent'] = $request['product_id'];
  241. return $args;
  242. }
  243. /**
  244. * Prepare a single variation for create or update.
  245. *
  246. * @param WP_REST_Request $request Request object.
  247. * @param bool $creating If is creating a new object.
  248. * @return WP_Error|WC_Data
  249. */
  250. protected function prepare_object_for_database( $request, $creating = false ) {
  251. if ( isset( $request['id'] ) ) {
  252. $variation = wc_get_product( absint( $request['id'] ) );
  253. } else {
  254. $variation = new WC_Product_Variation();
  255. }
  256. // Update parent ID just once.
  257. if ( 0 === $variation->get_parent_id() ) {
  258. $variation->set_parent_id( absint( $request['product_id'] ) );
  259. }
  260. // Status.
  261. if ( isset( $request['visible'] ) ) {
  262. $variation->set_status( false === $request['visible'] ? 'private' : 'publish' );
  263. }
  264. // SKU.
  265. if ( isset( $request['sku'] ) ) {
  266. $variation->set_sku( wc_clean( $request['sku'] ) );
  267. }
  268. // Thumbnail.
  269. if ( isset( $request['image'] ) ) {
  270. if ( is_array( $request['image'] ) && ! empty( $request['image'] ) ) {
  271. $image = $request['image'];
  272. if ( is_array( $image ) ) {
  273. $image['position'] = 0;
  274. }
  275. $variation = $this->set_product_images( $variation, array( $image ) );
  276. } else {
  277. $variation->set_image_id( '' );
  278. }
  279. }
  280. // Virtual variation.
  281. if ( isset( $request['virtual'] ) ) {
  282. $variation->set_virtual( $request['virtual'] );
  283. }
  284. // Downloadable variation.
  285. if ( isset( $request['downloadable'] ) ) {
  286. $variation->set_downloadable( $request['downloadable'] );
  287. }
  288. // Downloads.
  289. if ( $variation->get_downloadable() ) {
  290. // Downloadable files.
  291. if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) {
  292. $variation = $this->save_downloadable_files( $variation, $request['downloads'] );
  293. }
  294. // Download limit.
  295. if ( isset( $request['download_limit'] ) ) {
  296. $variation->set_download_limit( $request['download_limit'] );
  297. }
  298. // Download expiry.
  299. if ( isset( $request['download_expiry'] ) ) {
  300. $variation->set_download_expiry( $request['download_expiry'] );
  301. }
  302. }
  303. // Shipping data.
  304. $variation = $this->save_product_shipping_data( $variation, $request );
  305. // Stock handling.
  306. if ( isset( $request['manage_stock'] ) ) {
  307. if ( 'parent' === $request['manage_stock'] ) {
  308. $variation->set_manage_stock( false ); // This just indicates the variation does not manage stock, but the parent does.
  309. } else {
  310. $variation->set_manage_stock( wc_string_to_bool( $request['manage_stock'] ) );
  311. }
  312. }
  313. if ( isset( $request['in_stock'] ) ) {
  314. $variation->set_stock_status( true === $request['in_stock'] ? 'instock' : 'outofstock' );
  315. }
  316. if ( isset( $request['backorders'] ) ) {
  317. $variation->set_backorders( $request['backorders'] );
  318. }
  319. if ( $variation->get_manage_stock() ) {
  320. if ( isset( $request['stock_quantity'] ) ) {
  321. $variation->set_stock_quantity( $request['stock_quantity'] );
  322. } elseif ( isset( $request['inventory_delta'] ) ) {
  323. $stock_quantity = wc_stock_amount( $variation->get_stock_quantity() );
  324. $stock_quantity += wc_stock_amount( $request['inventory_delta'] );
  325. $variation->set_stock_quantity( $stock_quantity );
  326. }
  327. } else {
  328. $variation->set_backorders( 'no' );
  329. $variation->set_stock_quantity( '' );
  330. }
  331. // Regular Price.
  332. if ( isset( $request['regular_price'] ) ) {
  333. $variation->set_regular_price( $request['regular_price'] );
  334. }
  335. // Sale Price.
  336. if ( isset( $request['sale_price'] ) ) {
  337. $variation->set_sale_price( $request['sale_price'] );
  338. }
  339. if ( isset( $request['date_on_sale_from'] ) ) {
  340. $variation->set_date_on_sale_from( $request['date_on_sale_from'] );
  341. }
  342. if ( isset( $request['date_on_sale_from_gmt'] ) ) {
  343. $variation->set_date_on_sale_from( $request['date_on_sale_from_gmt'] ? strtotime( $request['date_on_sale_from_gmt'] ) : null );
  344. }
  345. if ( isset( $request['date_on_sale_to'] ) ) {
  346. $variation->set_date_on_sale_to( $request['date_on_sale_to'] );
  347. }
  348. if ( isset( $request['date_on_sale_to_gmt'] ) ) {
  349. $variation->set_date_on_sale_to( $request['date_on_sale_to_gmt'] ? strtotime( $request['date_on_sale_to_gmt'] ) : null );
  350. }
  351. // Tax class.
  352. if ( isset( $request['tax_class'] ) ) {
  353. $variation->set_tax_class( $request['tax_class'] );
  354. }
  355. // Description.
  356. if ( isset( $request['description'] ) ) {
  357. $variation->set_description( wp_kses_post( $request['description'] ) );
  358. }
  359. // Update taxonomies.
  360. if ( isset( $request['attributes'] ) ) {
  361. $attributes = array();
  362. $parent = wc_get_product( $variation->get_parent_id() );
  363. if ( ! $parent ) {
  364. return new WP_Error(
  365. // Translators: %d parent ID.
  366. "woocommerce_rest_{$this->post_type}_invalid_parent", sprintf( __( 'Cannot set attributes due to invalid parent product.', 'woocommerce' ), $variation->get_parent_id() ), array(
  367. 'status' => 404,
  368. )
  369. );
  370. }
  371. $parent_attributes = $parent->get_attributes();
  372. foreach ( $request['attributes'] as $attribute ) {
  373. $attribute_id = 0;
  374. $attribute_name = '';
  375. // Check ID for global attributes or name for product attributes.
  376. if ( ! empty( $attribute['id'] ) ) {
  377. $attribute_id = absint( $attribute['id'] );
  378. $raw_attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
  379. } elseif ( ! empty( $attribute['name'] ) ) {
  380. $raw_attribute_name = sanitize_title( $attribute['name'] );
  381. }
  382. if ( ! $attribute_id && ! $raw_attribute_name ) {
  383. continue;
  384. }
  385. $attribute_name = sanitize_title( $raw_attribute_name );
  386. if ( ! isset( $parent_attributes[ $attribute_name ] ) || ! $parent_attributes[ $attribute_name ]->get_variation() ) {
  387. continue;
  388. }
  389. $attribute_key = sanitize_title( $parent_attributes[ $attribute_name ]->get_name() );
  390. $attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : '';
  391. if ( $parent_attributes[ $attribute_name ]->is_taxonomy() ) {
  392. // If dealing with a taxonomy, we need to get the slug from the name posted to the API.
  393. $term = get_term_by( 'name', $attribute_value, $raw_attribute_name ); // @codingStandardsIgnoreLine
  394. if ( $term && ! is_wp_error( $term ) ) {
  395. $attribute_value = $term->slug;
  396. } else {
  397. $attribute_value = sanitize_title( $attribute_value );
  398. }
  399. }
  400. $attributes[ $attribute_key ] = $attribute_value;
  401. }
  402. $variation->set_attributes( $attributes );
  403. }
  404. // Menu order.
  405. if ( $request['menu_order'] ) {
  406. $variation->set_menu_order( $request['menu_order'] );
  407. }
  408. // Meta data.
  409. if ( is_array( $request['meta_data'] ) ) {
  410. foreach ( $request['meta_data'] as $meta ) {
  411. $variation->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' );
  412. }
  413. }
  414. /**
  415. * Filters an object before it is inserted via the REST API.
  416. *
  417. * The dynamic portion of the hook name, `$this->post_type`,
  418. * refers to the object type slug.
  419. *
  420. * @param WC_Data $variation Object object.
  421. * @param WP_REST_Request $request Request object.
  422. * @param bool $creating If is creating a new object.
  423. */
  424. return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $variation, $request, $creating );
  425. }
  426. /**
  427. * Clear caches here so in sync with any new variations.
  428. *
  429. * @param WC_Data $object Object data.
  430. */
  431. public function clear_transients( $object ) {
  432. wc_delete_product_transients( $object->get_parent_id() );
  433. wp_cache_delete( 'product-' . $object->get_parent_id(), 'products' );
  434. }
  435. /**
  436. * Delete a variation.
  437. *
  438. * @param WP_REST_Request $request Full details about the request.
  439. *
  440. * @return bool|WP_Error|WP_REST_Response
  441. */
  442. public function delete_item( $request ) {
  443. $force = (bool) $request['force'];
  444. $object = $this->get_object( (int) $request['id'] );
  445. $result = false;
  446. if ( ! $object || 0 === $object->get_id() ) {
  447. return new WP_Error(
  448. "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array(
  449. 'status' => 404,
  450. )
  451. );
  452. }
  453. $supports_trash = EMPTY_TRASH_DAYS > 0 && is_callable( array( $object, 'get_status' ) );
  454. /**
  455. * Filter whether an object is trashable.
  456. *
  457. * Return false to disable trash support for the object.
  458. *
  459. * @param boolean $supports_trash Whether the object type support trashing.
  460. * @param WC_Data $object The object being considered for trashing support.
  461. */
  462. $supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_object_trashable", $supports_trash, $object );
  463. if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $object->get_id() ) ) {
  464. return new WP_Error(
  465. /* translators: %s: post type */
  466. "woocommerce_rest_user_cannot_delete_{$this->post_type}", sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), array(
  467. 'status' => rest_authorization_required_code(),
  468. )
  469. );
  470. }
  471. $request->set_param( 'context', 'edit' );
  472. $response = $this->prepare_object_for_response( $object, $request );
  473. // If we're forcing, then delete permanently.
  474. if ( $force ) {
  475. $object->delete( true );
  476. $result = 0 === $object->get_id();
  477. } else {
  478. // If we don't support trashing for this type, error out.
  479. if ( ! $supports_trash ) {
  480. return new WP_Error(
  481. /* translators: %s: post type */
  482. 'woocommerce_rest_trash_not_supported', sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), array(
  483. 'status' => 501,
  484. )
  485. );
  486. }
  487. // Otherwise, only trash if we haven't already.
  488. if ( is_callable( array( $object, 'get_status' ) ) ) {
  489. if ( 'trash' === $object->get_status() ) {
  490. return new WP_Error(
  491. /* translators: %s: post type */
  492. 'woocommerce_rest_already_trashed', sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), array(
  493. 'status' => 410,
  494. )
  495. );
  496. }
  497. $object->delete();
  498. $result = 'trash' === $object->get_status();
  499. }
  500. }
  501. if ( ! $result ) {
  502. return new WP_Error(
  503. /* translators: %s: post type */
  504. 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array(
  505. 'status' => 500,
  506. )
  507. );
  508. }
  509. // Delete parent product transients.
  510. if ( 0 !== $object->get_parent_id() ) {
  511. wc_delete_product_transients( $object->get_parent_id() );
  512. }
  513. /**
  514. * Fires after a single object is deleted or trashed via the REST API.
  515. *
  516. * @param WC_Data $object The deleted or trashed object.
  517. * @param WP_REST_Response $response The response data.
  518. * @param WP_REST_Request $request The request sent to the API.
  519. */
  520. do_action( "woocommerce_rest_delete_{$this->post_type}_object", $object, $response, $request );
  521. return $response;
  522. }
  523. /**
  524. * Bulk create, update and delete items.
  525. *
  526. * @since 3.0.0
  527. * @param WP_REST_Request $request Full details about the request.
  528. * @return array Of WP_Error or WP_REST_Response.
  529. */
  530. public function batch_items( $request ) {
  531. $items = array_filter( $request->get_params() );
  532. $params = $request->get_url_params();
  533. $product_id = $params['product_id'];
  534. $body_params = array();
  535. foreach ( array( 'update', 'create', 'delete' ) as $batch_type ) {
  536. if ( ! empty( $items[ $batch_type ] ) ) {
  537. $injected_items = array();
  538. foreach ( $items[ $batch_type ] as $item ) {
  539. $injected_items[] = is_array( $item ) ? array_merge(
  540. array(
  541. 'product_id' => $product_id,
  542. ), $item
  543. ) : $item;
  544. }
  545. $body_params[ $batch_type ] = $injected_items;
  546. }
  547. }
  548. $request = new WP_REST_Request( $request->get_method() );
  549. $request->set_body_params( $body_params );
  550. return parent::batch_items( $request );
  551. }
  552. /**
  553. * Prepare links for the request.
  554. *
  555. * @param WC_Data $object Object data.
  556. * @param WP_REST_Request $request Request object.
  557. * @return array Links for the given post.
  558. */
  559. protected function prepare_links( $object, $request ) {
  560. $product_id = (int) $request['product_id'];
  561. $base = str_replace( '(?P<product_id>[\d]+)', $product_id, $this->rest_base );
  562. $links = array(
  563. 'self' => array(
  564. 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $object->get_id() ) ),
  565. ),
  566. 'collection' => array(
  567. 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ),
  568. ),
  569. 'up' => array(
  570. 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product_id ) ),
  571. ),
  572. );
  573. return $links;
  574. }
  575. /**
  576. * Get the Variation's schema, conforming to JSON Schema.
  577. *
  578. * @return array
  579. */
  580. public function get_item_schema() {
  581. $weight_unit = get_option( 'woocommerce_weight_unit' );
  582. $dimension_unit = get_option( 'woocommerce_dimension_unit' );
  583. $schema = array(
  584. '$schema' => 'http://json-schema.org/draft-04/schema#',
  585. 'title' => $this->post_type,
  586. 'type' => 'object',
  587. 'properties' => array(
  588. 'id' => array(
  589. 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
  590. 'type' => 'integer',
  591. 'context' => array( 'view', 'edit' ),
  592. 'readonly' => true,
  593. ),
  594. 'date_created' => array(
  595. 'description' => __( "The date the variation was created, in the site's timezone.", 'woocommerce' ),
  596. 'type' => 'date-time',
  597. 'context' => array( 'view', 'edit' ),
  598. 'readonly' => true,
  599. ),
  600. 'date_modified' => array(
  601. 'description' => __( "The date the variation was last modified, in the site's timezone.", 'woocommerce' ),
  602. 'type' => 'date-time',
  603. 'context' => array( 'view', 'edit' ),
  604. 'readonly' => true,
  605. ),
  606. 'description' => array(
  607. 'description' => __( 'Variation description.', 'woocommerce' ),
  608. 'type' => 'string',
  609. 'context' => array( 'view', 'edit' ),
  610. ),
  611. 'permalink' => array(
  612. 'description' => __( 'Variation URL.', 'woocommerce' ),
  613. 'type' => 'string',
  614. 'format' => 'uri',
  615. 'context' => array( 'view', 'edit' ),
  616. 'readonly' => true,
  617. ),
  618. 'sku' => array(
  619. 'description' => __( 'Unique identifier.', 'woocommerce' ),
  620. 'type' => 'string',
  621. 'context' => array( 'view', 'edit' ),
  622. ),
  623. 'price' => array(
  624. 'description' => __( 'Current variation price.', 'woocommerce' ),
  625. 'type' => 'string',
  626. 'context' => array( 'view', 'edit' ),
  627. 'readonly' => true,
  628. ),
  629. 'regular_price' => array(
  630. 'description' => __( 'Variation regular price.', 'woocommerce' ),
  631. 'type' => 'string',
  632. 'context' => array( 'view', 'edit' ),
  633. ),
  634. 'sale_price' => array(
  635. 'description' => __( 'Variation sale price.', 'woocommerce' ),
  636. 'type' => 'string',
  637. 'context' => array( 'view', 'edit' ),
  638. ),
  639. 'date_on_sale_from' => array(
  640. 'description' => __( "Start date of sale price, in the site's timezone.", 'woocommerce' ),
  641. 'type' => 'date-time',
  642. 'context' => array( 'view', 'edit' ),
  643. ),
  644. 'date_on_sale_from_gmt' => array(
  645. 'description' => __( 'Start date of sale price, as GMT.', 'woocommerce' ),
  646. 'type' => 'date-time',
  647. 'context' => array( 'view', 'edit' ),
  648. ),
  649. 'date_on_sale_to' => array(
  650. 'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ),
  651. 'type' => 'date-time',
  652. 'context' => array( 'view', 'edit' ),
  653. ),
  654. 'date_on_sale_to_gmt' => array(
  655. 'description' => __( 'End date of sale price, as GMT.', 'woocommerce' ),
  656. 'type' => 'date-time',
  657. 'context' => array( 'view', 'edit' ),
  658. ),
  659. 'on_sale' => array(
  660. 'description' => __( 'Shows if the variation is on sale.', 'woocommerce' ),
  661. 'type' => 'boolean',
  662. 'context' => array( 'view', 'edit' ),
  663. 'readonly' => true,
  664. ),
  665. 'visible' => array(
  666. 'description' => __( "Define if the variation is visible on the product's page.", 'woocommerce' ),
  667. 'type' => 'boolean',
  668. 'default' => true,
  669. 'context' => array( 'view', 'edit' ),
  670. ),
  671. 'purchasable' => array(
  672. 'description' => __( 'Shows if the variation can be bought.', 'woocommerce' ),
  673. 'type' => 'boolean',
  674. 'context' => array( 'view', 'edit' ),
  675. 'readonly' => true,
  676. ),
  677. 'virtual' => array(
  678. 'description' => __( 'If the variation is virtual.', 'woocommerce' ),
  679. 'type' => 'boolean',
  680. 'default' => false,
  681. 'context' => array( 'view', 'edit' ),
  682. ),
  683. 'downloadable' => array(
  684. 'description' => __( 'If the variation is downloadable.', 'woocommerce' ),
  685. 'type' => 'boolean',
  686. 'default' => false,
  687. 'context' => array( 'view', 'edit' ),
  688. ),
  689. 'downloads' => array(
  690. 'description' => __( 'List of downloadable files.', 'woocommerce' ),
  691. 'type' => 'array',
  692. 'context' => array( 'view', 'edit' ),
  693. 'items' => array(
  694. 'type' => 'object',
  695. 'properties' => array(
  696. 'id' => array(
  697. 'description' => __( 'File MD5 hash.', 'woocommerce' ),
  698. 'type' => 'string',
  699. 'context' => array( 'view', 'edit' ),
  700. 'readonly' => true,
  701. ),
  702. 'name' => array(
  703. 'description' => __( 'File name.', 'woocommerce' ),
  704. 'type' => 'string',
  705. 'context' => array( 'view', 'edit' ),
  706. ),
  707. 'file' => array(
  708. 'description' => __( 'File URL.', 'woocommerce' ),
  709. 'type' => 'string',
  710. 'context' => array( 'view', 'edit' ),
  711. ),
  712. ),
  713. ),
  714. ),
  715. 'download_limit' => array(
  716. 'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ),
  717. 'type' => 'integer',
  718. 'default' => -1,
  719. 'context' => array( 'view', 'edit' ),
  720. ),
  721. 'download_expiry' => array(
  722. 'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ),
  723. 'type' => 'integer',
  724. 'default' => -1,
  725. 'context' => array( 'view', 'edit' ),
  726. ),
  727. 'tax_status' => array(
  728. 'description' => __( 'Tax status.', 'woocommerce' ),
  729. 'type' => 'string',
  730. 'default' => 'taxable',
  731. 'enum' => array( 'taxable', 'shipping', 'none' ),
  732. 'context' => array( 'view', 'edit' ),
  733. ),
  734. 'tax_class' => array(
  735. 'description' => __( 'Tax class.', 'woocommerce' ),
  736. 'type' => 'string',
  737. 'context' => array( 'view', 'edit' ),
  738. ),
  739. 'manage_stock' => array(
  740. 'description' => __( 'Stock management at variation level.', 'woocommerce' ),
  741. 'type' => 'mixed',
  742. 'default' => false,
  743. 'context' => array( 'view', 'edit' ),
  744. ),
  745. 'stock_quantity' => array(
  746. 'description' => __( 'Stock quantity.', 'woocommerce' ),
  747. 'type' => 'integer',
  748. 'context' => array( 'view', 'edit' ),
  749. ),
  750. 'in_stock' => array(
  751. 'description' => __( 'Controls whether or not the variation is listed as "in stock" or "out of stock" on the frontend.', 'woocommerce' ),
  752. 'type' => 'boolean',
  753. 'default' => true,
  754. 'context' => array( 'view', 'edit' ),
  755. ),
  756. 'backorders' => array(
  757. 'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ),
  758. 'type' => 'string',
  759. 'default' => 'no',
  760. 'enum' => array( 'no', 'notify', 'yes' ),
  761. 'context' => array( 'view', 'edit' ),
  762. ),
  763. 'backorders_allowed' => array(
  764. 'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ),
  765. 'type' => 'boolean',
  766. 'context' => array( 'view', 'edit' ),
  767. 'readonly' => true,
  768. ),
  769. 'backordered' => array(
  770. 'description' => __( 'Shows if the variation is on backordered.', 'woocommerce' ),
  771. 'type' => 'boolean',
  772. 'context' => array( 'view', 'edit' ),
  773. 'readonly' => true,
  774. ),
  775. 'weight' => array(
  776. /* translators: %s: weight unit */
  777. 'description' => sprintf( __( 'Variation weight (%s).', 'woocommerce' ), $weight_unit ),
  778. 'type' => 'string',
  779. 'context' => array( 'view', 'edit' ),
  780. ),
  781. 'dimensions' => array(
  782. 'description' => __( 'Variation dimensions.', 'woocommerce' ),
  783. 'type' => 'object',
  784. 'context' => array( 'view', 'edit' ),
  785. 'properties' => array(
  786. 'length' => array(
  787. /* translators: %s: dimension unit */
  788. 'description' => sprintf( __( 'Variation length (%s).', 'woocommerce' ), $dimension_unit ),
  789. 'type' => 'string',
  790. 'context' => array( 'view', 'edit' ),
  791. ),
  792. 'width' => array(
  793. /* translators: %s: dimension unit */
  794. 'description' => sprintf( __( 'Variation width (%s).', 'woocommerce' ), $dimension_unit ),
  795. 'type' => 'string',
  796. 'context' => array( 'view', 'edit' ),
  797. ),
  798. 'height' => array(
  799. /* translators: %s: dimension unit */
  800. 'description' => sprintf( __( 'Variation height (%s).', 'woocommerce' ), $dimension_unit ),
  801. 'type' => 'string',
  802. 'context' => array( 'view', 'edit' ),
  803. ),
  804. ),
  805. ),
  806. 'shipping_class' => array(
  807. 'description' => __( 'Shipping class slug.', 'woocommerce' ),
  808. 'type' => 'string',
  809. 'context' => array( 'view', 'edit' ),
  810. ),
  811. 'shipping_class_id' => array(
  812. 'description' => __( 'Shipping class ID.', 'woocommerce' ),
  813. 'type' => 'string',
  814. 'context' => array( 'view', 'edit' ),
  815. 'readonly' => true,
  816. ),
  817. 'image' => array(
  818. 'description' => __( 'Variation image data.', 'woocommerce' ),
  819. 'type' => 'object',
  820. 'context' => array( 'view', 'edit' ),
  821. 'properties' => array(
  822. 'id' => array(
  823. 'description' => __( 'Image ID.', 'woocommerce' ),
  824. 'type' => 'integer',
  825. 'context' => array( 'view', 'edit' ),
  826. ),
  827. 'date_created' => array(
  828. 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ),
  829. 'type' => 'date-time',
  830. 'context' => array( 'view', 'edit' ),
  831. 'readonly' => true,
  832. ),
  833. 'date_created_gmt' => array(
  834. 'description' => __( 'The date the image was created, as GMT.', 'woocommerce' ),
  835. 'type' => 'date-time',
  836. 'context' => array( 'view', 'edit' ),
  837. 'readonly' => true,
  838. ),
  839. 'date_modified' => array(
  840. 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ),
  841. 'type' => 'date-time',
  842. 'context' => array( 'view', 'edit' ),
  843. 'readonly' => true,
  844. ),
  845. 'date_modified_gmt' => array(
  846. 'description' => __( 'The date the image was last modified, as GMT.', 'woocommerce' ),
  847. 'type' => 'date-time',
  848. 'context' => array( 'view', 'edit' ),
  849. 'readonly' => true,
  850. ),
  851. 'src' => array(
  852. 'description' => __( 'Image URL.', 'woocommerce' ),
  853. 'type' => 'string',
  854. 'format' => 'uri',
  855. 'context' => array( 'view', 'edit' ),
  856. ),
  857. 'name' => array(
  858. 'description' => __( 'Image name.', 'woocommerce' ),
  859. 'type' => 'string',
  860. 'context' => array( 'view', 'edit' ),
  861. ),
  862. 'alt' => array(
  863. 'description' => __( 'Image alternative text.', 'woocommerce' ),
  864. 'type' => 'string',
  865. 'context' => array( 'view', 'edit' ),
  866. ),
  867. 'position' => array(
  868. 'description' => __( 'Image position. 0 means that the image is featured.', 'woocommerce' ),
  869. 'type' => 'integer',
  870. 'context' => array( 'view', 'edit' ),
  871. ),
  872. ),
  873. ),
  874. 'attributes' => array(
  875. 'description' => __( 'List of attributes.', 'woocommerce' ),
  876. 'type' => 'array',
  877. 'context' => array( 'view', 'edit' ),
  878. 'items' => array(
  879. 'type' => 'object',
  880. 'properties' => array(
  881. 'id' => array(
  882. 'description' => __( 'Attribute ID.', 'woocommerce' ),
  883. 'type' => 'integer',
  884. 'context' => array( 'view', 'edit' ),
  885. ),
  886. 'name' => array(
  887. 'description' => __( 'Attribute name.', 'woocommerce' ),
  888. 'type' => 'string',
  889. 'context' => array( 'view', 'edit' ),
  890. ),
  891. 'option' => array(
  892. 'description' => __( 'Selected attribute term name.', 'woocommerce' ),
  893. 'type' => 'string',
  894. 'context' => array( 'view', 'edit' ),
  895. ),
  896. ),
  897. ),
  898. ),
  899. 'menu_order' => array(
  900. 'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ),
  901. 'type' => 'integer',
  902. 'context' => array( 'view', 'edit' ),
  903. ),
  904. 'meta_data' => array(
  905. 'description' => __( 'Meta data.', 'woocommerce' ),
  906. 'type' => 'array',
  907. 'context' => array( 'view', 'edit' ),
  908. 'items' => array(
  909. 'type' => 'object',
  910. 'properties' => array(
  911. 'id' => array(
  912. 'description' => __( 'Meta ID.', 'woocommerce' ),
  913. 'type' => 'integer',
  914. 'context' => array( 'view', 'edit' ),
  915. 'readonly' => true,
  916. ),
  917. 'key' => array(
  918. 'description' => __( 'Meta key.', 'woocommerce' ),
  919. 'type' => 'string',
  920. 'context' => array( 'view', 'edit' ),
  921. ),
  922. 'value' => array(
  923. 'description' => __( 'Meta value.', 'woocommerce' ),
  924. 'type' => 'mixed',
  925. 'context' => array( 'view', 'edit' ),
  926. ),
  927. ),
  928. ),
  929. ),
  930. ),
  931. );
  932. return $this->add_additional_fields_schema( $schema );
  933. }
  934. }