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