PluginProbe
WooCommerce / 11.1.0-beta.2
WooCommerce v11.1.0-beta.2
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 All 648 releases
woocommerce / includes / class-wc-ajax.php
class-wc-ajax.php
4,341 lines 134.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WooCommerce WC_AJAX. AJAX Event Handlers.
4 *
5 * @class WC_AJAX
6 * @package WooCommerce\Classes
7 */
8
9 use Automattic\Jetpack\Constants;
10 use Automattic\WooCommerce\Enums\ProductStatus;
11 use Automattic\WooCommerce\Enums\ProductStockStatus;
12 use Automattic\WooCommerce\Enums\ProductType;
13 use Automattic\WooCommerce\Internal\CostOfGoodsSold\CostOfGoodsSoldController;
14 use Automattic\WooCommerce\Internal\ProductAttributes\VisualAttributeTermMeta;
15 use Automattic\WooCommerce\Internal\Orders\CouponsController;
16 use Automattic\WooCommerce\Internal\Orders\TaxesController;
17 use Automattic\WooCommerce\Internal\Orders\OrderNoteGroup;
18 use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\CustomMetaBox;
19 use Automattic\WooCommerce\Internal\Utilities\Users;
20 use Automattic\WooCommerce\Proxies\LegacyProxy;
21 use Automattic\WooCommerce\Utilities\ArrayUtil;
22 use Automattic\WooCommerce\Utilities\NumberUtil;
23 use Automattic\WooCommerce\Utilities\OrderUtil;
24 use Automattic\WooCommerce\Utilities\StringUtil;
25
26 defined( 'ABSPATH' ) || exit;
27
28 /**
29 * WC_Ajax class.
30 */
31 class WC_AJAX {
32
33 /**
34 * Hook in ajax handlers.
35 *
36 * @return void
37 */
38 public static function init() {
39 add_action( 'init', array( __CLASS__, 'define_ajax' ), 0 );
40 add_action( 'template_redirect', array( __CLASS__, 'do_wc_ajax' ), 0 );
41 self::add_ajax_events();
42 }
43
44 /**
45 * Get WC Ajax Endpoint.
46 *
47 * @param string $request Optional.
48 *
49 * @return string
50 */
51 public static function get_endpoint( $request = '' ) {
52 return esc_url_raw( apply_filters( 'woocommerce_ajax_get_endpoint', add_query_arg( 'wc-ajax', $request, remove_query_arg( array( 'remove_item', 'add-to-cart', 'added-to-cart', 'order_again', '_wpnonce' ), home_url( '/', 'relative' ) ) ), $request ) );
53 }
54
55 /**
56 * Set the 'wc-ajax' argument in $wp_query.
57 *
58 * @return void
59 */
60 private static function set_wc_ajax_argument_in_query() {
61 global $wp_query;
62
63 // phpcs:disable WordPress.Security.NonceVerification.Recommended
64 if ( ! empty( $_GET['wc-ajax'] ) && empty( $wp_query->get( 'wc-ajax' ) ) ) {
65 $wp_query->set( 'wc-ajax', sanitize_text_field( wp_unslash( $_GET['wc-ajax'] ) ) );
66 }
67 // phpcs:enable WordPress.Security.NonceVerification.Recommended
68 }
69
70 /**
71 * Set WC AJAX constant and headers.
72 *
73 * @return void
74 */
75 public static function define_ajax() {
76 global $wp_query;
77
78 // phpcs:disable
79 self::set_wc_ajax_argument_in_query();
80 if ( ! empty( $wp_query->get( 'wc-ajax' ) ) ) {
81 wc_maybe_define_constant( 'DOING_AJAX', true );
82 wc_maybe_define_constant( 'WC_DOING_AJAX', true );
83 if ( ! WP_DEBUG || ( WP_DEBUG && ! WP_DEBUG_DISPLAY ) ) {
84 @ini_set( 'display_errors', 0 ); // Turn off display_errors during AJAX events to prevent malformed JSON.
85 }
86 $GLOBALS['wpdb']->hide_errors();
87 }
88 // phpcs:enable
89 }
90
91 /**
92 * Send headers for WC Ajax Requests.
93 *
94 * @since 2.5.0
95 *
96 * @return void
97 */
98 private static function wc_ajax_headers() {
99 if ( ! headers_sent() ) {
100 send_origin_headers();
101 send_nosniff_header();
102 wc_nocache_headers();
103 header( 'Content-Type: text/html; charset=' . get_option( 'blog_charset' ) );
104 header( 'X-Robots-Tag: noindex' );
105 status_header( 200 );
106 } elseif ( Constants::is_true( 'WP_DEBUG' ) ) {
107 headers_sent( $file, $line );
108 trigger_error( "wc_ajax_headers cannot set headers - headers already sent by {$file} on line {$line}", E_USER_NOTICE ); // @codingStandardsIgnoreLine
109 }
110 }
111
112 /**
113 * Check for WC Ajax request and fire action.
114 *
115 * @return void
116 */
117 public static function do_wc_ajax() {
118 global $wp_query;
119
120 // phpcs:disable WordPress.Security.NonceVerification.Recommended
121 self::set_wc_ajax_argument_in_query();
122
123 $action = $wp_query->get( 'wc-ajax' );
124
125 if ( $action ) {
126 self::wc_ajax_headers();
127 $action = sanitize_text_field( $action );
128 do_action( 'wc_ajax_' . $action );
129 wp_die();
130 }
131 // phpcs:enable
132 }
133
134 /**
135 * Hook in methods - uses WordPress ajax handlers (admin-ajax).
136 *
137 * @return void
138 */
139 public static function add_ajax_events() {
140 $ajax_events_nopriv = array(
141 'get_refreshed_fragments',
142 'apply_coupon',
143 'remove_coupon',
144 'update_shipping_method',
145 'get_cart_totals',
146 'update_order_review',
147 'add_to_cart',
148 'remove_from_cart',
149 'checkout',
150 'get_variation',
151 'get_customer_location',
152 );
153
154 foreach ( $ajax_events_nopriv as $ajax_event ) {
155 add_action( 'wp_ajax_woocommerce_' . $ajax_event, array( __CLASS__, $ajax_event ) );
156 add_action( 'wp_ajax_nopriv_woocommerce_' . $ajax_event, array( __CLASS__, $ajax_event ) );
157
158 // WC AJAX can be used for frontend ajax requests.
159 add_action( 'wc_ajax_' . $ajax_event, array( __CLASS__, $ajax_event ) );
160 }
161
162 $ajax_events = array(
163 'feature_product',
164 'mark_order_status',
165 'get_order_details',
166 'add_attribute',
167 'add_new_attribute',
168 'remove_variations',
169 'save_attributes',
170 'add_attributes_and_variations',
171 'add_variation',
172 'link_all_variations',
173 'revoke_access_to_download',
174 'grant_access_to_download',
175 'get_customer_details',
176 'add_order_item',
177 'add_order_fee',
178 'add_order_shipping',
179 'add_order_tax',
180 'add_coupon_discount',
181 'remove_order_coupon',
182 'remove_order_item',
183 'remove_order_tax',
184 'calc_line_taxes',
185 'save_order_items',
186 'load_order_items',
187 'add_order_note',
188 'delete_order_note',
189 'json_search_order_metakeys',
190 'json_search_products',
191 'json_search_products_and_variations',
192 'json_search_downloadable_products_and_variations',
193 'json_search_customers',
194 'json_search_categories',
195 'json_search_categories_tree',
196 'json_search_taxonomy_terms',
197 'json_search_product_attributes',
198 'json_search_pages',
199 'term_ordering',
200 'product_ordering',
201 'refund_line_items',
202 'delete_refund',
203 'rated',
204 'update_api_key',
205 'load_variations',
206 'save_variations',
207 'bulk_edit_variations',
208 'tax_rates_save_changes',
209 'shipping_zones_save_changes',
210 'shipping_zone_add_method',
211 'shipping_zone_remove_method',
212 'shipping_zone_methods_save_changes',
213 'shipping_zone_methods_save_settings',
214 'shipping_classes_save_changes',
215 'shipping_providers_save_changes',
216 'toggle_gateway_enabled',
217 'load_status_widget',
218 'load_recent_reviews_widget',
219 );
220
221 foreach ( $ajax_events as $ajax_event ) {
222 add_action( 'wp_ajax_woocommerce_' . $ajax_event, array( __CLASS__, $ajax_event ) );
223 }
224
225 $ajax_private_events = array(
226 'order_add_meta',
227 'order_delete_meta',
228 );
229
230 foreach ( $ajax_private_events as $ajax_event ) {
231 add_action(
232 'wp_ajax_woocommerce_' . $ajax_event,
233 function () use ( $ajax_event ) {
234 call_user_func( array( __CLASS__, $ajax_event ) );
235 }
236 );
237 }
238
239 // WP's heartbeat.
240 $ajax_heartbeat_callbacks = array(
241 'order_refresh_lock',
242 'check_locked_orders',
243 );
244 foreach ( $ajax_heartbeat_callbacks as $ajax_callback ) {
245 add_filter(
246 'heartbeat_received',
247 // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
248 function ( $response, $data ) use ( $ajax_callback ) {
249 return call_user_func_array( array( __CLASS__, $ajax_callback ), func_get_args() );
250 },
251 11,
252 2
253 );
254 }
255 }
256
257 /**
258 * Get a refreshed cart fragment, including the mini cart HTML.
259 *
260 * @return void
261 */
262 public static function get_refreshed_fragments() {
263 ob_start();
264
265 woocommerce_mini_cart();
266
267 $mini_cart = ob_get_clean();
268
269 $data = array(
270 'fragments' => apply_filters(
271 'woocommerce_add_to_cart_fragments',
272 array(
273 'div.widget_shopping_cart_content' => '<div class="widget_shopping_cart_content">' . $mini_cart . '</div>',
274 )
275 ),
276 'cart_hash' => WC()->cart->get_cart_hash(),
277 );
278
279 wp_send_json( $data );
280 }
281
282 /**
283 * AJAX apply coupon on checkout page.
284 *
285 * @return void
286 */
287 public static function apply_coupon() {
288
289 check_ajax_referer( 'apply-coupon', 'security' );
290
291 $coupon_code = ArrayUtil::get_value_or_default( $_POST, 'coupon_code' );
292 $billing_email = ArrayUtil::get_value_or_default( $_POST, 'billing_email' );
293
294 if ( is_string( $billing_email ) && is_email( $billing_email ) ) {
295 wc()->customer->set_billing_email( $billing_email );
296 }
297
298 if ( ! StringUtil::is_null_or_whitespace( $coupon_code ) ) {
299 WC()->cart->add_discount( wc_format_coupon_code( wp_unslash( $coupon_code ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
300 } else {
301 wc_add_notice( WC_Coupon::get_generic_coupon_error( WC_Coupon::E_WC_COUPON_PLEASE_ENTER ), 'error' );
302 }
303
304 wc_print_notices();
305 wp_die();
306 }
307
308 /**
309 * AJAX remove coupon on cart and checkout page.
310 *
311 * @return void
312 */
313 public static function remove_coupon() {
314 check_ajax_referer( 'remove-coupon', 'security' );
315
316 $coupon = isset( $_POST['coupon'] ) ? wc_format_coupon_code( wp_unslash( $_POST['coupon'] ) ) : false; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
317
318 if ( StringUtil::is_null_or_whitespace( $coupon ) ) {
319 wc_add_notice( __( 'Sorry there was a problem removing this coupon.', 'woocommerce' ), 'error' );
320 } else {
321 WC()->cart->remove_coupon( $coupon );
322 wc_add_notice( __( 'Coupon has been removed.', 'woocommerce' ) );
323 }
324
325 wc_print_notices();
326 wp_die();
327 }
328
329 /**
330 * AJAX update shipping method on cart page.
331 *
332 * @return void
333 */
334 public static function update_shipping_method() {
335 check_ajax_referer( 'update-shipping-method', 'security' );
336
337 wc_maybe_define_constant( 'WOOCOMMERCE_CART', true );
338
339 $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' );
340 $posted_shipping_methods = isset( $_POST['shipping_method'] ) ? wc_clean( wp_unslash( $_POST['shipping_method'] ) ) : array();
341
342 if ( is_array( $posted_shipping_methods ) ) {
343 foreach ( $posted_shipping_methods as $i => $value ) {
344 if ( ! is_string( $value ) ) {
345 continue;
346 }
347 $chosen_shipping_methods[ $i ] = $value;
348 }
349 }
350
351 WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods );
352
353 self::get_cart_totals();
354 }
355
356 /**
357 * AJAX receive updated cart_totals div.
358 *
359 * @return void
360 */
361 public static function get_cart_totals() {
362 wc_maybe_define_constant( 'WOOCOMMERCE_CART', true );
363 WC()->cart->calculate_totals();
364 woocommerce_cart_totals();
365 wp_die();
366 }
367
368 /**
369 * Session has expired.
370 *
371 * @return void
372 */
373 private static function update_order_review_expired() {
374 wp_send_json(
375 array(
376 'fragments' => apply_filters(
377 'woocommerce_update_order_review_fragments',
378 array(
379 'form.woocommerce-checkout' => wc_print_notice(
380 esc_html__( 'Sorry, your session has expired.', 'woocommerce' ) . ' <a href="' . esc_url( wc_get_page_permalink( 'shop' ) ) . '" class="wc-backward">' . esc_html__( 'Return to shop', 'woocommerce' ) . '</a>',
381 'error',
382 array(),
383 true
384 ),
385 )
386 ),
387 )
388 );
389 }
390
391 /**
392 * AJAX update order review on checkout.
393 *
394 * @return void
395 */
396 public static function update_order_review() {
397 check_ajax_referer( 'update-order-review', 'security' );
398
399 wc_maybe_define_constant( 'WOOCOMMERCE_CHECKOUT', true );
400
401 if ( WC()->cart->is_empty() && ! is_customize_preview() && apply_filters( 'woocommerce_checkout_update_order_review_expired', true ) ) {
402 self::update_order_review_expired();
403 }
404
405 do_action( 'woocommerce_checkout_update_order_review', isset( $_POST['post_data'] ) ? wp_unslash( $_POST['post_data'] ) : '' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
406
407 $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' );
408 $posted_shipping_methods = isset( $_POST['shipping_method'] ) ? wc_clean( wp_unslash( $_POST['shipping_method'] ) ) : array();
409
410 if ( is_array( $posted_shipping_methods ) ) {
411 foreach ( $posted_shipping_methods as $i => $value ) {
412 if ( ! is_string( $value ) ) {
413 continue;
414 }
415 $chosen_shipping_methods[ $i ] = $value;
416 }
417 }
418
419 WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods );
420 WC()->session->set( 'chosen_payment_method', empty( $_POST['payment_method'] ) ? '' : wc_clean( wp_unslash( $_POST['payment_method'] ) ) );
421 WC()->customer->set_props(
422 array(
423 'billing_country' => isset( $_POST['country'] ) ? wc_clean( wp_unslash( $_POST['country'] ) ) : null,
424 'billing_state' => isset( $_POST['state'] ) ? wc_clean( wp_unslash( $_POST['state'] ) ) : null,
425 'billing_postcode' => isset( $_POST['postcode'] ) ? wc_clean( wp_unslash( $_POST['postcode'] ) ) : null,
426 'billing_city' => isset( $_POST['city'] ) ? wc_clean( wp_unslash( $_POST['city'] ) ) : null,
427 'billing_address_1' => isset( $_POST['address'] ) ? wc_clean( wp_unslash( $_POST['address'] ) ) : null,
428 'billing_address_2' => isset( $_POST['address_2'] ) ? wc_clean( wp_unslash( $_POST['address_2'] ) ) : null,
429 )
430 );
431
432 if ( wc_ship_to_billing_address_only() ) {
433 WC()->customer->set_props(
434 array(
435 'shipping_country' => isset( $_POST['country'] ) ? wc_clean( wp_unslash( $_POST['country'] ) ) : null,
436 'shipping_state' => isset( $_POST['state'] ) ? wc_clean( wp_unslash( $_POST['state'] ) ) : null,
437 'shipping_postcode' => isset( $_POST['postcode'] ) ? wc_clean( wp_unslash( $_POST['postcode'] ) ) : null,
438 'shipping_city' => isset( $_POST['city'] ) ? wc_clean( wp_unslash( $_POST['city'] ) ) : null,
439 'shipping_address_1' => isset( $_POST['address'] ) ? wc_clean( wp_unslash( $_POST['address'] ) ) : null,
440 'shipping_address_2' => isset( $_POST['address_2'] ) ? wc_clean( wp_unslash( $_POST['address_2'] ) ) : null,
441 )
442 );
443 } else {
444 WC()->customer->set_props(
445 array(
446 'shipping_country' => isset( $_POST['s_country'] ) ? wc_clean( wp_unslash( $_POST['s_country'] ) ) : null,
447 'shipping_state' => isset( $_POST['s_state'] ) ? wc_clean( wp_unslash( $_POST['s_state'] ) ) : null,
448 'shipping_postcode' => isset( $_POST['s_postcode'] ) ? wc_clean( wp_unslash( $_POST['s_postcode'] ) ) : null,
449 'shipping_city' => isset( $_POST['s_city'] ) ? wc_clean( wp_unslash( $_POST['s_city'] ) ) : null,
450 'shipping_address_1' => isset( $_POST['s_address'] ) ? wc_clean( wp_unslash( $_POST['s_address'] ) ) : null,
451 'shipping_address_2' => isset( $_POST['s_address_2'] ) ? wc_clean( wp_unslash( $_POST['s_address_2'] ) ) : null,
452 )
453 );
454 }
455
456 if ( isset( $_POST['has_full_address'] ) && wc_string_to_bool( wc_clean( wp_unslash( $_POST['has_full_address'] ) ) ) ) {
457 WC()->customer->set_calculated_shipping( true );
458 } else {
459 WC()->customer->set_calculated_shipping( false );
460 }
461
462 WC()->customer->save();
463
464 // Calculate shipping before totals. This will ensure any shipping methods that affect things like taxes are chosen prior to final totals being calculated. Ref: #22708.
465 WC()->cart->calculate_shipping();
466 WC()->cart->calculate_totals();
467
468 // Get order review fragment.
469 ob_start();
470 woocommerce_order_review();
471 $woocommerce_order_review = ob_get_clean();
472
473 // Get checkout payment fragment.
474 ob_start();
475 woocommerce_checkout_payment();
476 $woocommerce_checkout_payment = ob_get_clean();
477
478 // Get messages if reload checkout is not true.
479 $reload_checkout = isset( WC()->session->reload_checkout );
480 if ( ! $reload_checkout ) {
481 $messages = wc_print_notices( true );
482 } else {
483 $messages = '';
484 }
485
486 unset( WC()->session->refresh_totals, WC()->session->reload_checkout );
487
488 wp_send_json(
489 array(
490 'result' => empty( $messages ) ? 'success' : 'failure',
491 'messages' => $messages,
492 'reload' => $reload_checkout,
493 'fragments' => apply_filters(
494 'woocommerce_update_order_review_fragments',
495 array(
496 '.woocommerce-checkout-review-order-table' => $woocommerce_order_review,
497 '.woocommerce-checkout-payment' => $woocommerce_checkout_payment,
498 )
499 ),
500 )
501 );
502 }
503
504 /**
505 * AJAX add to cart.
506 *
507 * @return void
508 */
509 public static function add_to_cart() {
510 ob_start();
511
512 // phpcs:disable WordPress.Security.NonceVerification.Missing
513 if ( ! isset( $_POST['product_id'] ) ) {
514 return;
515 }
516
517 $product_id = apply_filters( 'woocommerce_add_to_cart_product_id', absint( $_POST['product_id'] ) );
518 $product = wc_get_product( $product_id );
519 $quantity = empty( $_POST['quantity'] ) ? 1 : wc_stock_amount( wp_unslash( $_POST['quantity'] ) );
520 $passed_validation = apply_filters( 'woocommerce_add_to_cart_validation', true, $product_id, $quantity );
521 $product_status = get_post_status( $product_id );
522 $variation_id = 0;
523 $variation = array();
524
525 if ( $product && ProductType::VARIATION === $product->get_type() ) {
526 $variation_id = $product_id;
527 $product_id = $product->get_parent_id();
528 $variation = $product->get_variation_attributes();
529 }
530
531 if ( $passed_validation && false !== WC()->cart->add_to_cart( $product_id, $quantity, $variation_id, $variation ) && ProductStatus::PUBLISH === $product_status ) {
532
533 do_action( 'woocommerce_ajax_added_to_cart', $product_id );
534
535 /**
536 * Fires when an item is added to the cart from a user request.
537 *
538 * @param int $product_id Product ID.
539 * @param int|float $quantity Quantity added to the cart.
540 *
541 * @since 10.6.0
542 */
543 do_action( 'internal_woocommerce_cart_item_added_from_user_request', $variation_id ? $variation_id : $product_id, $quantity );
544
545 if ( 'yes' === get_option( 'woocommerce_cart_redirect_after_add' ) ) {
546 wc_add_to_cart_message( array( $product_id => $quantity ), true );
547 }
548
549 self::get_refreshed_fragments();
550
551 } else {
552
553 // If there was an error adding to the cart, redirect to the product page to show any errors.
554 $data = array(
555 'error' => true,
556 'product_url' => apply_filters( 'woocommerce_cart_redirect_after_error', get_permalink( $product_id ), $product_id ),
557 );
558
559 wp_send_json( $data );
560 }
561 // phpcs:enable
562 }
563
564 /**
565 * AJAX remove from cart.
566 *
567 * @return void
568 */
569 public static function remove_from_cart() {
570 ob_start();
571
572 // phpcs:ignore WordPress.Security.NonceVerification.Missing
573 $cart_item_key = wc_clean( isset( $_POST['cart_item_key'] ) ? wp_unslash( $_POST['cart_item_key'] ) : '' );
574
575 if ( $cart_item_key && is_string( $cart_item_key ) && false !== WC()->cart->remove_cart_item( $cart_item_key ) ) {
576 /**
577 * Fires when an item is removed from the cart from a user request.
578 *
579 * @param string $cart_item_key Cart item key.
580 * @param \WC_Cart $cart Cart object.
581 *
582 * @since 10.6.0
583 */
584 do_action( 'internal_woocommerce_cart_item_removed_from_user_request', $cart_item_key, WC()->cart );
585 self::get_refreshed_fragments();
586 } else {
587 wp_send_json_error();
588 }
589 }
590
591 /**
592 * Process ajax checkout form.
593 *
594 * @return void
595 */
596 public static function checkout() {
597 wc_maybe_define_constant( 'WOOCOMMERCE_CHECKOUT', true );
598 WC()->checkout()->process_checkout();
599 wp_die( 0 );
600 }
601
602 /**
603 * Get a matching variation based on posted attributes.
604 *
605 * @return void
606 */
607 public static function get_variation() {
608 ob_start();
609
610 // phpcs:disable WordPress.Security.NonceVerification.Missing
611 if ( empty( $_POST['product_id'] ) ) {
612 wp_die();
613 }
614
615 $variable_product = wc_get_product( absint( $_POST['product_id'] ) );
616
617 if ( ! $variable_product ) {
618 wp_die();
619 }
620
621 if ( ! $variable_product->is_viewable() ) {
622 wp_die();
623 }
624
625 $data_store = WC_Data_Store::load( 'product' );
626 $variation_id = $data_store->find_matching_product_variation( $variable_product, wp_unslash( $_POST ) );
627 $variation = $variation_id ? $variable_product->get_available_variation( $variation_id ) : false;
628 wp_send_json( $variation );
629 // phpcs:enable
630 }
631
632 /**
633 * Locate user via AJAX.
634 *
635 * @return void
636 */
637 public static function get_customer_location() {
638 $location_hash = WC_Cache_Helper::geolocation_ajax_get_location_hash();
639 wp_send_json_success( array( 'hash' => $location_hash ) );
640 }
641
642 /**
643 * Toggle Featured status of a product from admin.
644 *
645 * @return void
646 */
647 public static function feature_product() {
648 if ( current_user_can( 'edit_products' ) && check_admin_referer( 'woocommerce-feature-product' ) && isset( $_GET['product_id'] ) ) {
649 $product = wc_get_product( absint( $_GET['product_id'] ) );
650
651 if ( $product ) {
652 $product->set_featured( ! $product->get_featured() );
653 $product->save();
654 }
655 }
656
657 wp_safe_redirect( wp_get_referer() ? remove_query_arg( array( 'trashed', 'untrashed', 'deleted', 'ids' ), wp_get_referer() ) : admin_url( 'edit.php?post_type=product' ) );
658 exit;
659 }
660
661 /**
662 * Mark an order with a status.
663 *
664 * @return void
665 */
666 public static function mark_order_status() {
667 if ( current_user_can( 'edit_shop_orders' ) && check_admin_referer( 'woocommerce-mark-order-status' ) && isset( $_GET['status'], $_GET['order_id'] ) ) {
668 $status = sanitize_text_field( wp_unslash( $_GET['status'] ) );
669 $order = wc_get_order( absint( wp_unslash( $_GET['order_id'] ) ) );
670
671 if ( wc_is_order_status( 'wc-' . $status ) && $order ) {
672 // Initialize payment gateways in case order has hooked status transition actions.
673 WC()->payment_gateways();
674
675 $order->update_status( $status, '', true );
676 do_action( 'woocommerce_order_edit_status', $order->get_id(), $status );
677 }
678 }
679
680 wp_safe_redirect( wp_get_referer() ? wp_get_referer() : admin_url( 'edit.php?post_type=shop_order' ) );
681 exit;
682 }
683
684 /**
685 * Get order details.
686 *
687 * @return void
688 */
689 public static function get_order_details() {
690 check_admin_referer( 'woocommerce-preview-order', 'security' );
691
692 if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_GET['order_id'] ) ) {
693 wp_die( -1 );
694 }
695
696 $order = wc_get_order( absint( $_GET['order_id'] ) );
697
698 if ( $order ) {
699 include_once __DIR__ . '/admin/list-tables/class-wc-admin-list-table-orders.php';
700
701 wp_send_json_success( WC_Admin_List_Table_Orders::order_preview_get_order_details( $order ) );
702 }
703 wp_die();
704 }
705
706 /**
707 * Add an attribute row.
708 *
709 * @return void
710 */
711 public static function add_attribute() {
712 ob_start();
713
714 check_ajax_referer( 'add-attribute', 'security' );
715
716 if ( ! current_user_can( 'edit_products' ) || ! isset( $_POST['taxonomy'], $_POST['i'] ) ) {
717 wp_die( -1 );
718 }
719
720 $product_type = isset( $_POST['product_type'] ) ? sanitize_text_field( wp_unslash( $_POST['product_type'] ) ) : ProductType::SIMPLE;
721
722 $i = absint( $_POST['i'] );
723 $metabox_class = array();
724 $attribute = new WC_Product_Attribute();
725
726 $attribute->set_id( wc_attribute_taxonomy_id_by_name( sanitize_text_field( wp_unslash( $_POST['taxonomy'] ) ) ) );
727 $attribute->set_name( sanitize_text_field( wp_unslash( $_POST['taxonomy'] ) ) );
728 /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */
729 $attribute->set_visible( apply_filters( 'woocommerce_attribute_default_visibility', 1 ) );
730 $attribute->set_variation(
731 apply_filters(
732 'woocommerce_attribute_default_is_variation',
733 ProductType::VARIABLE === $product_type ? 1 : 0,
734 $product_type
735 )
736 );
737 /* phpcs: enable */
738
739 if ( $attribute->is_taxonomy() ) {
740 $metabox_class[] = 'taxonomy';
741 $metabox_class[] = $attribute->get_name();
742 }
743
744 include __DIR__ . '/admin/meta-boxes/views/html-product-attribute.php';
745 wp_die();
746 }
747
748 /**
749 * Add a new attribute via ajax function.
750 *
751 * @return void
752 */
753 public static function add_new_attribute() {
754 check_ajax_referer( 'add-attribute', 'security' );
755
756 if ( current_user_can( 'manage_product_terms' ) && isset( $_POST['taxonomy'], $_POST['term'] ) ) {
757 $taxonomy = esc_attr( wp_unslash( $_POST['taxonomy'] ) ); // phpcs:ignore
758 $term = wc_clean( wp_unslash( $_POST['term'] ) );
759
760 if ( taxonomy_exists( $taxonomy ) ) {
761
762 $result = wp_insert_term( $term, $taxonomy );
763
764 if ( is_wp_error( $result ) ) {
765 wp_send_json(
766 array(
767 'error' => $result->get_error_message(),
768 )
769 );
770 } else {
771 VisualAttributeTermMeta::save_term_visual_from_request( (int) $result['term_id'], $taxonomy, $_POST ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
772
773 $term = get_term_by( 'id', $result['term_id'], $taxonomy );
774
775 if ( ! $term ) {
776 wp_send_json(
777 array(
778 'error' => __( 'Term not found', 'woocommerce' ),
779 )
780 );
781 }
782
783 $response = array(
784 'term_id' => $term->term_id,
785 'name' => $term->name,
786 'slug' => $term->slug,
787 );
788
789 if ( VisualAttributeTermMeta::is_visual_attribute_taxonomy( $taxonomy ) ) {
790 $response['visual'] = VisualAttributeTermMeta::get_term_visual( (int) $term->term_id );
791 }
792
793 wp_send_json( $response );
794 }//end if
795 }//end if
796 }//end if
797 wp_die( -1 );
798 }
799
800 /**
801 * Delete variations via ajax function.
802 *
803 * @return void
804 */
805 public static function remove_variations() {
806 check_ajax_referer( 'delete-variations', 'security' );
807
808 if ( current_user_can( 'edit_products' ) && isset( $_POST['variation_ids'] ) ) {
809 $variation_ids = array_map( 'absint', (array) wp_unslash( $_POST['variation_ids'] ) );
810
811 foreach ( $variation_ids as $variation_id ) {
812 if ( 'product_variation' === get_post_type( $variation_id ) ) {
813 $variation = wc_get_product( $variation_id );
814 $variation->delete( true );
815 }
816 }
817 }
818
819 wp_die( -1 );
820 }
821
822 /**
823 * Save attributes via ajax.
824 *
825 * @return void
826 */
827 public static function save_attributes() {
828 check_ajax_referer( 'save-attributes', 'security' );
829
830 if ( ! current_user_can( 'edit_products' ) || ! isset( $_POST['data'], $_POST['post_id'] ) ) {
831 wp_die( -1 );
832 }
833
834 $response = array();
835
836 try {
837 parse_str( wp_unslash( $_POST['data'] ), $data ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
838
839 $product = self::create_product_with_attributes( $data );
840
841 ob_start();
842 $attributes = $product->get_attributes( 'edit' );
843 $i = -1;
844 if ( ! empty( $data['attribute_names'] ) ) {
845 foreach ( $data['attribute_names'] as $attribute_name ) {
846 $attribute = isset( $attributes[ sanitize_title( $attribute_name ) ] ) ? $attributes[ sanitize_title( $attribute_name ) ] : false;
847 if ( ! $attribute ) {
848 continue;
849 }
850 ++$i;
851 $metabox_class = array();
852
853 if ( $attribute->is_taxonomy() ) {
854 $metabox_class[] = 'taxonomy';
855 $metabox_class[] = $attribute->get_name();
856 }
857
858 include __DIR__ . '/admin/meta-boxes/views/html-product-attribute.php';
859 }
860 }
861
862 $response['html'] = ob_get_clean();
863 } catch ( Exception $e ) {
864 wp_send_json_error( array( 'error' => $e->getMessage() ) );
865 }
866
867 // wp_send_json_success must be outside the try block not to break phpunit tests.
868 wp_send_json_success( $response );
869 }
870
871 /**
872 * Save attributes and variations via ajax.
873 *
874 * @return void
875 */
876 public static function add_attributes_and_variations() {
877 check_ajax_referer( 'add-attributes-and-variations', 'security' );
878
879 if ( ! current_user_can( 'edit_products' ) || ! isset( $_POST['data'], $_POST['post_id'] ) ) {
880 wp_die( -1 );
881 }
882
883 try {
884 parse_str( wp_unslash( $_POST['data'] ), $data ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
885
886 $product = self::create_product_with_attributes( $data );
887 self::create_all_product_variations( $product );
888
889 wp_send_json_success();
890 wp_die();
891
892 } catch ( Exception $e ) {
893 wp_send_json_error( array( 'error' => $e->getMessage() ) );
894 }
895 }
896 /**
897 * Create product with attributes from POST data.
898 *
899 * @param array $data Attribute data.
900 * @return mixed Product class.
901 */
902 private static function create_product_with_attributes( $data ) {
903 // phpcs:disable WordPress.Security.NonceVerification.Missing
904 if ( ! isset( $_POST['post_id'] ) ) {
905 wp_die( -1 );
906 }
907 $attributes = WC_Meta_Box_Product_Data::prepare_attributes( $data );
908 $product_id = absint( wp_unslash( $_POST['post_id'] ) );
909 $product_type = ! empty( $_POST['product_type'] ) ? wc_clean( wp_unslash( $_POST['product_type'] ) ) : ProductType::SIMPLE;
910 $classname = WC_Product_Factory::get_product_classname( $product_id, $product_type );
911 $product = new $classname( $product_id );
912 $product->set_attributes( $attributes );
913 $product->save();
914 return $product;
915 }
916 /**
917 * Create all product variations from existing attributes.
918 *
919 * @param mixed $product Product class.
920 * @returns int Number of variations created.
921 */
922 private static function create_all_product_variations( $product ) {
923 $data_store = $product->get_data_store();
924 if ( ! is_callable( array( $data_store, 'create_all_product_variations' ) ) ) {
925 wp_die();
926 }
927 $number = $data_store->create_all_product_variations( $product, Constants::get_constant( 'WC_MAX_LINKED_VARIATIONS' ) );
928 $data_store->sort_all_product_variations( $product->get_id() );
929 return $number;
930 }
931
932 /**
933 * Add variation via ajax function.
934 *
935 * @return void
936 */
937 public static function add_variation() {
938 check_ajax_referer( 'add-variation', 'security' );
939
940 if ( ! current_user_can( 'edit_products' ) || ! isset( $_POST['post_id'], $_POST['loop'] ) ) {
941 wp_die( -1 );
942 }
943
944 global $post; // Set $post global so its available, like within the admin screens.
945
946 $product_id = intval( $_POST['post_id'] );
947 $post = get_post( $product_id ); // phpcs:ignore
948 $loop = intval( $_POST['loop'] );
949 $product_object = wc_get_product_object( ProductType::VARIABLE, $product_id ); // Forces type to variable in case product is unsaved.
950 $variation_object = wc_get_product_object( ProductType::VARIATION );
951 $variation_object->set_parent_id( $product_id );
952 $variation_object->set_attributes( array_fill_keys( array_map( 'sanitize_title', array_keys( $product_object->get_variation_attributes() ) ), '' ) );
953 $variation_object->save();
954 self::render_variation_html( $product_object, $variation_object, $loop, self::base_cost_or_null( $product_object ) );
955 wp_die();
956 }
957
958 /**
959 * Link all variations via ajax function.
960 *
961 * @return void
962 */
963 public static function link_all_variations() {
964 check_ajax_referer( 'link-variations', 'security' );
965
966 if ( ! current_user_can( 'edit_products' ) ) {
967 wp_die( -1 );
968 }
969
970 wc_maybe_define_constant( 'WC_MAX_LINKED_VARIATIONS', 50 );
971 wc_set_time_limit( 0 );
972
973 $post_id = isset( $_POST['post_id'] ) ? intval( $_POST['post_id'] ) : 0;
974
975 if ( ! $post_id ) {
976 wp_die();
977 }
978
979 $product = wc_get_product( $post_id );
980 $number_created = self::create_all_product_variations( $product );
981
982 echo esc_html( $number_created );
983
984 wp_die();
985 }
986
987 /**
988 * Delete download permissions via ajax function.
989 *
990 * @return void
991 */
992 public static function revoke_access_to_download() {
993 check_ajax_referer( 'revoke-access', 'security' );
994
995 if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['download_id'], $_POST['product_id'], $_POST['order_id'], $_POST['permission_id'] ) ) {
996 wp_die( -1 );
997 }
998 $download_id = wc_clean( wp_unslash( $_POST['download_id'] ) );
999 $product_id = intval( $_POST['product_id'] );
1000 $order_id = intval( $_POST['order_id'] );
1001 $permission_id = absint( $_POST['permission_id'] );
1002 $data_store = WC_Data_Store::load( 'customer-download' );
1003 $data_store->delete_by_id( $permission_id );
1004
1005 do_action( 'woocommerce_ajax_revoke_access_to_product_download', $download_id, $product_id, $order_id, $permission_id );
1006
1007 wp_die();
1008 }
1009
1010 /**
1011 * Grant download permissions via ajax function.
1012 *
1013 * @return void
1014 */
1015 public static function grant_access_to_download() {
1016
1017 check_ajax_referer( 'grant-access', 'security' );
1018
1019 if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['loop'], $_POST['order_id'], $_POST['product_ids'] ) ) {
1020 wp_die( -1 );
1021 }
1022
1023 global $wpdb;
1024
1025 $wpdb->hide_errors();
1026
1027 $order_id = intval( $_POST['order_id'] );
1028 $product_ids = array_filter( array_map( 'absint', (array) wp_unslash( $_POST['product_ids'] ) ) );
1029 $loop = intval( $_POST['loop'] );
1030 $file_counter = 0;
1031 $order = wc_get_order( $order_id );
1032
1033 if ( ! $order->get_billing_email() ) {
1034 wp_die();
1035 }
1036
1037 $data = array();
1038 $items = $order->get_items();
1039
1040 /**
1041 * Customer download data store.
1042 *
1043 * @var WC_Customer_Download_Data_Store $data_store
1044 */
1045 $data_store = WC_Data_Store::load( 'customer-download' );
1046 $existing_download_access = array();
1047
1048 foreach ( $data_store->get_downloads( array( 'order_id' => $order_id ) ) as $download ) {
1049 $existing_download_access[ $download->get_product_id() . '|' . $download->get_download_id() ] = true;
1050 }
1051
1052 // Check against order items first.
1053 foreach ( $items as $item ) {
1054 $product = $item->get_product();
1055
1056 if ( $product && $product->exists() && in_array( $product->get_id(), $product_ids, true ) && $product->is_downloadable() ) {
1057 $data[ $product->get_id() ] = array(
1058 'files' => $product->get_downloads(),
1059 'quantity' => $item->get_quantity(),
1060 'order_item' => $item,
1061 );
1062 }
1063 }
1064
1065 foreach ( $product_ids as $product_id ) {
1066 $product = wc_get_product( $product_id );
1067
1068 if ( isset( $data[ $product->get_id() ] ) ) {
1069 $download_data = $data[ $product->get_id() ];
1070 } else {
1071 $download_data = array(
1072 'files' => $product->get_downloads(),
1073 'quantity' => 1,
1074 'order_item' => null,
1075 );
1076 }
1077
1078 if ( ! empty( $download_data['files'] ) ) {
1079 foreach ( $download_data['files'] as $download_id => $file ) {
1080 $download_access_key = $product_id . '|' . $download_id;
1081
1082 if ( isset( $existing_download_access[ $download_access_key ] ) ) {
1083 continue;
1084 }
1085
1086 $inserted_id = wc_downloadable_file_permission( $download_id, $product->get_id(), $order, $download_data['quantity'], $download_data['order_item'] );
1087 if ( $inserted_id ) {
1088 $existing_download_access[ $download_access_key ] = true;
1089 $download = new WC_Customer_Download( $inserted_id );
1090 ++$loop;
1091 ++$file_counter;
1092
1093 if ( $file->get_name() ) {
1094 $file_count = $file->get_name();
1095 } else {
1096 /* translators: %d file count */
1097 $file_count = sprintf( __( 'File %d', 'woocommerce' ), $file_counter );
1098 }
1099 include __DIR__ . '/admin/meta-boxes/views/html-order-download-permission.php';
1100 }
1101 }
1102 }
1103 }
1104 wp_die();
1105 }
1106
1107 /**
1108 * Get customer details via ajax.
1109 *
1110 * @return void
1111 */
1112 public static function get_customer_details() {
1113 $legacy_proxy = wc_get_container()->get( LegacyProxy::class );
1114 $legacy_proxy->call_function( 'check_ajax_referer', 'get-customer-details', 'security' );
1115
1116 $user_id = absint(
1117 $legacy_proxy->call_function( 'filter_input', INPUT_POST, 'user_id', FILTER_VALIDATE_INT )
1118 );
1119
1120 if (
1121 ! current_user_can( 'edit_shop_orders' )
1122 || is_wp_error( Users::get_user_in_current_site( $user_id ) )
1123 ) {
1124 wp_die( -1 );
1125 }
1126
1127 $customer = new WC_Customer( $user_id );
1128
1129 if ( has_filter( 'woocommerce_found_customer_details' ) ) {
1130 wc_deprecated_function( 'The woocommerce_found_customer_details filter', '3.0', 'woocommerce_ajax_get_customer_details' );
1131 }
1132
1133 $data = $customer->get_data();
1134 $data['date_created'] = $data['date_created'] ? $data['date_created']->getTimestamp() : null;
1135 $data['date_modified'] = $data['date_modified'] ? $data['date_modified']->getTimestamp() : null;
1136
1137 unset( $data['meta_data'] );
1138
1139 $customer_data = apply_filters( 'woocommerce_ajax_get_customer_details', $data, $customer, $user_id );
1140 wp_send_json( $customer_data );
1141 }
1142
1143 /**
1144 * Add order item via ajax. Used on the edit order screen in WP Admin.
1145 *
1146 * @throws Exception If order is invalid.
1147 *
1148 * @return void
1149 */
1150 public static function add_order_item() {
1151 check_ajax_referer( 'order-item', 'security' );
1152
1153 if ( ! current_user_can( 'edit_shop_orders' ) ) {
1154 wp_die( -1 );
1155 }
1156
1157 if ( ! isset( $_POST['order_id'] ) ) {
1158 throw new Exception( __( 'Invalid order', 'woocommerce' ) );
1159 }
1160 $order_id = absint( wp_unslash( $_POST['order_id'] ) );
1161
1162 // If we passed through items it means we need to save first before adding a new one.
1163 $items = ( ! empty( $_POST['items'] ) ) ? wp_unslash( $_POST['items'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1164
1165 $items_to_add = isset( $_POST['data'] ) ? array_filter( wp_unslash( (array) $_POST['data'] ) ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1166
1167 try {
1168 $response = self::maybe_add_order_item( $order_id, $items, $items_to_add );
1169 wp_send_json_success( $response );
1170 } catch ( Exception $e ) {
1171 wp_send_json_error( array( 'error' => $e->getMessage() ) );
1172 }
1173 }
1174
1175 /**
1176 * Add order item via AJAX. This is refactored for better unit testing.
1177 *
1178 * @param int $order_id ID of order to add items to.
1179 * @param string|array $items Existing items in order. Empty string if no items to add.
1180 * @param array $items_to_add Array of items to add.
1181 *
1182 * @return array Fragments to render and notes HTML.
1183 * @throws Exception When unable to add item.
1184 */
1185 private static function maybe_add_order_item( $order_id, $items, $items_to_add ) {
1186 try {
1187 $order = wc_get_order( $order_id );
1188
1189 if ( ! $order ) {
1190 throw new Exception( __( 'Invalid order', 'woocommerce' ) );
1191 }
1192
1193 if ( ! empty( $items ) ) {
1194 $save_items = array();
1195 parse_str( $items, $save_items );
1196 wc_save_order_items( $order->get_id(), $save_items );
1197 }
1198
1199 // Add items to order.
1200 $order_notes = array();
1201 $added_items = array();
1202
1203 foreach ( $items_to_add as $item ) {
1204 if ( ! isset( $item['id'], $item['qty'] ) || empty( $item['id'] ) ) {
1205 continue;
1206 }
1207 $product_id = absint( $item['id'] );
1208 $qty = wc_stock_amount( $item['qty'] );
1209 $product = wc_get_product( $product_id );
1210
1211 if ( ! $product ) {
1212 throw new Exception( __( 'Invalid product ID', 'woocommerce' ) . ' ' . $product_id );
1213 }
1214 if ( ProductType::VARIABLE === $product->get_type() ) {
1215 /* translators: %s product name */
1216 throw new Exception( sprintf( __( '%s is a variable product parent and cannot be added.', 'woocommerce' ), $product->get_name() ) );
1217 }
1218 $validation_error = new WP_Error();
1219 $validation_error = apply_filters( 'woocommerce_ajax_add_order_item_validation', $validation_error, $product, $order, $qty );
1220
1221 if ( $validation_error->get_error_code() ) {
1222 /* translators: %s: error message */
1223 throw new Exception( sprintf( __( 'Error: %s', 'woocommerce' ), $validation_error->get_error_message() ) );
1224 }
1225 $item_id = $order->add_product( $product, $qty, array( 'order' => $order ) );
1226 $item = apply_filters( 'woocommerce_ajax_order_item', $order->get_item( $item_id ), $item_id, $order, $product );
1227 $added_items[ $item_id ] = $item;
1228 $order_notes[ $item_id ] = $product->get_formatted_name();
1229
1230 // We do not perform any stock operations here because they will be handled when order is moved to a status where stock operations are applied (like processing, completed etc).
1231
1232 do_action( 'woocommerce_ajax_add_order_item_meta', $item_id, $item, $order );
1233 }
1234
1235 /* translators: %s item name. */
1236 $order->add_order_note( sprintf( __( 'Added line items: %s', 'woocommerce' ), implode( ', ', $order_notes ) ), false, true, array( 'note_group' => OrderNoteGroup::ORDER_UPDATE ) );
1237
1238 do_action( 'woocommerce_ajax_order_items_added', $added_items, $order );
1239
1240 $data = get_post_meta( $order_id );
1241
1242 $order = wc_get_order( $order_id );
1243
1244 // Get HTML to return.
1245 ob_start();
1246 include __DIR__ . '/admin/meta-boxes/views/html-order-items.php';
1247 $items_html = ob_get_clean();
1248
1249 ob_start();
1250 $notes = wc_get_order_notes( array( 'order_id' => $order_id ) );
1251 include __DIR__ . '/admin/meta-boxes/views/html-order-notes.php';
1252 $notes_html = ob_get_clean();
1253
1254 return array(
1255 'html' => $items_html,
1256 'notes_html' => $notes_html,
1257 );
1258 } catch ( Exception $e ) {
1259 throw $e; // Forward exception to caller.
1260 }
1261 }
1262
1263 /**
1264 * Add order fee via ajax.
1265 *
1266 * @throws Exception If order is invalid.
1267 *
1268 * @return void
1269 */
1270 public static function add_order_fee() {
1271 check_ajax_referer( 'order-item', 'security' );
1272
1273 if ( ! current_user_can( 'edit_shop_orders' ) ) {
1274 wp_die( -1 );
1275 }
1276
1277 $response = array();
1278
1279 try {
1280 $order_id = isset( $_POST['order_id'] ) ? absint( $_POST['order_id'] ) : 0;
1281 $order = wc_get_order( $order_id );
1282
1283 if ( ! $order ) {
1284 throw new Exception( __( 'Invalid order', 'woocommerce' ) );
1285 }
1286
1287 $amount = isset( $_POST['amount'] ) ? wc_clean( wp_unslash( $_POST['amount'] ) ) : 0;
1288
1289 $calculate_tax_args = array(
1290 'country' => isset( $_POST['country'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['country'] ) ) ) : '',
1291 'state' => isset( $_POST['state'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['state'] ) ) ) : '',
1292 'postcode' => isset( $_POST['postcode'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['postcode'] ) ) ) : '',
1293 'city' => isset( $_POST['city'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['city'] ) ) ) : '',
1294 );
1295
1296 if ( strstr( $amount, '%' ) ) {
1297 // We need to calculate totals first, so that $order->get_total() is correct.
1298 $order->calculate_totals( false );
1299 $formatted_amount = $amount;
1300 $percent = floatval( trim( $amount, '%' ) );
1301 $amount = $order->get_total() * ( $percent / 100 );
1302 } else {
1303 $amount = floatval( $amount );
1304 $formatted_amount = wc_price( $amount, array( 'currency' => $order->get_currency() ) );
1305 }
1306
1307 $fee = new WC_Order_Item_Fee();
1308 $fee->set_amount( $amount );
1309 $fee->set_total( $amount );
1310 /* translators: %s fee amount */
1311 $fee->set_name( sprintf( __( '%s fee', 'woocommerce' ), wc_clean( $formatted_amount ) ) );
1312
1313 $order->add_item( $fee );
1314 $order->calculate_taxes( $calculate_tax_args );
1315 $order->calculate_totals( false );
1316 $order->save();
1317
1318 ob_start();
1319 include __DIR__ . '/admin/meta-boxes/views/html-order-items.php';
1320 $response['html'] = ob_get_clean();
1321 } catch ( Exception $e ) {
1322 wp_send_json_error( array( 'error' => $e->getMessage() ) );
1323 }
1324
1325 // wp_send_json_success must be outside the try block not to break phpunit tests.
1326 wp_send_json_success( $response );
1327 }
1328
1329 /**
1330 * Add order shipping cost via ajax.
1331 *
1332 * @throws Exception If order is invalid.
1333 *
1334 * @return void
1335 */
1336 public static function add_order_shipping() {
1337 check_ajax_referer( 'order-item', 'security' );
1338
1339 if ( ! current_user_can( 'edit_shop_orders' ) ) {
1340 wp_die( -1 );
1341 }
1342
1343 $response = array();
1344
1345 try {
1346 $order_id = isset( $_POST['order_id'] ) ? absint( $_POST['order_id'] ) : 0;
1347 $order = wc_get_order( $order_id );
1348
1349 if ( ! $order ) {
1350 throw new Exception( __( 'Invalid order', 'woocommerce' ) );
1351 }
1352
1353 $order_taxes = $order->get_taxes();
1354 $shipping_methods = WC()->shipping() ? WC()->shipping()->load_shipping_methods() : array();
1355 $cogs_is_enabled = wc_get_container()->get( CostOfGoodsSoldController::class )->feature_is_enabled();
1356
1357 // Add new shipping.
1358 $item = new WC_Order_Item_Shipping();
1359 $item->set_shipping_rate( new WC_Shipping_Rate() );
1360 $item->set_order_id( $order_id );
1361 $item_id = $item->save();
1362
1363 ob_start();
1364 include __DIR__ . '/admin/meta-boxes/views/html-order-shipping.php';
1365 $response['html'] = ob_get_clean();
1366 } catch ( Exception $e ) {
1367 wp_send_json_error( array( 'error' => $e->getMessage() ) );
1368 }
1369
1370 // wp_send_json_success must be outside the try block not to break phpunit tests.
1371 wp_send_json_success( $response );
1372 }
1373
1374 /**
1375 * Add order tax column via ajax.
1376 *
1377 * @throws Exception If order or tax rate is invalid.
1378 *
1379 * @return void
1380 */
1381 public static function add_order_tax() {
1382 check_ajax_referer( 'order-item', 'security' );
1383
1384 if ( ! current_user_can( 'edit_shop_orders' ) ) {
1385 wp_die( -1 );
1386 }
1387
1388 $response = array();
1389
1390 try {
1391 $order_id = isset( $_POST['order_id'] ) ? absint( $_POST['order_id'] ) : 0;
1392 $order = wc_get_order( $order_id );
1393
1394 if ( ! $order ) {
1395 throw new Exception( __( 'Invalid order', 'woocommerce' ) );
1396 }
1397
1398 $rate_id = isset( $_POST['rate_id'] ) ? absint( $_POST['rate_id'] ) : '';
1399
1400 if ( ! $rate_id ) {
1401 throw new Exception( __( 'Invalid rate', 'woocommerce' ) );
1402 }
1403
1404 $data = get_post_meta( $order_id );
1405
1406 // Add new tax.
1407 $item = new WC_Order_Item_Tax();
1408 $item->set_rate( $rate_id );
1409 $item->set_order_id( $order_id );
1410 $item->save();
1411
1412 ob_start();
1413 include __DIR__ . '/admin/meta-boxes/views/html-order-items.php';
1414 $response['html'] = ob_get_clean();
1415 } catch ( Exception $e ) {
1416 wp_send_json_error( array( 'error' => $e->getMessage() ) );
1417 }
1418
1419 // wp_send_json_success must be outside the try block not to break phpunit tests.
1420 wp_send_json_success( $response );
1421 }
1422
1423 /**
1424 * Add order discount via ajax.
1425 *
1426 * @throws Exception If order or coupon is invalid.
1427 *
1428 * @return void
1429 */
1430 public static function add_coupon_discount() {
1431 wc_get_container()->get( CouponsController::class )->add_coupon_discount_via_ajax();
1432 }
1433
1434 /**
1435 * Remove coupon from an order via ajax.
1436 *
1437 * @throws Exception If order or coupon is invalid.
1438 *
1439 * @return void
1440 */
1441 public static function remove_order_coupon() {
1442 check_ajax_referer( 'order-item', 'security' );
1443
1444 if ( ! current_user_can( 'edit_shop_orders' ) ) {
1445 wp_die( -1 );
1446 }
1447
1448 $response = array();
1449
1450 try {
1451 $order_id = isset( $_POST['order_id'] ) ? absint( $_POST['order_id'] ) : 0;
1452 $order = wc_get_order( $order_id );
1453 $calculate_tax_args = array(
1454 'country' => isset( $_POST['country'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['country'] ) ) ) : '',
1455 'state' => isset( $_POST['state'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['state'] ) ) ) : '',
1456 'postcode' => isset( $_POST['postcode'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['postcode'] ) ) ) : '',
1457 'city' => isset( $_POST['city'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['city'] ) ) ) : '',
1458 );
1459
1460 if ( ! $order ) {
1461 throw new Exception( __( 'Invalid order', 'woocommerce' ) );
1462 }
1463
1464 $coupon = ArrayUtil::get_value_or_default( $_POST, 'coupon' );
1465 if ( StringUtil::is_null_or_whitespace( $coupon ) ) {
1466 throw new Exception( __( 'Invalid coupon', 'woocommerce' ) );
1467 }
1468
1469 $code = wc_format_coupon_code( wp_unslash( $coupon ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1470 if ( $order->remove_coupon( $code ) ) {
1471 // translators: %s coupon code.
1472 $order->add_order_note( esc_html( sprintf( __( 'Coupon removed: "%s".', 'woocommerce' ), $code ) ), 0, true, array( 'note_group' => OrderNoteGroup::ORDER_UPDATE ) );
1473 }
1474 $order->calculate_taxes( $calculate_tax_args );
1475 $order->calculate_totals( false );
1476
1477 ob_start();
1478 include __DIR__ . '/admin/meta-boxes/views/html-order-items.php';
1479 $response['html'] = ob_get_clean();
1480
1481 ob_start();
1482 $notes = wc_get_order_notes( array( 'order_id' => $order_id ) );
1483 include __DIR__ . '/admin/meta-boxes/views/html-order-notes.php';
1484 $response['notes_html'] = ob_get_clean();
1485
1486 } catch ( Exception $e ) {
1487 wp_send_json_error( array( 'error' => $e->getMessage() ) );
1488 }
1489
1490 // wp_send_json_success must be outside the try block not to break phpunit tests.
1491 wp_send_json_success( $response );
1492 }
1493
1494 /**
1495 * Remove an order item.
1496 *
1497 * @throws Exception If order is invalid.
1498 *
1499 * @return void
1500 */
1501 public static function remove_order_item() {
1502 check_ajax_referer( 'order-item', 'security' );
1503
1504 if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['order_id'], $_POST['order_item_ids'] ) ) {
1505 wp_die( -1 );
1506 }
1507
1508 $response = array();
1509
1510 try {
1511 $order_id = absint( $_POST['order_id'] );
1512 $order = wc_get_order( $order_id );
1513
1514 if ( ! $order ) {
1515 throw new Exception( __( 'Invalid order', 'woocommerce' ) );
1516 }
1517
1518 if ( ! isset( $_POST['order_item_ids'] ) ) {
1519 throw new Exception( __( 'Invalid items', 'woocommerce' ) );
1520 }
1521
1522 $order_item_ids = wp_unslash( $_POST['order_item_ids'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1523 $items = ( ! empty( $_POST['items'] ) ) ? wp_unslash( $_POST['items'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1524 $calculate_tax_args = array(
1525 'country' => isset( $_POST['country'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['country'] ) ) ) : '',
1526 'state' => isset( $_POST['state'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['state'] ) ) ) : '',
1527 'postcode' => isset( $_POST['postcode'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['postcode'] ) ) ) : '',
1528 'city' => isset( $_POST['city'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['city'] ) ) ) : '',
1529 );
1530
1531 if ( is_numeric( $order_item_ids ) ) {
1532 $order_item_ids = array( $order_item_ids );
1533 }
1534
1535 // If we passed through items it means we need to save first before deleting.
1536 if ( ! empty( $items ) ) {
1537 $save_items = array();
1538 parse_str( $items, $save_items );
1539 wc_save_order_items( $order->get_id(), $save_items );
1540 }
1541
1542 if ( ! empty( $order_item_ids ) ) {
1543
1544 foreach ( $order_item_ids as $item_id ) {
1545 $item_id = absint( $item_id );
1546 $item = $order->get_item( $item_id );
1547
1548 if ( ! $item ) {
1549 continue;
1550 }
1551
1552 // Before deleting the item, adjust any stock values already reduced.
1553 if ( $item->is_type( 'line_item' ) ) {
1554 $changed_stock = wc_maybe_adjust_line_item_product_stock( $item, 0 );
1555
1556 if ( $changed_stock && ! is_wp_error( $changed_stock ) ) {
1557 /* translators: %1$s: item name %2$s: stock change */
1558 $order->add_order_note( sprintf( __( 'Deleted %1$s and adjusted stock (%2$s)', 'woocommerce' ), $item->get_name(), $changed_stock['from'] . '&rarr;' . $changed_stock['to'] ), false, true, array( 'note_group' => OrderNoteGroup::PRODUCT_STOCK ) );
1559 } else {
1560 /* translators: %s item name. */
1561 $order->add_order_note( sprintf( __( 'Deleted %s', 'woocommerce' ), $item->get_name() ), false, true, array( 'note_group' => OrderNoteGroup::ORDER_UPDATE ) );
1562 }
1563 }
1564
1565 wc_delete_order_item( $item_id );
1566 }
1567 }
1568
1569 $order = wc_get_order( $order_id );
1570 $order->calculate_taxes( $calculate_tax_args );
1571 $order->calculate_totals( false );
1572
1573 /**
1574 * Fires after order items are removed.
1575 *
1576 * @since 5.2.0
1577 *
1578 * @param int $item_id WC item ID.
1579 * @param WC_Order_Item|false $item As returned by $order->get_item( $item_id ).
1580 * @param bool|array|WP_Error $changed_store Result of wc_maybe_adjust_line_item_product_stock().
1581 * @param bool|WC_Order|WC_Order_Refund $order As returned by wc_get_order().
1582 */
1583 do_action( 'woocommerce_ajax_order_items_removed', $item_id ?? 0, $item ?? false, $changed_stock ?? false, $order );
1584
1585 // Get HTML to return.
1586 ob_start();
1587 include __DIR__ . '/admin/meta-boxes/views/html-order-items.php';
1588 $items_html = ob_get_clean();
1589
1590 ob_start();
1591 $notes = wc_get_order_notes( array( 'order_id' => $order_id ) );
1592 include __DIR__ . '/admin/meta-boxes/views/html-order-notes.php';
1593 $notes_html = ob_get_clean();
1594
1595 wp_send_json_success(
1596 array(
1597 'html' => $items_html,
1598 'notes_html' => $notes_html,
1599 )
1600 );
1601 } catch ( Exception $e ) {
1602 wp_send_json_error( array( 'error' => $e->getMessage() ) );
1603 }
1604
1605 // wp_send_json_success must be outside the try block not to break phpunit tests.
1606 wp_send_json_success( $response );
1607 }
1608
1609 /**
1610 * Remove an order tax.
1611 *
1612 * @throws Exception If there is an error whilst deleting the rate.
1613 *
1614 * @return void
1615 */
1616 public static function remove_order_tax() {
1617 check_ajax_referer( 'order-item', 'security' );
1618
1619 if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['order_id'], $_POST['rate_id'] ) ) {
1620 wp_die( -1 );
1621 }
1622
1623 $response = array();
1624
1625 try {
1626 $order_id = absint( $_POST['order_id'] );
1627 $rate_id = absint( $_POST['rate_id'] );
1628
1629 $order = wc_get_order( $order_id );
1630 if ( ! $order->is_editable() ) {
1631 throw new Exception( __( 'Order not editable', 'woocommerce' ) );
1632 }
1633
1634 wc_delete_order_item( $rate_id );
1635
1636 // Need to load order again after deleting to have latest items before calculating.
1637 $order = wc_get_order( $order_id );
1638 $order->calculate_totals( false );
1639
1640 ob_start();
1641 include __DIR__ . '/admin/meta-boxes/views/html-order-items.php';
1642 $response['html'] = ob_get_clean();
1643 } catch ( Exception $e ) {
1644 wp_send_json_error( array( 'error' => $e->getMessage() ) );
1645 }
1646
1647 // wp_send_json_success must be outside the try block not to break phpunit tests.
1648 wp_send_json_success( $response );
1649 }
1650
1651 /**
1652 * Calc line tax.
1653 *
1654 * @return void
1655 */
1656 public static function calc_line_taxes() {
1657 wc_get_container()->get( TaxesController::class )->calc_line_taxes_via_ajax();
1658 }
1659
1660 /**
1661 * Save order items via ajax.
1662 *
1663 * @return void
1664 */
1665 public static function save_order_items() {
1666 check_ajax_referer( 'order-item', 'security' );
1667
1668 if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['order_id'], $_POST['items'] ) ) {
1669 wp_die( -1 );
1670 }
1671
1672 if ( isset( $_POST['order_id'], $_POST['items'] ) ) {
1673 $order_id = absint( $_POST['order_id'] );
1674
1675 // Parse the jQuery serialized items.
1676 $items = array();
1677 parse_str( wp_unslash( $_POST['items'] ), $items ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1678
1679 // Save order items.
1680 wc_save_order_items( $order_id, $items );
1681
1682 // Return HTML items.
1683 $order = wc_get_order( $order_id );
1684
1685 // Get HTML to return.
1686 ob_start();
1687 include __DIR__ . '/admin/meta-boxes/views/html-order-items.php';
1688 $items_html = ob_get_clean();
1689
1690 ob_start();
1691 $notes = wc_get_order_notes( array( 'order_id' => $order_id ) );
1692 include __DIR__ . '/admin/meta-boxes/views/html-order-notes.php';
1693 $notes_html = ob_get_clean();
1694
1695 wp_send_json_success(
1696 array(
1697 'html' => $items_html,
1698 'notes_html' => $notes_html,
1699 )
1700 );
1701 }
1702 wp_die();
1703 }
1704
1705 /**
1706 * Load order items via ajax.
1707 *
1708 * @return void
1709 */
1710 public static function load_order_items() {
1711 check_ajax_referer( 'order-item', 'security' );
1712
1713 if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['order_id'] ) ) {
1714 wp_die( -1 );
1715 }
1716
1717 // Return HTML items.
1718 $order_id = absint( $_POST['order_id'] );
1719 $order = wc_get_order( $order_id );
1720 include __DIR__ . '/admin/meta-boxes/views/html-order-items.php';
1721 wp_die();
1722 }
1723
1724 /**
1725 * Add order note via ajax.
1726 *
1727 * @return void
1728 */
1729 public static function add_order_note() {
1730 check_ajax_referer( 'add-order-note', 'security' );
1731
1732 if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['post_id'], $_POST['note'], $_POST['note_type'] ) ) {
1733 wp_die( -1 );
1734 }
1735
1736 $post_id = absint( $_POST['post_id'] );
1737 $note = wp_kses_post( trim( wp_unslash( $_POST['note'] ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1738 $note_type = wc_clean( wp_unslash( $_POST['note_type'] ) );
1739
1740 $is_customer_note = ( 'customer' === $note_type ) ? 1 : 0;
1741
1742 if ( $post_id > 0 ) {
1743 $order = wc_get_order( $post_id );
1744 $comment_id = $order->add_order_note( $note, $is_customer_note, true );
1745 $note = wc_get_order_note( $comment_id );
1746
1747 if ( ! $note ) {
1748 wp_die();
1749 }
1750
1751 $note_classes = array( 'note' );
1752 $note_classes[] = $is_customer_note ? 'customer-note' : '';
1753 $note_classes = apply_filters( 'woocommerce_order_note_class', array_filter( $note_classes ), $note );
1754 ?>
1755 <li rel="<?php echo absint( $note->id ); ?>" class="<?php echo esc_attr( implode( ' ', $note_classes ) ); ?>">
1756 <div class="note_content">
1757 <?php if ( $is_customer_note ) : ?>
1758 <div class="note_header"><?php esc_html_e( 'Sent to customer', 'woocommerce' ); ?></div>
1759 <?php endif; ?>
1760 <div class="note_body">
1761 <?php
1762 $content = wc_wptexturize_order_note( $note->content );
1763 echo wp_kses_post( wpautop( make_clickable( $content ) ) );
1764 ?>
1765 </div>
1766 </div>
1767 <p class="meta">
1768 <abbr class="exact-date" title="<?php echo esc_attr( $note->date_created->date( 'Y-m-d H:i:s' ) ); ?>">
1769 <?php
1770 /* translators: %1$s: order note date, %2$s: order note time */
1771 printf( esc_html__( '%1$s at %2$s', 'woocommerce' ), esc_html( $note->date_created->date_i18n( wc_date_format() ) ), esc_html( $note->date_created->date_i18n( wc_time_format() ) ) );
1772 ?>
1773 </abbr>
1774 <?php
1775 if ( 'system' !== $note->added_by ) :
1776 /* translators: %s: order note author */
1777 printf( ' ' . esc_html__( 'by %s', 'woocommerce' ), esc_html( $note->added_by ) );
1778 endif;
1779 ?>
1780 <?php
1781 $note_date_label = $note->date_created->date_i18n( wc_date_format() );
1782 $delete_aria_label = 'system' === $note->added_by
1783 /* translators: %s: order note date */
1784 ? sprintf( __( 'Delete system note from %s', 'woocommerce' ), $note_date_label )
1785 /* translators: %1$s: order note author, %2$s: order note date */
1786 : sprintf( __( 'Delete note from %1$s on %2$s', 'woocommerce' ), $note->added_by, $note_date_label );
1787 ?>
1788 <a href="#" class="delete_note" role="button" aria-label="<?php echo esc_attr( $delete_aria_label ); ?>"><?php esc_html_e( 'Delete', 'woocommerce' ); ?></a>
1789 </p>
1790 </li>
1791 <?php
1792 }
1793 wp_die();
1794 }
1795
1796 /**
1797 * Delete order note via ajax.
1798 *
1799 * @return void
1800 */
1801 public static function delete_order_note() {
1802 check_ajax_referer( 'delete-order-note', 'security' );
1803
1804 if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['note_id'] ) ) {
1805 wp_die( -1 );
1806 }
1807
1808 $note_id = (int) $_POST['note_id'];
1809
1810 if ( $note_id > 0 ) {
1811 wc_delete_order_note( $note_id );
1812 }
1813 wp_die();
1814 }
1815
1816 /**
1817 * Search for products and echo json.
1818 *
1819 * @param string $term (default: '') Term to search for.
1820 * @param bool $include_variations in search or not.
1821 *
1822 * @return void
1823 */
1824 public static function json_search_products( $term = '', $include_variations = false ) {
1825 check_ajax_referer( 'search-products', 'security' );
1826
1827 if ( empty( $term ) && isset( $_GET['term'] ) ) {
1828 $term = (string) wc_clean( wp_unslash( $_GET['term'] ) );
1829 }
1830
1831 if ( empty( $term ) ) {
1832 wp_die();
1833 }
1834
1835 if ( ! empty( $_GET['limit'] ) ) {
1836 $limit = absint( $_GET['limit'] );
1837 } else {
1838 $limit = absint( apply_filters( 'woocommerce_json_search_limit', 30 ) );
1839 }
1840
1841 $include_ids = ! empty( $_GET['include'] ) ? array_map( 'absint', (array) wp_unslash( $_GET['include'] ) ) : array();
1842 $exclude_ids = ! empty( $_GET['exclude'] ) ? array_map( 'absint', (array) wp_unslash( $_GET['exclude'] ) ) : array();
1843
1844 $exclude_types = array();
1845 if ( ! empty( $_GET['exclude_type'] ) ) {
1846 // Support both comma-delimited and array format inputs.
1847 $exclude_types = wp_unslash( $_GET['exclude_type'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1848 if ( ! is_array( $exclude_types ) ) {
1849 $exclude_types = explode( ',', $exclude_types );
1850 }
1851
1852 // Sanitize the excluded types against valid product types.
1853 foreach ( $exclude_types as &$exclude_type ) {
1854 $exclude_type = strtolower( trim( $exclude_type ) );
1855 }
1856 $exclude_types = array_intersect(
1857 array_merge( array( ProductType::VARIATION ), array_keys( wc_get_product_types() ) ),
1858 $exclude_types
1859 );
1860 }
1861
1862 $data_store = WC_Data_Store::load( 'product' );
1863 $ids = $data_store->search_products( $term, '', (bool) $include_variations, false, $limit, $include_ids, $exclude_ids );
1864
1865 $products = array();
1866
1867 foreach ( $ids as $id ) {
1868 $product_object = wc_get_product( $id );
1869
1870 if ( ! $product_object || ! wc_products_array_filter_readable( $product_object ) ) {
1871 continue;
1872 }
1873
1874 $formatted_name = $product_object->get_formatted_name();
1875 $managing_stock = $product_object->managing_stock();
1876
1877 if ( in_array( $product_object->get_type(), $exclude_types, true ) ) {
1878 continue;
1879 }
1880
1881 if ( ! empty( $_GET['display_stock'] ) ) {
1882 $stock_parts = array();
1883
1884 if ( $managing_stock ) {
1885 $stock_amount = $product_object->get_stock_quantity();
1886 /* Translators: %s stock amount */
1887 $stock_parts[] = sprintf( __( 'Stock: %s', 'woocommerce' ), wc_format_stock_quantity_for_display( $stock_amount, $product_object ) );
1888 }
1889
1890 $stock_status = $product_object->get_stock_status();
1891 if ( ProductStockStatus::OUT_OF_STOCK === $stock_status ) {
1892 $stock_parts[] = __( 'Out of stock', 'woocommerce' );
1893 } elseif ( ProductStockStatus::ON_BACKORDER === $stock_status ) {
1894 $stock_parts[] = __( 'On backorder', 'woocommerce' );
1895 }
1896
1897 if ( ! empty( $stock_parts ) ) {
1898 $formatted_name .= ' (' . implode( ' &ndash; ', $stock_parts ) . ')';
1899 }
1900
1901 $product_status = $product_object->get_status();
1902 if ( ProductStatus::PRIVATE === $product_status ) {
1903 $formatted_name .= ' (' . __( 'Disabled', 'woocommerce' ) . ')';
1904 } elseif ( ProductStatus::DRAFT === $product_status ) {
1905 $formatted_name .= ' (' . __( 'Draft', 'woocommerce' ) . ')';
1906 }
1907 }//end if
1908
1909 $products[ $product_object->get_id() ] = rawurldecode( wp_strip_all_tags( $formatted_name ) );
1910 }
1911
1912 wp_send_json( apply_filters( 'woocommerce_json_search_found_products', $products ) );
1913 }
1914
1915 /**
1916 * Search for product variations and return json.
1917 *
1918 * @see WC_AJAX::json_search_products()
1919 *
1920 * @return void
1921 */
1922 public static function json_search_products_and_variations() {
1923 self::json_search_products( '', true );
1924 }
1925
1926 /**
1927 * Search for downloadable product variations and return json.
1928 *
1929 * @see WC_AJAX::json_search_products()
1930 *
1931 * @return void
1932 */
1933 public static function json_search_downloadable_products_and_variations() {
1934 check_ajax_referer( 'search-products', 'security' );
1935
1936 if ( ! empty( $_GET['limit'] ) ) {
1937 $limit = absint( $_GET['limit'] );
1938 } else {
1939 $limit = absint( apply_filters( 'woocommerce_json_search_limit', 30 ) );
1940 }
1941
1942 $include_ids = ! empty( $_GET['include'] ) ? array_map( 'absint', (array) wp_unslash( $_GET['include'] ) ) : array();
1943 $exclude_ids = ! empty( $_GET['exclude'] ) ? array_map( 'absint', (array) wp_unslash( $_GET['exclude'] ) ) : array();
1944
1945 $term = isset( $_GET['term'] ) ? (string) wc_clean( wp_unslash( $_GET['term'] ) ) : '';
1946 $data_store = WC_Data_Store::load( 'product' );
1947 $ids = $data_store->search_products( $term, 'downloadable', true, false, $limit );
1948
1949 _prime_post_caches( $ids );
1950 $product_objects = array_filter( array_map( 'wc_get_product', $ids ), 'wc_products_array_filter_readable' );
1951 $products = array();
1952
1953 foreach ( $product_objects as $product_object ) {
1954 $products[ $product_object->get_id() ] = rawurldecode( wp_strip_all_tags( $product_object->get_formatted_name() ) );
1955 }
1956
1957 wp_send_json( $products );
1958 }
1959
1960 /**
1961 * Search for customers and return json.
1962 *
1963 * @return void
1964 */
1965 public static function json_search_customers() {
1966 ob_start();
1967
1968 $legacy_proxy = wc_get_container()->get( LegacyProxy::class );
1969 $legacy_proxy->call_function( 'check_ajax_referer', 'search-customers', 'security' );
1970
1971 if ( ! current_user_can( 'edit_shop_orders' ) ) {
1972 wp_die( -1 );
1973 }
1974
1975 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1976 $term = isset( $_GET['term'] ) ? (string) wc_clean( wp_unslash( $_GET['term'] ) ) : '';
1977 $limit = 0;
1978
1979 if ( empty( $term ) ) {
1980 wp_die();
1981 }
1982
1983 $ids = array();
1984 // Search by ID.
1985 if ( is_numeric( $term ) ) {
1986 $customer = new WC_Customer( intval( $term ) );
1987
1988 // Only add existing/valid users. In a multisite context, they must be visible to the current user (in
1989 // general, this means that they must have been added to the current site).
1990 if ( 0 !== $customer->get_id() && ! is_wp_error( Users::get_user_in_current_site( $customer->get_id() ) ) ) {
1991 $ids = array( $customer->get_id() );
1992 }
1993 }
1994
1995 // Usernames can be numeric so we first check that no users was found by ID before searching for numeric username, this prevents performance issues with ID lookups.
1996 if ( empty( $ids ) ) {
1997 $data_store = WC_Data_Store::load( 'customer' );
1998
1999 // If search is smaller than 3 characters, limit result set to avoid
2000 // too many rows being returned.
2001 if ( 3 > strlen( $term ) ) {
2002 $limit = 20;
2003 }
2004
2005 // Multisite note: via its dependency on WP_User_Query, WC_Customer_Data_Store::search_customers() will only
2006 // look for users who have already been added to the current blog.
2007 $ids = $data_store->search_customers( $term, $limit );
2008 }
2009
2010 $found_customers = array();
2011
2012 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
2013 if ( ! empty( $_GET['exclude'] ) ) {
2014 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
2015 $ids = array_diff( $ids, array_map( 'absint', (array) wp_unslash( $_GET['exclude'] ) ) );
2016 }
2017
2018 foreach ( $ids as $id ) {
2019 $customer = new WC_Customer( $id );
2020 /* translators: 1: user display name 2: user ID 3: user email */
2021 $found_customers[ $id ] = sprintf(
2022 /* translators: $1: customer name, $2 customer id, $3: customer email */
2023 esc_html__( '%1$s (#%2$s &ndash; %3$s)', 'woocommerce' ),
2024 $customer->get_first_name() . ' ' . $customer->get_last_name(),
2025 $customer->get_id(),
2026 $customer->get_email()
2027 );
2028 }
2029
2030 wp_send_json( apply_filters( 'woocommerce_json_search_found_customers', $found_customers ) );
2031 }
2032
2033 /**
2034 * Search for categories and return json.
2035 *
2036 * @return void
2037 */
2038 public static function json_search_categories() {
2039 ob_start();
2040
2041 check_ajax_referer( 'search-categories', 'security' );
2042
2043 if ( ! current_user_can( 'edit_products' ) ) {
2044 wp_die( -1 );
2045 }
2046
2047 $search_text = isset( $_GET['term'] ) ? wc_clean( wp_unslash( $_GET['term'] ) ) : '';
2048
2049 if ( ! $search_text ) {
2050 wp_die();
2051 }
2052
2053 $show_empty = isset( $_GET['show_empty'] ) ? wp_validate_boolean( wc_clean( wp_unslash( $_GET['show_empty'] ) ) ) : false;
2054 $found_categories = array();
2055 $args = array(
2056 'taxonomy' => array( 'product_cat' ),
2057 'orderby' => 'id',
2058 'order' => 'ASC',
2059 'hide_empty' => ! $show_empty,
2060 'fields' => 'all',
2061 'name__like' => $search_text,
2062 );
2063
2064 $terms = get_terms( $args );
2065
2066 if ( $terms ) {
2067 foreach ( $terms as $term ) {
2068 $term->formatted_name = '';
2069
2070 $ancestors = array();
2071 if ( $term->parent ) {
2072 $ancestors = array_reverse( get_ancestors( $term->term_id, 'product_cat' ) );
2073 foreach ( $ancestors as $ancestor ) {
2074 $ancestor_term = get_term( $ancestor, 'product_cat' );
2075 if ( $ancestor_term ) {
2076 $term->formatted_name .= $ancestor_term->name . ' > ';
2077 }
2078 }
2079 }
2080
2081 $term->parents = $ancestors;
2082 $term->formatted_name .= $term->name . ' (' . $term->count . ')';
2083 $found_categories[ $term->term_id ] = $term;
2084 }
2085 }
2086
2087 wp_send_json( apply_filters( 'woocommerce_json_search_found_categories', $found_categories ) );
2088 }
2089
2090 /**
2091 * Search for categories and return json.
2092 *
2093 * @deprecated 10.9.0 This callback was used by the removed async product editor category field.
2094 *
2095 * @return void
2096 */
2097 public static function json_search_categories_tree() {
2098 wc_deprecated_function( __METHOD__, '10.9.0' );
2099
2100 ob_start();
2101
2102 check_ajax_referer( 'search-categories', 'security' );
2103
2104 if ( ! current_user_can( 'edit_products' ) ) {
2105 wp_die( -1 );
2106 }
2107
2108 $search_text = isset( $_GET['term'] ) ? wc_clean( wp_unslash( $_GET['term'] ) ) : '';
2109 $number = isset( $_GET['number'] ) ? absint( $_GET['number'] ) : 50;
2110
2111 $args = array(
2112 'taxonomy' => array( 'product_cat' ),
2113 'orderby' => 'name',
2114 'order' => 'ASC',
2115 'hide_empty' => false,
2116 'fields' => 'all',
2117 'number' => $number,
2118 'name__like' => $search_text,
2119 );
2120
2121 $terms = get_terms( $args );
2122
2123 $terms_map = array();
2124
2125 if ( $terms ) {
2126 foreach ( $terms as $term ) {
2127 $terms_map[ $term->term_id ] = $term;
2128
2129 if ( $term->parent ) {
2130 $ancestors = get_ancestors( $term->term_id, 'product_cat' );
2131 $current_child = $term;
2132 foreach ( $ancestors as $ancestor ) {
2133 if ( ! isset( $terms_map[ $ancestor ] ) ) {
2134 $ancestor_term = get_term( $ancestor, 'product_cat' );
2135 $terms_map[ $ancestor ] = $ancestor_term;
2136 }
2137 if ( ! $terms_map[ $ancestor ]->children ) {
2138 $terms_map[ $ancestor ]->children = array();
2139 }
2140 $item_exists = count(
2141 array_filter(
2142 $terms_map[ $ancestor ]->children,
2143 function ( $term ) use ( $current_child ) {
2144 return $term->term_id === $current_child->term_id;
2145 }
2146 )
2147 ) === 1;
2148 if ( ! $item_exists ) {
2149 $terms_map[ $ancestor ]->children[] = $current_child;
2150 }
2151 $current_child = $terms_map[ $ancestor ];
2152 }
2153 }
2154 }
2155 }
2156 $parent_terms = array_filter(
2157 array_values( $terms_map ),
2158 function ( $term ) {
2159 return 0 === $term->parent;
2160 }
2161 );
2162 wp_send_json( apply_filters( 'woocommerce_json_search_found_categories', $parent_terms ) );
2163 }
2164
2165 /**
2166 * Search for taxonomy terms and return json.
2167 *
2168 * @return void
2169 */
2170 public static function json_search_taxonomy_terms() {
2171 ob_start();
2172
2173 check_ajax_referer( 'search-taxonomy-terms', 'security' );
2174
2175 if ( ! current_user_can( 'edit_products' ) ) {
2176 wp_die( -1 );
2177 }
2178
2179 $search_text = isset( $_GET['term'] ) ? wc_clean( wp_unslash( $_GET['term'] ) ) : '';
2180 $limit = isset( $_GET['limit'] ) ? absint( wp_unslash( $_GET['limit'] ) ) : null;
2181 $taxonomy = isset( $_GET['taxonomy'] ) ? wc_clean( wp_unslash( $_GET['taxonomy'] ) ) : '';
2182 $orderby = isset( $_GET['orderby'] ) ? wc_clean( wp_unslash( $_GET['orderby'] ) ) : 'name';
2183 $order = isset( $_GET['order'] ) ? wc_clean( wp_unslash( $_GET['order'] ) ) : 'ASC';
2184
2185 $args = array(
2186 'taxonomy' => $taxonomy,
2187 'orderby' => $orderby,
2188 'order' => $order,
2189 'hide_empty' => false,
2190 'fields' => 'all',
2191 'number' => $limit,
2192 'name__like' => $search_text,
2193 'suppress_filter' => true,
2194 );
2195
2196 /**
2197 * Filter the product attribute term arguments used for search.
2198 *
2199 * @since 3.4.0
2200 * @param array $args The search arguments.
2201 */
2202 $terms = get_terms( apply_filters( 'woocommerce_product_attribute_terms', $args ) );
2203
2204 /**
2205 * Filter the product attribute terms search results.
2206 *
2207 * @since 7.0.0
2208 * @param array $terms The list of matched terms.
2209 * @param string $taxonomy The terms taxonomy.
2210 */
2211 wp_send_json( apply_filters( 'woocommerce_json_search_found_product_attribute_terms', $terms, $taxonomy ) );
2212 }
2213
2214 /**
2215 * Search for product attributes and return json.
2216 *
2217 * @return void
2218 */
2219 public static function json_search_product_attributes() {
2220 ob_start();
2221
2222 check_ajax_referer( 'search-product-attributes', 'security' );
2223
2224 if ( ! current_user_can( 'edit_products' ) ) {
2225 wp_die( -1 );
2226 }
2227
2228 $limit = isset( $_GET['limit'] ) ? absint( wp_unslash( $_GET['limit'] ) ) : 100;
2229 $search_text = isset( $_GET['term'] ) ? wc_clean( wp_unslash( $_GET['term'] ) ) : '';
2230
2231 $attributes = wc_get_attribute_taxonomies();
2232
2233 $found_product_categories = array();
2234
2235 foreach ( $attributes as $attribute_obj ) {
2236 if ( ! $search_text || false !== stripos( $attribute_obj->attribute_label, $search_text ) ) {
2237 $found_product_categories[] = array(
2238 'id' => (int) $attribute_obj->attribute_id,
2239 'name' => $attribute_obj->attribute_label,
2240 'slug' => wc_attribute_taxonomy_name( $attribute_obj->attribute_name ),
2241 'type' => $attribute_obj->attribute_type,
2242 'order_by' => $attribute_obj->attribute_orderby,
2243 'has_archives' => (bool) $attribute_obj->attribute_public,
2244 );
2245 }
2246 if ( count( $found_product_categories ) >= $limit ) {
2247 break;
2248 }
2249 }
2250
2251 /**
2252 * Filter the product category search results.
2253 *
2254 * @since 7.0.0
2255 * @param array $found_product_categories Array of matched product categories.
2256 * @param string $search_text Search text.
2257 */
2258 wp_send_json( apply_filters( 'woocommerce_json_search_found_product_categories', $found_product_categories, $search_text ) );
2259 }
2260
2261
2262 /**
2263 * Ajax request handling for page searching.
2264 *
2265 * @return void
2266 */
2267 public static function json_search_pages() {
2268 ob_start();
2269
2270 check_ajax_referer( 'search-pages', 'security' );
2271
2272 if ( ! current_user_can( 'manage_woocommerce' ) ) {
2273 wp_die( -1 );
2274 }
2275
2276 $search_text = isset( $_GET['term'] ) ? wc_clean( wp_unslash( $_GET['term'] ) ) : '';
2277 $limit = isset( $_GET['limit'] ) ? absint( wp_unslash( $_GET['limit'] ) ) : -1;
2278 $exclude_ids = ! empty( $_GET['exclude'] ) ? array_map( 'absint', (array) wp_unslash( $_GET['exclude'] ) ) : array();
2279
2280 $args = array(
2281 'no_found_rows' => true,
2282 'update_post_meta_cache' => false,
2283 'update_post_term_cache' => false,
2284 'posts_per_page' => $limit,
2285 'post_type' => 'page',
2286 'post_status' => array( 'publish', 'private', 'draft' ),
2287 's' => $search_text,
2288 'post__not_in' => $exclude_ids,
2289 );
2290 $search_results_query = new WP_Query( $args );
2291
2292 $pages_results = array();
2293 foreach ( $search_results_query->get_posts() as $post ) {
2294 $pages_results[ $post->ID ] = sprintf(
2295 /* translators: 1: page name 2: page ID */
2296 __( '%1$s (ID: %2$s)', 'woocommerce' ),
2297 get_the_title( $post ),
2298 $post->ID
2299 );
2300 }
2301
2302 wp_send_json( apply_filters( 'woocommerce_json_search_found_pages', $pages_results ) );
2303 }
2304
2305 /**
2306 * Ajax request handling for categories ordering.
2307 *
2308 * @return void
2309 */
2310 public static function term_ordering() {
2311 check_ajax_referer( 'term-ordering', 'security' );
2312
2313 if ( ! current_user_can( 'edit_products' ) || empty( $_POST['id'] ) ) {
2314 wp_die( -1 );
2315 }
2316
2317 $id = (int) $_POST['id'];
2318 $next_id = isset( $_POST['nextid'] ) && (int) $_POST['nextid'] ? (int) $_POST['nextid'] : null;
2319 $taxonomy = isset( $_POST['thetaxonomy'] ) ? esc_attr( wp_unslash( $_POST['thetaxonomy'] ) ) : null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
2320 $term = get_term_by( 'id', $id, $taxonomy );
2321
2322 if ( ! $id || ! $term || ! $taxonomy ) {
2323 wp_die( 0 );
2324 }
2325
2326 wc_reorder_terms( $term, $next_id, $taxonomy );
2327
2328 $children = get_terms( $taxonomy, "child_of=$id&menu_order=ASC&hide_empty=0" );
2329
2330 $children_count = is_countable( $children ) ? count( $children ) : 0;
2331 if ( $term && $children_count ) {
2332 echo 'children';
2333 wp_die();
2334 }
2335 }
2336
2337 /**
2338 * Ajax request handling for product ordering.
2339 *
2340 * Based on Simple Page Ordering by 10up (https://wordpress.org/plugins/simple-page-ordering/).
2341 *
2342 * @return void
2343 */
2344 public static function product_ordering() {
2345 global $wpdb;
2346
2347 check_ajax_referer( 'product-ordering', 'security' );
2348
2349 if ( ! current_user_can( 'edit_products' ) || empty( $_POST['id'] ) ) {
2350 wp_die( -1 );
2351 }
2352
2353 $sorting_id = absint( $_POST['id'] );
2354 $previd = absint( isset( $_POST['previd'] ) ? $_POST['previd'] : 0 );
2355 $nextid = absint( isset( $_POST['nextid'] ) ? $_POST['nextid'] : 0 );
2356 $menu_orders = wp_list_pluck( $wpdb->get_results( "SELECT ID, menu_order FROM {$wpdb->posts} WHERE post_type = 'product' ORDER BY menu_order ASC, post_title ASC" ), 'menu_order', 'ID' );
2357 $index = 0;
2358
2359 foreach ( $menu_orders as $id => $menu_order ) {
2360 $id = absint( $id );
2361
2362 if ( $sorting_id === $id ) {
2363 continue;
2364 }
2365 if ( $nextid === $id ) {
2366 ++$index;
2367 }
2368 ++$index;
2369 $menu_orders[ $id ] = $index;
2370
2371 if ( $wpdb->update( $wpdb->posts, array( 'menu_order' => $index ), array( 'ID' => $id ) ) ) {
2372 // We only need to clean the cache if the menu order was actually modified.
2373 clean_post_cache( $id );
2374 }
2375
2376 /**
2377 * When a single product has gotten it's ordering updated.
2378 * $id The product ID
2379 * $index The new menu order
2380 */
2381 do_action( 'woocommerce_after_single_product_ordering', $id, $index );
2382 }
2383
2384 if ( isset( $menu_orders[ $previd ] ) ) {
2385 $menu_orders[ $sorting_id ] = $menu_orders[ $previd ] + 1;
2386 } elseif ( isset( $menu_orders[ $nextid ] ) ) {
2387 $menu_orders[ $sorting_id ] = $menu_orders[ $nextid ] - 1;
2388 } else {
2389 $menu_orders[ $sorting_id ] = 0;
2390 }
2391
2392 if ( $wpdb->update( $wpdb->posts, array( 'menu_order' => $menu_orders[ $sorting_id ] ), array( 'ID' => $sorting_id ) ) ) {
2393 // We only need to clean the cache if the menu order was actually modified.
2394 clean_post_cache( $sorting_id );
2395 }
2396
2397 WC_Post_Data::delete_product_query_transients();
2398
2399 do_action( 'woocommerce_after_product_ordering', $sorting_id, $menu_orders );
2400 wp_send_json( $menu_orders );
2401 }
2402
2403 /**
2404 * Handle a refund via the edit order screen.
2405 *
2406 * @throws Exception To return errors.
2407 *
2408 * @return void
2409 */
2410 public static function refund_line_items() {
2411 ob_start();
2412
2413 check_ajax_referer( 'order-item', 'security' );
2414
2415 if ( ! current_user_can( 'edit_shop_orders' ) ) {
2416 wp_die( -1 );
2417 }
2418
2419 $order_id = isset( $_POST['order_id'] ) ? absint( $_POST['order_id'] ) : 0;
2420 $refund_amount = isset( $_POST['refund_amount'] ) ? wc_format_decimal( sanitize_text_field( wp_unslash( $_POST['refund_amount'] ) ), wc_get_price_decimals() ) : 0;
2421 $refunded_amount = isset( $_POST['refunded_amount'] ) ? wc_format_decimal( sanitize_text_field( wp_unslash( $_POST['refunded_amount'] ) ), wc_get_price_decimals() ) : 0;
2422 $refund_reason = isset( $_POST['refund_reason'] ) ? sanitize_text_field( wp_unslash( $_POST['refund_reason'] ) ) : '';
2423 $line_item_qtys = isset( $_POST['line_item_qtys'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['line_item_qtys'] ) ), true ) : array();
2424 $line_item_totals = isset( $_POST['line_item_totals'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['line_item_totals'] ) ), true ) : array();
2425 $line_item_tax_totals = isset( $_POST['line_item_tax_totals'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['line_item_tax_totals'] ) ), true ) : array();
2426 $api_refund = isset( $_POST['api_refund'] ) && 'true' === $_POST['api_refund'];
2427 $restock_refunded_items = isset( $_POST['restock_refunded_items'] ) && 'true' === $_POST['restock_refunded_items'];
2428 $refund = false;
2429 $response = array();
2430
2431 try {
2432 $order = wc_get_order( $order_id );
2433 $max_refund = wc_format_decimal( $order->get_total() - $order->get_total_refunded(), wc_get_price_decimals() );
2434
2435 if ( ( ! $refund_amount && ( wc_format_decimal( 0, wc_get_price_decimals() ) !== $refund_amount ) ) || $max_refund < $refund_amount || 0 > $refund_amount ) {
2436 throw new Exception( __( 'Invalid refund amount', 'woocommerce' ) );
2437 }
2438
2439 if ( wc_format_decimal( $order->get_total_refunded(), wc_get_price_decimals() ) !== $refunded_amount ) {
2440 throw new Exception( __( 'Error processing refund. Please try again.', 'woocommerce' ) );
2441 }
2442
2443 // Prepare line items which we are refunding.
2444 $line_items = array();
2445 $item_ids = array_unique( array_merge( array_keys( $line_item_qtys ), array_keys( $line_item_totals ) ) );
2446
2447 foreach ( $item_ids as $item_id ) {
2448 $line_items[ $item_id ] = array(
2449 'qty' => 0,
2450 'refund_total' => 0,
2451 'refund_tax' => array(),
2452 );
2453 }
2454 foreach ( $line_item_qtys as $item_id => $qty ) {
2455 $line_items[ $item_id ]['qty'] = max( $qty, 0 );
2456 }
2457 foreach ( $line_item_totals as $item_id => $total ) {
2458 $line_items[ $item_id ]['refund_total'] = wc_format_decimal( $total );
2459 }
2460 foreach ( $line_item_tax_totals as $item_id => $tax_totals ) {
2461 // Use is_numeric so a 0% tax amount ('0') is preserved. A callback-less array_filter would drop it as falsy, losing the 0-rate tax line (0% is a valid rate, not "no tax"). See #27118.
2462 $line_items[ $item_id ]['refund_tax'] = array_filter( array_map( 'wc_format_decimal', $tax_totals ), 'is_numeric' );
2463 }
2464
2465 // Create the refund object.
2466 $refund = wc_create_refund(
2467 array(
2468 'amount' => $refund_amount,
2469 'reason' => $refund_reason,
2470 'order_id' => $order_id,
2471 'line_items' => $line_items,
2472 'refund_payment' => $api_refund,
2473 'restock_items' => $restock_refunded_items,
2474 )
2475 );
2476
2477 if ( is_wp_error( $refund ) ) {
2478 throw new Exception( $refund->get_error_message() );
2479 }
2480
2481 if ( did_action( 'woocommerce_order_fully_refunded' ) ) {
2482 $response['status'] = 'fully_refunded';
2483 }
2484 } catch ( Exception $e ) {
2485 wp_send_json_error( array( 'error' => $e->getMessage() ) );
2486 }
2487
2488 // wp_send_json_success must be outside the try block not to break phpunit tests.
2489 wp_send_json_success( $response );
2490 }
2491
2492 /**
2493 * Delete a refund.
2494 *
2495 * @return void
2496 */
2497 public static function delete_refund() {
2498 check_ajax_referer( 'order-item', 'security' );
2499
2500 if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['refund_id'] ) ) {
2501 wp_die( -1 );
2502 }
2503
2504 $refund_ids = array_map( 'absint', is_array( $_POST['refund_id'] ) ? wp_unslash( $_POST['refund_id'] ) : array( wp_unslash( $_POST['refund_id'] ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
2505 foreach ( $refund_ids as $refund_id ) {
2506 if ( $refund_id && 'shop_order_refund' === OrderUtil::get_order_type( $refund_id ) ) {
2507 $refund = wc_get_order( $refund_id );
2508 $order_id = $refund->get_parent_id();
2509 $refund->delete( true );
2510 do_action( 'woocommerce_refund_deleted', $refund_id, $order_id );
2511 }
2512 }
2513 wp_die();
2514 }
2515
2516 /**
2517 * Triggered when clicking the rating footer.
2518 *
2519 * @return void
2520 */
2521 public static function rated() {
2522 if ( ! current_user_can( 'manage_woocommerce' ) ) {
2523 wp_die( -1 );
2524 }
2525 update_option( 'woocommerce_admin_footer_text_rated', 1 );
2526 wp_die();
2527 }
2528
2529 /**
2530 * Create/Update API key.
2531 *
2532 * @throws Exception On invalid or empty description, user, or permissions.
2533 *
2534 * @return void
2535 */
2536 public static function update_api_key() {
2537 ob_start();
2538
2539 global $wpdb;
2540
2541 check_ajax_referer( 'update-api-key', 'security' );
2542
2543 if ( ! current_user_can( 'manage_woocommerce' ) ) {
2544 wp_die( -1 );
2545 }
2546
2547 $response = array();
2548
2549 try {
2550 if ( empty( $_POST['description'] ) ) {
2551 throw new Exception( __( 'Description is missing.', 'woocommerce' ) );
2552 }
2553 if ( empty( $_POST['user'] ) ) {
2554 throw new Exception( __( 'User is missing.', 'woocommerce' ) );
2555 }
2556 if ( empty( $_POST['permissions'] ) ) {
2557 throw new Exception( __( 'Permissions is missing.', 'woocommerce' ) );
2558 }
2559
2560 $key_id = isset( $_POST['key_id'] ) ? absint( $_POST['key_id'] ) : 0;
2561 $description = sanitize_text_field( wp_unslash( $_POST['description'] ) );
2562 $permissions = ( in_array( wp_unslash( $_POST['permissions'] ), array( 'read', 'write', 'read_write' ), true ) ) ? sanitize_text_field( wp_unslash( $_POST['permissions'] ) ) : 'read';
2563 $user_id = absint( $_POST['user'] );
2564
2565 // Check if current user can edit other users.
2566 if ( $user_id && ! current_user_can( 'edit_user', $user_id ) ) {
2567 if ( get_current_user_id() !== $user_id ) {
2568 throw new Exception( __( 'You do not have permission to assign API Keys to the selected user.', 'woocommerce' ) );
2569 }
2570 }
2571
2572 if ( 0 < $key_id ) {
2573 $data = array(
2574 'user_id' => $user_id,
2575 'description' => $description,
2576 'permissions' => $permissions,
2577 );
2578
2579 $wpdb->update(
2580 $wpdb->prefix . 'woocommerce_api_keys',
2581 $data,
2582 array( 'key_id' => $key_id ),
2583 array(
2584 '%d',
2585 '%s',
2586 '%s',
2587 ),
2588 array( '%d' )
2589 );
2590
2591 $response = $data;
2592 $response['consumer_key'] = '';
2593 $response['consumer_secret'] = '';
2594 $response['message'] = __( 'API Key updated successfully.', 'woocommerce' );
2595 } else {
2596 $consumer_key = 'ck_' . wc_rand_hash();
2597 $consumer_secret = 'cs_' . wc_rand_hash();
2598
2599 $data = array(
2600 'user_id' => $user_id,
2601 'description' => $description,
2602 'permissions' => $permissions,
2603 'consumer_key' => wc_api_hash( $consumer_key ),
2604 'consumer_secret' => $consumer_secret,
2605 'truncated_key' => substr( $consumer_key, -7 ),
2606 );
2607
2608 $wpdb->insert(
2609 $wpdb->prefix . 'woocommerce_api_keys',
2610 $data,
2611 array(
2612 '%d',
2613 '%s',
2614 '%s',
2615 '%s',
2616 '%s',
2617 '%s',
2618 )
2619 );
2620
2621 if ( 0 === $wpdb->insert_id ) {
2622 throw new Exception( __( 'There was an error generating your API Key.', 'woocommerce' ) );
2623 }
2624
2625 $key_id = $wpdb->insert_id;
2626 $response = $data;
2627 $response['consumer_key'] = $consumer_key;
2628 $response['consumer_secret'] = $consumer_secret;
2629 $response['message'] = __( 'API Key generated successfully. Make sure to copy your new keys now as the secret key will be hidden once you leave this page.', 'woocommerce' );
2630 $response['revoke_url'] = '<a style="color: var(--wc-destructive, #cc1818); text-decoration: none;" href="' . esc_url( wp_nonce_url( add_query_arg( array( 'revoke-key' => $key_id ), admin_url( 'admin.php?page=wc-settings&tab=advanced&section=keys' ) ), 'revoke' ) ) . '">' . __( 'Revoke key', 'woocommerce' ) . '</a>';
2631 }
2632 } catch ( Exception $e ) {
2633 wp_send_json_error( array( 'message' => $e->getMessage() ) );
2634 }
2635
2636 // wp_send_json_success must be outside the try block not to break phpunit tests.
2637 wp_send_json_success( $response );
2638 }
2639
2640 /**
2641 * Load variations via AJAX.
2642 *
2643 * @return void
2644 */
2645 public static function load_variations() {
2646 ob_start();
2647
2648 check_ajax_referer( 'load-variations', 'security' );
2649
2650 if ( ! current_user_can( 'edit_products' ) || empty( $_POST['product_id'] ) ) {
2651 wp_die( -1 );
2652 }
2653
2654 // Set $post global so its available, like within the admin screens.
2655 global $post;
2656
2657 $loop = 0;
2658 $product_id = absint( $_POST['product_id'] );
2659 $post = get_post( $product_id ); // phpcs:ignore
2660 $product_object = wc_get_product( $product_id );
2661 $per_page = ! empty( $_POST['per_page'] ) ? absint( $_POST['per_page'] ) : 10;
2662 $page = ! empty( $_POST['page'] ) ? absint( $_POST['page'] ) : 1;
2663 $variations = wc_get_products(
2664 array(
2665 'status' => array( 'private', 'publish' ),
2666 'type' => ProductType::VARIATION,
2667 'parent' => $product_id,
2668 'limit' => $per_page,
2669 'page' => $page,
2670 'orderby' => array(
2671 'menu_order' => 'ASC',
2672 'ID' => 'DESC',
2673 ),
2674 'return' => 'objects',
2675 )
2676 );
2677
2678 if ( $variations ) {
2679 wc_render_invalid_variation_notice( $product_object );
2680
2681 $base_cost = self::base_cost_or_null( $product_object );
2682 foreach ( $variations as $variation_object ) {
2683 self::render_variation_html( $product_object, $variation_object, $loop, $base_cost );
2684 ++$loop;
2685 }
2686 }
2687 wp_die();
2688 }
2689
2690 /**
2691 * Save variations via AJAX.
2692 *
2693 * @return void
2694 */
2695 public static function save_variations() {
2696 ob_start();
2697
2698 check_ajax_referer( 'save-variations', 'security' );
2699
2700 // Check permissions again and make sure we have what we need.
2701 if ( ! current_user_can( 'edit_products' ) || empty( $_POST ) || empty( $_POST['product_id'] ) ) {
2702 wp_die( -1 );
2703 }
2704
2705 $product_id = absint( $_POST['product_id'] );
2706 WC_Admin_Meta_Boxes::$meta_box_errors = array();
2707 WC_Meta_Box_Product_Data::save_variations( $product_id, get_post( $product_id ) );
2708
2709 do_action( 'woocommerce_ajax_save_product_variations', $product_id );
2710
2711 $errors = WC_Admin_Meta_Boxes::$meta_box_errors;
2712
2713 if ( $errors ) {
2714 echo '<div class="error notice is-dismissible">';
2715
2716 foreach ( $errors as $error ) {
2717 echo '<p>' . wp_kses_post( $error ) . '</p>';
2718 }
2719
2720 echo '<button type="button" class="notice-dismiss"><span class="screen-reader-text">' . esc_html__( 'Dismiss this notice.', 'woocommerce' ) . '</span></button>';
2721 echo '</div>';
2722
2723 delete_option( WC_Admin_Meta_Boxes::ERROR_STORE );
2724 }
2725
2726 wp_die();
2727 }
2728
2729 /**
2730 * Bulk action - Toggle Enabled.
2731 *
2732 * @param array $variations List of variations.
2733 * @param array $data Data to set.
2734 *
2735 * @used-by bulk_edit_variations
2736 *
2737 * @return void
2738 */
2739 private static function variation_bulk_action_toggle_enabled( $variations, $data ) {
2740 foreach ( $variations as $variation_id ) {
2741 $variation = wc_get_product( $variation_id );
2742 $variation->set_status( ProductStatus::PRIVATE === $variation->get_status( 'edit' ) ? ProductStatus::PUBLISH : ProductStatus::PRIVATE );
2743 $variation->save();
2744 }
2745 }
2746
2747 /**
2748 * Bulk action - Toggle Downloadable Checkbox.
2749 *
2750 * @param array $variations List of variations.
2751 * @param array $data Data to set.
2752 *
2753 * @used-by bulk_edit_variations
2754 *
2755 * @return void
2756 */
2757 private static function variation_bulk_action_toggle_downloadable( $variations, $data ) {
2758 self::variation_bulk_toggle( $variations, 'downloadable' );
2759 }
2760
2761 /**
2762 * Bulk action - Toggle Virtual Checkbox.
2763 *
2764 * @param array $variations List of variations.
2765 * @param array $data Data to set.
2766 *
2767 * @used-by bulk_edit_variations
2768 *
2769 * @return void
2770 */
2771 private static function variation_bulk_action_toggle_virtual( $variations, $data ) {
2772 self::variation_bulk_toggle( $variations, 'virtual' );
2773 }
2774
2775 /**
2776 * Bulk action - Toggle Manage Stock Checkbox.
2777 *
2778 * @param array $variations List of variations.
2779 * @param array $data Data to set.
2780 *
2781 * @used-by bulk_edit_variations
2782 *
2783 * @return void
2784 */
2785 private static function variation_bulk_action_toggle_manage_stock( $variations, $data ) {
2786 self::variation_bulk_toggle( $variations, 'manage_stock' );
2787 }
2788
2789 /**
2790 * Bulk action - Set Regular Prices.
2791 *
2792 * @param array $variations List of variations.
2793 * @param array $data Data to set.
2794 *
2795 * @used-by bulk_edit_variations
2796 *
2797 * @return void
2798 */
2799 private static function variation_bulk_action_variable_regular_price( $variations, $data ) {
2800 self::variation_bulk_set( $variations, 'regular_price', $data['value'] );
2801 }
2802
2803 /**
2804 * Bulk action - Set Sale Prices.
2805 *
2806 * @param array $variations List of variations.
2807 * @param array $data Data to set.
2808 *
2809 * @used-by bulk_edit_variations
2810 *
2811 * @return void
2812 */
2813 private static function variation_bulk_action_variable_sale_price( $variations, $data ) {
2814 self::variation_bulk_set( $variations, 'sale_price', $data['value'] );
2815 }
2816
2817 /**
2818 * Bulk action - Set Stock Status as In Stock.
2819 *
2820 * @param array $variations List of variations.
2821 * @param array $data Data to set.
2822 *
2823 * @used-by bulk_edit_variations
2824 *
2825 * @return void
2826 */
2827 private static function variation_bulk_action_variable_stock_status_instock( $variations, $data ) {
2828 self::variation_bulk_set( $variations, 'stock_status', ProductStockStatus::IN_STOCK );
2829 }
2830
2831 /**
2832 * Bulk action - Set Stock Status as Out of Stock.
2833 *
2834 * @param array $variations List of variations.
2835 * @param array $data Data to set.
2836 *
2837 * @used-by bulk_edit_variations
2838 *
2839 * @return void
2840 */
2841 private static function variation_bulk_action_variable_stock_status_outofstock( $variations, $data ) {
2842 self::variation_bulk_set( $variations, 'stock_status', ProductStockStatus::OUT_OF_STOCK );
2843 }
2844
2845 /**
2846 * Bulk action - Set Stock Status as On Backorder.
2847 *
2848 * @param array $variations List of variations.
2849 * @param array $data Data to set.
2850 *
2851 * @used-by bulk_edit_variations
2852 *
2853 * @return void
2854 */
2855 private static function variation_bulk_action_variable_stock_status_onbackorder( $variations, $data ) {
2856 self::variation_bulk_set( $variations, 'stock_status', ProductStockStatus::ON_BACKORDER );
2857 }
2858
2859 /**
2860 * Bulk action - Set Stock.
2861 *
2862 * @param array $variations List of variations.
2863 * @param array $data Data to set.
2864 *
2865 * @used-by bulk_edit_variations
2866 *
2867 * @return void
2868 */
2869 private static function variation_bulk_action_variable_stock( $variations, $data ) {
2870 if ( ! isset( $data['value'] ) ) {
2871 return;
2872 }
2873
2874 $quantity = wc_stock_amount( wc_clean( $data['value'] ) );
2875
2876 foreach ( $variations as $variation_id ) {
2877 $variation = wc_get_product( $variation_id );
2878 if ( $variation->managing_stock() ) {
2879 $variation->set_stock_quantity( $quantity );
2880 } else {
2881 $variation->set_stock_quantity( null );
2882 }
2883 $variation->save();
2884 }
2885 }
2886
2887 /**
2888 * Bulk action - Set Low Stock Amount.
2889 *
2890 * @param array $variations List of variations.
2891 * @param array $data Data to set.
2892 *
2893 * @used-by bulk_edit_variations
2894 *
2895 * @return void
2896 */
2897 private static function variation_bulk_action_variable_low_stock_amount( $variations, $data ) {
2898 if ( ! isset( $data['value'] ) ) {
2899 return;
2900 }
2901
2902 $low_stock_amount = wc_stock_amount( wc_clean( $data['value'] ) );
2903
2904 foreach ( $variations as $variation_id ) {
2905 $variation = wc_get_product( $variation_id );
2906 if ( $variation->managing_stock() ) {
2907 $variation->set_low_stock_amount( $low_stock_amount );
2908 } else {
2909 $variation->set_low_stock_amount( '' );
2910 }
2911 $variation->save();
2912 }
2913 }
2914
2915 /**
2916 * Bulk action - Set Weight.
2917 *
2918 * @param array $variations List of variations.
2919 * @param array $data Data to set.
2920 *
2921 * @used-by bulk_edit_variations
2922 *
2923 * @return void
2924 */
2925 private static function variation_bulk_action_variable_weight( $variations, $data ) {
2926 self::variation_bulk_set( $variations, 'weight', $data['value'] );
2927 }
2928
2929 /**
2930 * Bulk action - Set Length.
2931 *
2932 * @param array $variations List of variations.
2933 * @param array $data Data to set.
2934 *
2935 * @used-by bulk_edit_variations
2936 *
2937 * @return void
2938 */
2939 private static function variation_bulk_action_variable_length( $variations, $data ) {
2940 self::variation_bulk_set( $variations, 'length', $data['value'] );
2941 }
2942
2943 /**
2944 * Bulk action - Set Width.
2945 *
2946 * @param array $variations List of variations.
2947 * @param array $data Data to set.
2948 *
2949 * @used-by bulk_edit_variations
2950 *
2951 * @return void
2952 */
2953 private static function variation_bulk_action_variable_width( $variations, $data ) {
2954 self::variation_bulk_set( $variations, 'width', $data['value'] );
2955 }
2956
2957 /**
2958 * Bulk action - Set Height.
2959 *
2960 * @param array $variations List of variations.
2961 * @param array $data Data to set.
2962 *
2963 * @used-by bulk_edit_variations
2964 *
2965 * @return void
2966 */
2967 private static function variation_bulk_action_variable_height( $variations, $data ) {
2968 self::variation_bulk_set( $variations, 'height', $data['value'] );
2969 }
2970
2971 /**
2972 * Bulk action - Set Download Limit.
2973 *
2974 * @param array $variations List of variations.
2975 * @param array $data Data to set.
2976 *
2977 * @used-by bulk_edit_variations
2978 *
2979 * @return void
2980 */
2981 private static function variation_bulk_action_variable_download_limit( $variations, $data ) {
2982 self::variation_bulk_set( $variations, 'download_limit', $data['value'] );
2983 }
2984
2985 /**
2986 * Bulk action - Set Download Expiry.
2987 *
2988 * @param array $variations List of variations.
2989 * @param array $data Data to set.
2990 *
2991 * @used-by bulk_edit_variations
2992 *
2993 * @return void
2994 */
2995 private static function variation_bulk_action_variable_download_expiry( $variations, $data ) {
2996 self::variation_bulk_set( $variations, 'download_expiry', $data['value'] );
2997 }
2998
2999 /**
3000 * Bulk action - Delete all.
3001 *
3002 * @param array $variations List of variations.
3003 * @param array $data Data to set.
3004 *
3005 * @used-by bulk_edit_variations
3006 *
3007 * @return void
3008 */
3009 private static function variation_bulk_action_delete_all( $variations, $data ) {
3010 if ( isset( $data['allowed'] ) && 'true' === $data['allowed'] ) {
3011 foreach ( $variations as $variation_id ) {
3012 $variation = wc_get_product( $variation_id );
3013 $variation->delete( true );
3014 }
3015 }
3016 }
3017
3018 /**
3019 * Bulk action - Sale Schedule.
3020 *
3021 * @param array $variations List of variations.
3022 * @param array $data Data to set.
3023 *
3024 * @used-by bulk_edit_variations
3025 *
3026 * @return void
3027 */
3028 private static function variation_bulk_action_variable_sale_schedule( $variations, $data ) {
3029 if ( ! isset( $data['date_from'] ) && ! isset( $data['date_to'] ) ) {
3030 return;
3031 }
3032
3033 $date_from = isset( $data['date_from'] ) ? wc_clean( $data['date_from'] ) : false;
3034 $date_to = isset( $data['date_to'] ) ? wc_clean( $data['date_to'] ) : false;
3035
3036 foreach ( $variations as $variation_id ) {
3037 $variation = wc_get_product( $variation_id );
3038
3039 if ( false !== $date_from && 'false' !== $date_from ) {
3040 $date_on_sale_from = '' === $date_from ? null : date( 'Y-m-d 00:00:00', strtotime( $date_from ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
3041 $variation->set_date_on_sale_from( $date_on_sale_from );
3042 }
3043
3044 if ( false !== $date_to && 'false' !== $date_to ) {
3045 $date_on_sale_to = '' === $date_to ? null : date( 'Y-m-d 23:59:59', strtotime( $date_to ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
3046 $variation->set_date_on_sale_to( $date_on_sale_to );
3047 }
3048
3049 $variation->save();
3050 }
3051 }
3052
3053 /**
3054 * Bulk action - Increase Regular Prices.
3055 *
3056 * @param array $variations List of variations.
3057 * @param array $data Data to set.
3058 *
3059 * @used-by bulk_edit_variations
3060 *
3061 * @return void
3062 */
3063 private static function variation_bulk_action_variable_regular_price_increase( $variations, $data ) {
3064 self::variation_bulk_adjust_price( $variations, 'regular_price', '+', wc_clean( $data['value'] ) );
3065 }
3066
3067 /**
3068 * Bulk action - Decrease Regular Prices.
3069 *
3070 * @param array $variations List of variations.
3071 * @param array $data Data to set.
3072 *
3073 * @used-by bulk_edit_variations
3074 *
3075 * @return void
3076 */
3077 private static function variation_bulk_action_variable_regular_price_decrease( $variations, $data ) {
3078 self::variation_bulk_adjust_price( $variations, 'regular_price', '-', wc_clean( $data['value'] ) );
3079 }
3080
3081 /**
3082 * Bulk action - Increase Sale Prices.
3083 *
3084 * @param array $variations List of variations.
3085 * @param array $data Data to set.
3086 *
3087 * @used-by bulk_edit_variations
3088 *
3089 * @return void
3090 */
3091 private static function variation_bulk_action_variable_sale_price_increase( $variations, $data ) {
3092 self::variation_bulk_adjust_price( $variations, 'sale_price', '+', wc_clean( $data['value'] ) );
3093 }
3094
3095 /**
3096 * Bulk action - Decrease Sale Prices.
3097 *
3098 * @param array $variations List of variations.
3099 * @param array $data Data to set.
3100 *
3101 * @used-by bulk_edit_variations
3102 *
3103 * @return void
3104 */
3105 private static function variation_bulk_action_variable_sale_price_decrease( $variations, $data ) {
3106 self::variation_bulk_adjust_price( $variations, 'sale_price', '-', wc_clean( $data['value'] ) );
3107 }
3108
3109 // phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
3110 /**
3111 * Bulk action - Unset cost values.
3112 *
3113 * @param array $variations List of variations.
3114 * @param array $data Data to set.
3115 *
3116 * @used-by bulk_edit_variations
3117 *
3118 * @return void
3119 */
3120 private static function variation_bulk_action_variable_unset_cogs_value( $variations, $data ) {
3121 if ( ! wc_get_container()->get( CostOfGoodsSoldController::class )->feature_is_enabled() ) {
3122 return;
3123 }
3124
3125 foreach ( $variations as $variation_id ) {
3126 $variation = wc_get_product( $variation_id );
3127 $variation->set_cogs_value( null );
3128 $variation->save();
3129 }
3130 }
3131 // phpcs:enable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
3132
3133 /**
3134 * Bulk action - Set Price.
3135 *
3136 * @param array $variations List of variations.
3137 * @param string $field price being adjusted _regular_price or _sale_price.
3138 * @param string $operator + or -.
3139 * @param string $value Price or Percent.
3140 *
3141 * @used-by bulk_edit_variations
3142 *
3143 * @return void
3144 */
3145 private static function variation_bulk_adjust_price( $variations, $field, $operator, $value ) {
3146 foreach ( $variations as $variation_id ) {
3147 $variation = wc_get_product( $variation_id );
3148 $field_value = $variation->{"get_$field"}( 'edit' );
3149
3150 // Skip variations without a price set
3151 if ( '' === $field_value || null === $field_value ) {
3152 continue;
3153 }
3154
3155 if ( '%' === substr( $value, -1 ) ) {
3156 $percent = wc_format_decimal( substr( $value, 0, -1 ) );
3157 $field_value += NumberUtil::round( ( $field_value / 100 ) * $percent, wc_get_price_decimals() ) * "{$operator}1";
3158 } else {
3159 $field_value += $value * "{$operator}1";
3160 }
3161
3162 $variation->{"set_$field"}( $field_value );
3163 $variation->save();
3164 }
3165 }
3166
3167 /**
3168 * Bulk set convenience function.
3169 *
3170 * @param array $variations List of variations.
3171 * @param string $field Field to set.
3172 * @param string $value to set.
3173 *
3174 * @return void
3175 */
3176 private static function variation_bulk_set( $variations, $field, $value ) {
3177 foreach ( $variations as $variation_id ) {
3178 $variation = wc_get_product( $variation_id );
3179 $variation->{ "set_$field" }( wc_clean( $value ) );
3180 $variation->save();
3181 }
3182 }
3183
3184 /**
3185 * Bulk toggle convenience function.
3186 *
3187 * @param array $variations List of variations.
3188 * @param string $field Field to toggle.
3189 *
3190 * @return void
3191 */
3192 private static function variation_bulk_toggle( $variations, $field ) {
3193 foreach ( $variations as $variation_id ) {
3194 $variation = wc_get_product( $variation_id );
3195 $prev_value = $variation->{ "get_$field" }( 'edit' );
3196 $variation->{ "set_$field" }( ! $prev_value );
3197 $variation->save();
3198 }
3199 }
3200
3201 /**
3202 * Bulk edit variations via AJAX.
3203 *
3204 * @uses WC_AJAX::variation_bulk_set()
3205 * @uses WC_AJAX::variation_bulk_adjust_price()
3206 * @uses WC_AJAX::variation_bulk_action_variable_sale_price_decrease()
3207 * @uses WC_AJAX::variation_bulk_action_variable_sale_price_increase()
3208 * @uses WC_AJAX::variation_bulk_action_variable_regular_price_decrease()
3209 * @uses WC_AJAX::variation_bulk_action_variable_regular_price_increase()
3210 * @uses WC_AJAX::variation_bulk_action_variable_sale_schedule()
3211 * @uses WC_AJAX::variation_bulk_action_delete_all()
3212 * @uses WC_AJAX::variation_bulk_action_variable_download_expiry()
3213 * @uses WC_AJAX::variation_bulk_action_variable_download_limit()
3214 * @uses WC_AJAX::variation_bulk_action_variable_height()
3215 * @uses WC_AJAX::variation_bulk_action_variable_width()
3216 * @uses WC_AJAX::variation_bulk_action_variable_length()
3217 * @uses WC_AJAX::variation_bulk_action_variable_weight()
3218 * @uses WC_AJAX::variation_bulk_action_variable_stock()
3219 * @uses WC_AJAX::variation_bulk_action_variable_sale_price()
3220 * @uses WC_AJAX::variation_bulk_action_variable_regular_price()
3221 * @uses WC_AJAX::variation_bulk_action_toggle_manage_stock()
3222 * @uses WC_AJAX::variation_bulk_action_toggle_virtual()
3223 * @uses WC_AJAX::variation_bulk_action_toggle_downloadable()
3224 * @uses WC_AJAX::variation_bulk_action_toggle_enabled
3225 * @uses WC_AJAX::variation_bulk_action_variable_low_stock_amount()
3226 *
3227 * @return void
3228 */
3229 public static function bulk_edit_variations() {
3230 ob_start();
3231
3232 check_ajax_referer( 'bulk-edit-variations', 'security' );
3233
3234 // Check permissions again and make sure we have what we need.
3235 if ( ! current_user_can( 'edit_products' ) || empty( $_POST['product_id'] ) || empty( $_POST['bulk_action'] ) ) {
3236 wp_die( -1 );
3237 }
3238
3239 $product_id = absint( $_POST['product_id'] );
3240 $bulk_action = wc_clean( wp_unslash( $_POST['bulk_action'] ) );
3241 $data = ! empty( $_POST['data'] ) ? wc_clean( wp_unslash( $_POST['data'] ) ) : array();
3242 $variations = array();
3243
3244 if ( apply_filters( 'woocommerce_bulk_edit_variations_need_children', true ) ) {
3245 $variations = get_posts(
3246 array(
3247 'post_parent' => $product_id,
3248 'posts_per_page' => -1,
3249 'post_type' => 'product_variation',
3250 'fields' => 'ids',
3251 'post_status' => array( 'publish', 'private' ),
3252 )
3253 );
3254 }
3255
3256 if ( method_exists( __CLASS__, "variation_bulk_action_$bulk_action" ) ) {
3257 call_user_func( array( __CLASS__, "variation_bulk_action_$bulk_action" ), $variations, $data );
3258 } else {
3259 do_action( 'woocommerce_bulk_edit_variations_default', $bulk_action, $data, $product_id, $variations );
3260 }
3261
3262 do_action( 'woocommerce_bulk_edit_variations', $bulk_action, $data, $product_id, $variations );
3263 WC_Product_Variable::sync( $product_id );
3264 wc_delete_product_transients( $product_id );
3265 wp_die();
3266 }
3267
3268 /**
3269 * Handle submissions from assets/js/settings-views-html-settings-tax.js Backbone model.
3270 *
3271 * @return void
3272 */
3273 public static function tax_rates_save_changes() {
3274 // phpcs:disable WordPress.Security.NonceVerification.Missing
3275 if ( ! isset( $_POST['wc_tax_nonce'], $_POST['changes'] ) ) {
3276 wp_send_json_error( 'missing_fields' );
3277 wp_die();
3278 }
3279
3280 $current_class = ! empty( $_POST['current_class'] ) ? wp_unslash( $_POST['current_class'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
3281
3282 if ( ! wp_verify_nonce( wp_unslash( $_POST['wc_tax_nonce'] ), 'wc_tax_nonce-class:' . $current_class ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
3283 wp_send_json_error( 'bad_nonce' );
3284 wp_die();
3285 }
3286
3287 $current_class = WC_Tax::format_tax_rate_class( $current_class );
3288
3289 // Check User Caps.
3290 if ( ! current_user_can( 'manage_woocommerce' ) ) {
3291 wp_send_json_error( 'missing_capabilities' );
3292 wp_die();
3293 }
3294
3295 $changes = wp_unslash( $_POST['changes'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
3296 foreach ( $changes as $tax_rate_id => $data ) {
3297 if ( isset( $data['deleted'] ) ) {
3298 if ( isset( $data['newRow'] ) ) {
3299 // So the user added and deleted a new row.
3300 // That's fine, it's not in the database anyways. NEXT!
3301 continue;
3302 }
3303 WC_Tax::_delete_tax_rate( $tax_rate_id );
3304 }
3305
3306 $tax_rate = array_intersect_key(
3307 $data,
3308 array(
3309 'tax_rate_country' => 1,
3310 'tax_rate_state' => 1,
3311 'tax_rate' => 1,
3312 'tax_rate_name' => 1,
3313 'tax_rate_priority' => 1,
3314 'tax_rate_compound' => 1,
3315 'tax_rate_shipping' => 1,
3316 'tax_rate_order' => 1,
3317 )
3318 );
3319
3320 if ( isset( $tax_rate['tax_rate'] ) ) {
3321 $tax_rate['tax_rate'] = wc_format_decimal( $tax_rate['tax_rate'] );
3322 }
3323
3324 if ( isset( $data['newRow'] ) ) {
3325 $tax_rate['tax_rate_class'] = $current_class;
3326 $tax_rate_id = WC_Tax::_insert_tax_rate( $tax_rate );
3327 } elseif ( ! empty( $tax_rate ) ) {
3328 WC_Tax::_update_tax_rate( $tax_rate_id, $tax_rate );
3329 }
3330
3331 if ( isset( $data['postcode'] ) ) {
3332 $postcode = array_map( 'wc_clean', $data['postcode'] );
3333 $postcode = array_map( 'wc_normalize_postcode', $postcode );
3334 WC_Tax::_update_tax_rate_postcodes( $tax_rate_id, $postcode );
3335 }
3336 if ( isset( $data['city'] ) ) {
3337 WC_Tax::_update_tax_rate_cities( $tax_rate_id, array_map( 'wc_clean', array_map( 'wp_unslash', $data['city'] ) ) );
3338 }
3339 }
3340
3341 WC_Cache_Helper::invalidate_cache_group( 'taxes' );
3342 WC_Cache_Helper::get_transient_version( 'shipping', true );
3343
3344 wp_send_json_success(
3345 array(
3346 'rates' => WC_Tax::get_rates_for_tax_class( $current_class ),
3347 )
3348 );
3349 // phpcs:enable
3350 }
3351
3352 /**
3353 * Handle submissions from assets/js/wc-shipping-zones.js Backbone model.
3354 *
3355 * @return void
3356 */
3357 public static function shipping_zones_save_changes() {
3358 if ( ! isset( $_POST['wc_shipping_zones_nonce'], $_POST['changes'] ) ) {
3359 wp_send_json_error( 'missing_fields' );
3360 wp_die();
3361 }
3362
3363 if ( ! wp_verify_nonce( wp_unslash( $_POST['wc_shipping_zones_nonce'] ), 'wc_shipping_zones_nonce' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
3364 wp_send_json_error( 'bad_nonce' );
3365 wp_die();
3366 }
3367
3368 // Check User Caps.
3369 if ( ! current_user_can( 'manage_woocommerce' ) ) {
3370 wp_send_json_error( 'missing_capabilities' );
3371 wp_die();
3372 }
3373
3374 $changes = wp_unslash( $_POST['changes'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
3375 foreach ( $changes as $zone_id => $data ) {
3376 if ( isset( $data['deleted'] ) ) {
3377 if ( isset( $data['newRow'] ) ) {
3378 // So the user added and deleted a new row.
3379 // That's fine, it's not in the database anyways. NEXT!
3380 continue;
3381 }
3382 /**
3383 * Notify that a non-option setting has been deleted.
3384 *
3385 * @since 7.8.0
3386 */
3387 do_action(
3388 'woocommerce_update_non_option_setting',
3389 array(
3390 'id' => 'shipping_zone',
3391 'action' => 'delete',
3392 )
3393 );
3394 WC_Shipping_Zones::delete_zone( $zone_id );
3395 continue;
3396 }
3397
3398 $zone_data = array_intersect_key(
3399 $data,
3400 array(
3401 'zone_id' => 1,
3402 'zone_order' => 1,
3403 )
3404 );
3405
3406 if ( isset( $zone_data['zone_id'] ) ) {
3407 $zone = new WC_Shipping_Zone( $zone_data['zone_id'] );
3408
3409 if ( isset( $zone_data['zone_order'] ) ) {
3410 /**
3411 * Notify that a non-option setting has been updated.
3412 *
3413 * @since 7.8.0
3414 */
3415 do_action(
3416 'woocommerce_update_non_option_setting',
3417 array(
3418 'id' => 'zone_order',
3419 )
3420 );
3421 $zone->set_zone_order( $zone_data['zone_order'] );
3422 }
3423 $zone->save();
3424 }
3425 }
3426
3427 global $current_tab;
3428 $current_tab = 'shipping';
3429 /**
3430 * Completes the saving process for options.
3431 *
3432 * @since 7.8.0
3433 */
3434 do_action( 'woocommerce_update_options' );
3435 wp_send_json_success(
3436 array(
3437 'zones' => WC_Shipping_Zones::get_zones_with_order_conflict_warnings( 'json' ),
3438 )
3439 );
3440 }
3441
3442 /**
3443 * Handle submissions from assets/js/wc-shipping-zone-methods.js Backbone model.
3444 *
3445 * @return void
3446 */
3447 public static function shipping_zone_add_method() {
3448 if ( ! isset( $_POST['wc_shipping_zones_nonce'], $_POST['zone_id'], $_POST['method_id'] ) ) {
3449 wp_send_json_error( 'missing_fields' );
3450 wp_die();
3451 }
3452
3453 if ( ! wp_verify_nonce( wp_unslash( $_POST['wc_shipping_zones_nonce'] ), 'wc_shipping_zones_nonce' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
3454 wp_send_json_error( 'bad_nonce' );
3455 wp_die();
3456 }
3457
3458 // Check User Caps.
3459 if ( ! current_user_can( 'manage_woocommerce' ) ) {
3460 wp_send_json_error( 'missing_capabilities' );
3461 wp_die();
3462 }
3463
3464 $zone_id = wc_clean( wp_unslash( $_POST['zone_id'] ) );
3465 $zone = new WC_Shipping_Zone( $zone_id );
3466 // A shipping zone can be created here if the user is adding a method without first saving the shipping zone.
3467 if ( '' === $zone_id ) {
3468 /**
3469 * Notified that a non-option setting has been added.
3470 *
3471 * @since 7.8.0
3472 */
3473 do_action(
3474 'woocommerce_update_non_option_setting',
3475 array(
3476 'id' => 'shipping_zone',
3477 'action' => 'add',
3478 )
3479 );
3480 }
3481 /**
3482 * Notify that a non-option setting has been added.
3483 *
3484 * @since 7.8.0
3485 */
3486 do_action(
3487 'woocommerce_update_non_option_setting',
3488 array(
3489 'id' => 'zone_method',
3490 'action' => 'add',
3491 )
3492 );
3493 $instance_id = $zone->add_shipping_method( wc_clean( wp_unslash( $_POST['method_id'] ) ) );
3494
3495 global $current_tab;
3496 $current_tab = 'shipping';
3497 /**
3498 * Completes the saving process for options.
3499 *
3500 * @since 7.8.0
3501 */
3502 do_action( 'woocommerce_update_options' );
3503
3504 wp_send_json_success(
3505 array(
3506 'instance_id' => $instance_id,
3507 'zone_id' => $zone->get_id(),
3508 'zone_name' => $zone->get_zone_name(),
3509 'methods' => $zone->get_shipping_methods( false, 'json' ),
3510 )
3511 );
3512 }
3513
3514 /**
3515 * Handle submissions from assets/js/wc-shipping-zone-methods.js Backbone model.
3516 *
3517 * @return void
3518 */
3519 public static function shipping_zone_remove_method() {
3520 if ( ! isset( $_POST['wc_shipping_zones_nonce'], $_POST['instance_id'], $_POST['zone_id'] ) ) {
3521 wp_send_json_error( 'missing_fields' );
3522 wp_die();
3523 }
3524
3525 if ( ! wp_verify_nonce( wp_unslash( $_POST['wc_shipping_zones_nonce'] ), 'wc_shipping_zones_nonce' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
3526 wp_send_json_error( 'bad_nonce' );
3527 wp_die();
3528 }
3529
3530 // Check User Caps.
3531 if ( ! current_user_can( 'manage_woocommerce' ) ) {
3532 wp_send_json_error( 'missing_capabilities' );
3533 wp_die();
3534 }
3535
3536 $zone_id = wc_clean( wp_unslash( $_POST['zone_id'] ) );
3537 $zone = new WC_Shipping_Zone( $zone_id );
3538 $instance_id = wc_clean( wp_unslash( $_POST['instance_id'] ) );
3539
3540 /**
3541 * Notify that a non-option setting has been updated.
3542 *
3543 * @since 7.8.0
3544 */
3545 do_action(
3546 'woocommerce_update_non_option_setting',
3547 array(
3548 'id' => $instance_id,
3549 )
3550 );
3551 if ( ! $zone->delete_shipping_method( $instance_id ) ) {
3552 wp_send_json_error( 'missing_shipping_method_instance_id' );
3553 wp_die();
3554 }
3555
3556 global $current_tab;
3557 $current_tab = 'shipping';
3558 /**
3559 * Completes the saving process for options.
3560 *
3561 * @since 7.8.0
3562 */
3563 do_action( 'woocommerce_update_options' );
3564
3565 wp_send_json_success(
3566 array(
3567 'instance_id' => $instance_id,
3568 'methods' => $zone->get_shipping_methods( false, 'json' ),
3569 )
3570 );
3571 }
3572
3573 /**
3574 * Handle submissions from assets/js/wc-shipping-zone-methods.js Backbone model.
3575 *
3576 * @return void
3577 */
3578 public static function shipping_zone_methods_save_changes() {
3579 if ( ! isset( $_POST['wc_shipping_zones_nonce'], $_POST['zone_id'], $_POST['changes'] ) ) {
3580 wp_send_json_error( 'missing_fields' );
3581 wp_die();
3582 }
3583
3584 if ( ! wp_verify_nonce( wp_unslash( $_POST['wc_shipping_zones_nonce'] ), 'wc_shipping_zones_nonce' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
3585 wp_send_json_error( 'bad_nonce' );
3586 wp_die();
3587 }
3588
3589 if ( ! current_user_can( 'manage_woocommerce' ) ) {
3590 wp_send_json_error( 'missing_capabilities' );
3591 wp_die();
3592 }
3593
3594 global $wpdb;
3595
3596 $zone_id = wc_clean( wp_unslash( $_POST['zone_id'] ) );
3597 $zone = new WC_Shipping_Zone( $zone_id );
3598 // A shipping zone can be created here if the user is adding a method without first saving the shipping zone.
3599 if ( '' === $zone_id ) {
3600 /**
3601 * Notifies that a non-option setting has been added.
3602 *
3603 * @since 7.8.0
3604 */
3605 do_action(
3606 'woocommerce_update_non_option_setting',
3607 array(
3608 'id' => 'shipping_zone',
3609 'action' => 'add',
3610 )
3611 );
3612 }
3613 $changes = wp_unslash( $_POST['changes'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
3614
3615 if ( isset( $changes['zone_name'] ) ) {
3616 /**
3617 * Notifies that a non-option setting has been updated.
3618 *
3619 * @since 7.8.0
3620 */
3621 do_action( 'woocommerce_update_non_option_setting', array( 'id' => 'zone_name' ) );
3622 $zone->set_zone_name( wc_clean( $changes['zone_name'] ) );
3623 }
3624
3625 if ( isset( $changes['zone_locations'] ) ) {
3626 /**
3627 * Notifies that a non-option setting has been updated.
3628 *
3629 * @since 7.8.0
3630 */
3631 do_action( 'woocommerce_update_non_option_setting', array( 'id' => 'zone_locations' ) );
3632 $zone->clear_locations( array( 'state', 'country', 'continent' ) );
3633 $locations = array_filter( array_map( 'wc_clean', (array) $changes['zone_locations'] ) );
3634 foreach ( $locations as $location ) {
3635 // Each posted location will be in the format type:code.
3636 $location_parts = explode( ':', $location );
3637 switch ( $location_parts[0] ) {
3638 case 'state':
3639 $zone->add_location( $location_parts[1] . ':' . $location_parts[2], 'state' );
3640 break;
3641 case 'country':
3642 $zone->add_location( $location_parts[1], 'country' );
3643 break;
3644 case 'continent':
3645 $zone->add_location( $location_parts[1], 'continent' );
3646 break;
3647 }
3648 }
3649 }
3650
3651 if ( isset( $changes['zone_postcodes'] ) ) {
3652 /**
3653 * Notifies that a non-option setting has been updated.
3654 *
3655 * @since 7.8.0
3656 */
3657 do_action( 'woocommerce_update_non_option_setting', array( 'id' => 'zone_postcodes' ) );
3658 $zone->clear_locations( 'postcode' );
3659 $postcodes = array_filter( array_map( 'strtoupper', array_map( 'wc_clean', explode( "\n", $changes['zone_postcodes'] ) ) ) );
3660 foreach ( $postcodes as $postcode ) {
3661 $zone->add_location( $postcode, 'postcode' );
3662 }
3663 }
3664
3665 if ( isset( $changes['methods'] ) ) {
3666 foreach ( $changes['methods'] as $instance_id => $data ) {
3667 $method_id = $wpdb->get_var( $wpdb->prepare( "SELECT method_id FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE instance_id = %d", $instance_id ) );
3668
3669 if ( isset( $data['deleted'] ) ) {
3670 $shipping_method = WC_Shipping_Zones::get_shipping_method( $instance_id );
3671 $option_key = $shipping_method->get_instance_option_key();
3672 if ( $wpdb->delete( "{$wpdb->prefix}woocommerce_shipping_zone_methods", array( 'instance_id' => $instance_id ) ) ) {
3673 delete_option( $option_key );
3674 /**
3675 * Notifies that a non-option setting has been deleted.
3676 *
3677 * @since 7.8.0
3678 */
3679 do_action(
3680 'woocommerce_update_non_option_setting',
3681 array(
3682 'id' => 'zone_method',
3683 'action' => 'delete',
3684 )
3685 );
3686 do_action( 'woocommerce_shipping_zone_method_deleted', $instance_id, $method_id, $zone_id );
3687 }
3688 continue;
3689 }
3690
3691 $method_data = array_intersect_key(
3692 $data,
3693 array(
3694 'method_order' => 1,
3695 'enabled' => 1,
3696 )
3697 );
3698
3699 if ( isset( $method_data['method_order'] ) ) {
3700 /**
3701 * Notifies that a non-option setting has been updated.
3702 *
3703 * @since 7.8.0
3704 */
3705 do_action( 'woocommerce_update_non_option_setting', array( 'id' => 'zone_methods_order' ) );
3706 $wpdb->update( "{$wpdb->prefix}woocommerce_shipping_zone_methods", array( 'method_order' => absint( $method_data['method_order'] ) ), array( 'instance_id' => absint( $instance_id ) ) );
3707 }
3708
3709 if ( isset( $method_data['enabled'] ) ) {
3710 /**
3711 * Notifies that a non-option setting has been updated.
3712 *
3713 * @since 7.8.0
3714 */
3715 do_action( 'woocommerce_update_non_option_setting', array( 'id' => 'zone_methods_enabled' ) );
3716 $is_enabled = absint( 'yes' === $method_data['enabled'] );
3717 if ( $wpdb->update( "{$wpdb->prefix}woocommerce_shipping_zone_methods", array( 'is_enabled' => $is_enabled ), array( 'instance_id' => absint( $instance_id ) ) ) ) {
3718 do_action( 'woocommerce_shipping_zone_method_status_toggled', $instance_id, $method_id, $zone_id, $is_enabled );
3719 }
3720 }
3721 }
3722 }
3723
3724 $zone->save();
3725
3726 global $current_tab;
3727 $current_tab = 'shipping';
3728 /**
3729 * Completes the saving process for options.
3730 *
3731 * @since 7.8.0
3732 */
3733 do_action( 'woocommerce_update_options' );
3734
3735 wp_send_json_success(
3736 array(
3737 'zone_id' => $zone->get_id(),
3738 'zone_name' => $zone->get_zone_name(),
3739 'methods' => $zone->get_shipping_methods( false, 'json' ),
3740 )
3741 );
3742 }
3743
3744 /**
3745 * Save method settings
3746 *
3747 * @return void
3748 */
3749 public static function shipping_zone_methods_save_settings() {
3750 if ( ! isset( $_POST['wc_shipping_zones_nonce'], $_POST['instance_id'], $_POST['data'] ) ) {
3751 wp_send_json_error( 'missing_fields' );
3752 wp_die();
3753 }
3754
3755 if ( ! wp_verify_nonce( wp_unslash( $_POST['wc_shipping_zones_nonce'] ), 'wc_shipping_zones_nonce' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
3756 wp_send_json_error( 'bad_nonce' );
3757 wp_die();
3758 }
3759
3760 if ( ! current_user_can( 'manage_woocommerce' ) ) {
3761 wp_send_json_error( 'missing_capabilities' );
3762 wp_die();
3763 }
3764
3765 $instance_id = absint( $_POST['instance_id'] );
3766 $zone = WC_Shipping_Zones::get_zone_by( 'instance_id', $instance_id );
3767 $shipping_method = WC_Shipping_Zones::get_shipping_method( $instance_id );
3768 /**
3769 * Notify that a non-option setting has been updated.
3770 *
3771 * @since 7.8.0
3772 */
3773 do_action( 'woocommerce_update_non_option_setting', array( 'id' => 'zone_method_settings' ) );
3774 $shipping_method->set_post_data( wp_unslash( $_POST['data'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
3775
3776 global $current_tab;
3777 $current_tab = 'shipping';
3778 /**
3779 * Completes the saving process for options.
3780 *
3781 * @since 7.8.0
3782 */
3783 do_action( 'woocommerce_update_options' );
3784 $shipping_method->process_admin_options();
3785
3786 WC_Cache_Helper::get_transient_version( 'shipping', true );
3787
3788 wp_send_json_success(
3789 array(
3790 'zone_id' => $zone->get_id(),
3791 'zone_name' => $zone->get_zone_name(),
3792 'methods' => $zone->get_shipping_methods( false, 'json' ),
3793 'errors' => $shipping_method->get_errors(),
3794 )
3795 );
3796 }
3797
3798 /**
3799 * Handle submissions from assets/js/wc-shipping-classes.js Backbone model.
3800 *
3801 * @return void
3802 */
3803 public static function shipping_classes_save_changes() {
3804 if ( ! isset( $_POST['wc_shipping_classes_nonce'], $_POST['changes'] ) ) {
3805 wp_send_json_error( 'missing_fields' );
3806 wp_die();
3807 }
3808
3809 if ( ! wp_verify_nonce( wp_unslash( $_POST['wc_shipping_classes_nonce'] ), 'wc_shipping_classes_nonce' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
3810 wp_send_json_error( 'bad_nonce' );
3811 wp_die();
3812 }
3813
3814 if ( ! current_user_can( 'manage_woocommerce' ) ) {
3815 wp_send_json_error( 'missing_capabilities' );
3816 wp_die();
3817 }
3818
3819 $changes = wp_unslash( $_POST['changes'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
3820
3821 foreach ( $changes as $term_id => $data ) {
3822 $term_id = absint( $term_id );
3823
3824 if ( isset( $data['deleted'] ) ) {
3825 if ( isset( $data['newRow'] ) ) {
3826 // So the user added and deleted a new row.
3827 // That's fine, it's not in the database anyways. NEXT!
3828 continue;
3829 }
3830 /**
3831 * Notifies that a non-option setting has been deleted.
3832 *
3833 * @since 7.8.0
3834 */
3835 do_action(
3836 'woocommerce_update_non_option_setting',
3837 array(
3838 'id' => 'shipping_class',
3839 'action' => 'delete',
3840 )
3841 );
3842 wp_delete_term( $term_id, 'product_shipping_class' );
3843 continue;
3844 }
3845
3846 $update_args = array();
3847
3848 if ( isset( $data['name'] ) ) {
3849 /**
3850 * Notify that a non-option setting has been updated.
3851 *
3852 * @since 7.8.0
3853 */
3854 do_action( 'woocommerce_update_non_option_setting', array( 'id' => 'shipping_class_name' ) );
3855 $update_args['name'] = wc_clean( $data['name'] );
3856 }
3857
3858 if ( isset( $data['slug'] ) ) {
3859 /**
3860 * Notify that a non-option setting has been updated.
3861 *
3862 * @since 7.8.0
3863 */
3864 do_action( 'woocommerce_update_non_option_setting', array( 'id' => 'shipping_class_slug' ) );
3865 $update_args['slug'] = wc_clean( $data['slug'] );
3866 }
3867
3868 if ( isset( $data['description'] ) ) {
3869 /**
3870 * Notify that a non-option setting has been updated.
3871 *
3872 * @since 7.8.0
3873 */
3874 do_action( 'woocommerce_update_non_option_setting', array( 'id' => 'shipping_class_description' ) );
3875 $update_args['description'] = wc_clean( $data['description'] );
3876 }
3877
3878 if ( isset( $data['newRow'] ) ) {
3879 $update_args = array_filter( $update_args );
3880 if ( empty( $update_args['name'] ) ) {
3881 continue;
3882 }
3883 /**
3884 * Notifies that a non-option setting has been added.
3885 *
3886 * @since 7.8.0
3887 */
3888 do_action(
3889 'woocommerce_update_non_option_setting',
3890 array(
3891 'id' => 'shipping_class',
3892 'action' => 'add',
3893 )
3894 );
3895 $inserted_term = wp_insert_term( $update_args['name'], 'product_shipping_class', $update_args );
3896 $term_id = is_wp_error( $inserted_term ) ? 0 : $inserted_term['term_id'];
3897 } else {
3898 /**
3899 * Notifies that a non-option setting has been updated.
3900 *
3901 * @since 7.8.0
3902 */
3903 do_action( 'woocommerce_update_non_option_setting', array( 'id' => 'shipping_class' ) );
3904 wp_update_term( $term_id, 'product_shipping_class', $update_args );
3905 }
3906
3907 do_action( 'woocommerce_shipping_classes_save_class', $term_id, $data );
3908 }
3909
3910 global $current_tab, $current_section;
3911 $current_tab = 'shipping';
3912 $current_section = 'classes';
3913 /**
3914 * Completes the saving process for options.
3915 *
3916 * @since 7.8.0
3917 */
3918 do_action( 'woocommerce_update_options' );
3919 $wc_shipping = WC_Shipping::instance();
3920
3921 wp_send_json_success(
3922 array(
3923 'shipping_classes' => $wc_shipping->get_shipping_classes(),
3924 )
3925 );
3926 }
3927
3928 /**
3929 * Handle AJAX save for custom shipping providers (taxonomy-based).
3930 *
3931 * @since 10.7.0
3932 */
3933 public static function shipping_providers_save_changes(): void {
3934 if ( ! \Automattic\WooCommerce\Utilities\FeaturesUtil::feature_is_enabled( 'fulfillments' ) ) {
3935 wp_send_json_error( 'feature_disabled' );
3936 }
3937
3938 if ( ! isset( $_POST['wc_shipping_providers_nonce'], $_POST['changes'] ) ) {
3939 wp_send_json_error( 'missing_fields' );
3940 }
3941
3942 if ( ! wp_verify_nonce( wp_unslash( $_POST['wc_shipping_providers_nonce'] ), 'wc_shipping_providers_nonce' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
3943 wp_send_json_error( 'bad_nonce' );
3944 }
3945
3946 if ( ! current_user_can( 'manage_woocommerce' ) ) {
3947 wp_send_json_error( 'missing_capabilities' );
3948 }
3949
3950 $taxonomy = 'wc_fulfillment_shipping_provider';
3951 $changes = wp_unslash( $_POST['changes'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
3952
3953 if ( ! is_array( $changes ) ) {
3954 wp_send_json_error( 'invalid_changes' );
3955 }
3956
3957 // Collect only built-in provider keys (class-based, not custom taxonomy providers).
3958 $all_providers = \Automattic\WooCommerce\Admin\Features\Fulfillments\FulfillmentUtils::get_shipping_providers();
3959 $built_in_keys = array();
3960 foreach ( $all_providers as $provider ) {
3961 if ( ! $provider instanceof \Automattic\WooCommerce\Admin\Features\Fulfillments\Providers\CustomShippingProvider ) {
3962 $built_in_keys[] = $provider->get_key();
3963 }
3964 }
3965 $reserved_slug_error = '';
3966
3967 foreach ( $changes as $term_id => $data ) {
3968 if ( ! is_numeric( $term_id ) && ! isset( $data['newRow'] ) ) {
3969 continue;
3970 }
3971 $term_id = absint( $term_id );
3972
3973 if ( isset( $data['deleted'] ) ) {
3974 if ( isset( $data['newRow'] ) ) {
3975 continue;
3976 }
3977 $term_to_delete = get_term( $term_id, $taxonomy );
3978 if ( $term_to_delete instanceof \WP_Term && self::is_shipping_provider_in_use( $term_to_delete->slug ) ) {
3979 $reserved_slug_error = sprintf(
3980 /* translators: %s: provider name */
3981 __( 'Cannot delete "%s" because it is used by existing fulfillments. Remove all fulfillments using this provider first.', 'woocommerce' ),
3982 $term_to_delete->name
3983 );
3984 continue;
3985 }
3986 $delete_result = wp_delete_term( $term_id, $taxonomy );
3987 if ( is_wp_error( $delete_result ) || false === $delete_result ) {
3988 $reserved_slug_error = is_wp_error( $delete_result )
3989 ? $delete_result->get_error_message()
3990 : __( 'Failed to delete the shipping provider.', 'woocommerce' );
3991 }
3992 continue;
3993 }
3994
3995 $update_args = array();
3996
3997 if ( isset( $data['name'] ) && is_string( $data['name'] ) ) {
3998 $update_args['name'] = sanitize_text_field( $data['name'] );
3999 }
4000
4001 // Validate and set slug only on new rows. Slug is immutable after creation.
4002 if ( isset( $data['newRow'] ) && isset( $data['slug'] ) && is_string( $data['slug'] ) && '' !== $data['slug'] ) {
4003 $candidate_slug = sanitize_title( $data['slug'] );
4004 if ( in_array( $candidate_slug, $built_in_keys, true ) ) {
4005 $reserved_slug_error = sprintf(
4006 /* translators: %s: slug value */
4007 __( 'The slug "%s" is already used by a built-in shipping provider. Please choose a different slug.', 'woocommerce' ),
4008 $candidate_slug
4009 );
4010 continue;
4011 }
4012 $update_args['slug'] = $candidate_slug;
4013 }
4014
4015 // Validate tracking URL template: must be a valid http/https URL.
4016 // null means "not submitted" (preserve existing), empty string means "clear".
4017 $tracking_url_template = null;
4018 if ( isset( $data['tracking_url_template'] ) && is_string( $data['tracking_url_template'] ) ) {
4019 if ( '' === $data['tracking_url_template'] ) {
4020 $tracking_url_template = '';
4021 } else {
4022 $testable_url = str_replace( '__PLACEHOLDER__', 'test', $data['tracking_url_template'] );
4023 if ( filter_var( $testable_url, FILTER_VALIDATE_URL ) && preg_match( '#^https?://#i', $testable_url ) ) {
4024 $tracking_url_template = esc_url_raw( $data['tracking_url_template'], array( 'http', 'https' ) );
4025 } else {
4026 $reserved_slug_error = __( 'The tracking URL template must be a valid HTTP or HTTPS URL.', 'woocommerce' );
4027 }
4028 }
4029 }
4030
4031 // Validate icon URL: must be a valid http/https URL.
4032 $icon_url = null;
4033 if ( isset( $data['icon'] ) && is_string( $data['icon'] ) ) {
4034 if ( '' === $data['icon'] ) {
4035 $icon_url = '';
4036 } elseif ( filter_var( $data['icon'], FILTER_VALIDATE_URL ) && preg_match( '#^https?://#i', $data['icon'] ) ) {
4037 $icon_url = esc_url_raw( $data['icon'], array( 'http', 'https' ) );
4038 } else {
4039 $reserved_slug_error = __( 'The icon URL must be a valid HTTP or HTTPS URL.', 'woocommerce' );
4040 }
4041 }
4042
4043 if ( isset( $data['newRow'] ) ) {
4044 $provider_name = strval( $update_args['name'] ?? '' );
4045 $update_args = array_filter( $update_args );
4046 if ( empty( $provider_name ) ) {
4047 continue;
4048 }
4049
4050 $inserted_term = wp_insert_term( $provider_name, $taxonomy, $update_args );
4051 if ( is_wp_error( $inserted_term ) ) {
4052 $reserved_slug_error = $inserted_term->get_error_message();
4053 continue;
4054 }
4055 $term_id = $inserted_term['term_id'];
4056
4057 // Verify auto-generated slug doesn't collide with built-in keys.
4058 $new_term = get_term( $term_id, $taxonomy );
4059 if ( ! $new_term instanceof \WP_Term ) {
4060 continue;
4061 }
4062 if ( in_array( $new_term->slug, $built_in_keys, true ) ) {
4063 wp_delete_term( $term_id, $taxonomy );
4064 $reserved_slug_error = sprintf(
4065 /* translators: %s: provider name */
4066 __( 'Could not create provider "%s" because its auto-generated slug conflicts with a built-in shipping provider. Please specify a different slug.', 'woocommerce' ),
4067 $provider_name
4068 );
4069 continue;
4070 }
4071 } else {
4072 $update_result = wp_update_term( $term_id, $taxonomy, $update_args );
4073 if ( is_wp_error( $update_result ) ) {
4074 $reserved_slug_error = $update_result->get_error_message();
4075 continue;
4076 }
4077 }
4078
4079 if ( $term_id ) {
4080 if ( null !== $tracking_url_template ) {
4081 update_term_meta( $term_id, 'tracking_url_template', $tracking_url_template );
4082 }
4083 if ( null !== $icon_url ) {
4084 update_term_meta( $term_id, 'icon', $icon_url );
4085 }
4086 }
4087 }
4088
4089 $terms = get_terms(
4090 array(
4091 'taxonomy' => $taxonomy,
4092 'hide_empty' => false,
4093 )
4094 );
4095 $shipping_providers = array();
4096
4097 if ( ! is_wp_error( $terms ) ) {
4098 foreach ( $terms as $term ) {
4099 $shipping_providers[] = array(
4100 'term_id' => $term->term_id,
4101 'name' => $term->name,
4102 'slug' => $term->slug,
4103 'tracking_url_template' => get_term_meta( $term->term_id, 'tracking_url_template', true ),
4104 'icon' => get_term_meta( $term->term_id, 'icon', true ),
4105 );
4106 }
4107 }
4108
4109 $response = array(
4110 'shipping_providers' => $shipping_providers,
4111 );
4112
4113 if ( ! empty( $reserved_slug_error ) ) {
4114 $response['error'] = $reserved_slug_error;
4115 }
4116
4117 wp_send_json_success(
4118 $response
4119 );
4120 }
4121
4122 /**
4123 * Check if a shipping provider slug is referenced by any fulfillment record.
4124 *
4125 * @since 10.7.0
4126 *
4127 * @param string $provider_slug The provider slug to check.
4128 * @return bool True if the provider is in use.
4129 */
4130 private static function is_shipping_provider_in_use( string $provider_slug ): bool {
4131 global $wpdb;
4132
4133 $fulfillments_table = $wpdb->prefix . 'wc_order_fulfillments';
4134 $meta_table = $wpdb->prefix . 'wc_order_fulfillment_meta';
4135
4136 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
4137 $exists = $wpdb->get_var(
4138 $wpdb->prepare(
4139 "SELECT 1 FROM {$fulfillments_table} f
4140 INNER JOIN {$meta_table} m ON f.fulfillment_id = m.fulfillment_id
4141 WHERE m.meta_key = '_shipment_provider'
4142 AND m.meta_value = %s
4143 AND f.date_deleted IS NULL
4144 AND m.date_deleted IS NULL
4145 LIMIT 1",
4146 wp_json_encode( $provider_slug )
4147 )
4148 );
4149 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
4150
4151 // Fail safe: assume in use if the query itself failed.
4152 if ( $wpdb->last_error ) {
4153 return true;
4154 }
4155
4156 return null !== $exists;
4157 }
4158
4159 /**
4160 * Toggle payment gateway on or off via AJAX.
4161 *
4162 * @since 3.4.0
4163 *
4164 * @return void
4165 */
4166 public static function toggle_gateway_enabled() {
4167 if ( current_user_can( 'manage_woocommerce' ) && check_ajax_referer( 'woocommerce-toggle-payment-gateway-enabled', 'security' ) && isset( $_POST['gateway_id'] ) ) {
4168 // Set current tab.
4169 $referer = wp_get_referer();
4170 if ( $referer ) {
4171 global $current_tab;
4172 parse_str( wp_parse_url( $referer, PHP_URL_QUERY ), $queries );
4173 $current_tab = $queries['tab'] ?? '';
4174 }
4175
4176 // Load gateways.
4177 $payment_gateways = WC()->payment_gateways->payment_gateways();
4178
4179 // Get posted gateway.
4180 $gateway_id = wc_clean( wp_unslash( $_POST['gateway_id'] ) );
4181
4182 foreach ( $payment_gateways as $gateway ) {
4183 if ( ! in_array( $gateway_id, array( $gateway->id, sanitize_title( get_class( $gateway ) ) ), true ) ) {
4184 continue;
4185 }
4186 $enabled = $gateway->get_option( 'enabled', 'no' );
4187 $option = array(
4188 'id' => $gateway->get_option_key(),
4189 );
4190
4191 if ( ! wc_string_to_bool( $enabled ) ) {
4192 if ( $gateway->needs_setup() ) {
4193 wp_send_json_error( 'needs_setup' );
4194 wp_die();
4195 } else {
4196 do_action( 'woocommerce_update_option', $option );
4197 $gateway->update_option( 'enabled', 'yes' );
4198 }
4199 } else {
4200 do_action( 'woocommerce_update_option', $option );
4201 // Disable the gateway.
4202 $gateway->update_option( 'enabled', 'no' );
4203 }
4204 do_action( 'woocommerce_update_options' );
4205 wp_send_json_success( ! wc_string_to_bool( $enabled ) );
4206 wp_die();
4207 }
4208 }
4209
4210 wp_send_json_error( 'invalid_gateway_id' );
4211 wp_die();
4212 }
4213
4214 /**
4215 * AJAX handler for asynchronously loading the status widget content.
4216 *
4217 * @return void
4218 */
4219 public static function load_status_widget() {
4220 check_ajax_referer( 'wc-status-widget', 'security' );
4221
4222 if ( ! current_user_can( 'manage_woocommerce' ) || ! current_user_can( 'view_woocommerce_reports' ) || ! current_user_can( 'publish_shop_orders' ) ) {
4223 wp_send_json_error( 'missing_permissions' );
4224 wp_die();
4225 }
4226
4227 include_once __DIR__ . '/admin/class-wc-admin-dashboard.php';
4228 ob_start();
4229 $wc_admin_dashboard = new WC_Admin_Dashboard();
4230 $wc_admin_dashboard->status_widget_content();
4231 $content = ob_get_clean();
4232 wp_send_json_success( array( 'content' => $content ) );
4233 }
4234
4235 /**
4236 * AJAX handler for asynchronously loading the recent reviews widget content.
4237 *
4238 * @return void
4239 */
4240 public static function load_recent_reviews_widget() {
4241 check_ajax_referer( 'wc-recent-reviews-widget', 'security' );
4242
4243 if ( ! current_user_can( 'publish_shop_orders' ) || ! post_type_supports( 'product', 'comments' ) ) {
4244 wp_send_json_error( 'missing_permissions' );
4245 }
4246
4247 include_once __DIR__ . '/admin/class-wc-admin-dashboard.php';
4248 ob_start();
4249 $wc_admin_dashboard = new WC_Admin_Dashboard();
4250 $wc_admin_dashboard->recent_reviews_content();
4251 $content = ob_get_clean();
4252 wp_send_json_success( array( 'content' => $content ) );
4253 }
4254
4255 /**
4256 * Reimplementation of WP core's `wp_ajax_add_meta` method to support order custom meta updates with custom tables.
4257 *
4258 * @return void
4259 */
4260 private static function order_add_meta() {
4261 wc_get_container()->get( CustomMetaBox::class )->add_meta_ajax();
4262 }
4263
4264 /**
4265 * Reimplementation of WP core's `wp_ajax_delete_meta` method to support order custom meta updates with custom tables.
4266 *
4267 * @return void
4268 */
4269 private static function order_delete_meta(): void {
4270 wc_get_container()->get( CustomMetaBox::class )->delete_meta_ajax();
4271 }
4272
4273 /**
4274 * Hooked into `wp_ajax_woocommerce_json_search_order_metakeys` to return the list of unique meta keys for the
4275 * edit order screen custom fields metabox.
4276 *
4277 * @return void
4278 */
4279 public static function json_search_order_metakeys(): void {
4280 wc_get_container()->get( CustomMetaBox::class )->search_metakeys_ajax();
4281 }
4282
4283 /**
4284 * Hooked to 'heartbeat_received' on the edit order page to refresh the lock on an order being edited by the current user.
4285 *
4286 * @param array $response The heartbeat response to be sent.
4287 * @param array $data Data sent through the heartbeat.
4288 * @return array Response to be sent.
4289 */
4290 private static function order_refresh_lock( $response, $data ) {
4291 return wc_get_container()->get( Automattic\WooCommerce\Internal\Admin\Orders\EditLock::class )->refresh_lock_ajax( $response, $data );
4292 }
4293
4294 /**
4295 * Hooked to 'heartbeat_received' on the orders screen to refresh the locked status of orders in the list table.
4296 *
4297 * @since 7.8.0
4298 *
4299 * @param array $response The heartbeat response to be sent.
4300 * @param array $data Data sent through the heartbeat.
4301 * @return array Response to be sent.
4302 */
4303 private static function check_locked_orders( $response, $data ) {
4304 return wc_get_container()->get( Automattic\WooCommerce\Internal\Admin\Orders\EditLock::class )->check_locked_orders_ajax( $response, $data );
4305 }
4306
4307 // phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
4308 /**
4309 * Render a variation editor.
4310 *
4311 * NOTE! Do NOT remove the apparently unused function arguments.
4312 * These are actually used inside the included html-variation-admin template.
4313 *
4314 * @param WC_Product $product_object PArent product of the variation being edited.
4315 * @param WC_Product $variation_object Variation being edited.
4316 * @param int $loop Index of the variation being rendered.
4317 * @param float|null $base_cost Default cost for variations, null if the Cost of Goods Sold feature is disabled.
4318 *
4319 * @return void
4320 */
4321 private static function render_variation_html( WC_Product $product_object, WC_Product $variation_object, $loop, ?float $base_cost ) {
4322 $variation_id = $variation_object->get_id();
4323 $variation = get_post( $variation_id );
4324 $variation_data = array_merge( get_post_custom( $variation_id ), wc_get_product_variation_attributes( $variation_id ) ); // kept for BW compatibility.
4325 include __DIR__ . '/admin/meta-boxes/views/html-variation-admin.php';
4326 }
4327 // phpcs:enable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
4328
4329 /**
4330 * Get the Cost of Goods Sold value for a product (0 if it's null), return null if the Cost of Goods Sold feature is disabled.
4331 *
4332 * @param WC_Product $product_object Product object.
4333 * @return float|null Cost of the product, or null.
4334 */
4335 private static function base_cost_or_null( WC_Product $product_object ): ?float {
4336 return wc_get_container()->get( CostOfGoodsSoldController::class )->feature_is_enabled() ? ( $product_object->get_cogs_value() ?? 0 ) : null;
4337 }
4338 }
4339
4340 WC_AJAX::init();
4341