PageRenderTime 33ms CodeModel.GetById 1ms RepoModel.GetById 0ms app.codeStats 0ms

/wp-content/plugins/woocommerce/includes/api/v2/class-wc-api-products.php

https://gitlab.com/webkod3r/tripolis
PHP | 1440 lines | 927 code | 260 blank | 253 comment | 237 complexity | ee7f92086dcff488b18317d87290bf82 MD5 | raw file
  1. <?php
  2. /**
  3. * WooCommerce API Products Class
  4. *
  5. * Handles requests to the /products endpoint
  6. *
  7. * @author WooThemes
  8. * @category API
  9. * @package WooCommerce/API
  10. * @since 2.1
  11. */
  12. if ( ! defined( 'ABSPATH' ) ) {
  13. exit; // Exit if accessed directly
  14. }
  15. class WC_API_Products extends WC_API_Resource {
  16. /** @var string $base the route base */
  17. protected $base = '/products';
  18. /**
  19. * Register the routes for this class
  20. *
  21. * GET/POST /products
  22. * GET /products/count
  23. * GET/PUT/DELETE /products/<id>
  24. * GET /products/<id>/reviews
  25. *
  26. * @since 2.1
  27. * @param array $routes
  28. * @return array
  29. */
  30. public function register_routes( $routes ) {
  31. # GET/POST /products
  32. $routes[ $this->base ] = array(
  33. array( array( $this, 'get_products' ), WC_API_Server::READABLE ),
  34. array( array( $this, 'create_product' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ),
  35. );
  36. # GET /products/count
  37. $routes[ $this->base . '/count'] = array(
  38. array( array( $this, 'get_products_count' ), WC_API_Server::READABLE ),
  39. );
  40. # GET/PUT/DELETE /products/<id>
  41. $routes[ $this->base . '/(?P<id>\d+)' ] = array(
  42. array( array( $this, 'get_product' ), WC_API_Server::READABLE ),
  43. array( array( $this, 'edit_product' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
  44. array( array( $this, 'delete_product' ), WC_API_Server::DELETABLE ),
  45. );
  46. # GET /products/<id>/reviews
  47. $routes[ $this->base . '/(?P<id>\d+)/reviews' ] = array(
  48. array( array( $this, 'get_product_reviews' ), WC_API_Server::READABLE ),
  49. );
  50. # GET /products/<id>/orders
  51. $routes[ $this->base . '/(?P<id>\d+)/orders' ] = array(
  52. array( array( $this, 'get_product_orders' ), WC_API_Server::READABLE ),
  53. );
  54. # GET /products/categories
  55. $routes[ $this->base . '/categories' ] = array(
  56. array( array( $this, 'get_product_categories' ), WC_API_Server::READABLE ),
  57. );
  58. # GET /products/categories/<id>
  59. $routes[ $this->base . '/categories/(?P<id>\d+)' ] = array(
  60. array( array( $this, 'get_product_category' ), WC_API_Server::READABLE ),
  61. );
  62. # GET/POST /products/attributes
  63. $routes[ $this->base . '/attributes' ] = array(
  64. array( array( $this, 'get_product_attributes' ), WC_API_Server::READABLE ),
  65. array( array( $this, 'create_product_attribute' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ),
  66. );
  67. # GET/PUT/DELETE /attributes/<id>
  68. $routes[ $this->base . '/attributes/(?P<id>\d+)' ] = array(
  69. array( array( $this, 'get_product_attribute' ), WC_API_Server::READABLE ),
  70. array( array( $this, 'edit_product_attribute' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
  71. array( array( $this, 'delete_product_attribute' ), WC_API_Server::DELETABLE ),
  72. );
  73. # GET /products/sku/<product sku>
  74. $routes[ $this->base . '/sku/(?P<sku>\w[\w\s\-]*)' ] = array(
  75. array( array( $this, 'get_product_by_sku' ), WC_API_Server::READABLE ),
  76. );
  77. # POST|PUT /products/bulk
  78. $routes[ $this->base . '/bulk' ] = array(
  79. array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
  80. );
  81. return $routes;
  82. }
  83. /**
  84. * Get all products
  85. *
  86. * @since 2.1
  87. * @param string $fields
  88. * @param string $type
  89. * @param array $filter
  90. * @param int $page
  91. * @return array
  92. */
  93. public function get_products( $fields = null, $type = null, $filter = array(), $page = 1 ) {
  94. if ( ! empty( $type ) ) {
  95. $filter['type'] = $type;
  96. }
  97. $filter['page'] = $page;
  98. $query = $this->query_products( $filter );
  99. $products = array();
  100. foreach ( $query->posts as $product_id ) {
  101. if ( ! $this->is_readable( $product_id ) ) {
  102. continue;
  103. }
  104. $products[] = current( $this->get_product( $product_id, $fields ) );
  105. }
  106. $this->server->add_pagination_headers( $query );
  107. return array( 'products' => $products );
  108. }
  109. /**
  110. * Get the product for the given ID
  111. *
  112. * @since 2.1
  113. * @param int $id the product ID
  114. * @param string $fields
  115. * @return array
  116. */
  117. public function get_product( $id, $fields = null ) {
  118. $id = $this->validate_request( $id, 'product', 'read' );
  119. if ( is_wp_error( $id ) ) {
  120. return $id;
  121. }
  122. $product = wc_get_product( $id );
  123. // add data that applies to every product type
  124. $product_data = $this->get_product_data( $product );
  125. // add variations to variable products
  126. if ( $product->is_type( 'variable' ) && $product->has_child() ) {
  127. $product_data['variations'] = $this->get_variation_data( $product );
  128. }
  129. // add the parent product data to an individual variation
  130. if ( $product->is_type( 'variation' ) && $product->parent ) {
  131. $product_data['parent'] = $this->get_product_data( $product->parent );
  132. }
  133. return array( 'product' => apply_filters( 'woocommerce_api_product_response', $product_data, $product, $fields, $this->server ) );
  134. }
  135. /**
  136. * Get the total number of products
  137. *
  138. * @since 2.1
  139. * @param string $type
  140. * @param array $filter
  141. * @return array
  142. */
  143. public function get_products_count( $type = null, $filter = array() ) {
  144. try {
  145. if ( ! current_user_can( 'read_private_products' ) ) {
  146. throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_products_count', __( 'You do not have permission to read the products count', 'woocommerce' ), 401 );
  147. }
  148. if ( ! empty( $type ) ) {
  149. $filter['type'] = $type;
  150. }
  151. $query = $this->query_products( $filter );
  152. return array( 'count' => (int) $query->found_posts );
  153. } catch ( WC_API_Exception $e ) {
  154. return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
  155. }
  156. }
  157. /**
  158. * Create a new product
  159. *
  160. * @since 2.2
  161. * @param array $data posted data
  162. * @return array
  163. */
  164. public function create_product( $data ) {
  165. $id = 0;
  166. try {
  167. if ( ! isset( $data['product'] ) ) {
  168. throw new WC_API_Exception( 'woocommerce_api_missing_product_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product' ), 400 );
  169. }
  170. $data = $data['product'];
  171. // Check permissions
  172. if ( ! current_user_can( 'publish_products' ) ) {
  173. throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product', __( 'You do not have permission to create products', 'woocommerce' ), 401 );
  174. }
  175. $data = apply_filters( 'woocommerce_api_create_product_data', $data, $this );
  176. // Check if product title is specified
  177. if ( ! isset( $data['title'] ) ) {
  178. throw new WC_API_Exception( 'woocommerce_api_missing_product_title', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'title' ), 400 );
  179. }
  180. // Check product type
  181. if ( ! isset( $data['type'] ) ) {
  182. $data['type'] = 'simple';
  183. }
  184. // Set visible visibility when not sent
  185. if ( ! isset( $data['catalog_visibility'] ) ) {
  186. $data['catalog_visibility'] = 'visible';
  187. }
  188. // Validate the product type
  189. if ( ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) {
  190. throw new WC_API_Exception( 'woocommerce_api_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_product_types() ) ) ), 400 );
  191. }
  192. // Enable description html tags.
  193. $post_content = isset( $data['description'] ) ? wc_clean( $data['description'] ) : '';
  194. if ( $post_content && isset( $data['enable_html_description'] ) && true === $data['enable_html_description'] ) {
  195. $post_content = $data['description'];
  196. }
  197. // Enable short description html tags.
  198. $post_excerpt = isset( $data['short_description'] ) ? wc_clean( $data['short_description'] ) : '';
  199. if ( $post_excerpt && isset( $data['enable_html_short_description'] ) && true === $data['enable_html_short_description'] ) {
  200. $post_excerpt = $data['short_description'];
  201. }
  202. $new_product = array(
  203. 'post_title' => wc_clean( $data['title'] ),
  204. 'post_status' => ( isset( $data['status'] ) ? wc_clean( $data['status'] ) : 'publish' ),
  205. 'post_type' => 'product',
  206. 'post_excerpt' => ( isset( $data['short_description'] ) ? $post_excerpt : '' ),
  207. 'post_content' => ( isset( $data['description'] ) ? $post_content : '' ),
  208. 'post_author' => get_current_user_id(),
  209. );
  210. // Attempts to create the new product
  211. $id = wp_insert_post( $new_product, true );
  212. // Checks for an error in the product creation
  213. if ( is_wp_error( $id ) ) {
  214. throw new WC_API_Exception( 'woocommerce_api_cannot_create_product', $id->get_error_message(), 400 );
  215. }
  216. // Check for featured/gallery images, upload it and set it
  217. if ( isset( $data['images'] ) ) {
  218. $this->save_product_images( $id, $data['images'] );
  219. }
  220. // Save product meta fields
  221. $this->save_product_meta( $id, $data );
  222. // Save variations
  223. if ( isset( $data['type'] ) && 'variable' == $data['type'] && isset( $data['variations'] ) && is_array( $data['variations'] ) ) {
  224. $this->save_variations( $id, $data );
  225. }
  226. do_action( 'woocommerce_api_create_product', $id, $data );
  227. // Clear cache/transients
  228. wc_delete_product_transients( $id );
  229. $this->server->send_status( 201 );
  230. return $this->get_product( $id );
  231. } catch ( WC_API_Exception $e ) {
  232. // Remove the product when fails
  233. $this->clear_product( $id );
  234. return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
  235. }
  236. }
  237. /**
  238. * Edit a product
  239. *
  240. * @since 2.2
  241. * @param int $id the product ID
  242. * @param array $data
  243. * @return array
  244. */
  245. public function edit_product( $id, $data ) {
  246. try {
  247. if ( ! isset( $data['product'] ) ) {
  248. throw new WC_API_Exception( 'woocommerce_api_missing_product_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product' ), 400 );
  249. }
  250. $data = $data['product'];
  251. $id = $this->validate_request( $id, 'product', 'edit' );
  252. if ( is_wp_error( $id ) ) {
  253. return $id;
  254. }
  255. $data = apply_filters( 'woocommerce_api_edit_product_data', $data, $this );
  256. // Product title.
  257. if ( isset( $data['title'] ) ) {
  258. wp_update_post( array( 'ID' => $id, 'post_title' => wc_clean( $data['title'] ) ) );
  259. }
  260. // Product name (slug).
  261. if ( isset( $data['name'] ) ) {
  262. wp_update_post( array( 'ID' => $id, 'post_name' => sanitize_title( $data['name'] ) ) );
  263. }
  264. // Product status.
  265. if ( isset( $data['status'] ) ) {
  266. wp_update_post( array( 'ID' => $id, 'post_status' => wc_clean( $data['status'] ) ) );
  267. }
  268. // Product short description.
  269. if ( isset( $data['short_description'] ) ) {
  270. // Enable short description html tags.
  271. $post_excerpt = ( isset( $data['enable_html_short_description'] ) && true === $data['enable_html_short_description'] ) ? $data['short_description'] : wc_clean( $data['short_description'] );
  272. wp_update_post( array( 'ID' => $id, 'post_excerpt' => $post_excerpt ) );
  273. }
  274. // Product description.
  275. if ( isset( $data['description'] ) ) {
  276. // Enable description html tags.
  277. $post_content = ( isset( $data['enable_html_description'] ) && true === $data['enable_html_description'] ) ? $data['description'] : wc_clean( $data['description'] );
  278. wp_update_post( array( 'ID' => $id, 'post_content' => $post_content ) );
  279. }
  280. // Validate the product type
  281. if ( isset( $data['type'] ) && ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) {
  282. throw new WC_API_Exception( 'woocommerce_api_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_product_types() ) ) ), 400 );
  283. }
  284. // Check for featured/gallery images, upload it and set it
  285. if ( isset( $data['images'] ) ) {
  286. $this->save_product_images( $id, $data['images'] );
  287. }
  288. // Save product meta fields
  289. $this->save_product_meta( $id, $data );
  290. // Save variations
  291. $product = get_product( $id );
  292. if ( $product->is_type( 'variable' ) ) {
  293. if ( isset( $data['variations'] ) && is_array( $data['variations'] ) ) {
  294. $this->save_variations( $id, $data );
  295. } else {
  296. // Just sync variations
  297. WC_Product_Variable::sync( $id );
  298. }
  299. }
  300. do_action( 'woocommerce_api_edit_product', $id, $data );
  301. // Clear cache/transients
  302. wc_delete_product_transients( $id );
  303. return $this->get_product( $id );
  304. } catch ( WC_API_Exception $e ) {
  305. return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
  306. }
  307. }
  308. /**
  309. * Delete a product.
  310. *
  311. * @since 2.2
  312. * @param int $id the product ID.
  313. * @param bool $force true to permanently delete order, false to move to trash.
  314. * @return array
  315. */
  316. public function delete_product( $id, $force = false ) {
  317. $id = $this->validate_request( $id, 'product', 'delete' );
  318. if ( is_wp_error( $id ) ) {
  319. return $id;
  320. }
  321. do_action( 'woocommerce_api_delete_product', $id, $this );
  322. $parent_id = wp_get_post_parent_id( $id );
  323. $result = ( $force ) ? wp_delete_post( $id, true ) : wp_trash_post( $id );
  324. if ( ! $result ) {
  325. return new WP_Error( 'woocommerce_api_cannot_delete_product', sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), 'product' ), array( 'status' => 500 ) );
  326. }
  327. // Delete parent product transients.
  328. if ( $parent_id ) {
  329. wc_delete_product_transients( $parent_id );
  330. }
  331. if ( $force ) {
  332. return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), 'product' ) );
  333. } else {
  334. $this->server->send_status( '202' );
  335. return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product' ) );
  336. }
  337. }
  338. /**
  339. * Get the reviews for a product
  340. *
  341. * @since 2.1
  342. * @param int $id the product ID to get reviews for
  343. * @param string $fields fields to include in response
  344. * @return array
  345. */
  346. public function get_product_reviews( $id, $fields = null ) {
  347. $id = $this->validate_request( $id, 'product', 'read' );
  348. if ( is_wp_error( $id ) ) {
  349. return $id;
  350. }
  351. $comments = get_approved_comments( $id );
  352. $reviews = array();
  353. foreach ( $comments as $comment ) {
  354. $reviews[] = array(
  355. 'id' => intval( $comment->comment_ID ),
  356. 'created_at' => $this->server->format_datetime( $comment->comment_date_gmt ),
  357. 'review' => $comment->comment_content,
  358. 'rating' => get_comment_meta( $comment->comment_ID, 'rating', true ),
  359. 'reviewer_name' => $comment->comment_author,
  360. 'reviewer_email' => $comment->comment_author_email,
  361. 'verified' => wc_review_is_from_verified_owner( $comment->comment_ID ),
  362. );
  363. }
  364. return array( 'product_reviews' => apply_filters( 'woocommerce_api_product_reviews_response', $reviews, $id, $fields, $comments, $this->server ) );
  365. }
  366. /**
  367. * Get the orders for a product
  368. *
  369. * @since 2.4.0
  370. * @param int $id the product ID to get orders for
  371. * @param string fields fields to retrieve
  372. * @param string $filter filters to include in response
  373. * @param string $status the order status to retrieve
  374. * @param $page $page page to retrieve
  375. * @return array
  376. */
  377. public function get_product_orders( $id, $fields = null, $filter = array(), $status = null, $page = 1 ) {
  378. global $wpdb;
  379. $id = $this->validate_request( $id, 'product', 'read' );
  380. if ( is_wp_error( $id ) ) {
  381. return $id;
  382. }
  383. $order_ids = $wpdb->get_col( $wpdb->prepare( "
  384. SELECT order_id
  385. FROM {$wpdb->prefix}woocommerce_order_items
  386. WHERE order_item_id IN ( SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_product_id' AND meta_value = %d )
  387. AND order_item_type = 'line_item'
  388. ", $id ) );
  389. if ( empty( $order_ids ) ) {
  390. return array( 'orders' => array() );
  391. }
  392. $filter = array_merge( $filter, array(
  393. 'in' => implode( ',', $order_ids )
  394. ) );
  395. $orders = WC()->api->WC_API_Orders->get_orders( $fields, $filter, $status, $page );
  396. return array( 'orders' => apply_filters( 'woocommerce_api_product_orders_response', $orders['orders'], $id, $filter, $fields, $this->server ) );
  397. }
  398. /**
  399. * Get a listing of product categories
  400. *
  401. * @since 2.2
  402. * @param string|null $fields fields to limit response to
  403. * @return array
  404. */
  405. public function get_product_categories( $fields = null ) {
  406. try {
  407. // Permissions check
  408. if ( ! current_user_can( 'manage_product_terms' ) ) {
  409. throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product categories', 'woocommerce' ), 401 );
  410. }
  411. $product_categories = array();
  412. $terms = get_terms( 'product_cat', array( 'hide_empty' => false, 'fields' => 'ids' ) );
  413. foreach ( $terms as $term_id ) {
  414. $product_categories[] = current( $this->get_product_category( $term_id, $fields ) );
  415. }
  416. return array( 'product_categories' => apply_filters( 'woocommerce_api_product_categories_response', $product_categories, $terms, $fields, $this ) );
  417. } catch ( WC_API_Exception $e ) {
  418. return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
  419. }
  420. }
  421. /**
  422. * Get the product category for the given ID
  423. *
  424. * @since 2.2
  425. * @param string $id product category term ID
  426. * @param string|null $fields fields to limit response to
  427. * @return array
  428. */
  429. public function get_product_category( $id, $fields = null ) {
  430. try {
  431. $id = absint( $id );
  432. // Validate ID
  433. if ( empty( $id ) ) {
  434. throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_id', __( 'Invalid product category ID', 'woocommerce' ), 400 );
  435. }
  436. // Permissions check
  437. if ( ! current_user_can( 'manage_product_terms' ) ) {
  438. throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product categories', 'woocommerce' ), 401 );
  439. }
  440. $term = get_term( $id, 'product_cat' );
  441. if ( is_wp_error( $term ) || is_null( $term ) ) {
  442. throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_id', __( 'A product category with the provided ID could not be found', 'woocommerce' ), 404 );
  443. }
  444. $term_id = intval( $term->term_id );
  445. // Get category display type
  446. $display_type = get_woocommerce_term_meta( $term_id, 'display_type' );
  447. // Get category image
  448. $image = '';
  449. if ( $image_id = get_woocommerce_term_meta( $term_id, 'thumbnail_id' ) ) {
  450. $image = wp_get_attachment_url( $image_id );
  451. }
  452. $product_category = array(
  453. 'id' => $term_id,
  454. 'name' => $term->name,
  455. 'slug' => $term->slug,
  456. 'parent' => $term->parent,
  457. 'description' => $term->description,
  458. 'display' => $display_type ? $display_type : 'default',
  459. 'image' => $image ? esc_url( $image ) : '',
  460. 'count' => intval( $term->count )
  461. );
  462. return array( 'product_category' => apply_filters( 'woocommerce_api_product_category_response', $product_category, $id, $fields, $term, $this ) );
  463. } catch ( WC_API_Exception $e ) {
  464. return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
  465. }
  466. }
  467. /**
  468. * Helper method to get product post objects
  469. *
  470. * @since 2.1
  471. * @param array $args request arguments for filtering query
  472. * @return WP_Query
  473. */
  474. private function query_products( $args ) {
  475. // Set base query arguments
  476. $query_args = array(
  477. 'fields' => 'ids',
  478. 'post_type' => 'product',
  479. 'post_status' => 'publish',
  480. 'meta_query' => array(),
  481. );
  482. if ( ! empty( $args['type'] ) ) {
  483. $types = explode( ',', $args['type'] );
  484. $query_args['tax_query'] = array(
  485. array(
  486. 'taxonomy' => 'product_type',
  487. 'field' => 'slug',
  488. 'terms' => $types,
  489. ),
  490. );
  491. unset( $args['type'] );
  492. }
  493. // Filter products by category
  494. if ( ! empty( $args['category'] ) ) {
  495. $query_args['product_cat'] = $args['category'];
  496. }
  497. // Filter by specific sku
  498. if ( ! empty( $args['sku'] ) ) {
  499. if ( ! is_array( $query_args['meta_query'] ) ) {
  500. $query_args['meta_query'] = array();
  501. }
  502. $query_args['meta_query'][] = array(
  503. 'key' => '_sku',
  504. 'value' => $args['sku'],
  505. 'compare' => '='
  506. );
  507. $query_args['post_type'] = array( 'product', 'product_variation' );
  508. }
  509. $query_args = $this->merge_query_args( $query_args, $args );
  510. return new WP_Query( $query_args );
  511. }
  512. /**
  513. * Get standard product data that applies to every product type
  514. *
  515. * @since 2.1
  516. * @param WC_Product $product
  517. * @return WC_Product
  518. */
  519. private function get_product_data( $product ) {
  520. $prices_precision = wc_get_price_decimals();
  521. return array(
  522. 'title' => $product->get_title(),
  523. 'id' => (int) $product->is_type( 'variation' ) ? $product->get_variation_id() : $product->id,
  524. 'created_at' => $this->server->format_datetime( $product->get_post_data()->post_date_gmt ),
  525. 'updated_at' => $this->server->format_datetime( $product->get_post_data()->post_modified_gmt ),
  526. 'type' => $product->product_type,
  527. 'status' => $product->get_post_data()->post_status,
  528. 'downloadable' => $product->is_downloadable(),
  529. 'virtual' => $product->is_virtual(),
  530. 'permalink' => $product->get_permalink(),
  531. 'sku' => $product->get_sku(),
  532. 'price' => wc_format_decimal( $product->get_price(), $prices_precision ),
  533. 'regular_price' => wc_format_decimal( $product->get_regular_price(), $prices_precision ),
  534. 'sale_price' => $product->get_sale_price() ? wc_format_decimal( $product->get_sale_price(), $prices_precision ) : null,
  535. 'price_html' => $product->get_price_html(),
  536. 'taxable' => $product->is_taxable(),
  537. 'tax_status' => $product->get_tax_status(),
  538. 'tax_class' => $product->get_tax_class(),
  539. 'managing_stock' => $product->managing_stock(),
  540. 'stock_quantity' => (int) $product->get_stock_quantity(),
  541. 'in_stock' => $product->is_in_stock(),
  542. 'backorders_allowed' => $product->backorders_allowed(),
  543. 'backordered' => $product->is_on_backorder(),
  544. 'sold_individually' => $product->is_sold_individually(),
  545. 'purchaseable' => $product->is_purchasable(),
  546. 'featured' => $product->is_featured(),
  547. 'visible' => $product->is_visible(),
  548. 'catalog_visibility' => $product->visibility,
  549. 'on_sale' => $product->is_on_sale(),
  550. 'product_url' => $product->is_type( 'external' ) ? $product->get_product_url() : '',
  551. 'button_text' => $product->is_type( 'external' ) ? $product->get_button_text() : '',
  552. 'weight' => $product->get_weight() ? wc_format_decimal( $product->get_weight(), 2 ) : null,
  553. 'dimensions' => array(
  554. 'length' => $product->length,
  555. 'width' => $product->width,
  556. 'height' => $product->height,
  557. 'unit' => get_option( 'woocommerce_dimension_unit' ),
  558. ),
  559. 'shipping_required' => $product->needs_shipping(),
  560. 'shipping_taxable' => $product->is_shipping_taxable(),
  561. 'shipping_class' => $product->get_shipping_class(),
  562. 'shipping_class_id' => ( 0 !== $product->get_shipping_class_id() ) ? $product->get_shipping_class_id() : null,
  563. 'description' => wpautop( do_shortcode( $product->get_post_data()->post_content ) ),
  564. 'short_description' => apply_filters( 'woocommerce_short_description', $product->get_post_data()->post_excerpt ),
  565. 'reviews_allowed' => ( 'open' === $product->get_post_data()->comment_status ),
  566. 'average_rating' => wc_format_decimal( $product->get_average_rating(), 2 ),
  567. 'rating_count' => (int) $product->get_rating_count(),
  568. 'related_ids' => array_map( 'absint', array_values( $product->get_related() ) ),
  569. 'upsell_ids' => array_map( 'absint', $product->get_upsells() ),
  570. 'cross_sell_ids' => array_map( 'absint', $product->get_cross_sells() ),
  571. 'parent_id' => $product->post->post_parent,
  572. 'categories' => wp_get_post_terms( $product->id, 'product_cat', array( 'fields' => 'names' ) ),
  573. 'tags' => wp_get_post_terms( $product->id, 'product_tag', array( 'fields' => 'names' ) ),
  574. 'images' => $this->get_images( $product ),
  575. 'featured_src' => (string) wp_get_attachment_url( get_post_thumbnail_id( $product->is_type( 'variation' ) ? $product->variation_id : $product->id ) ),
  576. 'attributes' => $this->get_attributes( $product ),
  577. 'downloads' => $this->get_downloads( $product ),
  578. 'download_limit' => (int) $product->download_limit,
  579. 'download_expiry' => (int) $product->download_expiry,
  580. 'download_type' => $product->download_type,
  581. 'purchase_note' => wpautop( do_shortcode( wp_kses_post( $product->purchase_note ) ) ),
  582. 'total_sales' => metadata_exists( 'post', $product->id, 'total_sales' ) ? (int) get_post_meta( $product->id, 'total_sales', true ) : 0,
  583. 'variations' => array(),
  584. 'parent' => array(),
  585. );
  586. }
  587. /**
  588. * Get an individual variation's data
  589. *
  590. * @since 2.1
  591. * @param WC_Product $product
  592. * @return array
  593. */
  594. private function get_variation_data( $product ) {
  595. $prices_precision = wc_get_price_decimals();
  596. $variations = array();
  597. foreach ( $product->get_children() as $child_id ) {
  598. $variation = $product->get_child( $child_id );
  599. if ( ! $variation->exists() ) {
  600. continue;
  601. }
  602. $variations[] = array(
  603. 'id' => $variation->get_variation_id(),
  604. 'created_at' => $this->server->format_datetime( $variation->get_post_data()->post_date_gmt ),
  605. 'updated_at' => $this->server->format_datetime( $variation->get_post_data()->post_modified_gmt ),
  606. 'downloadable' => $variation->is_downloadable(),
  607. 'virtual' => $variation->is_virtual(),
  608. 'permalink' => $variation->get_permalink(),
  609. 'sku' => $variation->get_sku(),
  610. 'price' => wc_format_decimal( $variation->get_price(), $prices_precision ),
  611. 'regular_price' => wc_format_decimal( $variation->get_regular_price(), $prices_precision ),
  612. 'sale_price' => $variation->get_sale_price() ? wc_format_decimal( $variation->get_sale_price(), $prices_precision ) : null,
  613. 'taxable' => $variation->is_taxable(),
  614. 'tax_status' => $variation->get_tax_status(),
  615. 'tax_class' => $variation->get_tax_class(),
  616. 'managing_stock' => $variation->managing_stock(),
  617. 'stock_quantity' => (int) $variation->get_stock_quantity(),
  618. 'in_stock' => $variation->is_in_stock(),
  619. 'backordered' => $variation->is_on_backorder(),
  620. 'purchaseable' => $variation->is_purchasable(),
  621. 'visible' => $variation->variation_is_visible(),
  622. 'on_sale' => $variation->is_on_sale(),
  623. 'weight' => $variation->get_weight() ? wc_format_decimal( $variation->get_weight(), 2 ) : null,
  624. 'dimensions' => array(
  625. 'length' => $variation->length,
  626. 'width' => $variation->width,
  627. 'height' => $variation->height,
  628. 'unit' => get_option( 'woocommerce_dimension_unit' ),
  629. ),
  630. 'shipping_class' => $variation->get_shipping_class(),
  631. 'shipping_class_id' => ( 0 !== $variation->get_shipping_class_id() ) ? $variation->get_shipping_class_id() : null,
  632. 'image' => $this->get_images( $variation ),
  633. 'attributes' => $this->get_attributes( $variation ),
  634. 'downloads' => $this->get_downloads( $variation ),
  635. 'download_limit' => (int) $product->download_limit,
  636. 'download_expiry' => (int) $product->download_expiry,
  637. );
  638. }
  639. return $variations;
  640. }
  641. /**
  642. * Save product meta
  643. *
  644. * @since 2.2
  645. * @param int $product_id
  646. * @param array $data
  647. * @return bool
  648. * @throws WC_API_Exception
  649. */
  650. protected function save_product_meta( $product_id, $data ) {
  651. global $wpdb;
  652. // Product Type
  653. $product_type = null;
  654. if ( isset( $data['type'] ) ) {
  655. $product_type = wc_clean( $data['type'] );
  656. wp_set_object_terms( $product_id, $product_type, 'product_type' );
  657. } else {
  658. $_product_type = get_the_terms( $product_id, 'product_type' );
  659. if ( is_array( $_product_type ) ) {
  660. $_product_type = current( $_product_type );
  661. $product_type = $_product_type->slug;
  662. }
  663. }
  664. // Virtual
  665. if ( isset( $data['virtual'] ) ) {
  666. update_post_meta( $product_id, '_virtual', ( true === $data['virtual'] ) ? 'yes' : 'no' );
  667. }
  668. // Tax status
  669. if ( isset( $data['tax_status'] ) ) {
  670. update_post_meta( $product_id, '_tax_status', wc_clean( $data['tax_status'] ) );
  671. }
  672. // Tax Class
  673. if ( isset( $data['tax_class'] ) ) {
  674. update_post_meta( $product_id, '_tax_class', wc_clean( $data['tax_class'] ) );
  675. }
  676. // Catalog Visibility
  677. if ( isset( $data['catalog_visibility'] ) ) {
  678. update_post_meta( $product_id, '_visibility', wc_clean( $data['catalog_visibility'] ) );
  679. }
  680. // Purchase Note
  681. if ( isset( $data['purchase_note'] ) ) {
  682. update_post_meta( $product_id, '_purchase_note', wc_clean( $data['purchase_note'] ) );
  683. }
  684. // Featured Product
  685. if ( isset( $data['featured'] ) ) {
  686. update_post_meta( $product_id, '_featured', ( true === $data['featured'] ) ? 'yes' : 'no' );
  687. }
  688. // Shipping data
  689. $this->save_product_shipping_data( $product_id, $data );
  690. // SKU
  691. if ( isset( $data['sku'] ) ) {
  692. $sku = get_post_meta( $product_id, '_sku', true );
  693. $new_sku = wc_clean( $data['sku'] );
  694. if ( '' == $new_sku ) {
  695. update_post_meta( $product_id, '_sku', '' );
  696. } elseif ( $new_sku !== $sku ) {
  697. if ( ! empty( $new_sku ) ) {
  698. $unique_sku = wc_product_has_unique_sku( $product_id, $new_sku );
  699. if ( ! $unique_sku ) {
  700. throw new WC_API_Exception( 'woocommerce_api_product_sku_already_exists', __( 'The SKU already exists on another product', 'woocommerce' ), 400 );
  701. } else {
  702. update_post_meta( $product_id, '_sku', $new_sku );
  703. }
  704. } else {
  705. update_post_meta( $product_id, '_sku', '' );
  706. }
  707. }
  708. }
  709. // Attributes
  710. if ( isset( $data['attributes'] ) ) {
  711. $attributes = array();
  712. foreach ( $data['attributes'] as $attribute ) {
  713. $is_taxonomy = 0;
  714. $taxonomy = 0;
  715. if ( ! isset( $attribute['name'] ) ) {
  716. continue;
  717. }
  718. $attribute_slug = sanitize_title( $attribute['name'] );
  719. if ( isset( $attribute['slug'] ) ) {
  720. $taxonomy = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] );
  721. $attribute_slug = sanitize_title( $attribute['slug'] );
  722. }
  723. if ( $taxonomy ) {
  724. $is_taxonomy = 1;
  725. }
  726. if ( $is_taxonomy ) {
  727. if ( isset( $attribute['options'] ) ) {
  728. $options = $attribute['options'];
  729. if ( ! is_array( $attribute['options'] ) ) {
  730. // Text based attributes - Posted values are term names
  731. $options = explode( WC_DELIMITER, $options );
  732. }
  733. $values = array_map( 'wc_sanitize_term_text_based', $options );
  734. $values = array_filter( $values, 'strlen' );
  735. } else {
  736. $values = array();
  737. }
  738. // Update post terms
  739. if ( taxonomy_exists( $taxonomy ) ) {
  740. wp_set_object_terms( $product_id, $values, $taxonomy );
  741. }
  742. if ( $values ) {
  743. // Add attribute to array, but don't set values
  744. $attributes[ $taxonomy ] = array(
  745. 'name' => $taxonomy,
  746. 'value' => '',
  747. 'position' => isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0,
  748. 'is_visible' => ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0,
  749. 'is_variation' => ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0,
  750. 'is_taxonomy' => $is_taxonomy
  751. );
  752. }
  753. } elseif ( isset( $attribute['options'] ) ) {
  754. // Array based
  755. if ( is_array( $attribute['options'] ) ) {
  756. $values = implode( ' ' . WC_DELIMITER . ' ', array_map( 'wc_clean', $attribute['options'] ) );
  757. // Text based, separate by pipe
  758. } else {
  759. $values = implode( ' ' . WC_DELIMITER . ' ', array_map( 'wc_clean', explode( WC_DELIMITER, $attribute['options'] ) ) );
  760. }
  761. // Custom attribute - Add attribute to array and set the values
  762. $attributes[ $attribute_slug ] = array(
  763. 'name' => wc_clean( $attribute['name'] ),
  764. 'value' => $values,
  765. 'position' => isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0,
  766. 'is_visible' => ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0,
  767. 'is_variation' => ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0,
  768. 'is_taxonomy' => $is_taxonomy
  769. );
  770. }
  771. }
  772. if ( ! function_exists( 'attributes_cmp' ) ) {
  773. function attributes_cmp( $a, $b ) {
  774. if ( $a['position'] == $b['position'] ) {
  775. return 0;
  776. }
  777. return ( $a['position'] < $b['position'] ) ? -1 : 1;
  778. }
  779. }
  780. uasort( $attributes, 'attributes_cmp' );
  781. update_post_meta( $product_id, '_product_attributes', $attributes );
  782. }
  783. // Sales and prices
  784. if ( in_array( $product_type, array( 'variable', 'grouped' ) ) ) {
  785. // Variable and grouped products have no prices
  786. update_post_meta( $product_id, '_regular_price', '' );
  787. update_post_meta( $product_id, '_sale_price', '' );
  788. update_post_meta( $product_id, '_sale_price_dates_from', '' );
  789. update_post_meta( $product_id, '_sale_price_dates_to', '' );
  790. update_post_meta( $product_id, '_price', '' );
  791. } else {
  792. // Regular Price
  793. if ( isset( $data['regular_price'] ) ) {
  794. $regular_price = ( '' === $data['regular_price'] ) ? '' : wc_format_decimal( $data['regular_price'] );
  795. update_post_meta( $product_id, '_regular_price', $regular_price );
  796. } else {
  797. $regular_price = get_post_meta( $product_id, '_regular_price', true );
  798. }
  799. // Sale Price
  800. if ( isset( $data['sale_price'] ) ) {
  801. $sale_price = ( '' === $data['sale_price'] ) ? '' : wc_format_decimal( $data['sale_price'] );
  802. update_post_meta( $product_id, '_sale_price', $sale_price );
  803. } else {
  804. $sale_price = get_post_meta( $product_id, '_sale_price', true );
  805. }
  806. $date_from = isset( $data['sale_price_dates_from'] ) ? strtotime( $data['sale_price_dates_from'] ) : get_post_meta( $product_id, '_sale_price_dates_from', true );
  807. $date_to = isset( $data['sale_price_dates_to'] ) ? strtotime( $data['sale_price_dates_to'] ) : get_post_meta( $product_id, '_sale_price_dates_to', true );
  808. // Dates
  809. if ( $date_from ) {
  810. update_post_meta( $product_id, '_sale_price_dates_from', $date_from );
  811. } else {
  812. update_post_meta( $product_id, '_sale_price_dates_from', '' );
  813. }
  814. if ( $date_to ) {
  815. update_post_meta( $product_id, '_sale_price_dates_to', $date_to );
  816. } else {
  817. update_post_meta( $product_id, '_sale_price_dates_to', '' );
  818. }
  819. if ( $date_to && ! $date_from ) {
  820. $date_from = strtotime( 'NOW', current_time( 'timestamp' ) );
  821. update_post_meta( $product_id, '_sale_price_dates_from', $date_from );
  822. }
  823. // Update price if on sale
  824. if ( '' !== $sale_price && '' == $date_to && '' == $date_from ) {
  825. update_post_meta( $product_id, '_price', wc_format_decimal( $sale_price ) );
  826. } else {
  827. update_post_meta( $product_id, '_price', $regular_price );
  828. }
  829. if ( '' !== $sale_price && $date_from && $date_from <= strtotime( 'NOW', current_time( 'timestamp' ) ) ) {
  830. update_post_meta( $product_id, '_price', wc_format_decimal( $sale_price ) );
  831. }
  832. if ( $date_to && $date_to < strtotime( 'NOW', current_time( 'timestamp' ) ) ) {
  833. update_post_meta( $product_id, '_price', $regular_price );
  834. update_post_meta( $product_id, '_sale_price_dates_from', '' );
  835. update_post_meta( $product_id, '_sale_price_dates_to', '' );
  836. }
  837. }
  838. // Product parent ID for groups
  839. if ( isset( $data['parent_id'] ) ) {
  840. wp_update_post( array( 'ID' => $product_id, 'post_parent' => absint( $data['parent_id'] ) ) );
  841. }
  842. // Update parent if grouped so price sorting works and stays in sync with the cheapest child
  843. $_product = wc_get_product( $product_id );
  844. if ( $_product->post->post_parent > 0 || $product_type == 'grouped' ) {
  845. $clear_parent_ids = array();
  846. if ( $_product->post->post_parent > 0 ) {
  847. $clear_parent_ids[] = $_product->post->post_parent;
  848. }
  849. if ( $product_type == 'grouped' ) {
  850. $clear_parent_ids[] = $product_id;
  851. }
  852. if ( $clear_parent_ids ) {
  853. foreach ( $clear_parent_ids as $clear_id ) {
  854. $children_by_price = get_posts( array(
  855. 'post_parent' => $clear_id,
  856. 'orderby' => 'meta_value_num',
  857. 'order' => 'asc',
  858. 'meta_key' => '_price',
  859. 'posts_per_page' => 1,
  860. 'post_type' => 'product',
  861. 'fields' => 'ids'
  862. ) );
  863. if ( $children_by_price ) {
  864. foreach ( $children_by_price as $child ) {
  865. $child_price = get_post_meta( $child, '_price', true );
  866. update_post_meta( $clear_id, '_price', $child_price );
  867. }
  868. }
  869. }
  870. }
  871. }
  872. // Sold Individually
  873. if ( isset( $data['sold_individually'] ) ) {
  874. update_post_meta( $product_id, '_sold_individually', ( true === $data['sold_individually'] ) ? 'yes' : '' );
  875. }
  876. // Stock status
  877. if ( isset( $data['in_stock'] ) ) {
  878. $stock_status = ( true === $data['in_stock'] ) ? 'instock' : 'outofstock';
  879. } else {
  880. $stock_status = get_post_meta( $product_id, '_stock_status', true );
  881. if ( '' === $stock_status ) {
  882. $stock_status = 'instock';
  883. }
  884. }
  885. // Stock Data
  886. if ( 'yes' == get_option( 'woocommerce_manage_stock' ) ) {
  887. // Manage stock
  888. if ( isset( $data['managing_stock'] ) ) {
  889. $managing_stock = ( true === $data['managing_stock'] ) ? 'yes' : 'no';
  890. update_post_meta( $product_id, '_manage_stock', $managing_stock );
  891. } else {
  892. $managing_stock = get_post_meta( $product_id, '_manage_stock', true );
  893. }
  894. // Backorders
  895. if ( isset( $data['backorders'] ) ) {
  896. if ( 'notify' == $data['backorders'] ) {
  897. $backorders = 'notify';
  898. } else {
  899. $backorders = ( true === $data['backorders'] ) ? 'yes' : 'no';
  900. }
  901. update_post_meta( $product_id, '_backorders', $backorders );
  902. } else {
  903. $backorders = get_post_meta( $product_id, '_backorders', true );
  904. }
  905. if ( 'grouped' == $product_type ) {
  906. update_post_meta( $product_id, '_manage_stock', 'no' );
  907. update_post_meta( $product_id, '_backorders', 'no' );
  908. update_post_meta( $product_id, '_stock', '' );
  909. wc_update_product_stock_status( $product_id, $stock_status );
  910. } elseif ( 'external' == $product_type ) {
  911. update_post_meta( $product_id, '_manage_stock', 'no' );
  912. update_post_meta( $product_id, '_backorders', 'no' );
  913. update_post_meta( $product_id, '_stock', '' );
  914. wc_update_product_stock_status( $product_id, 'instock' );
  915. } elseif ( 'yes' == $managing_stock ) {
  916. update_post_meta( $product_id, '_backorders', $backorders );
  917. wc_update_product_stock_status( $product_id, $stock_status );
  918. // Stock quantity
  919. if ( isset( $data['stock_quantity'] ) ) {
  920. wc_update_product_stock( $product_id, intval( $data['stock_quantity'] ) );
  921. }
  922. } else {
  923. // Don't manage stock
  924. update_post_meta( $product_id, '_manage_stock', 'no' );
  925. update_post_meta( $product_id, '_backorders', $backorders );
  926. update_post_meta( $product_id, '_stock', '' );
  927. wc_update_product_stock_status( $product_id, $stock_status );
  928. }
  929. } else {
  930. wc_update_product_stock_status( $product_id, $stock_status );
  931. }
  932. // Upsells
  933. if ( isset( $data['upsell_ids'] ) ) {
  934. $upsells = array();
  935. $ids = $data['upsell_ids'];
  936. if ( ! empty( $ids ) ) {
  937. foreach ( $ids as $id ) {
  938. if ( $id && $id > 0 ) {
  939. $upsells[] = $id;
  940. }
  941. }
  942. update_post_meta( $product_id, '_upsell_ids', $upsells );
  943. } else {
  944. delete_post_meta( $product_id, '_upsell_ids' );
  945. }
  946. }
  947. // Cross sells
  948. if ( isset( $data['cross_sell_ids'] ) ) {
  949. $crosssells = array();
  950. $ids = $data['cross_sell_ids'];
  951. if ( ! empty( $ids ) ) {
  952. foreach ( $ids as $id ) {
  953. if ( $id && $id > 0 ) {
  954. $crosssells[] = $id;
  955. }
  956. }
  957. update_post_meta( $product_id, '_crosssell_ids', $crosssells );
  958. } else {
  959. delete_post_meta( $product_id, '_crosssell_ids' );
  960. }
  961. }
  962. // Product categories
  963. if ( isset( $data['categories'] ) && is_array( $data['categories'] ) ) {
  964. $term_ids = array_unique( array_map( 'intval', $data['categories'] ) );
  965. wp_set_object_terms( $product_id, $term_ids, 'product_cat' );
  966. }
  967. // Product tags
  968. if ( isset( $data['tags'] ) && is_array( $data['tags'] ) ) {
  969. $term_ids = array_unique( array_map( 'intval', $data['tags'] ) );
  970. wp_set_object_terms( $product_id, $term_ids, 'product_tag' );
  971. }
  972. // Downloadable
  973. if ( isset( $data['downloadable'] ) ) {
  974. $is_downloadable = ( true === $data['downloadable'] ) ? 'yes' : 'no';
  975. update_post_meta( $product_id, '_downloadable', $is_downloadable );
  976. } else {
  977. $is_downloadable = get_post_meta( $product_id, '_downloadable', true );
  978. }
  979. // Downloadable options
  980. if ( 'yes' == $is_downloadable ) {
  981. // Downloadable files
  982. if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) {
  983. $this->save_downloadable_files( $product_id, $data['downloads'] );
  984. }
  985. // Download limit
  986. if ( isset( $data['download_limit'] ) ) {
  987. update_post_meta( $product_id, '_download_limit', ( '' === $data['download_limit'] ) ? '' : absint( $data['download_limit'] ) );
  988. }
  989. // Download expiry
  990. if ( isset( $data['download_expiry'] ) ) {
  991. update_post_meta( $product_id, '_download_expiry', ( '' === $data['download_expiry'] ) ? '' : absint( $data['download_expiry'] ) );
  992. }
  993. // Download type
  994. if ( isset( $data['download_type'] ) ) {
  995. update_post_meta( $product_id, '_download_type', wc_clean( $data['download_type'] ) );
  996. }
  997. }
  998. // Product url
  999. if ( $product_type == 'external' ) {
  1000. if ( isset( $data['product_url'] ) ) {
  1001. update_post_meta( $product_id, '_product_url', wc_clean( $data['product_url'] ) );
  1002. }
  1003. if ( isset( $data['button_text'] ) ) {
  1004. update_post_meta( $product_id, '_button_text', wc_clean( $data['button_text'] ) );
  1005. }
  1006. }
  1007. // Reviews allowed
  1008. if ( isset( $data['reviews_allowed'] ) ) {
  1009. $reviews_allowed = ( true === $data['reviews_allowed'] ) ? 'open' : 'closed';
  1010. $wpdb->update( $wpdb->posts, array( 'comment_status' => $reviews_allowed ), array( 'ID' => $product_id ) );
  1011. }
  1012. // Do action for product type
  1013. do_action( 'woocommerce_api_process_product_meta_' . $product_type, $product_id, $data );
  1014. return true;
  1015. }
  1016. /**
  1017. * Save variations
  1018. *
  1019. * @since 2.2
  1020. * @param int $id
  1021. * @param array $data
  1022. * @return bool
  1023. * @throws WC_API_Exception
  1024. */
  1025. protected function save_variations( $id, $data ) {
  1026. global $wpdb;
  1027. $variations = $data['variations'];
  1028. $attributes = (array) maybe_unserialize( get_post_meta( $id, '_product_attributes', true ) );
  1029. foreach ( $variations as $menu_order => $variation ) {
  1030. $variation_id = isset( $variation['id'] ) ? absint( $variation['id'] ) : 0;
  1031. // Generate a useful post title
  1032. $variation_post_title = sprintf( __( 'Variation #%s of %s', 'woocommerce' ), $variation_id, esc_html( get_the_title( $id ) ) );
  1033. // Update or Add post
  1034. if ( ! $variation_id ) {
  1035. $post_status = ( isset( $variation['visible'] ) && false === $variation['visible'] ) ? 'private' : 'publish';
  1036. $new_variation = array(
  1037. 'post_title' => $variation_post_title,
  1038. 'post_content' => '',
  1039. 'post_status' => $post_status,
  1040. 'post_author' => get_current_user_id(),
  1041. 'post_parent' => $id,
  1042. 'post_type' => 'product_variation',
  1043. 'menu_order' => $menu_order
  1044. );
  1045. $variation_id = wp_insert_post( $new_variation );
  1046. do_action( 'woocommerce_create_product_variation', $variation_id );
  1047. } else {
  1048. $update_variation = array( 'post_title' => $variation_post_title, 'menu_order' => $menu_order );
  1049. if ( isset( $variation['visible'] ) ) {
  1050. $post_status = ( false === $variation['visible'] ) ? 'private' : 'publish';
  1051. $update_variation['post_status'] = $post_status;
  1052. }
  1053. $wpdb->update( $wpdb->posts, $update_variation, array( 'ID' => $variation_id ) );
  1054. do_action( 'woocommerce_update_product_variation', $variation_id );
  1055. }
  1056. // Stop with we don't have a variation ID
  1057. if ( is_wp_error( $variation_id ) ) {
  1058. throw new WC_API_Exception( 'woocommerce_api_cannot_save_product_variation', $variation_id->get_error_message(), 400 );
  1059. }
  1060. // SKU
  1061. if ( isset( $variation['sku'] ) ) {
  1062. $sku = get_post_meta( $variation_id, '_sku', true );
  1063. $new_sku = wc_clean( $variation['sku'] );
  1064. if ( '' == $new_sku ) {
  1065. update_post_meta( $variation_id, '_sku', '' );
  1066. } elseif ( $new_sku !== $sku ) {
  1067. if ( ! empty( $new_sku ) ) {
  1068. $unique_sku = wc_product_has_unique_sku( $variation_id, $new_sku );
  1069. if ( ! $unique_sku ) {
  1070. throw new WC_API_Exception( 'woocommerce_api_product_sku_already_exists', __( 'The SKU already exists on another product', 'woocommerce' ), 400 );
  1071. } else {
  1072. update_post_meta( $variation_id, '_sku', $new_sku );
  1073. }
  1074. } else {
  1075. update_post_meta( $variation_id, '_sku', '' );
  1076. }
  1077. }
  1078. }
  1079. // Thumbnail
  1080. if ( isset( $variation['image'] ) && is_array( $variation['image'] ) ) {
  1081. $image = current( $variation['image'] );
  1082. if ( $image && is_array( $image ) ) {
  1083. if ( isset( $image['position'] ) && isset( $image['src'] ) && $image['position'] == 0 ) {
  1084. $upload = $this->upload_product_image( wc_clean( $image['src'] ) );
  1085. if ( is_wp_error( $upload ) ) {
  1086. throw new WC_API_Exception( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), 400 );
  1087. }
  1088. $attachment_id = $this->set_product_image_as_attachment( $upload, $id );
  1089. update_post_meta( $variation_id, '_thumbnail_id', $attachment_id );
  1090. }
  1091. } else {
  1092. delete_post_meta( $variation_id, '_thumbnail_id' );
  1093. }
  1094. }
  1095. // Virtual variation
  1096. if ( isset( $variation['virtual'] ) ) {
  1097. $is_virtual = ( true === $variation['virtual'] ) ? 'yes' : 'no';
  1098. update_post_meta( $variation_id, '_virtual', $is_virtual );
  1099. }
  1100. // Downloadable variation
  1101. if ( isset( $variation['downloadable'] ) ) {
  1102. $is_downloadable = ( true === $variation['downloadable'] ) ? 'yes' : 'no';
  1103. update_post_meta( $variation_id, '_downloadable', $is_downloadable );
  1104. } else {
  1105. $is_downloadable = get_post_meta( $variation_id, '_downloadable', true );
  1106. }
  1107. // Shipping data
  1108. $this->save_product_shipping_data( $variation_id, $variation );
  1109. // Stock handling
  1110. if ( isset( $variation['managing_stock'] ) ) {
  1111. $managing_stock = ( true === $variation['managing_stock'] ) ? 'yes' : 'no';
  1112. update_post_meta( $variation_id, '_manage_stock', $managing_stock );
  1113. } else {
  1114. $managing_stock = get_post_meta( $variation_id, '_manage_stock', true );
  1115. }
  1116. // Only update stock status to user setting if changed by the user, but do so before looking at stock levels at variation level
  1117. if ( isset( $variation['in_stock'] ) ) {
  1118. $stock_status = ( true === $variation['in_stock'] ) ? 'instock' : 'outofstock';
  1119. wc_update_product_stock_status( $variation_id, $stock_status );
  1120. }
  1121. if ( 'yes' === $managing_stock ) {
  1122. $backorders = get_post_meta( $variation_id, '_backorders', true );
  1123. if ( isset( $variation['backorders'] ) ) {
  1124. if ( 'notify' == $variation['backorders'] ) {
  1125. $backorders = 'notify';
  1126. } else {
  1127. $backorders = ( true === $variation['backorders'] ) ? 'yes' : 'no';
  1128. }
  1129. }
  1130. update_post_meta( $variation_id, '_backorders', '' === $backorders ? 'no' : $backorders );
  1131. if ( isset( $variation['stock_quantity'] ) ) {
  1132. wc_update_product_stock( $variation_id, wc_stock_amount( $variation['stock_quantity'] ) );
  1133. }
  1134. } else {
  1135. delete_post_meta( $variation_id, '_backorders' );
  1136. delete_post_meta( $variation_id, '_stock' );
  1137. }
  1138. // Regular Price
  1139. if ( isset( $variation['regular_price'] ) ) {
  1140. $regular_price = ( '' === $variation['regular_price'] ) ? '' : wc_format_decimal( $variation['regular_price'] );
  1141. update_post_meta( $variation_id, '_regular_price', $regular_price );
  1142. } else {
  1143. $regular_price = get_post_meta( $variation_id, '_regular_price', true );
  1144. }
  1145. // Sale Price
  1146. if ( isset( $variation['sale_price'] ) ) {
  1147. $sale_price = ( '' === $variation['sale_price'] ) ? '' : wc_format_decimal( $variation['sale_price'] );
  1148. update_post_meta( $variation_id, '_sale_price', $sale_price );
  1149. } else {
  1150. $sale_price = get_post_meta( $variation_id, '_sale_price', true );
  1151. }
  1152. $date_from = isset( $variation['sale_price_dates_from'] ) ? strtotime( $variation['sale_price_dates_from'] ) : get_post_meta( $variation_id, '_sale_price_dates_from', true );
  1153. $date_to = isset( $variation['sale_price_dates_to'] ) ? strtotime( $variation['sale_price_dates_to'] ) : get_post_meta( $variation_id, '_sale_price_dates_to', true );
  1154. // Save Dates
  1155. if ( $date_from ) {
  1156. update_post_meta( $variation_id, '_sale_price_dates_from', $date_from );
  1157. } else {
  1158. update_post_meta( $variation_id, '_sale_price_dates_from', '' );
  1159. }
  1160. if ( $date_to ) {
  1161. update_post_meta( $variation_id, '_sale_price_dates_to', $date_to );
  1162. } else {
  1163. update_post_meta( $variation_id, '_sale_price_dates_to', '' );
  1164. }
  1165. if ( $date_to && ! $date_from ) {
  1166. update_post_meta( $variation_id, '_sale_price_dates_from', strtotime( 'NOW', current_time( 'timestamp' ) ) );
  1167. }
  1168. // Update price if on sale
  1169. if ( '' != $sale_price && '' == $date_to && '' == $date_from ) {
  1170. update_post_meta( $variation_id, '_price', $sale_price );
  1171. } else {
  1172. update_post_meta( $variation_id, '_price', $regular_price );
  1173. }
  1174. if ( '' != $sale_price && $date_from && $date_from < strtotime( 'NOW', current_time( 'timestamp' ) ) ) {
  1175. update_post_meta( $variation_id, '_price', $sale_price );
  1176. }
  1177. if ( $date_to && $date_to < strtotime( 'NOW', current_time( 'timestamp' ) ) ) {
  1178. update_post_meta( $variation_id, '_price', $regular_price );
  1179. update_post_meta( $variation_id, '_sale_price_dates_from', '' );
  1180. update_post_meta