No Description

class-wc-admin-duplicate-product.php 9.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. <?php
  2. /**
  3. * Duplicate product functionality
  4. *
  5. * @package WooCommerce\Admin
  6. * @version 3.0.0
  7. */
  8. if ( ! defined( 'ABSPATH' ) ) {
  9. exit;
  10. }
  11. if ( class_exists( 'WC_Admin_Duplicate_Product', false ) ) {
  12. return new WC_Admin_Duplicate_Product();
  13. }
  14. /**
  15. * WC_Admin_Duplicate_Product Class.
  16. */
  17. class WC_Admin_Duplicate_Product {
  18. /**
  19. * Constructor.
  20. */
  21. public function __construct() {
  22. add_action( 'admin_action_duplicate_product', array( $this, 'duplicate_product_action' ) );
  23. add_filter( 'post_row_actions', array( $this, 'dupe_link' ), 10, 2 );
  24. add_action( 'post_submitbox_start', array( $this, 'dupe_button' ) );
  25. }
  26. /**
  27. * Show the "Duplicate" link in admin products list.
  28. *
  29. * @param array $actions Array of actions.
  30. * @param WP_Post $post Post object.
  31. * @return array
  32. */
  33. public function dupe_link( $actions, $post ) {
  34. global $the_product;
  35. if ( ! current_user_can( apply_filters( 'woocommerce_duplicate_product_capability', 'manage_woocommerce' ) ) ) {
  36. return $actions;
  37. }
  38. if ( 'product' !== $post->post_type ) {
  39. return $actions;
  40. }
  41. // Add Class to Delete Permanently link in row actions.
  42. if ( empty( $the_product ) || $the_product->get_id() !== $post->ID ) {
  43. $the_product = wc_get_product( $post );
  44. }
  45. if ( 'publish' === $post->post_status && $the_product && 0 < $the_product->get_total_sales() ) {
  46. $actions['trash'] = sprintf(
  47. '<a href="%s" class="submitdelete trash-product" aria-label="%s">%s</a>',
  48. get_delete_post_link( $the_product->get_id(), '', false ),
  49. /* translators: %s: post title */
  50. esc_attr( sprintf( __( 'Move &#8220;%s&#8221; to the Trash', 'woocommerce' ), $the_product->get_name() ) ),
  51. esc_html__( 'Trash', 'woocommerce' )
  52. );
  53. }
  54. $actions['duplicate'] = '<a href="' . wp_nonce_url( admin_url( 'edit.php?post_type=product&action=duplicate_product&amp;post=' . $post->ID ), 'woocommerce-duplicate-product_' . $post->ID ) . '" aria-label="' . esc_attr__( 'Make a duplicate from this product', 'woocommerce' )
  55. . '" rel="permalink">' . esc_html__( 'Duplicate', 'woocommerce' ) . '</a>';
  56. return $actions;
  57. }
  58. /**
  59. * Show the dupe product link in admin.
  60. */
  61. public function dupe_button() {
  62. global $post;
  63. if ( ! current_user_can( apply_filters( 'woocommerce_duplicate_product_capability', 'manage_woocommerce' ) ) ) {
  64. return;
  65. }
  66. if ( ! is_object( $post ) ) {
  67. return;
  68. }
  69. if ( 'product' !== $post->post_type ) {
  70. return;
  71. }
  72. $notify_url = wp_nonce_url( admin_url( 'edit.php?post_type=product&action=duplicate_product&post=' . absint( $post->ID ) ), 'woocommerce-duplicate-product_' . $post->ID );
  73. ?>
  74. <div id="duplicate-action"><a class="submitduplicate duplication" href="<?php echo esc_url( $notify_url ); ?>"><?php esc_html_e( 'Copy to a new draft', 'woocommerce' ); ?></a></div>
  75. <?php
  76. }
  77. /**
  78. * Duplicate a product action.
  79. */
  80. public function duplicate_product_action() {
  81. if ( empty( $_REQUEST['post'] ) ) {
  82. wp_die( esc_html__( 'No product to duplicate has been supplied!', 'woocommerce' ) );
  83. }
  84. $product_id = isset( $_REQUEST['post'] ) ? absint( $_REQUEST['post'] ) : '';
  85. check_admin_referer( 'woocommerce-duplicate-product_' . $product_id );
  86. $product = wc_get_product( $product_id );
  87. if ( false === $product ) {
  88. /* translators: %s: product id */
  89. wp_die( sprintf( esc_html__( 'Product creation failed, could not find original product: %s', 'woocommerce' ), esc_html( $product_id ) ) );
  90. }
  91. $duplicate = $this->product_duplicate( $product );
  92. // Hook rename to match other woocommerce_product_* hooks, and to move away from depending on a response from the wp_posts table.
  93. do_action( 'woocommerce_product_duplicate', $duplicate, $product );
  94. wc_do_deprecated_action( 'woocommerce_duplicate_product', array( $duplicate->get_id(), $this->get_product_to_duplicate( $product_id ) ), '3.0', 'Use woocommerce_product_duplicate action instead.' );
  95. // Redirect to the edit screen for the new draft page.
  96. wp_redirect( admin_url( 'post.php?action=edit&post=' . $duplicate->get_id() ) );
  97. exit;
  98. }
  99. /**
  100. * Function to create the duplicate of the product.
  101. *
  102. * @param WC_Product $product The product to duplicate.
  103. * @return WC_Product The duplicate.
  104. */
  105. public function product_duplicate( $product ) {
  106. /**
  107. * Filter to allow us to exclude meta keys from product duplication..
  108. *
  109. * @param array $exclude_meta The keys to exclude from the duplicate.
  110. * @param array $existing_meta_keys The meta keys that the product already has.
  111. * @since 2.6
  112. */
  113. $meta_to_exclude = array_filter(
  114. apply_filters(
  115. 'woocommerce_duplicate_product_exclude_meta',
  116. array(),
  117. array_map(
  118. function ( $datum ) {
  119. return $datum->key;
  120. },
  121. $product->get_meta_data()
  122. )
  123. )
  124. );
  125. $duplicate = clone $product;
  126. $duplicate->set_id( 0 );
  127. /* translators: %s contains the name of the original product. */
  128. $duplicate->set_name( sprintf( esc_html__( '%s (Copy)', 'woocommerce' ), $duplicate->get_name() ) );
  129. $duplicate->set_total_sales( 0 );
  130. if ( '' !== $product->get_sku( 'edit' ) ) {
  131. $duplicate->set_sku( wc_product_generate_unique_sku( 0, $product->get_sku( 'edit' ) ) );
  132. }
  133. $duplicate->set_status( 'draft' );
  134. $duplicate->set_date_created( null );
  135. $duplicate->set_slug( '' );
  136. $duplicate->set_rating_counts( 0 );
  137. $duplicate->set_average_rating( 0 );
  138. $duplicate->set_review_count( 0 );
  139. foreach ( $meta_to_exclude as $meta_key ) {
  140. $duplicate->delete_meta_data( $meta_key );
  141. }
  142. /**
  143. * This action can be used to modify the object further before it is created - it will be passed by reference.
  144. *
  145. * @since 3.0
  146. */
  147. do_action( 'woocommerce_product_duplicate_before_save', $duplicate, $product );
  148. // Save parent product.
  149. $duplicate->save();
  150. // Duplicate children of a variable product.
  151. if ( ! apply_filters( 'woocommerce_duplicate_product_exclude_children', false, $product ) && $product->is_type( 'variable' ) ) {
  152. foreach ( $product->get_children() as $child_id ) {
  153. $child = wc_get_product( $child_id );
  154. $child_duplicate = clone $child;
  155. $child_duplicate->set_parent_id( $duplicate->get_id() );
  156. $child_duplicate->set_id( 0 );
  157. $child_duplicate->set_date_created( null );
  158. // If we wait and let the insertion generate the slug, we will see extreme performance degradation
  159. // in the case where a product is used as a template. Every time the template is duplicated, each
  160. // variation will query every consecutive slug until it finds an empty one. To avoid this, we can
  161. // optimize the generation ourselves, avoiding the issue altogether.
  162. $this->generate_unique_slug( $child_duplicate );
  163. if ( '' !== $child->get_sku( 'edit' ) ) {
  164. $child_duplicate->set_sku( wc_product_generate_unique_sku( 0, $child->get_sku( 'edit' ) ) );
  165. }
  166. foreach ( $meta_to_exclude as $meta_key ) {
  167. $child_duplicate->delete_meta_data( $meta_key );
  168. }
  169. /**
  170. * This action can be used to modify the object further before it is created - it will be passed by reference.
  171. *
  172. * @since 3.0
  173. */
  174. do_action( 'woocommerce_product_duplicate_before_save', $child_duplicate, $child );
  175. $child_duplicate->save();
  176. }
  177. // Get new object to reflect new children.
  178. $duplicate = wc_get_product( $duplicate->get_id() );
  179. }
  180. return $duplicate;
  181. }
  182. /**
  183. * Get a product from the database to duplicate.
  184. *
  185. * @deprecated 3.0.0
  186. * @param mixed $id The ID of the product to duplicate.
  187. * @return object|bool
  188. * @see duplicate_product
  189. */
  190. private function get_product_to_duplicate( $id ) {
  191. global $wpdb;
  192. $id = absint( $id );
  193. if ( ! $id ) {
  194. return false;
  195. }
  196. $post = $wpdb->get_row( $wpdb->prepare( "SELECT {$wpdb->posts}.* FROM {$wpdb->posts} WHERE ID = %d", $id ) );
  197. if ( isset( $post->post_type ) && 'revision' === $post->post_type ) {
  198. $id = $post->post_parent;
  199. $post = $wpdb->get_row( $wpdb->prepare( "SELECT {$wpdb->posts}.* FROM {$wpdb->posts} WHERE ID = %d", $id ) );
  200. }
  201. return $post;
  202. }
  203. /**
  204. * Generates a unique slug for a given product. We do this so that we can override the
  205. * behavior of wp_unique_post_slug(). The normal slug generation will run single
  206. * select queries on every non-unique slug, resulting in very bad performance.
  207. *
  208. * @param WC_Product $product The product to generate a slug for.
  209. * @since 3.9.0
  210. */
  211. private function generate_unique_slug( $product ) {
  212. global $wpdb;
  213. // We want to remove the suffix from the slug so that we can find the maximum suffix using this root slug.
  214. // This will allow us to find the next-highest suffix that is unique. While this does not support gap
  215. // filling, this shouldn't matter for our use-case.
  216. $root_slug = preg_replace( '/-[0-9]+$/', '', $product->get_slug() );
  217. $results = $wpdb->get_results(
  218. $wpdb->prepare( "SELECT post_name FROM $wpdb->posts WHERE post_name LIKE %s AND post_type IN ( 'product', 'product_variation' )", $root_slug . '%' )
  219. );
  220. // The slug is already unique!
  221. if ( empty( $results ) ) {
  222. return;
  223. }
  224. // Find the maximum suffix so we can ensure uniqueness.
  225. $max_suffix = 1;
  226. foreach ( $results as $result ) {
  227. // Pull a numerical suffix off the slug after the last hyphen.
  228. $suffix = intval( substr( $result->post_name, strrpos( $result->post_name, '-' ) + 1 ) );
  229. if ( $suffix > $max_suffix ) {
  230. $max_suffix = $suffix;
  231. }
  232. }
  233. $product->set_slug( $root_slug . '-' . ( $max_suffix + 1 ) );
  234. }
  235. }
  236. return new WC_Admin_Duplicate_Product();