PluginProbe ʕ •ᴥ•ʔ
WooCommerce / 11.1.0
WooCommerce v11.1.0
11.1.0 11.1.0-rc.2 11.1.0-rc.1 11.1.0-beta.2 11.1.0-beta.1 11.0.1 11.0.0 11.0.0-rc.3 11.0.0-rc.2 11.0.0-rc.1 11.0.0-beta.2 11.0.0-beta.1 10.9.4 10.9.3 10.9.2 10.9.1 10.9.0 10.9.0-rc.1 10.9.0-beta.2 10.9.0-beta.1 10.8.1 10.8.0 10.8.0-rc.1 10.8.0-beta.2 10.8.0-beta.1 7.8.0-beta.1 7.8.0-beta.2 7.8.0-rc.1 7.8.0-rc.2 7.8.1 7.8.2 7.8.3 7.8.4 7.9.0 7.9.0-beta.1 7.9.0-beta.2 7.9.0-rc.2 7.9.0-rc.3 7.9.1 7.9.2 8.0.0 8.0.0-beta.1 8.0.0-beta.2 8.0.0-rc.1 8.0.0-rc.2 8.0.1 8.0.2 8.0.3 8.0.4 8.0.5 8.1.0 8.1.0-beta.1 8.1.0-rc.1 8.1.0-rc.2 8.1.1 8.1.2 8.1.3 8.1.4 8.2.0 8.2.0-beta.1 8.2.0-rc.1 8.2.0-rc.2 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.3.0 8.3.0-beta.1 8.3.0-rc.1 8.3.0-rc.2 8.3.1 8.3.2 8.3.3 8.3.4 8.4.0 8.4.0-beta.1 8.4.0-rc.1 8.4.1 8.4.2 8.4.3 8.5.0 8.5.0-beta.1 8.5.0-rc.1 8.5.1 8.5.2 8.5.3 8.5.4 8.5.5 8.6.0 8.6.0-beta.1 8.6.0-rc.1 8.6.1 8.6.2 8.6.3 8.6.4 8.7.0 8.7.0-beta.1 8.7.0-beta.2 8.7.0-rc.1 8.7.1 8.7.2 8.7.3 8.8.0 8.8.0-beta.1 8.8.0-rc.1 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.8.6 8.8.7 8.9.0 8.9.0-beta.1 8.9.0-rc.1 8.9.1 8.9.2 8.9.3 8.9.4 8.9.5 9.0.0 9.0.0-beta.1 9.0.0-beta.2 9.0.0-rc.1 9.0.1 9.0.2 9.0.3 9.0.4 9.1.0 9.1.0-beta.1 9.1.0-rc.1 9.1.1 9.1.2 9.1.3 9.1.4 9.1.5 9.1.6 9.2.0 9.2.0-beta.1 9.2.0-rc.1 9.2.1 9.2.2 9.2.3 9.2.4 9.2.5 9.3.0 9.3.0-beta.1 9.3.0-rc.1 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.3.6 9.4.0 9.4.0-beta.1 9.4.0-beta.2 9.4.0-rc.1 9.4.0-rc.2 9.4.0-rc.3 9.4.0-rc.4 9.4.1 9.4.2 9.4.3 9.4.4 9.4.5 9.5.0 9.5.0-beta.1 9.5.0-beta.2 9.5.0-rc.1 9.5.1 9.5.2 9.5.3 9.5.4 9.6.0 9.6.0-beta.1 9.6.0-beta.2 9.6.0-rc.1 9.6.1 9.6.2 9.6.3 9.6.4 9.7.0 9.7.0-beta.1 9.7.0-rc.1 9.7.1 9.7.2 9.7.3 9.8.0 9.8.0-beta.1 9.8.0-rc.1 9.8.1 9.8.2 9.8.3 9.8.4 9.8.5 9.8.6 9.8.7 9.9.0 9.9.0-beta.1 9.9.0-rc.1 9.9.1 9.9.2 9.9.3 9.9.4 9.9.5 9.9.6 9.9.7 3.7.3 7.1.2 3.8.0 7.2.0 3.8.0-beta.1 7.2.0-beta.1 3.8.0-rc.1 7.2.0-beta.2 3.8.0-rc.2 7.2.0-rc.1 3.8.1 7.2.0-rc.2 3.8.2 7.2.1 3.8.3 7.2.2 3.9.0 7.2.3 3.9.0-beta.1 7.2.4 3.9.0-beta.2 7.3.0 3.9.0-rc.1 7.3.0-beta.1 3.9.0-rc.2 7.3.0-beta.2 3.9.0-rc.3 7.3.0-rc.1 3.9.0-rc.4 7.3.0-rc.2 3.9.1 7.3.1 3.9.2 7.4.0 3.9.3 7.4.0-beta.1 3.9.4 7.4.0-beta.2 3.9.5 7.4.0-rc.1 4.0.0 7.4.0-rc.2 4.0.0-beta.1 7.4.1 4.0.0-rc.1 7.4.2 4.0.0-rc.2 7.5.0 4.0.1 7.5.0-beta.1 4.0.2 7.5.0-beta.2 4.0.3 7.5.0-rc.1 4.0.4 7.5.1 4.1.0 7.5.2 4.1.0-beta.1 7.6.0 4.1.0-beta.2 7.6.0-beta.1 4.1.0-rc.1 7.6.0-beta.2 4.1.0-rc.2 7.6.0-rc.1 4.1.1 7.6.0-rc.2 4.1.2 7.6.0-rc.3 4.1.3 7.6.1 4.1.4 7.6.2 4.2.0 7.7.0 4.2.0-RC.1 7.7.0-beta.1 4.2.0-RC.2 7.7.0-beta.2 4.2.0-beta.1 7.7.0-rc.1 4.2.1 7.7.1 4.2.2 7.7.2 4.2.3 7.7.3 4.2.4 7.8.0 4.2.5 4.3.0 4.3.0-beta.1 4.3.0-rc.1 4.3.0-rc.2 4.3.0-rc.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.4.0 4.4.0-beta.1 4.4.0-rc.1 4.4.1 4.4.2 4.4.3 4.4.4 4.5.0 4.5.0-beta.1 4.5.0-rc.1 4.5.0-rc.3 4.5.1 4.5.2 4.5.3 4.5.4 4.5.5 4.6.0 4.6.0-beta.1 4.6.0-rc.1 4.6.1 4.6.2 4.6.3 4.6.4 4.6.5 4.7.0 4.7.0-beta.1 4.7.0-beta.2 4.7.0-rc.1 4.7.1 4.7.1-beta.1 4.7.2 4.7.3 4.7.4 4.8.0 4.8.0-beta.1 4.8.0-rc.1 4.8.0-rc.2 4.8.1 4.8.2 4.8.3 4.9.0 4.9.0-beta.1 4.9.0-rc.1 4.9.0-rc.2 4.9.1 4.9.2 4.9.3 4.9.4 4.9.5 5.0.0 5.0.0-beta.1 5.0.0-beta.2 5.0.0-rc.1 5.0.0-rc.2 5.0.0-rc.3 5.0.1 5.0.2 5.0.3 5.1.0 5.1.0-beta.1 5.1.0-rc.1 trunk 5.1.1 10.0.0 5.1.2 10.0.0-rc.1 5.1.3 10.0.0-rc.2 5.2.0 10.0.1 5.2.0-beta.1 10.0.2 5.2.0-rc.1 10.0.3 5.2.0-rc.2 10.0.4 5.2.1 10.0.5 5.2.2 10.0.6 5.2.3 10.1.0 5.2.4 10.1.0-rc.1 5.2.5 10.1.0-rc.2 5.3.0 10.1.0-rc.3 5.3.0-beta.1 10.1.0-rc.4 5.3.0-rc.1 10.1.1 5.3.0-rc.2 10.1.2 5.3.1 10.1.3 5.3.2 10.1.4 5.3.3 10.2.0 5.4.0 10.2.0-beta.1 5.4.0-beta.1 10.2.0-beta.2 5.4.0-rc.1 10.2.0-rc.1 5.4.1 10.2.1 5.4.2 10.2.2 5.4.3 10.2.3 5.4.4 10.2.4 5.4.5 10.3.0 5.5.0 10.3.0-beta.1 5.5.0-beta.1 10.3.0-beta.2 5.5.0-rc.1 10.3.0-rc.1 5.5.0-rc.2 10.3.0-rc.2 5.5.1 10.3.1 5.5.2 10.3.2 5.5.3 10.3.3 5.5.4 10.3.4 5.5.5 10.3.5 5.6.0 10.3.6 5.6.0-beta.1 10.3.7 5.6.0-rc.1 10.3.8 5.6.0-rc.2 10.4.0 5.6.1 10.4.0-beta.1 5.6.2 10.4.0-beta.2 5.6.3 10.4.0-rc.1 5.7.0 10.4.1 5.7.0-beta.1 10.4.2 5.7.0-rc.1 10.4.3 5.7.1 10.4.4 5.7.2 10.5.0 5.7.3 10.5.0-beta.1 5.8.0 10.5.0-beta.2 5.8.0-beta.1 10.5.0-rc.1 5.8.0-beta.2 10.5.0-rc.2 5.8.0-rc.1 10.5.0-rc.3 5.8.1 10.5.1 5.8.2 10.5.2 5.9.0 10.5.3 5.9.0-beta.1 10.6.0 5.9.0-rc.1 10.6.0-beta.1 5.9.0-rc.2 10.6.0-beta.2 5.9.1 10.6.0-rc.1 5.9.2 10.6.1 6.0.0 10.6.2 6.0.0-beta.1 10.7.0 6.0.0-rc.1 10.7.0-beta.1 6.0.1 10.7.0-beta.2 6.0.2 10.7.0-rc.1 6.1.0 3.0.0 6.1.0-beta.1 3.0.1 6.1.0-rc.1 3.0.2 6.1.0-rc.2 3.0.3 6.1.1 3.0.4 6.1.2 3.0.5 6.1.3 3.0.6 6.2.0 3.0.7 6.2.0-beta.1 3.0.8 6.2.0-rc.1 3.0.9 6.2.0-rc.2 3.1.0 6.2.1 3.1.1 6.2.2 3.1.2 6.2.3 3.2.0 6.3.0 3.2.1 6.3.0-beta.1 3.2.2 6.3.0-rc.1 3.2.3 6.3.0-rc.2 3.2.4 6.3.1 3.2.5 6.3.2 3.2.6 6.4.0 3.3.0 6.4.0-beta.1 3.3.1 6.4.0-rc.1 3.3.2 6.4.1 3.3.2-rc.1 6.4.2 3.3.3 6.5.0 3.3.4 6.5.0-beta.1 3.3.5 6.5.0-rc.1 3.3.6 6.5.0-rc.2 3.4.0 6.5.1 3.4.0-beta.1 6.5.2 3.4.0-rc.2 6.6.0 3.4.1 6.6.0-beta.1 3.4.2 6.6.0-rc.1 3.4.3 6.6.0-rc.2 3.4.4 6.6.1 3.4.5 6.6.2 3.4.6 6.7.0 3.4.7 6.7.0-beta.1 3.4.8 6.7.0-beta.2 3.5.0 6.7.0-rc.1 3.5.0-beta.1 6.7.1 3.5.0-rc.1 6.8.0 3.5.0-rc.2 6.8.0-beta.1 3.5.1 6.8.0-beta.2 3.5.10 6.8.0-rc.1 3.5.2 6.8.1 3.5.3 6.8.2 3.5.4 6.8.3 3.5.5 6.9.0 3.5.6 6.9.0-beta.1 3.5.7 6.9.0-beta.2 3.5.8 6.9.0-rc.1 3.5.9 6.9.1 3.6.0 6.9.2 3.6.0-beta.1 6.9.3 3.6.0-rc.1 6.9.4 3.6.0-rc.2 6.9.5 3.6.0-rc.3 7.0.0 3.6.1 7.0.0-beta.1 3.6.2 7.0.0-beta.2 3.6.3 7.0.0-beta.3 3.6.4 7.0.0-rc.1 3.6.5 7.0.0-rc.2 3.6.6 7.0.1 3.6.7 7.0.2 3.7.0 7.1.0 3.7.0-beta.1 7.1.0-beta.1 3.7.0-rc.1 7.1.0-beta.2 3.7.0-rc.2 7.1.0-rc.1 3.7.1 7.1.0-rc.2 3.7.2 7.1.1
woocommerce / includes / admin / class-wc-admin-duplicate-product.php
woocommerce / includes / admin Last commit date
helper 2 weeks ago importers 2 weeks ago list-tables 1 week ago marketplace-suggestions 1 year ago meta-boxes 1 week ago notes 3 months ago plugin-updates 2 years ago reports 2 weeks ago settings 2 weeks ago views 2 weeks ago class-wc-admin-addons.php 10 months ago class-wc-admin-api-keys-table-list.php 3 years ago class-wc-admin-api-keys.php 1 month ago class-wc-admin-assets.php 2 weeks ago class-wc-admin-attributes.php 2 weeks ago class-wc-admin-brands.php 2 weeks ago class-wc-admin-customize.php 6 years ago class-wc-admin-dashboard-setup.php 2 weeks ago class-wc-admin-dashboard.php 2 weeks ago class-wc-admin-duplicate-product.php 2 weeks ago class-wc-admin-exporters.php 1 year ago class-wc-admin-help.php 2 years ago class-wc-admin-importers.php 1 year ago class-wc-admin-log-table-list.php 6 months ago class-wc-admin-marketplace-promotions.php 1 month ago class-wc-admin-menus.php 2 weeks ago class-wc-admin-meta-boxes.php 2 months ago class-wc-admin-notices.php 2 weeks ago class-wc-admin-permalink-settings.php 5 years ago class-wc-admin-pointers.php 3 years ago class-wc-admin-post-types.php 2 weeks ago class-wc-admin-profile.php 1 year ago class-wc-admin-reports.php 6 months ago class-wc-admin-settings.php 2 weeks ago class-wc-admin-setup-wizard.php 4 days ago class-wc-admin-status.php 1 year ago class-wc-admin-taxonomies.php 2 months ago class-wc-admin-upload-downloadable-product.php 2 years ago class-wc-admin-webhooks-table-list.php 2 months ago class-wc-admin-webhooks.php 1 month ago class-wc-admin.php 2 weeks ago wc-admin-functions.php 2 weeks ago wc-meta-box-functions.php 1 year ago woocommerce-legacy-reports.php 1 year ago
class-wc-admin-duplicate-product.php
427 lines
1 <?php
2 /**
3 * Duplicate product functionality
4 *
5 * @package WooCommerce\Admin
6 * @version 3.0.0
7 */
8
9 use Automattic\WooCommerce\Enums\ProductStatus;
10 use Automattic\WooCommerce\Enums\ProductType;
11 use Automattic\WooCommerce\Internal\ProductGallery\ProductMediaGallery;
12
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit;
15 }
16
17 if ( class_exists( 'WC_Admin_Duplicate_Product', false ) ) {
18 return new WC_Admin_Duplicate_Product();
19 }
20
21 /**
22 * WC_Admin_Duplicate_Product Class.
23 */
24 class WC_Admin_Duplicate_Product {
25
26 /**
27 * Constructor.
28 */
29 public function __construct() {
30 add_action( 'admin_action_duplicate_product', array( $this, 'duplicate_product_action' ) );
31 add_filter( 'post_row_actions', array( $this, 'dupe_link' ), 10, 2 );
32 add_action( 'post_submitbox_start', array( $this, 'dupe_button' ) );
33 }
34
35 /**
36 * Show the "Duplicate" link in admin products list.
37 *
38 * @param array $actions Array of actions.
39 * @param WP_Post $post Post object.
40 * @return array
41 */
42 public function dupe_link( $actions, $post ) {
43 global $the_product;
44
45 if ( ! current_user_can( apply_filters( 'woocommerce_duplicate_product_capability', 'manage_woocommerce' ) ) ) {
46 return $actions;
47 }
48
49 if ( 'product' !== $post->post_type ) {
50 return $actions;
51 }
52
53 // Add Class to Delete Permanently link in row actions.
54 if ( empty( $the_product ) || $the_product->get_id() !== $post->ID ) {
55 $the_product = wc_get_product( $post );
56 }
57
58 if ( $the_product && ProductStatus::PUBLISH === $the_product->get_status() && 0 < $the_product->get_total_sales() ) {
59 $actions['trash'] = sprintf(
60 '<a href="%s" class="submitdelete trash-product" aria-label="%s">%s</a>',
61 get_delete_post_link( $the_product->get_id(), '', false ),
62 /* translators: %s: post title */
63 esc_attr( sprintf( __( 'Move &#8220;%s&#8221; to the Trash', 'woocommerce' ), $the_product->get_name() ) ),
64 esc_html__( 'Trash', 'woocommerce' )
65 );
66 }
67
68 $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' )
69 . '" rel="permalink">' . esc_html__( 'Duplicate', 'woocommerce' ) . '</a>';
70
71 return $actions;
72 }
73
74 /**
75 * Show the dupe product link in admin.
76 */
77 public function dupe_button() {
78 global $post;
79
80 if ( ! current_user_can( apply_filters( 'woocommerce_duplicate_product_capability', 'manage_woocommerce' ) ) ) {
81 return;
82 }
83
84 if ( ! is_object( $post ) ) {
85 return;
86 }
87
88 if ( 'product' !== $post->post_type ) {
89 return;
90 }
91
92 $notify_url = wp_nonce_url( admin_url( 'edit.php?post_type=product&action=duplicate_product&post=' . absint( $post->ID ) ), 'woocommerce-duplicate-product_' . $post->ID );
93 ?>
94 <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>
95 <?php
96 }
97
98 /**
99 * Duplicate a product action.
100 */
101 public function duplicate_product_action() {
102 if ( empty( $_REQUEST['post'] ) ) {
103 wp_die( esc_html__( 'No product to duplicate has been supplied!', 'woocommerce' ) );
104 }
105
106 $product_id = isset( $_REQUEST['post'] ) ? absint( $_REQUEST['post'] ) : '';
107
108 check_admin_referer( 'woocommerce-duplicate-product_' . $product_id );
109
110 $product = wc_get_product( $product_id );
111
112 if ( false === $product ) {
113 /* translators: %s: product id */
114 wp_die( sprintf( esc_html__( 'Product creation failed, could not find original product: %s', 'woocommerce' ), esc_html( $product_id ) ) );
115 }
116
117 $duplicate = $this->product_duplicate( $product );
118
119 // Hook rename to match other woocommerce_product_* hooks, and to move away from depending on a response from the wp_posts table.
120 do_action( 'woocommerce_product_duplicate', $duplicate, $product );
121 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.' );
122
123 // Redirect to the edit screen for the new draft page.
124 wp_redirect( admin_url( 'post.php?action=edit&post=' . $duplicate->get_id() ) );
125 exit;
126 }
127
128 /**
129 * Function to create the duplicate of the product.
130 *
131 * @param WC_Product $product The product to duplicate.
132 * @return WC_Product The duplicate.
133 */
134 public function product_duplicate( $product ) {
135 if ( ! $product instanceof WC_Product ) {
136 wc_doing_it_wrong( __METHOD__, 'product_duplicate() expects a WC_Product instance', '10.5.0' );
137 return new WC_Product();
138 }
139
140 /**
141 * Filter to allow us to exclude meta keys from product duplication..
142 *
143 * @param array $exclude_meta The keys to exclude from the duplicate.
144 * @param array $existing_meta_keys The meta keys that the product already has.
145 * @since 2.6
146 */
147 $meta_to_exclude = array_filter(
148 apply_filters(
149 'woocommerce_duplicate_product_exclude_meta',
150 array(),
151 array_map(
152 function ( $datum ) {
153 return $datum->key;
154 },
155 $product->get_meta_data()
156 )
157 )
158 );
159
160 $duplicate = clone $product;
161 $duplicate->set_id( 0 );
162 /* translators: %s contains the name of the original product. */
163 $duplicate->set_name( sprintf( esc_html__( '%s (Copy)', 'woocommerce' ), $duplicate->get_name() ) );
164 $duplicate->set_total_sales( 0 );
165 if ( '' !== $product->get_sku( 'edit' ) ) {
166 $this->generate_unique_sku( $duplicate );
167 }
168 if ( '' !== $product->get_global_unique_id( 'edit' ) ) {
169 $duplicate->set_global_unique_id( '' );
170 }
171 $duplicate->set_status( ProductStatus::DRAFT );
172 $duplicate->set_date_created( null );
173 $duplicate->set_slug( '' );
174 $duplicate->set_rating_counts( 0 );
175 $duplicate->set_average_rating( 0 );
176 $duplicate->set_review_count( 0 );
177
178 foreach ( $meta_to_exclude as $meta_key ) {
179 $duplicate->delete_meta_data( $meta_key );
180 }
181
182 /**
183 * This action can be used to modify the object further before it is created - it will be passed by reference.
184 *
185 * @since 3.0
186 */
187 do_action( 'woocommerce_product_duplicate_before_save', $duplicate, $product );
188
189 // Save parent product.
190 $duplicate->save();
191
192 if ( ProductMediaGallery::is_feature_enabled() ) {
193 ProductMediaGallery::copy_stored_video_gallery_items( $product, $duplicate );
194 }
195
196 /**
197 * Duplicate children of a variable product.
198 *
199 * @since 2.7
200 */
201 if ( ! apply_filters( 'woocommerce_duplicate_product_exclude_children', false, $product ) && $product->is_type( ProductType::VARIABLE ) ) {
202 foreach ( $product->get_children() as $child_id ) {
203 $child = wc_get_product( $child_id );
204
205 if ( ! $child instanceof WC_Product ) {
206 wc_doing_it_wrong( __METHOD__, 'product_duplicate() expects product children to be WC_Product instances', '10.5.0' );
207 continue;
208 }
209
210 $child->read_meta_data();
211 $child_duplicate = clone $child;
212 $child_duplicate->set_parent_id( $duplicate->get_id() );
213 $child_duplicate->set_id( 0 );
214 $child_duplicate->set_date_created( null );
215
216 // If we wait and let the insertion generate the slug, we will see extreme performance degradation
217 // in the case where a product is used as a template. Every time the template is duplicated, each
218 // variation will query every consecutive slug until it finds an empty one. To avoid this, we can
219 // optimize the generation ourselves, avoiding the issue altogether.
220 $this->generate_unique_slug( $child_duplicate );
221
222 if ( '' !== $child->get_sku( 'edit' ) ) {
223 $this->generate_unique_sku( $child_duplicate );
224 }
225 if ( '' !== $child->get_global_unique_id( 'edit' ) ) {
226 $child_duplicate->set_global_unique_id( '' );
227 }
228
229 foreach ( $meta_to_exclude as $meta_key ) {
230 $child_duplicate->delete_meta_data( $meta_key );
231 }
232
233 /**
234 * This action can be used to modify the object further before it is created - it will be passed by reference.
235 *
236 * @since 3.0
237 */
238 do_action( 'woocommerce_product_duplicate_before_save', $child_duplicate, $child );
239
240 $child_duplicate->save();
241 }
242
243 // Get new object to reflect new children.
244 $duplicate = wc_get_product( $duplicate->get_id() );
245 }
246
247 return $duplicate;
248 }
249
250 /**
251 * Get a product from the database to duplicate.
252 *
253 * @deprecated 3.0.0
254 * @param mixed $id The ID of the product to duplicate.
255 * @return object|bool
256 * @see duplicate_product
257 */
258 private function get_product_to_duplicate( $id ) {
259 global $wpdb;
260
261 $id = absint( $id );
262
263 if ( ! $id ) {
264 return false;
265 }
266
267 $post = $wpdb->get_row( $wpdb->prepare( "SELECT {$wpdb->posts}.* FROM {$wpdb->posts} WHERE ID = %d", $id ) );
268
269 if ( isset( $post->post_type ) && 'revision' === $post->post_type ) {
270 $id = $post->post_parent;
271 $post = $wpdb->get_row( $wpdb->prepare( "SELECT {$wpdb->posts}.* FROM {$wpdb->posts} WHERE ID = %d", $id ) );
272 }
273
274 return $post;
275 }
276
277 /**
278 * Generates a unique slug for a given product. We do this so that we can override the
279 * behavior of wp_unique_post_slug(). The normal slug generation will run single
280 * select queries on every non-unique slug, resulting in very bad performance.
281 *
282 * @param WC_Product $product The product to generate a slug for.
283 * @since 3.9.0
284 */
285 private function generate_unique_slug( $product ) {
286 global $wpdb;
287
288 // We want to remove the suffix from the slug so that we can find the maximum suffix using this root slug.
289 // This will allow us to find the next-highest suffix that is unique. While this does not support gap
290 // filling, this shouldn't matter for our use-case.
291 $root_slug = preg_replace( '/-[0-9]+$/', '', $product->get_slug() );
292
293 $results = $wpdb->get_results(
294 $wpdb->prepare( "SELECT post_name FROM $wpdb->posts WHERE post_name LIKE %s AND post_type IN ( 'product', 'product_variation' )", $root_slug . '%' )
295 );
296
297 // The slug is already unique!
298 if ( empty( $results ) ) {
299 return;
300 }
301
302 // Find the maximum suffix so we can ensure uniqueness.
303 $max_suffix = 1;
304 foreach ( $results as $result ) {
305 // Pull a numerical suffix off the slug after the last hyphen.
306 $suffix = intval( substr( $result->post_name, strrpos( $result->post_name, '-' ) + 1 ) );
307 if ( $suffix > $max_suffix ) {
308 $max_suffix = $suffix;
309 }
310 }
311
312 $product->set_slug( $root_slug . '-' . ( $max_suffix + 1 ) );
313 }
314
315 /**
316 * Generates a unique sku for a given product.
317 *
318 * Appends a numeric suffix to the original SKU. For example, if the original SKU
319 * is "SKU-123", the duplicate will get "SKU-123-1". This preserves hyphens in the
320 * original SKU rather than treating them as suffix delimiters.
321 *
322 * @param WC_Product $product The product to generate a sku for.
323 * @return void
324 * @since 10.5.0
325 *
326 * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
327 */
328 private function generate_unique_sku( $product ) {
329 global $wpdb;
330
331 $original_sku = $product->get_sku();
332
333 // If the product has no SKU, don't do anything.
334 if ( '' === $original_sku ) {
335 return;
336 }
337
338 // If the source SKU is not used by any saved product, use it as-is.
339 // This preserves backward compatibility with the REST API duplicate endpoint,
340 // which lets callers set a custom SKU on the duplicate and expects that SKU
341 // to be applied directly when it is unique.
342 $source_sku_in_use = (bool) $wpdb->get_var(
343 $wpdb->prepare(
344 "SELECT COUNT(*) FROM {$wpdb->wc_product_meta_lookup} WHERE sku = %s",
345 $original_sku
346 )
347 );
348
349 if ( ! $source_sku_in_use ) {
350 $product->set_sku( $original_sku );
351 return;
352 }
353
354 // Query existing SKUs that match the pattern: original SKU + hyphen + number.
355 $existing_skus = $wpdb->get_col(
356 $wpdb->prepare(
357 "SELECT lookup.sku
358 FROM {$wpdb->posts} as posts
359 INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id
360 WHERE posts.post_type IN ( 'product', 'product_variation' )
361 AND lookup.sku LIKE %s",
362 $wpdb->esc_like( $original_sku ) . '-%'
363 )
364 );
365
366 // Find the maximum suffix.
367 $max_suffix = 0;
368 // +1 for the hyphen separator between original SKU and suffix.
369 $prefix_len = strlen( $original_sku ) + 1;
370 foreach ( $existing_skus as $existing_sku ) {
371 // Check if the SKU starts with our base and has a numeric suffix.
372 if ( stripos( $existing_sku, $original_sku . '-' ) === 0 ) {
373 $suffix_str = substr( $existing_sku, $prefix_len );
374 // Only consider pure numeric suffixes.
375 if ( is_numeric( $suffix_str ) ) {
376 $suffix = intval( $suffix_str );
377 if ( $suffix > $max_suffix ) {
378 $max_suffix = $suffix;
379 }
380 }
381 }
382 }
383
384 // Set the new SKU with the next available suffix.
385 $new_sku = $original_sku . '-' . ( $max_suffix + 1 );
386 $product_id = $product->get_id();
387
388 /**
389 * Gives plugins an opportunity to verify SKU uniqueness themselves. Filter added to keep backwards
390 * compatibility with `wc_product_has_unique_sku()`.
391 * See: https://github.com/woocommerce/woocommerce/pull/62628
392 *
393 * @since 10.5.0
394 *
395 * @param bool|null $has_unique_sku Set to a boolean value to short-circuit the default SKU check.
396 * @param int $product_id The ID of the current product.
397 * @param string $sku The SKU to check for uniqueness.
398 */
399 $pre_has_unique_sku = apply_filters( 'wc_product_pre_has_unique_sku', true, $product_id, $new_sku );
400
401 if ( $pre_has_unique_sku ) {
402 /**
403 * Gives plugins an opportunity to verify SKU uniqueness themselves. Filter added to keep backwards
404 * compatibility with `wc_product_has_unique_sku()`.
405 * See: https://github.com/woocommerce/woocommerce/pull/62628
406 *
407 * @since 10.5.0
408 *
409 * @param bool|null $sku_found Set to a boolean value to short-circuit the default SKU check.
410 * @param int $product_id The ID of the current product.
411 * @param string $sku The SKU to check for uniqueness.
412 */
413 $sku_found = apply_filters( 'wc_product_has_unique_sku', false, $product_id, $new_sku );
414
415 if ( ! $sku_found ) {
416 $product->set_sku( $new_sku );
417 return;
418 }
419 }
420
421 // Fallback: use the standard unique SKU generator which loops until it finds a unique value.
422 $product->set_sku( wc_product_generate_unique_sku( $product_id, $original_sku, $max_suffix + 1 ) );
423 }
424 }
425
426 return new WC_Admin_Duplicate_Product();
427