WC_AJAX::remove_order_item()   F
last analyzed

Complexity

Conditions 19
Paths 18256

Size

Total Lines 92

Duplication

Lines 5
Ratio 5.43 %

Code Coverage

Tests 0
CRAP Score 380

Importance

Changes 0
Metric Value
cc 19
nc 18256
nop 0
dl 5
loc 92
ccs 0
cts 50
cp 0
crap 380
rs 0.3499
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * WooCommerce WC_AJAX. AJAX Event Handlers.
4
 *
5
 * @class   WC_AJAX
6
 * @package WooCommerce/Classes
7
 */
8
9
defined( 'ABSPATH' ) || exit;
10
11
/**
12
 * WC_Ajax class.
13
 */
14
class WC_AJAX {
15
16
	/**
17
	 * Hook in ajax handlers.
18
	 */
19
	public static function init() {
20
		add_action( 'init', array( __CLASS__, 'define_ajax' ), 0 );
21
		add_action( 'template_redirect', array( __CLASS__, 'do_wc_ajax' ), 0 );
22
		self::add_ajax_events();
23
	}
24
25
	/**
26
	 * Get WC Ajax Endpoint.
27
	 *
28
	 * @param string $request Optional.
29
	 *
30
	 * @return string
31
	 */
32
	public static function get_endpoint( $request = '' ) {
33
		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 ) );
34
	}
35
36
	/**
37
	 * Set WC AJAX constant and headers.
38
	 */
39
	public static function define_ajax() {
40
		// phpcs:disable
41
		if ( ! empty( $_GET['wc-ajax'] ) ) {
42
			wc_maybe_define_constant( 'DOING_AJAX', true );
43
			wc_maybe_define_constant( 'WC_DOING_AJAX', true );
44
			if ( ! WP_DEBUG || ( WP_DEBUG && ! WP_DEBUG_DISPLAY ) ) {
45
				@ini_set( 'display_errors', 0 ); // Turn off display_errors during AJAX events to prevent malformed JSON.
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
46
			}
47
			$GLOBALS['wpdb']->hide_errors();
48
		}
49
		// phpcs:enable
50
	}
51
52
	/**
53
	 * Send headers for WC Ajax Requests.
54
	 *
55
	 * @since 2.5.0
56
	 */
57
	private static function wc_ajax_headers() {
58
		if ( ! headers_sent() ) {
59
			send_origin_headers();
60
			send_nosniff_header();
61
			wc_nocache_headers();
62
			header( 'Content-Type: text/html; charset=' . get_option( 'blog_charset' ) );
63
			header( 'X-Robots-Tag: noindex' );
64
			status_header( 200 );
65 View Code Duplication
		} elseif ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
66
			headers_sent( $file, $line );
67
			trigger_error( "wc_ajax_headers cannot set headers - headers already sent by {$file} on line {$line}", E_USER_NOTICE ); // @codingStandardsIgnoreLine
68
		}
69
	}
70
71
	/**
72
	 * Check for WC Ajax request and fire action.
73
	 */
74
	public static function do_wc_ajax() {
75
		global $wp_query;
76
77
		// phpcs:disable WordPress.Security.NonceVerification.NoNonceVerification
78
		if ( ! empty( $_GET['wc-ajax'] ) ) {
79
			$wp_query->set( 'wc-ajax', sanitize_text_field( wp_unslash( $_GET['wc-ajax'] ) ) );
80
		}
81
82
		$action = $wp_query->get( 'wc-ajax' );
83
84
		if ( $action ) {
85
			self::wc_ajax_headers();
86
			$action = sanitize_text_field( $action );
87
			do_action( 'wc_ajax_' . $action );
88
			wp_die();
89
		}
90
		// phpcs:enable
91
	}
92
93
	/**
94
	 * Hook in methods - uses WordPress ajax handlers (admin-ajax).
95
	 */
96
	public static function add_ajax_events() {
97
		$ajax_events_nopriv = array(
98
			'get_refreshed_fragments',
99
			'apply_coupon',
100
			'remove_coupon',
101
			'update_shipping_method',
102
			'get_cart_totals',
103
			'update_order_review',
104
			'add_to_cart',
105
			'remove_from_cart',
106
			'checkout',
107
			'get_variation',
108
			'get_customer_location',
109
		);
110
111
		foreach ( $ajax_events_nopriv as $ajax_event ) {
112
			add_action( 'wp_ajax_woocommerce_' . $ajax_event, array( __CLASS__, $ajax_event ) );
113
			add_action( 'wp_ajax_nopriv_woocommerce_' . $ajax_event, array( __CLASS__, $ajax_event ) );
114
115
			// WC AJAX can be used for frontend ajax requests.
116
			add_action( 'wc_ajax_' . $ajax_event, array( __CLASS__, $ajax_event ) );
117
		}
118
119
		$ajax_events = array(
120
			'feature_product',
121
			'mark_order_status',
122
			'get_order_details',
123
			'add_attribute',
124
			'add_new_attribute',
125
			'remove_variation',
126
			'remove_variations',
127
			'save_attributes',
128
			'add_variation',
129
			'link_all_variations',
130
			'revoke_access_to_download',
131
			'grant_access_to_download',
132
			'get_customer_details',
133
			'add_order_item',
134
			'add_order_fee',
135
			'add_order_shipping',
136
			'add_order_tax',
137
			'add_coupon_discount',
138
			'remove_order_coupon',
139
			'remove_order_item',
140
			'remove_order_tax',
141
			'reduce_order_item_stock',
142
			'increase_order_item_stock',
143
			'add_order_item_meta',
144
			'remove_order_item_meta',
145
			'calc_line_taxes',
146
			'save_order_items',
147
			'load_order_items',
148
			'add_order_note',
149
			'delete_order_note',
150
			'json_search_products',
151
			'json_search_products_and_variations',
152
			'json_search_downloadable_products_and_variations',
153
			'json_search_customers',
154
			'json_search_categories',
155
			'term_ordering',
156
			'product_ordering',
157
			'refund_line_items',
158
			'delete_refund',
159
			'rated',
160
			'update_api_key',
161
			'load_variations',
162
			'save_variations',
163
			'bulk_edit_variations',
164
			'tax_rates_save_changes',
165
			'shipping_zones_save_changes',
166
			'shipping_zone_add_method',
167
			'shipping_zone_methods_save_changes',
168
			'shipping_zone_methods_save_settings',
169
			'shipping_classes_save_changes',
170
			'toggle_gateway_enabled',
171
		);
172
173
		foreach ( $ajax_events as $ajax_event ) {
174
			add_action( 'wp_ajax_woocommerce_' . $ajax_event, array( __CLASS__, $ajax_event ) );
175
		}
176
	}
177
178
	/**
179
	 * Get a refreshed cart fragment, including the mini cart HTML.
180
	 */
181
	public static function get_refreshed_fragments() {
182
		ob_start();
183
184
		woocommerce_mini_cart();
185
186
		$mini_cart = ob_get_clean();
187
188
		$data = array(
189
			'fragments' => apply_filters(
190
				'woocommerce_add_to_cart_fragments',
191
				array(
192
					'div.widget_shopping_cart_content' => '<div class="widget_shopping_cart_content">' . $mini_cart . '</div>',
193
				)
194
			),
195
			'cart_hash' => WC()->cart->get_cart_hash(),
196
		);
197
198
		wp_send_json( $data );
199
	}
200
201
	/**
202
	 * AJAX apply coupon on checkout page.
203
	 */
204
	public static function apply_coupon() {
205
206
		check_ajax_referer( 'apply-coupon', 'security' );
207
208
		if ( ! empty( $_POST['coupon_code'] ) ) {
209
			WC()->cart->add_discount( wc_format_coupon_code( wp_unslash( $_POST['coupon_code'] ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
210
		} else {
211
			wc_add_notice( WC_Coupon::get_generic_coupon_error( WC_Coupon::E_WC_COUPON_PLEASE_ENTER ), 'error' );
212
		}
213
214
		wc_print_notices();
215
		wp_die();
216
	}
217
218
	/**
219
	 * AJAX remove coupon on cart and checkout page.
220
	 */
221
	public static function remove_coupon() {
222
		check_ajax_referer( 'remove-coupon', 'security' );
223
224
		$coupon = isset( $_POST['coupon'] ) ? wc_format_coupon_code( wp_unslash( $_POST['coupon'] ) ) : false; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
225
226
		if ( empty( $coupon ) ) {
227
			wc_add_notice( __( 'Sorry there was a problem removing this coupon.', 'woocommerce' ), 'error' );
228
		} else {
229
			WC()->cart->remove_coupon( $coupon );
230
			wc_add_notice( __( 'Coupon has been removed.', 'woocommerce' ) );
231
		}
232
233
		wc_print_notices();
234
		wp_die();
235
	}
236
237
	/**
238
	 * AJAX update shipping method on cart page.
239
	 */
240
	public static function update_shipping_method() {
241
		check_ajax_referer( 'update-shipping-method', 'security' );
242
243
		wc_maybe_define_constant( 'WOOCOMMERCE_CART', true );
244
245
		$chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' );
246
		$posted_shipping_methods = isset( $_POST['shipping_method'] ) ? wc_clean( wp_unslash( $_POST['shipping_method'] ) ) : array();
247
248
		if ( is_array( $posted_shipping_methods ) ) {
249
			foreach ( $posted_shipping_methods as $i => $value ) {
250
				$chosen_shipping_methods[ $i ] = $value;
251
			}
252
		}
253
254
		WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods );
255
256
		self::get_cart_totals();
257
	}
258
259
	/**
260
	 * AJAX receive updated cart_totals div.
261
	 */
262
	public static function get_cart_totals() {
263
		wc_maybe_define_constant( 'WOOCOMMERCE_CART', true );
264
		WC()->cart->calculate_totals();
265
		woocommerce_cart_totals();
266
		wp_die();
267
	}
268
269
	/**
270
	 * Session has expired.
271
	 */
272
	private static function update_order_review_expired() {
273
		wp_send_json(
274
			array(
275
				'fragments' => apply_filters(
276
					'woocommerce_update_order_review_fragments',
277
					array(
278
						'form.woocommerce-checkout' => '<div class="woocommerce-error">' . __( 'Sorry, your session has expired.', 'woocommerce' ) . ' <a href="' . esc_url( wc_get_page_permalink( 'shop' ) ) . '" class="wc-backward">' . __( 'Return to shop', 'woocommerce' ) . '</a></div>',
279
					)
280
				),
281
			)
282
		);
283
	}
284
285
	/**
286
	 * AJAX update order review on checkout.
287
	 */
288
	public static function update_order_review() {
289
		check_ajax_referer( 'update-order-review', 'security' );
290
291
		wc_maybe_define_constant( 'WOOCOMMERCE_CHECKOUT', true );
292
293 View Code Duplication
		if ( WC()->cart->is_empty() && ! is_customize_preview() && apply_filters( 'woocommerce_checkout_update_order_review_expired', true ) ) {
294
			self::update_order_review_expired();
295
		}
296
297
		do_action( 'woocommerce_checkout_update_order_review', isset( $_POST['post_data'] ) ? wp_unslash( $_POST['post_data'] ) : '' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
298
299
		$chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' );
300
		$posted_shipping_methods = isset( $_POST['shipping_method'] ) ? wc_clean( wp_unslash( $_POST['shipping_method'] ) ) : array();
301
302
		if ( is_array( $posted_shipping_methods ) ) {
303
			foreach ( $posted_shipping_methods as $i => $value ) {
304
				$chosen_shipping_methods[ $i ] = $value;
305
			}
306
		}
307
308
		WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods );
309
		WC()->session->set( 'chosen_payment_method', empty( $_POST['payment_method'] ) ? '' : wc_clean( wp_unslash( $_POST['payment_method'] ) ) );
310
		WC()->customer->set_props(
311
			array(
312
				'billing_country'   => isset( $_POST['country'] ) ? wc_clean( wp_unslash( $_POST['country'] ) ) : null,
313
				'billing_state'     => isset( $_POST['state'] ) ? wc_clean( wp_unslash( $_POST['state'] ) ) : null,
314
				'billing_postcode'  => isset( $_POST['postcode'] ) ? wc_clean( wp_unslash( $_POST['postcode'] ) ) : null,
315
				'billing_city'      => isset( $_POST['city'] ) ? wc_clean( wp_unslash( $_POST['city'] ) ) : null,
316
				'billing_address_1' => isset( $_POST['address'] ) ? wc_clean( wp_unslash( $_POST['address'] ) ) : null,
317
				'billing_address_2' => isset( $_POST['address_2'] ) ? wc_clean( wp_unslash( $_POST['address_2'] ) ) : null,
318
			)
319
		);
320
321
		if ( wc_ship_to_billing_address_only() ) {
322
			WC()->customer->set_props(
323
				array(
324
					'shipping_country'   => isset( $_POST['country'] ) ? wc_clean( wp_unslash( $_POST['country'] ) ) : null,
325
					'shipping_state'     => isset( $_POST['state'] ) ? wc_clean( wp_unslash( $_POST['state'] ) ) : null,
326
					'shipping_postcode'  => isset( $_POST['postcode'] ) ? wc_clean( wp_unslash( $_POST['postcode'] ) ) : null,
327
					'shipping_city'      => isset( $_POST['city'] ) ? wc_clean( wp_unslash( $_POST['city'] ) ) : null,
328
					'shipping_address_1' => isset( $_POST['address'] ) ? wc_clean( wp_unslash( $_POST['address'] ) ) : null,
329
					'shipping_address_2' => isset( $_POST['address_2'] ) ? wc_clean( wp_unslash( $_POST['address_2'] ) ) : null,
330
				)
331
			);
332
		} else {
333
			WC()->customer->set_props(
334
				array(
335
					'shipping_country'   => isset( $_POST['s_country'] ) ? wc_clean( wp_unslash( $_POST['s_country'] ) ) : null,
336
					'shipping_state'     => isset( $_POST['s_state'] ) ? wc_clean( wp_unslash( $_POST['s_state'] ) ) : null,
337
					'shipping_postcode'  => isset( $_POST['s_postcode'] ) ? wc_clean( wp_unslash( $_POST['s_postcode'] ) ) : null,
338
					'shipping_city'      => isset( $_POST['s_city'] ) ? wc_clean( wp_unslash( $_POST['s_city'] ) ) : null,
339
					'shipping_address_1' => isset( $_POST['s_address'] ) ? wc_clean( wp_unslash( $_POST['s_address'] ) ) : null,
340
					'shipping_address_2' => isset( $_POST['s_address_2'] ) ? wc_clean( wp_unslash( $_POST['s_address_2'] ) ) : null,
341
				)
342
			);
343
		}
344
345
		if ( isset( $_POST['has_full_address'] ) && wc_string_to_bool( wc_clean( wp_unslash( $_POST['has_full_address'] ) ) ) ) {
0 ignored issues
show
Bug introduced by
It seems like wc_clean(wp_unslash($_POST['has_full_address'])) targeting wc_clean() can also be of type array; however, wc_string_to_bool() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
346
			WC()->customer->set_calculated_shipping( true );
347
		} else {
348
			WC()->customer->set_calculated_shipping( false );
349
		}
350
351
		WC()->customer->save();
352
353
		// 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.
354
		WC()->cart->calculate_shipping();
355
		WC()->cart->calculate_totals();
356
357
		// Get order review fragment.
358
		ob_start();
359
		woocommerce_order_review();
360
		$woocommerce_order_review = ob_get_clean();
361
362
		// Get checkout payment fragment.
363
		ob_start();
364
		woocommerce_checkout_payment();
365
		$woocommerce_checkout_payment = ob_get_clean();
366
367
		// Get messages if reload checkout is not true.
368
		$reload_checkout = isset( WC()->session->reload_checkout ) ? true : false;
369
		if ( ! $reload_checkout ) {
370
			$messages = wc_print_notices( true );
371
		} else {
372
			$messages = '';
373
		}
374
375
		unset( WC()->session->refresh_totals, WC()->session->reload_checkout );
376
377
		wp_send_json(
378
			array(
379
				'result'    => empty( $messages ) ? 'success' : 'failure',
380
				'messages'  => $messages,
381
				'reload'    => $reload_checkout,
382
				'fragments' => apply_filters(
383
					'woocommerce_update_order_review_fragments',
384
					array(
385
						'.woocommerce-checkout-review-order-table' => $woocommerce_order_review,
386
						'.woocommerce-checkout-payment' => $woocommerce_checkout_payment,
387
					)
388
				),
389
			)
390
		);
391
	}
392
393
	/**
394
	 * AJAX add to cart.
395
	 */
396
	public static function add_to_cart() {
397
		ob_start();
398
399
		// phpcs:disable WordPress.Security.NonceVerification.NoNonceVerification
400
		if ( ! isset( $_POST['product_id'] ) ) {
401
			return;
402
		}
403
404
		$product_id        = apply_filters( 'woocommerce_add_to_cart_product_id', absint( $_POST['product_id'] ) );
405
		$product           = wc_get_product( $product_id );
406
		$quantity          = empty( $_POST['quantity'] ) ? 1 : wc_stock_amount( wp_unslash( $_POST['quantity'] ) );
407
		$passed_validation = apply_filters( 'woocommerce_add_to_cart_validation', true, $product_id, $quantity );
408
		$product_status    = get_post_status( $product_id );
409
		$variation_id      = 0;
410
		$variation         = array();
411
412
		if ( $product && 'variation' === $product->get_type() ) {
413
			$variation_id = $product_id;
414
			$product_id   = $product->get_parent_id();
415
			$variation    = $product->get_variation_attributes();
416
		}
417
418
		if ( $passed_validation && false !== WC()->cart->add_to_cart( $product_id, $quantity, $variation_id, $variation ) && 'publish' === $product_status ) {
419
420
			do_action( 'woocommerce_ajax_added_to_cart', $product_id );
421
422
			if ( 'yes' === get_option( 'woocommerce_cart_redirect_after_add' ) ) {
423
				wc_add_to_cart_message( array( $product_id => $quantity ), true );
424
			}
425
426
			self::get_refreshed_fragments();
427
428
		} else {
429
430
			// If there was an error adding to the cart, redirect to the product page to show any errors.
431
			$data = array(
432
				'error'       => true,
433
				'product_url' => apply_filters( 'woocommerce_cart_redirect_after_error', get_permalink( $product_id ), $product_id ),
434
			);
435
436
			wp_send_json( $data );
437
		}
438
		// phpcs:enable
439
	}
440
441
	/**
442
	 * AJAX remove from cart.
443
	 */
444
	public static function remove_from_cart() {
445
		ob_start();
446
447
		// phpcs:ignore WordPress.Security.NonceVerification.NoNonceVerification
448
		$cart_item_key = wc_clean( isset( $_POST['cart_item_key'] ) ? wp_unslash( $_POST['cart_item_key'] ) : '' );
449
450
		if ( $cart_item_key && false !== WC()->cart->remove_cart_item( $cart_item_key ) ) {
451
			self::get_refreshed_fragments();
452
		} else {
453
			wp_send_json_error();
454
		}
455
	}
456
457
	/**
458
	 * Process ajax checkout form.
459
	 */
460
	public static function checkout() {
461
		wc_maybe_define_constant( 'WOOCOMMERCE_CHECKOUT', true );
462
		WC()->checkout()->process_checkout();
463
		wp_die( 0 );
464
	}
465
466
	/**
467
	 * Get a matching variation based on posted attributes.
468
	 */
469
	public static function get_variation() {
470
		ob_start();
471
472
		// phpcs:disable WordPress.Security.NonceVerification.NoNonceVerification
473
		if ( empty( $_POST['product_id'] ) ) {
474
			wp_die();
475
		}
476
477
		$variable_product = wc_get_product( absint( $_POST['product_id'] ) );
478
479
		if ( ! $variable_product ) {
480
			wp_die();
481
		}
482
483
		$data_store   = WC_Data_Store::load( 'product' );
484
		$variation_id = $data_store->find_matching_product_variation( $variable_product, wp_unslash( $_POST ) );
0 ignored issues
show
Documentation Bug introduced by
The method find_matching_product_variation does not exist on object<WC_Data_Store>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
485
		$variation    = $variation_id ? $variable_product->get_available_variation( $variation_id ) : false;
486
		wp_send_json( $variation );
487
		// phpcs:enable
488
	}
489
490
	/**
491
	 * Locate user via AJAX.
492
	 */
493
	public static function get_customer_location() {
494
		$location_hash = WC_Cache_Helper::geolocation_ajax_get_location_hash();
495
		wp_send_json_success( array( 'hash' => $location_hash ) );
496
	}
497
498
	/**
499
	 * Toggle Featured status of a product from admin.
500
	 */
501
	public static function feature_product() {
502
		if ( current_user_can( 'edit_products' ) && check_admin_referer( 'woocommerce-feature-product' ) && isset( $_GET['product_id'] ) ) {
503
			$product = wc_get_product( absint( $_GET['product_id'] ) );
504
505
			if ( $product ) {
506
				$product->set_featured( ! $product->get_featured() );
507
				$product->save();
508
			}
509
		}
510
511
		wp_safe_redirect( wp_get_referer() ? remove_query_arg( array( 'trashed', 'untrashed', 'deleted', 'ids' ), wp_get_referer() ) : admin_url( 'edit.php?post_type=product' ) );
512
		exit;
513
	}
514
515
	/**
516
	 * Mark an order with a status.
517
	 */
518
	public static function mark_order_status() {
519
		if ( current_user_can( 'edit_shop_orders' ) && check_admin_referer( 'woocommerce-mark-order-status' ) && isset( $_GET['status'], $_GET['order_id'] ) ) {
520
			$status = sanitize_text_field( wp_unslash( $_GET['status'] ) );
521
			$order  = wc_get_order( absint( wp_unslash( $_GET['order_id'] ) ) );
522
523
			if ( wc_is_order_status( 'wc-' . $status ) && $order ) {
524
				// Initialize payment gateways in case order has hooked status transition actions.
525
				WC()->payment_gateways();
526
527
				$order->update_status( $status, '', true );
528
				do_action( 'woocommerce_order_edit_status', $order->get_id(), $status );
529
			}
530
		}
531
532
		wp_safe_redirect( wp_get_referer() ? wp_get_referer() : admin_url( 'edit.php?post_type=shop_order' ) );
533
		exit;
534
	}
535
536
	/**
537
	 * Get order details.
538
	 */
539
	public static function get_order_details() {
540
		check_admin_referer( 'woocommerce-preview-order', 'security' );
541
542
		if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_GET['order_id'] ) ) {
543
			wp_die( -1 );
544
		}
545
546
		$order = wc_get_order( absint( $_GET['order_id'] ) ); // WPCS: sanitization ok.
547
548
		if ( $order ) {
549
			include_once 'admin/list-tables/class-wc-admin-list-table-orders.php';
550
551
			wp_send_json_success( WC_Admin_List_Table_Orders::order_preview_get_order_details( $order ) );
552
		}
553
		wp_die();
554
	}
555
556
	/**
557
	 * Add an attribute row.
558
	 */
559
	public static function add_attribute() {
560
		ob_start();
561
562
		check_ajax_referer( 'add-attribute', 'security' );
563
564
		if ( ! current_user_can( 'edit_products' ) || ! isset( $_POST['taxonomy'], $_POST['i'] ) ) {
565
			wp_die( -1 );
566
		}
567
568
		$i             = absint( $_POST['i'] );
569
		$metabox_class = array();
570
		$attribute     = new WC_Product_Attribute();
571
572
		$attribute->set_id( wc_attribute_taxonomy_id_by_name( sanitize_text_field( wp_unslash( $_POST['taxonomy'] ) ) ) );
573
		$attribute->set_name( sanitize_text_field( wp_unslash( $_POST['taxonomy'] ) ) );
574
		$attribute->set_visible( apply_filters( 'woocommerce_attribute_default_visibility', 1 ) );
575
		$attribute->set_variation( apply_filters( 'woocommerce_attribute_default_is_variation', 0 ) );
576
577
		if ( $attribute->is_taxonomy() ) {
578
			$metabox_class[] = 'taxonomy';
579
			$metabox_class[] = $attribute->get_name();
580
		}
581
582
		include 'admin/meta-boxes/views/html-product-attribute.php';
583
		wp_die();
584
	}
585
586
	/**
587
	 * Add a new attribute via ajax function.
588
	 */
589
	public static function add_new_attribute() {
590
		check_ajax_referer( 'add-attribute', 'security' );
591
592
		if ( current_user_can( 'manage_product_terms' ) && isset( $_POST['taxonomy'], $_POST['term'] ) ) {
593
			$taxonomy = esc_attr( wp_unslash( $_POST['taxonomy'] ) ); // phpcs:ignore
594
			$term     = wc_clean( wp_unslash( $_POST['term'] ) );
595
596
			if ( taxonomy_exists( $taxonomy ) ) {
597
598
				$result = wp_insert_term( $term, $taxonomy );
599
600
				if ( is_wp_error( $result ) ) {
601
					wp_send_json(
602
						array(
603
							'error' => $result->get_error_message(),
604
						)
605
					);
606
				} else {
607
					$term = get_term_by( 'id', $result['term_id'], $taxonomy );
608
					wp_send_json(
609
						array(
610
							'term_id' => $term->term_id,
611
							'name'    => $term->name,
612
							'slug'    => $term->slug,
613
						)
614
					);
615
				}
616
			}
617
		}
618
		wp_die( -1 );
619
	}
620
621
	/**
622
	 * Delete variations via ajax function.
623
	 */
624
	public static function remove_variations() {
625
		check_ajax_referer( 'delete-variations', 'security' );
626
627
		if ( current_user_can( 'edit_products' ) && isset( $_POST['variation_ids'] ) ) {
628
			$variation_ids = array_map( 'absint', (array) wp_unslash( $_POST['variation_ids'] ) );
629
630
			foreach ( $variation_ids as $variation_id ) {
631
				if ( 'product_variation' === get_post_type( $variation_id ) ) {
632
					$variation = wc_get_product( $variation_id );
633
					$variation->delete( true );
634
				}
635
			}
636
		}
637
638
		wp_die( -1 );
639
	}
640
641
	/**
642
	 * Save attributes via ajax.
643
	 */
644
	public static function save_attributes() {
645
		check_ajax_referer( 'save-attributes', 'security' );
646
647 View Code Duplication
		if ( ! current_user_can( 'edit_products' ) || ! isset( $_POST['data'], $_POST['post_id'] ) ) {
648
			wp_die( -1 );
649
		}
650
651
		$response = array();
652
653
		try {
654
			parse_str( wp_unslash( $_POST['data'] ), $data ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
655
656
			$attributes   = WC_Meta_Box_Product_Data::prepare_attributes( $data );
0 ignored issues
show
Bug introduced by
It seems like $data can also be of type null; however, WC_Meta_Box_Product_Data::prepare_attributes() does only seem to accept false|array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
657
			$product_id   = absint( wp_unslash( $_POST['post_id'] ) );
658
			$product_type = ! empty( $_POST['product_type'] ) ? wc_clean( wp_unslash( $_POST['product_type'] ) ) : 'simple';
659
			$classname    = WC_Product_Factory::get_product_classname( $product_id, $product_type );
0 ignored issues
show
Bug introduced by
It seems like $product_type defined by !empty($_POST['product_t...uct_type'])) : 'simple' on line 658 can also be of type array; however, WC_Product_Factory::get_product_classname() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
660
			$product      = new $classname( $product_id );
661
662
			$product->set_attributes( $attributes );
663
			$product->save();
664
665
			ob_start();
666
			$attributes = $product->get_attributes( 'edit' );
667
			$i          = -1;
668
			if ( ! empty( $data['attribute_names'] ) ) {
669
				foreach ( $data['attribute_names'] as $attribute_name ) {
670
					$attribute = isset( $attributes[ sanitize_title( $attribute_name ) ] ) ? $attributes[ sanitize_title( $attribute_name ) ] : false;
671
					if ( ! $attribute ) {
672
						continue;
673
					}
674
					$i++;
675
					$metabox_class = array();
676
677
					if ( $attribute->is_taxonomy() ) {
678
						$metabox_class[] = 'taxonomy';
679
						$metabox_class[] = $attribute->get_name();
680
					}
681
682
					include 'admin/meta-boxes/views/html-product-attribute.php';
683
				}
684
			}
685
686
			$response['html'] = ob_get_clean();
687
		} catch ( Exception $e ) {
688
			wp_send_json_error( array( 'error' => $e->getMessage() ) );
689
		}
690
691
		// wp_send_json_success must be outside the try block not to break phpunit tests.
692
		wp_send_json_success( $response );
693
	}
694
695
	/**
696
	 * Add variation via ajax function.
697
	 */
698
	public static function add_variation() {
699
		check_ajax_referer( 'add-variation', 'security' );
700
701 View Code Duplication
		if ( ! current_user_can( 'edit_products' ) || ! isset( $_POST['post_id'], $_POST['loop'] ) ) {
702
			wp_die( -1 );
703
		}
704
705
		global $post; // Set $post global so its available, like within the admin screens.
706
707
		$product_id       = intval( $_POST['post_id'] );
708
		$post             = get_post( $product_id ); // phpcs:ignore
709
		$loop             = intval( $_POST['loop'] );
710
		$product_object   = new WC_Product_Variable( $product_id ); // Forces type to variable in case product is unsaved.
711
		$variation_object = new WC_Product_Variation();
712
		$variation_object->set_parent_id( $product_id );
713
		$variation_object->set_attributes( array_fill_keys( array_map( 'sanitize_title', array_keys( $product_object->get_variation_attributes() ) ), '' ) );
714
		$variation_id   = $variation_object->save();
715
		$variation      = get_post( $variation_id );
716
		$variation_data = array_merge( get_post_custom( $variation_id ), wc_get_product_variation_attributes( $variation_id ) ); // kept for BW compatibility.
717
		include 'admin/meta-boxes/views/html-variation-admin.php';
718
		wp_die();
719
	}
720
721
	/**
722
	 * Link all variations via ajax function.
723
	 */
724
	public static function link_all_variations() {
725
		check_ajax_referer( 'link-variations', 'security' );
726
727
		if ( ! current_user_can( 'edit_products' ) ) {
728
			wp_die( -1 );
729
		}
730
731
		wc_maybe_define_constant( 'WC_MAX_LINKED_VARIATIONS', 50 );
732
		wc_set_time_limit( 0 );
733
734
		$post_id = isset( $_POST['post_id'] ) ? intval( $_POST['post_id'] ) : 0;
735
736
		if ( ! $post_id ) {
737
			wp_die();
738
		}
739
740
		$product    = wc_get_product( $post_id );
741
		$data_store = $product->get_data_store();
742
743
		if ( ! is_callable( array( $data_store, 'create_all_product_variations' ) ) ) {
744
			wp_die();
745
		}
746
747
		echo esc_html( $data_store->create_all_product_variations( $product, WC_MAX_LINKED_VARIATIONS ) );
748
749
		$data_store->sort_all_product_variations( $product->get_id() );
750
		wp_die();
751
	}
752
753
	/**
754
	 * Delete download permissions via ajax function.
755
	 */
756
	public static function revoke_access_to_download() {
757
		check_ajax_referer( 'revoke-access', 'security' );
758
759
		if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['download_id'], $_POST['product_id'], $_POST['order_id'], $_POST['permission_id'] ) ) {
760
			wp_die( -1 );
761
		}
762
		$download_id   = wc_clean( wp_unslash( $_POST['download_id'] ) );
763
		$product_id    = intval( $_POST['product_id'] );
764
		$order_id      = intval( $_POST['order_id'] );
765
		$permission_id = absint( $_POST['permission_id'] );
766
		$data_store    = WC_Data_Store::load( 'customer-download' );
767
		$data_store->delete_by_id( $permission_id );
0 ignored issues
show
Documentation Bug introduced by
The method delete_by_id does not exist on object<WC_Data_Store>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
768
769
		do_action( 'woocommerce_ajax_revoke_access_to_product_download', $download_id, $product_id, $order_id, $permission_id );
770
771
		wp_die();
772
	}
773
774
	/**
775
	 * Grant download permissions via ajax function.
776
	 */
777
	public static function grant_access_to_download() {
778
779
		check_ajax_referer( 'grant-access', 'security' );
780
781 View Code Duplication
		if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['loop'], $_POST['order_id'], $_POST['product_ids'] ) ) {
782
			wp_die( -1 );
783
		}
784
785
		global $wpdb;
786
787
		$wpdb->hide_errors();
788
789
		$order_id     = intval( $_POST['order_id'] );
790
		$product_ids  = array_filter( array_map( 'absint', (array) wp_unslash( $_POST['product_ids'] ) ) );
791
		$loop         = intval( $_POST['loop'] );
792
		$file_counter = 0;
793
		$order        = wc_get_order( $order_id );
794
795
		foreach ( $product_ids as $product_id ) {
796
			$product = wc_get_product( $product_id );
797
			$files   = $product->get_downloads();
798
799
			if ( ! $order->get_billing_email() ) {
800
				wp_die();
801
			}
802
803
			if ( ! empty( $files ) ) {
804
				foreach ( $files as $download_id => $file ) {
805
					$inserted_id = wc_downloadable_file_permission( $download_id, $product_id, $order );
0 ignored issues
show
Documentation introduced by
$order is of type false|object, but the function expects a object<WC_Order>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
806
					if ( $inserted_id ) {
807
						$download = new WC_Customer_Download( $inserted_id );
0 ignored issues
show
Bug introduced by
It seems like $inserted_id defined by wc_downloadable_file_per...d, $product_id, $order) on line 805 can also be of type boolean; however, WC_Customer_Download::__construct() does only seem to accept integer, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
808
						$loop ++;
809
						$file_counter ++;
810
811
						if ( $file->get_name() ) {
812
							$file_count = $file->get_name();
813
						} else {
814
							/* translators: %d file count */
815
							$file_count = sprintf( __( 'File %d', 'woocommerce' ), $file_counter );
816
						}
817
						include 'admin/meta-boxes/views/html-order-download-permission.php';
818
					}
819
				}
820
			}
821
		}
822
		wp_die();
823
	}
824
825
	/**
826
	 * Get customer details via ajax.
827
	 */
828
	public static function get_customer_details() {
829
		check_ajax_referer( 'get-customer-details', 'security' );
830
831
		if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['user_id'] ) ) {
832
			wp_die( -1 );
833
		}
834
835
		$user_id  = absint( $_POST['user_id'] );
836
		$customer = new WC_Customer( $user_id );
837
838
		if ( has_filter( 'woocommerce_found_customer_details' ) ) {
839
			wc_deprecated_function( 'The woocommerce_found_customer_details filter', '3.0', 'woocommerce_ajax_get_customer_details' );
840
		}
841
842
		$data                  = $customer->get_data();
843
		$data['date_created']  = $data['date_created'] ? $data['date_created']->getTimestamp() : null;
844
		$data['date_modified'] = $data['date_modified'] ? $data['date_modified']->getTimestamp() : null;
845
846
		$customer_data = apply_filters( 'woocommerce_ajax_get_customer_details', $data, $customer, $user_id );
847
		wp_send_json( $customer_data );
848
	}
849
850
	/**
851
	 * Add order item via ajax. Used on the edit order screen in WP Admin.
852
	 *
853
	 * @throws Exception If order is invalid.
854
	 */
855
	public static function add_order_item() {
856
		check_ajax_referer( 'order-item', 'security' );
857
858
		if ( ! current_user_can( 'edit_shop_orders' ) ) {
859
			wp_die( -1 );
860
		}
861
862
		$response = array();
863
864
		try {
865
			if ( ! isset( $_POST['order_id'] ) ) {
866
				throw new Exception( __( 'Invalid order', 'woocommerce' ) );
867
			}
868
869
			$order_id = absint( wp_unslash( $_POST['order_id'] ) ); // WPCS: input var ok.
870
			$order    = wc_get_order( $order_id );
871
872
			if ( ! $order ) {
873
				throw new Exception( __( 'Invalid order', 'woocommerce' ) );
874
			}
875
876
			// If we passed through items it means we need to save first before adding a new one.
877
			$items = ( ! empty( $_POST['items'] ) ) ? wp_unslash( $_POST['items'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
878
879 View Code Duplication
			if ( ! empty( $items ) ) {
880
				$save_items = array();
881
				parse_str( $items, $save_items );
882
				wc_save_order_items( $order->get_id(), $save_items );
0 ignored issues
show
Bug introduced by
It seems like $save_items can also be of type null; however, wc_save_order_items() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
883
			}
884
885
			$items_to_add = isset( $_POST['data'] ) ? array_filter( wp_unslash( (array) $_POST['data'] ) ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
886
887
			// Add items to order.
888
			$order_notes = array();
889
890
			foreach ( $items_to_add as $item ) {
891
				if ( ! isset( $item['id'], $item['qty'] ) || empty( $item['id'] ) ) {
892
					continue;
893
				}
894
				$product_id = absint( $item['id'] );
895
				$qty        = wc_stock_amount( $item['qty'] );
896
				$product    = wc_get_product( $product_id );
897
898
				if ( ! $product ) {
899
					throw new Exception( __( 'Invalid product ID', 'woocommerce' ) . ' ' . $product_id );
900
				}
901
				$validation_error = new WP_Error();
902
				$validation_error = apply_filters( 'woocommerce_ajax_add_order_item_validation', $validation_error, $product, $order, $qty );
903
904 View Code Duplication
				if ( $validation_error->get_error_code() ) {
905
					throw new Exception( '<strong>' . __( 'Error:', 'woocommerce' ) . '</strong> ' . $validation_error->get_error_message() );
906
				}
907
				$item_id                 = $order->add_product( $product, $qty );
908
				$item                    = apply_filters( 'woocommerce_ajax_order_item', $order->get_item( $item_id ), $item_id, $order, $product );
909
				$added_items[ $item_id ] = $item;
910
				$order_notes[ $item_id ] = $product->get_formatted_name();
911
912
				if ( $product->managing_stock() ) {
913
					$new_stock               = wc_update_product_stock( $product, $qty, 'decrease' );
914
					$order_notes[ $item_id ] = $product->get_formatted_name() . ' &ndash; ' . ( $new_stock + $qty ) . '&rarr;' . $new_stock;
915
					$item->add_meta_data( '_reduced_stock', $qty, true );
916
					$item->save();
917
				}
918
919
				do_action( 'woocommerce_ajax_add_order_item_meta', $item_id, $item, $order );
920
			}
921
922
			/* translators: %s item name. */
923
			$order->add_order_note( sprintf( __( 'Added line items: %s', 'woocommerce' ), implode( ', ', $order_notes ) ), false, true );
924
925
			do_action( 'woocommerce_ajax_order_items_added', $added_items, $order );
926
927
			$data = get_post_meta( $order_id );
928
929
			// Get HTML to return.
930
			ob_start();
931
			include 'admin/meta-boxes/views/html-order-items.php';
932
			$items_html = ob_get_clean();
933
934
			ob_start();
935
			$notes = wc_get_order_notes( array( 'order_id' => $order_id ) );
936
			include 'admin/meta-boxes/views/html-order-notes.php';
937
			$notes_html = ob_get_clean();
938
939
			wp_send_json_success(
940
				array(
941
					'html'       => $items_html,
942
					'notes_html' => $notes_html,
943
				)
944
			);
945
		} catch ( Exception $e ) {
946
			wp_send_json_error( array( 'error' => $e->getMessage() ) );
947
		}
948
949
		// wp_send_json_success must be outside the try block not to break phpunit tests.
950
		wp_send_json_success( $response );
951
	}
952
953
	/**
954
	 * Add order fee via ajax.
955
	 *
956
	 * @throws Exception If order is invalid.
957
	 */
958
	public static function add_order_fee() {
959
		check_ajax_referer( 'order-item', 'security' );
960
961
		if ( ! current_user_can( 'edit_shop_orders' ) ) {
962
			wp_die( -1 );
963
		}
964
965
		$response = array();
966
967
		try {
968
			$order_id = isset( $_POST['order_id'] ) ? absint( $_POST['order_id'] ) : 0;
969
			$order    = wc_get_order( $order_id );
970
971
			if ( ! $order ) {
972
				throw new Exception( __( 'Invalid order', 'woocommerce' ) );
973
			}
974
975
			$amount = isset( $_POST['amount'] ) ? wc_clean( wp_unslash( $_POST['amount'] ) ) : 0;
976
977
			$calculate_tax_args = array(
978
				'country'  => isset( $_POST['country'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['country'] ) ) ) : '',
0 ignored issues
show
Bug introduced by
It seems like wc_clean(wp_unslash($_POST['country'])) targeting wc_clean() can also be of type array; however, wc_strtoupper() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
979
				'state'    => isset( $_POST['state'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['state'] ) ) ) : '',
0 ignored issues
show
Bug introduced by
It seems like wc_clean(wp_unslash($_POST['state'])) targeting wc_clean() can also be of type array; however, wc_strtoupper() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
980
				'postcode' => isset( $_POST['postcode'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['postcode'] ) ) ) : '',
0 ignored issues
show
Bug introduced by
It seems like wc_clean(wp_unslash($_POST['postcode'])) targeting wc_clean() can also be of type array; however, wc_strtoupper() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
981
				'city'     => isset( $_POST['city'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['city'] ) ) ) : '',
0 ignored issues
show
Bug introduced by
It seems like wc_clean(wp_unslash($_POST['city'])) targeting wc_clean() can also be of type array; however, wc_strtoupper() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
982
			);
983
984
			if ( strstr( $amount, '%' ) ) {
985
				$formatted_amount = $amount;
986
				$percent          = floatval( trim( $amount, '%' ) );
987
				$amount           = $order->get_total() * ( $percent / 100 );
988
			} else {
989
				$amount           = floatval( $amount );
990
				$formatted_amount = wc_price( $amount, array( 'currency' => $order->get_currency() ) );
991
			}
992
993
			$fee = new WC_Order_Item_Fee();
994
			$fee->set_amount( $amount );
995
			$fee->set_total( $amount );
996
			/* translators: %s fee amount */
997
			$fee->set_name( sprintf( __( '%s fee', 'woocommerce' ), wc_clean( $formatted_amount ) ) );
998
999
			$order->add_item( $fee );
1000
			$order->calculate_taxes( $calculate_tax_args );
1001
			$order->calculate_totals( false );
1002
			$order->save();
1003
1004
			ob_start();
1005
			include 'admin/meta-boxes/views/html-order-items.php';
1006
			$response['html'] = ob_get_clean();
1007
		} catch ( Exception $e ) {
1008
			wp_send_json_error( array( 'error' => $e->getMessage() ) );
1009
		}
1010
1011
		// wp_send_json_success must be outside the try block not to break phpunit tests.
1012
		wp_send_json_success( $response );
1013
	}
1014
1015
	/**
1016
	 * Add order shipping cost via ajax.
1017
	 *
1018
	 * @throws Exception If order is invalid.
1019
	 */
1020
	public static function add_order_shipping() {
1021
		check_ajax_referer( 'order-item', 'security' );
1022
1023
		if ( ! current_user_can( 'edit_shop_orders' ) ) {
1024
			wp_die( -1 );
1025
		}
1026
1027
		$response = array();
1028
1029
		try {
1030
			$order_id = isset( $_POST['order_id'] ) ? absint( $_POST['order_id'] ) : 0;
1031
			$order    = wc_get_order( $order_id );
1032
1033
			if ( ! $order ) {
1034
				throw new Exception( __( 'Invalid order', 'woocommerce' ) );
1035
			}
1036
1037
			$order_taxes      = $order->get_taxes();
1038
			$shipping_methods = WC()->shipping() ? WC()->shipping()->load_shipping_methods() : array();
1039
1040
			// Add new shipping.
1041
			$item = new WC_Order_Item_Shipping();
1042
			$item->set_shipping_rate( new WC_Shipping_Rate() );
1043
			$item->set_order_id( $order_id );
1044
			$item_id = $item->save();
1045
1046
			ob_start();
1047
			include 'admin/meta-boxes/views/html-order-shipping.php';
1048
			$response['html'] = ob_get_clean();
1049
		} catch ( Exception $e ) {
1050
			wp_send_json_error( array( 'error' => $e->getMessage() ) );
1051
		}
1052
1053
		// wp_send_json_success must be outside the try block not to break phpunit tests.
1054
		wp_send_json_success( $response );
1055
	}
1056
1057
	/**
1058
	 * Add order tax column via ajax.
1059
	 *
1060
	 * @throws Exception If order or tax rate is invalid.
1061
	 */
1062
	public static function add_order_tax() {
1063
		check_ajax_referer( 'order-item', 'security' );
1064
1065
		if ( ! current_user_can( 'edit_shop_orders' ) ) {
1066
			wp_die( -1 );
1067
		}
1068
1069
		$response = array();
1070
1071
		try {
1072
			$order_id = isset( $_POST['order_id'] ) ? absint( $_POST['order_id'] ) : 0;
1073
			$order    = wc_get_order( $order_id );
1074
1075
			if ( ! $order ) {
1076
				throw new Exception( __( 'Invalid order', 'woocommerce' ) );
1077
			}
1078
1079
			$rate_id = isset( $_POST['rate_id'] ) ? absint( $_POST['rate_id'] ) : '';
1080
1081
			if ( ! $rate_id ) {
1082
				throw new Exception( __( 'Invalid rate', 'woocommerce' ) );
1083
			}
1084
1085
			$data = get_post_meta( $order_id );
1086
1087
			// Add new tax.
1088
			$item = new WC_Order_Item_Tax();
1089
			$item->set_rate( $rate_id );
1090
			$item->set_order_id( $order_id );
1091
			$item->save();
1092
1093
			ob_start();
1094
			include 'admin/meta-boxes/views/html-order-items.php';
1095
			$response['html'] = ob_get_clean();
1096
		} catch ( Exception $e ) {
1097
			wp_send_json_error( array( 'error' => $e->getMessage() ) );
1098
		}
1099
1100
		// wp_send_json_success must be outside the try block not to break phpunit tests.
1101
		wp_send_json_success( $response );
1102
	}
1103
1104
	/**
1105
	 * Add order discount via ajax.
1106
	 *
1107
	 * @throws Exception If order or coupon is invalid.
1108
	 */
1109
	public static function add_coupon_discount() {
1110
		check_ajax_referer( 'order-item', 'security' );
1111
1112
		if ( ! current_user_can( 'edit_shop_orders' ) ) {
1113
			wp_die( -1 );
1114
		}
1115
1116
		$response = array();
1117
1118
		try {
1119
			$order_id           = isset( $_POST['order_id'] ) ? absint( $_POST['order_id'] ) : 0;
1120
			$order              = wc_get_order( $order_id );
1121
			$calculate_tax_args = array(
1122
				'country'  => isset( $_POST['country'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['country'] ) ) ) : '',
0 ignored issues
show
Bug introduced by
It seems like wc_clean(wp_unslash($_POST['country'])) targeting wc_clean() can also be of type array; however, wc_strtoupper() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1123
				'state'    => isset( $_POST['state'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['state'] ) ) ) : '',
0 ignored issues
show
Bug introduced by
It seems like wc_clean(wp_unslash($_POST['state'])) targeting wc_clean() can also be of type array; however, wc_strtoupper() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1124
				'postcode' => isset( $_POST['postcode'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['postcode'] ) ) ) : '',
0 ignored issues
show
Bug introduced by
It seems like wc_clean(wp_unslash($_POST['postcode'])) targeting wc_clean() can also be of type array; however, wc_strtoupper() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1125
				'city'     => isset( $_POST['city'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['city'] ) ) ) : '',
0 ignored issues
show
Bug introduced by
It seems like wc_clean(wp_unslash($_POST['city'])) targeting wc_clean() can also be of type array; however, wc_strtoupper() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1126
			);
1127
1128
			if ( ! $order ) {
1129
				throw new Exception( __( 'Invalid order', 'woocommerce' ) );
1130
			}
1131
1132
			if ( empty( $_POST['coupon'] ) ) {
1133
				throw new Exception( __( 'Invalid coupon', 'woocommerce' ) );
1134
			}
1135
1136
			// Add user ID and/or email so validation for coupon limits works.
1137
			$user_id_arg    = isset( $_POST['user_id'] ) ? absint( $_POST['user_id'] ) : 0;
1138
			$user_email_arg = isset( $_POST['user_email'] ) ? sanitize_email( wp_unslash( $_POST['user_email'] ) ) : '';
1139
1140
			if ( $user_id_arg ) {
1141
				$order->set_customer_id( $user_id_arg );
1142
			}
1143
			if ( $user_email_arg ) {
1144
				$order->set_billing_email( $user_email_arg );
1145
			}
1146
1147
			$result = $order->apply_coupon( wc_format_coupon_code( wp_unslash( $_POST['coupon'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.NoNonceVerification, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1148
1149
			if ( is_wp_error( $result ) ) {
1150
				throw new Exception( html_entity_decode( wp_strip_all_tags( $result->get_error_message() ) ) );
1151
			}
1152
1153
			$order->calculate_taxes( $calculate_tax_args );
1154
			$order->calculate_totals( false );
1155
1156
			ob_start();
1157
			include 'admin/meta-boxes/views/html-order-items.php';
1158
			$response['html'] = ob_get_clean();
1159
		} catch ( Exception $e ) {
1160
			wp_send_json_error( array( 'error' => $e->getMessage() ) );
1161
		}
1162
1163
		// wp_send_json_success must be outside the try block not to break phpunit tests.
1164
		wp_send_json_success( $response );
1165
	}
1166
1167
	/**
1168
	 * Remove coupon from an order via ajax.
1169
	 *
1170
	 * @throws Exception If order or coupon is invalid.
1171
	 */
1172
	public static function remove_order_coupon() {
1173
		check_ajax_referer( 'order-item', 'security' );
1174
1175
		if ( ! current_user_can( 'edit_shop_orders' ) ) {
1176
			wp_die( -1 );
1177
		}
1178
1179
		$response = array();
1180
1181
		try {
1182
			$order_id           = isset( $_POST['order_id'] ) ? absint( $_POST['order_id'] ) : 0;
1183
			$order              = wc_get_order( $order_id );
1184
			$calculate_tax_args = array(
1185
				'country'  => isset( $_POST['country'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['country'] ) ) ) : '',
0 ignored issues
show
Bug introduced by
It seems like wc_clean(wp_unslash($_POST['country'])) targeting wc_clean() can also be of type array; however, wc_strtoupper() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1186
				'state'    => isset( $_POST['state'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['state'] ) ) ) : '',
0 ignored issues
show
Bug introduced by
It seems like wc_clean(wp_unslash($_POST['state'])) targeting wc_clean() can also be of type array; however, wc_strtoupper() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1187
				'postcode' => isset( $_POST['postcode'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['postcode'] ) ) ) : '',
0 ignored issues
show
Bug introduced by
It seems like wc_clean(wp_unslash($_POST['postcode'])) targeting wc_clean() can also be of type array; however, wc_strtoupper() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1188
				'city'     => isset( $_POST['city'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['city'] ) ) ) : '',
0 ignored issues
show
Bug introduced by
It seems like wc_clean(wp_unslash($_POST['city'])) targeting wc_clean() can also be of type array; however, wc_strtoupper() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1189
			);
1190
1191
			if ( ! $order ) {
1192
				throw new Exception( __( 'Invalid order', 'woocommerce' ) );
1193
			}
1194
1195
			if ( empty( $_POST['coupon'] ) ) {
1196
				throw new Exception( __( 'Invalid coupon', 'woocommerce' ) );
1197
			}
1198
1199
			$order->remove_coupon( wc_format_coupon_code( wp_unslash( $_POST['coupon'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.NoNonceVerification, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1200
			$order->calculate_taxes( $calculate_tax_args );
1201
			$order->calculate_totals( false );
1202
1203
			ob_start();
1204
			include 'admin/meta-boxes/views/html-order-items.php';
1205
			$response['html'] = ob_get_clean();
1206
		} catch ( Exception $e ) {
1207
			wp_send_json_error( array( 'error' => $e->getMessage() ) );
1208
		}
1209
1210
		// wp_send_json_success must be outside the try block not to break phpunit tests.
1211
		wp_send_json_success( $response );
1212
	}
1213
1214
	/**
1215
	 * Remove an order item.
1216
	 *
1217
	 * @throws Exception If order is invalid.
1218
	 */
1219
	public static function remove_order_item() {
1220
		check_ajax_referer( 'order-item', 'security' );
1221
1222
		if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['order_id'], $_POST['order_item_ids'] ) ) {
1223
			wp_die( -1 );
1224
		}
1225
1226
		$response = array();
1227
1228
		try {
1229
			$order_id = absint( $_POST['order_id'] );
1230
			$order    = wc_get_order( $order_id );
1231
1232
			if ( ! $order ) {
1233
				throw new Exception( __( 'Invalid order', 'woocommerce' ) );
1234
			}
1235
1236
			if ( ! isset( $_POST['order_item_ids'] ) ) {
1237
				throw new Exception( __( 'Invalid items', 'woocommerce' ) );
1238
			}
1239
1240
			$order_item_ids     = wp_unslash( $_POST['order_item_ids'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1241
			$items              = ( ! empty( $_POST['items'] ) ) ? wp_unslash( $_POST['items'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1242
			$calculate_tax_args = array(
1243
				'country'  => isset( $_POST['country'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['country'] ) ) ) : '',
0 ignored issues
show
Bug introduced by
It seems like wc_clean(wp_unslash($_POST['country'])) targeting wc_clean() can also be of type array; however, wc_strtoupper() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1244
				'state'    => isset( $_POST['state'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['state'] ) ) ) : '',
0 ignored issues
show
Bug introduced by
It seems like wc_clean(wp_unslash($_POST['state'])) targeting wc_clean() can also be of type array; however, wc_strtoupper() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1245
				'postcode' => isset( $_POST['postcode'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['postcode'] ) ) ) : '',
0 ignored issues
show
Bug introduced by
It seems like wc_clean(wp_unslash($_POST['postcode'])) targeting wc_clean() can also be of type array; however, wc_strtoupper() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1246
				'city'     => isset( $_POST['city'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['city'] ) ) ) : '',
0 ignored issues
show
Bug introduced by
It seems like wc_clean(wp_unslash($_POST['city'])) targeting wc_clean() can also be of type array; however, wc_strtoupper() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1247
			);
1248
1249
			if ( ! is_array( $order_item_ids ) && is_numeric( $order_item_ids ) ) {
1250
				$order_item_ids = array( $order_item_ids );
1251
			}
1252
1253
			// If we passed through items it means we need to save first before deleting.
1254 View Code Duplication
			if ( ! empty( $items ) ) {
1255
				$save_items = array();
1256
				parse_str( $items, $save_items );
1257
				wc_save_order_items( $order->get_id(), $save_items );
0 ignored issues
show
Bug introduced by
It seems like $save_items can also be of type null; however, wc_save_order_items() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1258
			}
1259
1260
			if ( ! empty( $order_item_ids ) ) {
1261
				$order_notes = array();
1262
1263
				foreach ( $order_item_ids as $item_id ) {
1264
					$item_id = absint( $item_id );
1265
					$item    = $order->get_item( $item_id );
1266
1267
					// Before deleting the item, adjust any stock values already reduced.
1268
					if ( $item->is_type( 'line_item' ) ) {
1269
						$changed_stock = wc_maybe_adjust_line_item_product_stock( $item, 0 );
1270
1271
						if ( $changed_stock && ! is_wp_error( $changed_stock ) ) {
1272
							/* translators: %1$s: item name %2$s: stock change */
1273
							$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 );
1274
						} else {
1275
							/* translators: %s item name. */
1276
							$order->add_order_note( sprintf( __( 'Deleted %s', 'woocommerce' ), $item->get_name() ), false, true );
1277
						}
1278
					}
1279
1280
					wc_delete_order_item( $item_id );
1281
				}
1282
			}
1283
1284
			$order = wc_get_order( $order_id );
1285
			$order->calculate_taxes( $calculate_tax_args );
1286
			$order->calculate_totals( false );
1287
1288
			// Get HTML to return.
1289
			ob_start();
1290
			include 'admin/meta-boxes/views/html-order-items.php';
1291
			$items_html = ob_get_clean();
1292
1293
			ob_start();
1294
			$notes = wc_get_order_notes( array( 'order_id' => $order_id ) );
1295
			include 'admin/meta-boxes/views/html-order-notes.php';
1296
			$notes_html = ob_get_clean();
1297
1298
			wp_send_json_success(
1299
				array(
1300
					'html'       => $items_html,
1301
					'notes_html' => $notes_html,
1302
				)
1303
			);
1304
		} catch ( Exception $e ) {
1305
			wp_send_json_error( array( 'error' => $e->getMessage() ) );
1306
		}
1307
1308
		// wp_send_json_success must be outside the try block not to break phpunit tests.
1309
		wp_send_json_success( $response );
1310
	}
1311
1312
	/**
1313
	 * Remove an order tax.
1314
	 *
1315
	 * @throws Exception If there is an error whilst deleting the rate.
1316
	 */
1317
	public static function remove_order_tax() {
1318
		check_ajax_referer( 'order-item', 'security' );
1319
1320
		if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['order_id'], $_POST['rate_id'] ) ) {
1321
			wp_die( -1 );
1322
		}
1323
1324
		$response = array();
1325
1326
		try {
1327
			$order_id = absint( $_POST['order_id'] );
1328
			$rate_id  = absint( $_POST['rate_id'] );
1329
1330
			$order = wc_get_order( $order_id );
1331
			if ( ! $order->is_editable() ) {
1332
				throw new Exception( __( 'Order not editable', 'woocommerce' ) );
1333
			}
1334
1335
			wc_delete_order_item( $rate_id );
1336
1337
			// Need to load order again after deleting to have latest items before calculating.
1338
			$order = wc_get_order( $order_id );
1339
			$order->calculate_totals( false );
1340
1341
			ob_start();
1342
			include 'admin/meta-boxes/views/html-order-items.php';
1343
			$response['html'] = ob_get_clean();
1344
		} catch ( Exception $e ) {
1345
			wp_send_json_error( array( 'error' => $e->getMessage() ) );
1346
		}
1347
1348
		// wp_send_json_success must be outside the try block not to break phpunit tests.
1349
		wp_send_json_success( $response );
1350
	}
1351
1352
	/**
1353
	 * Calc line tax.
1354
	 */
1355
	public static function calc_line_taxes() {
1356
		check_ajax_referer( 'calc-totals', 'security' );
1357
1358
		if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['order_id'], $_POST['items'] ) ) {
1359
			wp_die( -1 );
1360
		}
1361
1362
		$order_id           = absint( $_POST['order_id'] );
1363
		$calculate_tax_args = array(
1364
			'country'  => isset( $_POST['country'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['country'] ) ) ) : '',
0 ignored issues
show
Bug introduced by
It seems like wc_clean(wp_unslash($_POST['country'])) targeting wc_clean() can also be of type array; however, wc_strtoupper() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1365
			'state'    => isset( $_POST['state'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['state'] ) ) ) : '',
0 ignored issues
show
Bug introduced by
It seems like wc_clean(wp_unslash($_POST['state'])) targeting wc_clean() can also be of type array; however, wc_strtoupper() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1366
			'postcode' => isset( $_POST['postcode'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['postcode'] ) ) ) : '',
0 ignored issues
show
Bug introduced by
It seems like wc_clean(wp_unslash($_POST['postcode'])) targeting wc_clean() can also be of type array; however, wc_strtoupper() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1367
			'city'     => isset( $_POST['city'] ) ? wc_strtoupper( wc_clean( wp_unslash( $_POST['city'] ) ) ) : '',
0 ignored issues
show
Bug introduced by
It seems like wc_clean(wp_unslash($_POST['city'])) targeting wc_clean() can also be of type array; however, wc_strtoupper() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1368
		);
1369
1370
		// Parse the jQuery serialized items.
1371
		$items = array();
1372
		parse_str( wp_unslash( $_POST['items'] ), $items ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1373
1374
		// Save order items first.
1375
		wc_save_order_items( $order_id, $items );
0 ignored issues
show
Bug introduced by
It seems like $items can also be of type null; however, wc_save_order_items() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1376
1377
		// Grab the order and recalculate taxes.
1378
		$order = wc_get_order( $order_id );
1379
		$order->calculate_taxes( $calculate_tax_args );
1380
		$order->calculate_totals( false );
1381
		include 'admin/meta-boxes/views/html-order-items.php';
1382
		wp_die();
1383
	}
1384
1385
	/**
1386
	 * Save order items via ajax.
1387
	 */
1388
	public static function save_order_items() {
1389
		check_ajax_referer( 'order-item', 'security' );
1390
1391
		if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['order_id'], $_POST['items'] ) ) {
1392
			wp_die( -1 );
1393
		}
1394
1395
		if ( isset( $_POST['order_id'], $_POST['items'] ) ) {
1396
			$order_id = absint( $_POST['order_id'] );
1397
1398
			// Parse the jQuery serialized items.
1399
			$items = array();
1400
			parse_str( wp_unslash( $_POST['items'] ), $items ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1401
1402
			// Save order items.
1403
			wc_save_order_items( $order_id, $items );
0 ignored issues
show
Bug introduced by
It seems like $items can also be of type null; however, wc_save_order_items() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1404
1405
			// Return HTML items.
1406
			$order = wc_get_order( $order_id );
1407
1408
			// Get HTML to return.
1409
			ob_start();
1410
			include 'admin/meta-boxes/views/html-order-items.php';
1411
			$items_html = ob_get_clean();
1412
1413
			ob_start();
1414
			$notes = wc_get_order_notes( array( 'order_id' => $order_id ) );
1415
			include 'admin/meta-boxes/views/html-order-notes.php';
1416
			$notes_html = ob_get_clean();
1417
1418
			wp_send_json_success(
1419
				array(
1420
					'html'       => $items_html,
1421
					'notes_html' => $notes_html,
1422
				)
1423
			);
1424
		}
1425
		wp_die();
1426
	}
1427
1428
	/**
1429
	 * Load order items via ajax.
1430
	 */
1431 View Code Duplication
	public static function load_order_items() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1432
		check_ajax_referer( 'order-item', 'security' );
1433
1434
		if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['order_id'] ) ) {
1435
			wp_die( -1 );
1436
		}
1437
1438
		// Return HTML items.
1439
		$order_id = absint( $_POST['order_id'] );
1440
		$order    = wc_get_order( $order_id );
1441
		include 'admin/meta-boxes/views/html-order-items.php';
1442
		wp_die();
1443
	}
1444
1445
	/**
1446
	 * Add order note via ajax.
1447
	 */
1448
	public static function add_order_note() {
1449
		check_ajax_referer( 'add-order-note', 'security' );
1450
1451 View Code Duplication
		if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['post_id'], $_POST['note'], $_POST['note_type'] ) ) {
1452
			wp_die( -1 );
1453
		}
1454
1455
		$post_id   = absint( $_POST['post_id'] );
1456
		$note      = wp_kses_post( trim( wp_unslash( $_POST['note'] ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1457
		$note_type = wc_clean( wp_unslash( $_POST['note_type'] ) );
1458
1459
		$is_customer_note = ( 'customer' === $note_type ) ? 1 : 0;
1460
1461
		if ( $post_id > 0 ) {
1462
			$order      = wc_get_order( $post_id );
1463
			$comment_id = $order->add_order_note( $note, $is_customer_note, true );
1464
			$note       = wc_get_order_note( $comment_id );
1465
1466
			$note_classes   = array( 'note' );
1467
			$note_classes[] = $is_customer_note ? 'customer-note' : '';
1468
			$note_classes   = apply_filters( 'woocommerce_order_note_class', array_filter( $note_classes ), $note );
1469
			?>
1470
			<li rel="<?php echo absint( $note->id ); ?>" class="<?php echo esc_attr( implode( ' ', $note_classes ) ); ?>">
1471
				<div class="note_content">
1472
					<?php echo wp_kses_post( wpautop( wptexturize( make_clickable( $note->content ) ) ) ); ?>
1473
				</div>
1474
				<p class="meta">
1475
					<abbr class="exact-date" title="<?php echo esc_attr( $note->date_created->date( 'y-m-d h:i:s' ) ); ?>">
1476
						<?php
1477
						/* translators: $1: Date created, $2 Time created */
1478
						printf( esc_html__( 'added on %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() ) ) );
1479
						?>
1480
					</abbr>
1481
					<?php
1482
					if ( 'system' !== $note->added_by ) :
1483
						/* translators: %s: note author */
1484
						printf( ' ' . esc_html__( 'by %s', 'woocommerce' ), esc_html( $note->added_by ) );
1485
					endif;
1486
					?>
1487
					<a href="#" class="delete_note" role="button"><?php esc_html_e( 'Delete note', 'woocommerce' ); ?></a>
1488
				</p>
1489
			</li>
1490
			<?php
1491
		}
1492
		wp_die();
1493
	}
1494
1495
	/**
1496
	 * Delete order note via ajax.
1497
	 */
1498 View Code Duplication
	public static function delete_order_note() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1499
		check_ajax_referer( 'delete-order-note', 'security' );
1500
1501
		if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['note_id'] ) ) {
1502
			wp_die( -1 );
1503
		}
1504
1505
		$note_id = (int) $_POST['note_id'];
1506
1507
		if ( $note_id > 0 ) {
1508
			wc_delete_order_note( $note_id );
1509
		}
1510
		wp_die();
1511
	}
1512
1513
	/**
1514
	 * Search for products and echo json.
1515
	 *
1516
	 * @param string $term (default: '') Term to search for.
1517
	 * @param bool   $include_variations in search or not.
1518
	 */
1519
	public static function json_search_products( $term = '', $include_variations = false ) {
1520
		check_ajax_referer( 'search-products', 'security' );
1521
1522 View Code Duplication
		if ( empty( $term ) && isset( $_GET['term'] ) ) {
1523
			$term = (string) wc_clean( wp_unslash( $_GET['term'] ) );
1524
		}
1525
1526
		if ( empty( $term ) ) {
1527
			wp_die();
1528
		}
1529
1530 View Code Duplication
		if ( ! empty( $_GET['limit'] ) ) {
1531
			$limit = absint( $_GET['limit'] );
1532
		} else {
1533
			$limit = absint( apply_filters( 'woocommerce_json_search_limit', 30 ) );
1534
		}
1535
1536
		$include_ids = ! empty( $_GET['include'] ) ? array_map( 'absint', (array) wp_unslash( $_GET['include'] ) ) : array();
1537
		$exclude_ids = ! empty( $_GET['exclude'] ) ? array_map( 'absint', (array) wp_unslash( $_GET['exclude'] ) ) : array();
1538
1539
		$data_store = WC_Data_Store::load( 'product' );
1540
		$ids        = $data_store->search_products( $term, '', (bool) $include_variations, false, $limit, $include_ids, $exclude_ids );
0 ignored issues
show
Documentation Bug introduced by
The method search_products does not exist on object<WC_Data_Store>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
1541
1542
		$product_objects = array_filter( array_map( 'wc_get_product', $ids ), 'wc_products_array_filter_readable' );
1543
		$products        = array();
1544
1545
		foreach ( $product_objects as $product_object ) {
1546
			$formatted_name = $product_object->get_formatted_name();
1547
			$managing_stock = $product_object->managing_stock();
1548
1549
			if ( $managing_stock && ! empty( $_GET['display_stock'] ) ) {
1550
				$stock_amount    = $product_object->get_stock_quantity();
1551
				/* Translators: %d stock amount */
1552
				$formatted_name .= ' &ndash; ' . sprintf( __( 'Stock: %d', 'woocommerce' ), wc_format_stock_quantity_for_display( $stock_amount, $product_object ) );
1553
			}
1554
1555
			$products[ $product_object->get_id() ] = rawurldecode( $formatted_name );
1556
		}
1557
1558
		wp_send_json( apply_filters( 'woocommerce_json_search_found_products', $products ) );
1559
	}
1560
1561
	/**
1562
	 * Search for product variations and return json.
1563
	 *
1564
	 * @see WC_AJAX::json_search_products()
1565
	 */
1566
	public static function json_search_products_and_variations() {
1567
		self::json_search_products( '', true );
1568
	}
1569
1570
	/**
1571
	 * Search for downloadable product variations and return json.
1572
	 *
1573
	 * @see WC_AJAX::json_search_products()
1574
	 */
1575
	public static function json_search_downloadable_products_and_variations() {
1576
		check_ajax_referer( 'search-products', 'security' );
1577
1578 View Code Duplication
		if ( ! empty( $_GET['limit'] ) ) {
1579
			$limit = absint( $_GET['limit'] );
1580
		} else {
1581
			$limit = absint( apply_filters( 'woocommerce_json_search_limit', 30 ) );
1582
		}
1583
1584
		$include_ids = ! empty( $_GET['include'] ) ? array_map( 'absint', (array) wp_unslash( $_GET['include'] ) ) : array();
0 ignored issues
show
Unused Code introduced by
$include_ids is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1585
		$exclude_ids = ! empty( $_GET['exclude'] ) ? array_map( 'absint', (array) wp_unslash( $_GET['exclude'] ) ) : array();
0 ignored issues
show
Unused Code introduced by
$exclude_ids is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1586
1587
		$term       = isset( $_GET['term'] ) ? (string) wc_clean( wp_unslash( $_GET['term'] ) ) : '';
1588
		$data_store = WC_Data_Store::load( 'product' );
1589
		$ids        = $data_store->search_products( $term, 'downloadable', true, false, $limit );
0 ignored issues
show
Documentation Bug introduced by
The method search_products does not exist on object<WC_Data_Store>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
1590
1591
		$product_objects = array_filter( array_map( 'wc_get_product', $ids ), 'wc_products_array_filter_readable' );
1592
		$products        = array();
1593
1594
		foreach ( $product_objects as $product_object ) {
1595
			$products[ $product_object->get_id() ] = rawurldecode( $product_object->get_formatted_name() );
1596
		}
1597
1598
		wp_send_json( $products );
1599
	}
1600
1601
	/**
1602
	 * Search for customers and return json.
1603
	 */
1604
	public static function json_search_customers() {
1605
		ob_start();
1606
1607
		check_ajax_referer( 'search-customers', 'security' );
1608
1609
		if ( ! current_user_can( 'edit_shop_orders' ) ) {
1610
			wp_die( -1 );
1611
		}
1612
1613
		$term  = isset( $_GET['term'] ) ? (string) wc_clean( wp_unslash( $_GET['term'] ) ) : '';
1614
		$limit = 0;
1615
1616
		if ( empty( $term ) ) {
1617
			wp_die();
1618
		}
1619
1620
		$ids = array();
1621
		// Search by ID.
1622
		if ( is_numeric( $term ) ) {
1623
			$customer = new WC_Customer( intval( $term ) );
1624
1625
			// Customer does not exists.
1626
			if ( 0 !== $customer->get_id() ) {
1627
				$ids = array( $customer->get_id() );
1628
			}
1629
		}
1630
1631
		// 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.
1632
		if ( empty( $ids ) ) {
1633
			$data_store = WC_Data_Store::load( 'customer' );
1634
1635
			// If search is smaller than 3 characters, limit result set to avoid
1636
			// too many rows being returned.
1637
			if ( 3 > strlen( $term ) ) {
1638
				$limit = 20;
1639
			}
1640
			$ids = $data_store->search_customers( $term, $limit );
0 ignored issues
show
Documentation Bug introduced by
The method search_customers does not exist on object<WC_Data_Store>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
1641
		}
1642
1643
		$found_customers = array();
1644
1645
		if ( ! empty( $_GET['exclude'] ) ) {
1646
			$ids = array_diff( $ids, array_map( 'absint', (array) wp_unslash( $_GET['exclude'] ) ) );
1647
		}
1648
1649
		foreach ( $ids as $id ) {
1650
			$customer = new WC_Customer( $id );
1651
			/* translators: 1: user display name 2: user ID 3: user email */
1652
			$found_customers[ $id ] = sprintf(
1653
				/* translators: $1: customer name, $2 customer id, $3: customer email */
1654
				esc_html__( '%1$s (#%2$s &ndash; %3$s)', 'woocommerce' ),
1655
				$customer->get_first_name() . ' ' . $customer->get_last_name(),
1656
				$customer->get_id(),
1657
				$customer->get_email()
1658
			);
1659
		}
1660
1661
		wp_send_json( apply_filters( 'woocommerce_json_search_found_customers', $found_customers ) );
1662
	}
1663
1664
	/**
1665
	 * Search for categories and return json.
1666
	 */
1667
	public static function json_search_categories() {
1668
		ob_start();
1669
1670
		check_ajax_referer( 'search-categories', 'security' );
1671
1672
		if ( ! current_user_can( 'edit_products' ) ) {
1673
			wp_die( -1 );
1674
		}
1675
1676
		$search_text = isset( $_GET['term'] ) ? wc_clean( wp_unslash( $_GET['term'] ) ) : '';
1677
1678
		if ( ! $search_text ) {
1679
			wp_die();
1680
		}
1681
1682
		$found_categories = array();
1683
		$args             = array(
1684
			'taxonomy'   => array( 'product_cat' ),
1685
			'orderby'    => 'id',
1686
			'order'      => 'ASC',
1687
			'hide_empty' => true,
1688
			'fields'     => 'all',
1689
			'name__like' => $search_text,
1690
		);
1691
1692
		$terms = get_terms( $args );
1693
1694
		if ( $terms ) {
1695
			foreach ( $terms as $term ) {
1696
				$term->formatted_name = '';
1697
1698
				if ( $term->parent ) {
1699
					$ancestors = array_reverse( get_ancestors( $term->term_id, 'product_cat' ) );
1700
					foreach ( $ancestors as $ancestor ) {
1701
						$ancestor_term = get_term( $ancestor, 'product_cat' );
1702
						if ( $ancestor_term ) {
1703
							$term->formatted_name .= $ancestor_term->name . ' > ';
1704
						}
1705
					}
1706
				}
1707
1708
				$term->formatted_name              .= $term->name . ' (' . $term->count . ')';
1709
				$found_categories[ $term->term_id ] = $term;
1710
			}
1711
		}
1712
1713
		wp_send_json( apply_filters( 'woocommerce_json_search_found_categories', $found_categories ) );
1714
	}
1715
1716
	/**
1717
	 * Ajax request handling for categories ordering.
1718
	 */
1719
	public static function term_ordering() {
1720
		// phpcs:disable WordPress.Security.NonceVerification.NoNonceVerification
1721
		if ( ! current_user_can( 'edit_products' ) || empty( $_POST['id'] ) ) {
1722
			wp_die( -1 );
1723
		}
1724
1725
		$id       = (int) $_POST['id'];
1726
		$next_id  = isset( $_POST['nextid'] ) && (int) $_POST['nextid'] ? (int) $_POST['nextid'] : null;
1727
		$taxonomy = isset( $_POST['thetaxonomy'] ) ? esc_attr( wp_unslash( $_POST['thetaxonomy'] ) ) : null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1728
		$term     = get_term_by( 'id', $id, $taxonomy );
1729
1730
		if ( ! $id || ! $term || ! $taxonomy ) {
1731
			wp_die( 0 );
1732
		}
1733
1734
		wc_reorder_terms( $term, $next_id, $taxonomy );
1735
1736
		$children = get_terms( $taxonomy, "child_of=$id&menu_order=ASC&hide_empty=0" );
1737
1738
		if ( $term && count( $children ) ) {
1739
			echo 'children';
1740
			wp_die();
1741
		}
1742
		// phpcs:enable
1743
	}
1744
1745
	/**
1746
	 * Ajax request handling for product ordering.
1747
	 *
1748
	 * Based on Simple Page Ordering by 10up (https://wordpress.org/plugins/simple-page-ordering/).
1749
	 */
1750
	public static function product_ordering() {
1751
		global $wpdb;
1752
1753
		// phpcs:disable WordPress.Security.NonceVerification.NoNonceVerification
1754
		if ( ! current_user_can( 'edit_products' ) || empty( $_POST['id'] ) ) {
1755
			wp_die( -1 );
1756
		}
1757
1758
		$sorting_id  = absint( $_POST['id'] );
1759
		$previd      = absint( isset( $_POST['previd'] ) ? $_POST['previd'] : 0 );
1760
		$nextid      = absint( isset( $_POST['nextid'] ) ? $_POST['nextid'] : 0 );
1761
		$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' );
1762
		$index       = 0;
1763
1764
		foreach ( $menu_orders as $id => $menu_order ) {
1765
			$id = absint( $id );
1766
1767
			if ( $sorting_id === $id ) {
1768
				continue;
1769
			}
1770
			if ( $nextid === $id ) {
1771
				$index ++;
1772
			}
1773
			$index ++;
1774
			$menu_orders[ $id ] = $index;
1775
			$wpdb->update( $wpdb->posts, array( 'menu_order' => $index ), array( 'ID' => $id ) );
1776
1777
			/**
1778
			 * When a single product has gotten it's ordering updated.
1779
			 * $id The product ID
1780
			 * $index The new menu order
1781
			*/
1782
			do_action( 'woocommerce_after_single_product_ordering', $id, $index );
1783
		}
1784
1785
		if ( isset( $menu_orders[ $previd ] ) ) {
1786
			$menu_orders[ $sorting_id ] = $menu_orders[ $previd ] + 1;
1787
		} elseif ( isset( $menu_orders[ $nextid ] ) ) {
1788
			$menu_orders[ $sorting_id ] = $menu_orders[ $nextid ] - 1;
1789
		} else {
1790
			$menu_orders[ $sorting_id ] = 0;
1791
		}
1792
1793
		$wpdb->update( $wpdb->posts, array( 'menu_order' => $menu_orders[ $sorting_id ] ), array( 'ID' => $sorting_id ) );
1794
1795
		do_action( 'woocommerce_after_product_ordering', $sorting_id, $menu_orders );
1796
		wp_send_json( $menu_orders );
1797
		// phpcs:enable
1798
	}
1799
1800
	/**
1801
	 * Handle a refund via the edit order screen.
1802
	 *
1803
	 * @throws Exception To return errors.
1804
	 */
1805
	public static function refund_line_items() {
1806
		ob_start();
1807
1808
		check_ajax_referer( 'order-item', 'security' );
1809
1810
		if ( ! current_user_can( 'edit_shop_orders' ) ) {
1811
			wp_die( -1 );
1812
		}
1813
1814
		$order_id               = isset( $_POST['order_id'] ) ? absint( $_POST['order_id'] ) : 0;
1815
		$refund_amount          = isset( $_POST['refund_amount'] ) ? wc_format_decimal( sanitize_text_field( wp_unslash( $_POST['refund_amount'] ) ), wc_get_price_decimals() ) : 0;
1816
		$refunded_amount        = isset( $_POST['refunded_amount'] ) ? wc_format_decimal( sanitize_text_field( wp_unslash( $_POST['refunded_amount'] ) ), wc_get_price_decimals() ) : 0;
1817
		$refund_reason          = isset( $_POST['refund_reason'] ) ? sanitize_text_field( wp_unslash( $_POST['refund_reason'] ) ) : '';
1818
		$line_item_qtys         = isset( $_POST['line_item_qtys'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['line_item_qtys'] ) ), true ) : array();
1819
		$line_item_totals       = isset( $_POST['line_item_totals'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['line_item_totals'] ) ), true ) : array();
1820
		$line_item_tax_totals   = isset( $_POST['line_item_tax_totals'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['line_item_tax_totals'] ) ), true ) : array();
1821
		$api_refund             = isset( $_POST['api_refund'] ) && 'true' === $_POST['api_refund'];
1822
		$restock_refunded_items = isset( $_POST['restock_refunded_items'] ) && 'true' === $_POST['restock_refunded_items'];
1823
		$refund                 = false;
1824
		$response               = array();
1825
1826
		try {
1827
			$order       = wc_get_order( $order_id );
1828
			$max_refund  = wc_format_decimal( $order->get_total() - $order->get_total_refunded(), wc_get_price_decimals() );
1829
1830
			if ( ! $refund_amount || $max_refund < $refund_amount || 0 > $refund_amount ) {
1831
				throw new Exception( __( 'Invalid refund amount', 'woocommerce' ) );
1832
			}
1833
1834
			if ( wc_format_decimal( $order->get_total_refunded(), wc_get_price_decimals() ) !== $refunded_amount ) {
1835
				throw new Exception( __( 'Error processing refund. Please try again.', 'woocommerce' ) );
1836
			}
1837
1838
			// Prepare line items which we are refunding.
1839
			$line_items = array();
1840
			$item_ids   = array_unique( array_merge( array_keys( $line_item_qtys ), array_keys( $line_item_totals ) ) );
1841
1842
			foreach ( $item_ids as $item_id ) {
1843
				$line_items[ $item_id ] = array(
1844
					'qty'          => 0,
1845
					'refund_total' => 0,
1846
					'refund_tax'   => array(),
1847
				);
1848
			}
1849
			foreach ( $line_item_qtys as $item_id => $qty ) {
1850
				$line_items[ $item_id ]['qty'] = max( $qty, 0 );
1851
			}
1852
			foreach ( $line_item_totals as $item_id => $total ) {
1853
				$line_items[ $item_id ]['refund_total'] = wc_format_decimal( $total );
1854
			}
1855
			foreach ( $line_item_tax_totals as $item_id => $tax_totals ) {
1856
				$line_items[ $item_id ]['refund_tax'] = array_filter( array_map( 'wc_format_decimal', $tax_totals ) );
1857
			}
1858
1859
			// Create the refund object.
1860
			$refund = wc_create_refund(
1861
				array(
1862
					'amount'         => $refund_amount,
1863
					'reason'         => $refund_reason,
1864
					'order_id'       => $order_id,
1865
					'line_items'     => $line_items,
1866
					'refund_payment' => $api_refund,
1867
					'restock_items'  => $restock_refunded_items,
1868
				)
1869
			);
1870
1871
			if ( is_wp_error( $refund ) ) {
1872
				throw new Exception( $refund->get_error_message() );
1873
			}
1874
1875
			if ( did_action( 'woocommerce_order_fully_refunded' ) ) {
1876
				$response['status'] = 'fully_refunded';
1877
			}
1878
		} catch ( Exception $e ) {
1879
			wp_send_json_error( array( 'error' => $e->getMessage() ) );
1880
		}
1881
1882
		// wp_send_json_success must be outside the try block not to break phpunit tests.
1883
		wp_send_json_success( $response );
1884
	}
1885
1886
	/**
1887
	 * Delete a refund.
1888
	 */
1889
	public static function delete_refund() {
1890
		check_ajax_referer( 'order-item', 'security' );
1891
1892
		if ( ! current_user_can( 'edit_shop_orders' ) || ! isset( $_POST['refund_id'] ) ) {
1893
			wp_die( -1 );
1894
		}
1895
1896
		$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
1897
		foreach ( $refund_ids as $refund_id ) {
1898
			if ( $refund_id && 'shop_order_refund' === get_post_type( $refund_id ) ) {
1899
				$refund   = wc_get_order( $refund_id );
1900
				$order_id = $refund->get_parent_id();
1901
				$refund->delete( true );
1902
				do_action( 'woocommerce_refund_deleted', $refund_id, $order_id );
1903
			}
1904
		}
1905
		wp_die();
1906
	}
1907
1908
	/**
1909
	 * Triggered when clicking the rating footer.
1910
	 */
1911
	public static function rated() {
1912
		if ( ! current_user_can( 'manage_woocommerce' ) ) {
1913
			wp_die( -1 );
1914
		}
1915
		update_option( 'woocommerce_admin_footer_text_rated', 1 );
1916
		wp_die();
1917
	}
1918
1919
	/**
1920
	 * Create/Update API key.
1921
	 *
1922
	 * @throws Exception On invalid or empty description, user, or permissions.
1923
	 */
1924
	public static function update_api_key() {
1925
		ob_start();
1926
1927
		global $wpdb;
1928
1929
		check_ajax_referer( 'update-api-key', 'security' );
1930
1931
		if ( ! current_user_can( 'manage_woocommerce' ) ) {
1932
			wp_die( -1 );
1933
		}
1934
1935
		$response = array();
1936
1937
		try {
1938
			if ( empty( $_POST['description'] ) ) {
1939
				throw new Exception( __( 'Description is missing.', 'woocommerce' ) );
1940
			}
1941
			if ( empty( $_POST['user'] ) ) {
1942
				throw new Exception( __( 'User is missing.', 'woocommerce' ) );
1943
			}
1944
			if ( empty( $_POST['permissions'] ) ) {
1945
				throw new Exception( __( 'Permissions is missing.', 'woocommerce' ) );
1946
			}
1947
1948
			$key_id      = isset( $_POST['key_id'] ) ? absint( $_POST['key_id'] ) : 0;
1949
			$description = sanitize_text_field( wp_unslash( $_POST['description'] ) );
1950
			$permissions = ( in_array( wp_unslash( $_POST['permissions'] ), array( 'read', 'write', 'read_write' ), true ) ) ? sanitize_text_field( wp_unslash( $_POST['permissions'] ) ) : 'read';
1951
			$user_id     = absint( $_POST['user'] );
1952
1953
			// Check if current user can edit other users.
1954
			if ( $user_id && ! current_user_can( 'edit_user', $user_id ) ) {
1955
				if ( get_current_user_id() !== $user_id ) {
1956
					throw new Exception( __( 'You do not have permission to assign API Keys to the selected user.', 'woocommerce' ) );
1957
				}
1958
			}
1959
1960
			if ( 0 < $key_id ) {
1961
				$data = array(
1962
					'user_id'     => $user_id,
1963
					'description' => $description,
1964
					'permissions' => $permissions,
1965
				);
1966
1967
				$wpdb->update(
1968
					$wpdb->prefix . 'woocommerce_api_keys',
1969
					$data,
1970
					array( 'key_id' => $key_id ),
1971
					array(
1972
						'%d',
1973
						'%s',
1974
						'%s',
1975
					),
1976
					array( '%d' )
1977
				);
1978
1979
				$response                    = $data;
1980
				$response['consumer_key']    = '';
1981
				$response['consumer_secret'] = '';
1982
				$response['message']         = __( 'API Key updated successfully.', 'woocommerce' );
1983
			} else {
1984
				$consumer_key    = 'ck_' . wc_rand_hash();
1985
				$consumer_secret = 'cs_' . wc_rand_hash();
1986
1987
				$data = array(
1988
					'user_id'         => $user_id,
1989
					'description'     => $description,
1990
					'permissions'     => $permissions,
1991
					'consumer_key'    => wc_api_hash( $consumer_key ),
1992
					'consumer_secret' => $consumer_secret,
1993
					'truncated_key'   => substr( $consumer_key, -7 ),
1994
				);
1995
1996
				$wpdb->insert(
1997
					$wpdb->prefix . 'woocommerce_api_keys',
1998
					$data,
1999
					array(
2000
						'%d',
2001
						'%s',
2002
						'%s',
2003
						'%s',
2004
						'%s',
2005
						'%s',
2006
					)
2007
				);
2008
2009
				$key_id                      = $wpdb->insert_id;
2010
				$response                    = $data;
2011
				$response['consumer_key']    = $consumer_key;
2012
				$response['consumer_secret'] = $consumer_secret;
2013
				$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' );
2014
				$response['revoke_url']      = '<a style="color: #a00; 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>';
2015
			}
2016
		} catch ( Exception $e ) {
2017
			wp_send_json_error( array( 'message' => $e->getMessage() ) );
2018
		}
2019
2020
		// wp_send_json_success must be outside the try block not to break phpunit tests.
2021
		wp_send_json_success( $response );
2022
	}
2023
2024
	/**
2025
	 * Load variations via AJAX.
2026
	 */
2027
	public static function load_variations() {
2028
		ob_start();
2029
2030
		check_ajax_referer( 'load-variations', 'security' );
2031
2032
		if ( ! current_user_can( 'edit_products' ) || empty( $_POST['product_id'] ) ) {
2033
			wp_die( -1 );
2034
		}
2035
2036
		// Set $post global so its available, like within the admin screens.
2037
		global $post;
2038
2039
		$loop           = 0;
2040
		$product_id     = absint( $_POST['product_id'] );
2041
		$post           = get_post( $product_id ); // phpcs:ignore
2042
		$product_object = wc_get_product( $product_id );
2043
		$per_page       = ! empty( $_POST['per_page'] ) ? absint( $_POST['per_page'] ) : 10;
2044
		$page           = ! empty( $_POST['page'] ) ? absint( $_POST['page'] ) : 1;
2045
		$variations     = wc_get_products(
2046
			array(
2047
				'status'  => array( 'private', 'publish' ),
2048
				'type'    => 'variation',
2049
				'parent'  => $product_id,
2050
				'limit'   => $per_page,
2051
				'page'    => $page,
2052
				'orderby' => array(
2053
					'menu_order' => 'ASC',
2054
					'ID'         => 'DESC',
2055
				),
2056
				'return'  => 'objects',
2057
			)
2058
		);
2059
2060
		if ( $variations ) {
2061
			wc_render_invalid_variation_notice( $product_object );
0 ignored issues
show
Documentation introduced by
$product_object is of type false|object, but the function expects a object<WC_Product>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2062
2063
			foreach ( $variations as $variation_object ) {
2064
				$variation_id   = $variation_object->get_id();
2065
				$variation      = get_post( $variation_id );
2066
				$variation_data = array_merge( get_post_custom( $variation_id ), wc_get_product_variation_attributes( $variation_id ) ); // kept for BW compatibility.
2067
				include 'admin/meta-boxes/views/html-variation-admin.php';
2068
				$loop++;
2069
			}
2070
		}
2071
		wp_die();
2072
	}
2073
2074
	/**
2075
	 * Save variations via AJAX.
2076
	 */
2077
	public static function save_variations() {
2078
		ob_start();
2079
2080
		check_ajax_referer( 'save-variations', 'security' );
2081
2082
		// Check permissions again and make sure we have what we need.
2083 View Code Duplication
		if ( ! current_user_can( 'edit_products' ) || empty( $_POST ) || empty( $_POST['product_id'] ) ) {
2084
			wp_die( -1 );
2085
		}
2086
2087
		$product_id                           = absint( $_POST['product_id'] );
2088
		WC_Admin_Meta_Boxes::$meta_box_errors = array();
2089
		WC_Meta_Box_Product_Data::save_variations( $product_id, get_post( $product_id ) );
2090
2091
		do_action( 'woocommerce_ajax_save_product_variations', $product_id );
2092
2093
		$errors = WC_Admin_Meta_Boxes::$meta_box_errors;
2094
2095
		if ( $errors ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $errors of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
2096
			echo '<div class="error notice is-dismissible">';
2097
2098
			foreach ( $errors as $error ) {
2099
				echo '<p>' . wp_kses_post( $error ) . '</p>';
2100
			}
2101
2102
			echo '<button type="button" class="notice-dismiss"><span class="screen-reader-text">' . esc_html__( 'Dismiss this notice.', 'woocommerce' ) . '</span></button>';
2103
			echo '</div>';
2104
2105
			delete_option( 'woocommerce_meta_box_errors' );
2106
		}
2107
2108
		wp_die();
2109
	}
2110
2111
	/**
2112
	 * Bulk action - Toggle Enabled.
2113
	 *
2114
	 * @param array $variations List of variations.
2115
	 * @param array $data Data to set.
2116
	 *
2117
	 * @used-by bulk_edit_variations
2118
	 */
2119
	private static function variation_bulk_action_toggle_enabled( $variations, $data ) {
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
Unused Code introduced by
The parameter $data is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
2120
		foreach ( $variations as $variation_id ) {
2121
			$variation = wc_get_product( $variation_id );
2122
			$variation->set_status( 'private' === $variation->get_status( 'edit' ) ? 'publish' : 'private' );
2123
			$variation->save();
2124
		}
2125
	}
2126
2127
	/**
2128
	 * Bulk action - Toggle Downloadable Checkbox.
2129
	 *
2130
	 * @param array $variations List of variations.
2131
	 * @param array $data Data to set.
2132
	 *
2133
	 * @used-by bulk_edit_variations
2134
	 */
2135
	private static function variation_bulk_action_toggle_downloadable( $variations, $data ) {
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
Unused Code introduced by
The parameter $data is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
2136
		self::variation_bulk_toggle( $variations, 'downloadable' );
2137
	}
2138
2139
	/**
2140
	 * Bulk action - Toggle Virtual Checkbox.
2141
	 *
2142
	 * @param array $variations List of variations.
2143
	 * @param array $data Data to set.
2144
	 *
2145
	 * @used-by bulk_edit_variations
2146
	 */
2147
	private static function variation_bulk_action_toggle_virtual( $variations, $data ) {
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
Unused Code introduced by
The parameter $data is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
2148
		self::variation_bulk_toggle( $variations, 'virtual' );
2149
	}
2150
2151
	/**
2152
	 * Bulk action - Toggle Manage Stock Checkbox.
2153
	 *
2154
	 * @param array $variations List of variations.
2155
	 * @param array $data Data to set.
2156
	 *
2157
	 * @used-by bulk_edit_variations
2158
	 */
2159
	private static function variation_bulk_action_toggle_manage_stock( $variations, $data ) {
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
Unused Code introduced by
The parameter $data is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
2160
		self::variation_bulk_toggle( $variations, 'manage_stock' );
2161
	}
2162
2163
	/**
2164
	 * Bulk action - Set Regular Prices.
2165
	 *
2166
	 * @param array $variations List of variations.
2167
	 * @param array $data Data to set.
2168
	 *
2169
	 * @used-by bulk_edit_variations
2170
	 */
2171
	private static function variation_bulk_action_variable_regular_price( $variations, $data ) {
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
2172
		self::variation_bulk_set( $variations, 'regular_price', $data['value'] );
2173
	}
2174
2175
	/**
2176
	 * Bulk action - Set Sale Prices.
2177
	 *
2178
	 * @param array $variations List of variations.
2179
	 * @param array $data Data to set.
2180
	 *
2181
	 * @used-by bulk_edit_variations
2182
	 */
2183
	private static function variation_bulk_action_variable_sale_price( $variations, $data ) {
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
2184
		self::variation_bulk_set( $variations, 'sale_price', $data['value'] );
2185
	}
2186
2187
	/**
2188
	 * Bulk action - Set Stock Status as In Stock.
2189
	 *
2190
	 * @param array $variations List of variations.
2191
	 * @param array $data Data to set.
2192
	 *
2193
	 * @used-by bulk_edit_variations
2194
	 */
2195
	private static function variation_bulk_action_variable_stock_status_instock( $variations, $data ) {
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
Unused Code introduced by
The parameter $data is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
2196
		self::variation_bulk_set( $variations, 'stock_status', 'instock' );
2197
	}
2198
2199
	/**
2200
	 * Bulk action - Set Stock Status as Out of Stock.
2201
	 *
2202
	 * @param array $variations List of variations.
2203
	 * @param array $data Data to set.
2204
	 *
2205
	 * @used-by bulk_edit_variations
2206
	 */
2207
	private static function variation_bulk_action_variable_stock_status_outofstock( $variations, $data ) {
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
Unused Code introduced by
The parameter $data is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
2208
		self::variation_bulk_set( $variations, 'stock_status', 'outofstock' );
2209
	}
2210
2211
	/**
2212
	 * Bulk action - Set Stock Status as On Backorder.
2213
	 *
2214
	 * @param array $variations List of variations.
2215
	 * @param array $data Data to set.
2216
	 *
2217
	 * @used-by bulk_edit_variations
2218
	 */
2219
	private static function variation_bulk_action_variable_stock_status_onbackorder( $variations, $data ) {
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
Unused Code introduced by
The parameter $data is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
2220
		self::variation_bulk_set( $variations, 'stock_status', 'onbackorder' );
2221
	}
2222
2223
	/**
2224
	 * Bulk action - Set Stock.
2225
	 *
2226
	 * @param array $variations List of variations.
2227
	 * @param array $data Data to set.
2228
	 *
2229
	 * @used-by bulk_edit_variations
2230
	 */
2231
	private static function variation_bulk_action_variable_stock( $variations, $data ) {
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
2232
		if ( ! isset( $data['value'] ) ) {
2233
			return;
2234
		}
2235
2236
		$quantity = wc_stock_amount( wc_clean( $data['value'] ) );
0 ignored issues
show
Documentation introduced by
wc_clean($data['value']) is of type string|array, but the function expects a integer|double.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2237
2238
		foreach ( $variations as $variation_id ) {
2239
			$variation = wc_get_product( $variation_id );
2240
			if ( $variation->managing_stock() ) {
2241
				$variation->set_stock_quantity( $quantity );
2242
			} else {
2243
				$variation->set_stock_quantity( null );
2244
			}
2245
			$variation->save();
2246
		}
2247
	}
2248
2249
	/**
2250
	 * Bulk action - Set Weight.
2251
	 *
2252
	 * @param array $variations List of variations.
2253
	 * @param array $data Data to set.
2254
	 *
2255
	 * @used-by bulk_edit_variations
2256
	 */
2257
	private static function variation_bulk_action_variable_weight( $variations, $data ) {
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
2258
		self::variation_bulk_set( $variations, 'weight', $data['value'] );
2259
	}
2260
2261
	/**
2262
	 * Bulk action - Set Length.
2263
	 *
2264
	 * @param array $variations List of variations.
2265
	 * @param array $data Data to set.
2266
	 *
2267
	 * @used-by bulk_edit_variations
2268
	 */
2269
	private static function variation_bulk_action_variable_length( $variations, $data ) {
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
2270
		self::variation_bulk_set( $variations, 'length', $data['value'] );
2271
	}
2272
2273
	/**
2274
	 * Bulk action - Set Width.
2275
	 *
2276
	 * @param array $variations List of variations.
2277
	 * @param array $data Data to set.
2278
	 *
2279
	 * @used-by bulk_edit_variations
2280
	 */
2281
	private static function variation_bulk_action_variable_width( $variations, $data ) {
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
2282
		self::variation_bulk_set( $variations, 'width', $data['value'] );
2283
	}
2284
2285
	/**
2286
	 * Bulk action - Set Height.
2287
	 *
2288
	 * @param array $variations List of variations.
2289
	 * @param array $data Data to set.
2290
	 *
2291
	 * @used-by bulk_edit_variations
2292
	 */
2293
	private static function variation_bulk_action_variable_height( $variations, $data ) {
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
2294
		self::variation_bulk_set( $variations, 'height', $data['value'] );
2295
	}
2296
2297
	/**
2298
	 * Bulk action - Set Download Limit.
2299
	 *
2300
	 * @param array $variations List of variations.
2301
	 * @param array $data Data to set.
2302
	 *
2303
	 * @used-by bulk_edit_variations
2304
	 */
2305
	private static function variation_bulk_action_variable_download_limit( $variations, $data ) {
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
2306
		self::variation_bulk_set( $variations, 'download_limit', $data['value'] );
2307
	}
2308
2309
	/**
2310
	 * Bulk action - Set Download Expiry.
2311
	 *
2312
	 * @param array $variations List of variations.
2313
	 * @param array $data Data to set.
2314
	 *
2315
	 * @used-by bulk_edit_variations
2316
	 */
2317
	private static function variation_bulk_action_variable_download_expiry( $variations, $data ) {
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
2318
		self::variation_bulk_set( $variations, 'download_expiry', $data['value'] );
2319
	}
2320
2321
	/**
2322
	 * Bulk action - Delete all.
2323
	 *
2324
	 * @param array $variations List of variations.
2325
	 * @param array $data Data to set.
2326
	 *
2327
	 * @used-by bulk_edit_variations
2328
	 */
2329
	private static function variation_bulk_action_delete_all( $variations, $data ) {
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
2330
		if ( isset( $data['allowed'] ) && 'true' === $data['allowed'] ) {
2331
			foreach ( $variations as $variation_id ) {
2332
				$variation = wc_get_product( $variation_id );
2333
				$variation->delete( true );
2334
			}
2335
		}
2336
	}
2337
2338
	/**
2339
	 * Bulk action - Sale Schedule.
2340
	 *
2341
	 * @param array $variations List of variations.
2342
	 * @param array $data Data to set.
2343
	 *
2344
	 * @used-by bulk_edit_variations
2345
	 */
2346
	private static function variation_bulk_action_variable_sale_schedule( $variations, $data ) {
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
2347
		if ( ! isset( $data['date_from'] ) && ! isset( $data['date_to'] ) ) {
2348
			return;
2349
		}
2350
2351
		foreach ( $variations as $variation_id ) {
2352
			$variation = wc_get_product( $variation_id );
2353
2354
			if ( 'false' !== $data['date_from'] ) {
2355
				$variation->set_date_on_sale_from( wc_clean( $data['date_from'] ) );
2356
			}
2357
2358
			if ( 'false' !== $data['date_to'] ) {
2359
				$variation->set_date_on_sale_to( wc_clean( $data['date_to'] ) );
2360
			}
2361
2362
			$variation->save();
2363
		}
2364
	}
2365
2366
	/**
2367
	 * Bulk action - Increase Regular Prices.
2368
	 *
2369
	 * @param array $variations List of variations.
2370
	 * @param array $data Data to set.
2371
	 *
2372
	 * @used-by bulk_edit_variations
2373
	 */
2374
	private static function variation_bulk_action_variable_regular_price_increase( $variations, $data ) {
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
2375
		self::variation_bulk_adjust_price( $variations, 'regular_price', '+', wc_clean( $data['value'] ) );
0 ignored issues
show
Bug introduced by
It seems like wc_clean($data['value']) targeting wc_clean() can also be of type array; however, WC_AJAX::variation_bulk_adjust_price() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
2376
	}
2377
2378
	/**
2379
	 * Bulk action - Decrease Regular Prices.
2380
	 *
2381
	 * @param array $variations List of variations.
2382
	 * @param array $data Data to set.
2383
	 *
2384
	 * @used-by bulk_edit_variations
2385
	 */
2386
	private static function variation_bulk_action_variable_regular_price_decrease( $variations, $data ) {
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
2387
		self::variation_bulk_adjust_price( $variations, 'regular_price', '-', wc_clean( $data['value'] ) );
0 ignored issues
show
Bug introduced by
It seems like wc_clean($data['value']) targeting wc_clean() can also be of type array; however, WC_AJAX::variation_bulk_adjust_price() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
2388
	}
2389
2390
	/**
2391
	 * Bulk action - Increase Sale Prices.
2392
	 *
2393
	 * @param array $variations List of variations.
2394
	 * @param array $data Data to set.
2395
	 *
2396
	 * @used-by bulk_edit_variations
2397
	 */
2398
	private static function variation_bulk_action_variable_sale_price_increase( $variations, $data ) {
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
2399
		self::variation_bulk_adjust_price( $variations, 'sale_price', '+', wc_clean( $data['value'] ) );
0 ignored issues
show
Bug introduced by
It seems like wc_clean($data['value']) targeting wc_clean() can also be of type array; however, WC_AJAX::variation_bulk_adjust_price() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
2400
	}
2401
2402
	/**
2403
	 * Bulk action - Decrease Sale Prices.
2404
	 *
2405
	 * @param array $variations List of variations.
2406
	 * @param array $data Data to set.
2407
	 *
2408
	 * @used-by bulk_edit_variations
2409
	 */
2410
	private static function variation_bulk_action_variable_sale_price_decrease( $variations, $data ) {
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
2411
		self::variation_bulk_adjust_price( $variations, 'sale_price', '-', wc_clean( $data['value'] ) );
0 ignored issues
show
Bug introduced by
It seems like wc_clean($data['value']) targeting wc_clean() can also be of type array; however, WC_AJAX::variation_bulk_adjust_price() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
2412
	}
2413
2414
	/**
2415
	 * Bulk action - Set Price.
2416
	 *
2417
	 * @param array  $variations List of variations.
2418
	 * @param string $field price being adjusted _regular_price or _sale_price.
2419
	 * @param string $operator + or -.
2420
	 * @param string $value Price or Percent.
2421
	 *
2422
	 * @used-by bulk_edit_variations
2423
	 */
2424
	private static function variation_bulk_adjust_price( $variations, $field, $operator, $value ) {
2425
		foreach ( $variations as $variation_id ) {
2426
			$variation   = wc_get_product( $variation_id );
2427
			$field_value = $variation->{"get_$field"}( 'edit' );
2428
2429
			if ( '%' === substr( $value, -1 ) ) {
2430
				$percent      = wc_format_decimal( substr( $value, 0, -1 ) );
2431
				$field_value += round( ( $field_value / 100 ) * $percent, wc_get_price_decimals() ) * "{$operator}1";
2432
			} else {
2433
				$field_value += $value * "{$operator}1";
2434
			}
2435
2436
			$variation->{"set_$field"}( $field_value );
2437
			$variation->save();
2438
		}
2439
	}
2440
2441
	/**
2442
	 * Bulk set convenience function.
2443
	 *
2444
	 * @param array  $variations List of variations.
2445
	 * @param string $field Field to set.
2446
	 * @param string $value to set.
2447
	 */
2448
	private static function variation_bulk_set( $variations, $field, $value ) {
2449
		foreach ( $variations as $variation_id ) {
2450
			$variation = wc_get_product( $variation_id );
2451
			$variation->{ "set_$field" }( wc_clean( $value ) );
2452
			$variation->save();
2453
		}
2454
	}
2455
2456
	/**
2457
	 * Bulk toggle convenience function.
2458
	 *
2459
	 * @param array  $variations List of variations.
2460
	 * @param string $field Field to toggle.
2461
	 */
2462
	private static function variation_bulk_toggle( $variations, $field ) {
2463
		foreach ( $variations as $variation_id ) {
2464
			$variation  = wc_get_product( $variation_id );
2465
			$prev_value = $variation->{ "get_$field" }( 'edit' );
2466
			$variation->{ "set_$field" }( ! $prev_value );
2467
			$variation->save();
2468
		}
2469
	}
2470
2471
	/**
2472
	 * Bulk edit variations via AJAX.
2473
	 *
2474
	 * @uses WC_AJAX::variation_bulk_set()
2475
	 * @uses WC_AJAX::variation_bulk_adjust_price()
2476
	 * @uses WC_AJAX::variation_bulk_action_variable_sale_price_decrease()
2477
	 * @uses WC_AJAX::variation_bulk_action_variable_sale_price_increase()
2478
	 * @uses WC_AJAX::variation_bulk_action_variable_regular_price_decrease()
2479
	 * @uses WC_AJAX::variation_bulk_action_variable_regular_price_increase()
2480
	 * @uses WC_AJAX::variation_bulk_action_variable_sale_schedule()
2481
	 * @uses WC_AJAX::variation_bulk_action_delete_all()
2482
	 * @uses WC_AJAX::variation_bulk_action_variable_download_expiry()
2483
	 * @uses WC_AJAX::variation_bulk_action_variable_download_limit()
2484
	 * @uses WC_AJAX::variation_bulk_action_variable_height()
2485
	 * @uses WC_AJAX::variation_bulk_action_variable_width()
2486
	 * @uses WC_AJAX::variation_bulk_action_variable_length()
2487
	 * @uses WC_AJAX::variation_bulk_action_variable_weight()
2488
	 * @uses WC_AJAX::variation_bulk_action_variable_stock()
2489
	 * @uses WC_AJAX::variation_bulk_action_variable_sale_price()
2490
	 * @uses WC_AJAX::variation_bulk_action_variable_regular_price()
2491
	 * @uses WC_AJAX::variation_bulk_action_toggle_manage_stock()
2492
	 * @uses WC_AJAX::variation_bulk_action_toggle_virtual()
2493
	 * @uses WC_AJAX::variation_bulk_action_toggle_downloadable()
2494
	 * @uses WC_AJAX::variation_bulk_action_toggle_enabled
2495
	 */
2496
	public static function bulk_edit_variations() {
2497
		ob_start();
2498
2499
		check_ajax_referer( 'bulk-edit-variations', 'security' );
2500
2501
		// Check permissions again and make sure we have what we need.
2502 View Code Duplication
		if ( ! current_user_can( 'edit_products' ) || empty( $_POST['product_id'] ) || empty( $_POST['bulk_action'] ) ) {
2503
			wp_die( -1 );
2504
		}
2505
2506
		$product_id  = absint( $_POST['product_id'] );
2507
		$bulk_action = wc_clean( wp_unslash( $_POST['bulk_action'] ) );
2508
		$data        = ! empty( $_POST['data'] ) ? wc_clean( wp_unslash( $_POST['data'] ) ) : array();
2509
		$variations  = array();
2510
2511
		if ( apply_filters( 'woocommerce_bulk_edit_variations_need_children', true ) ) {
2512
			$variations = get_posts(
2513
				array(
2514
					'post_parent'    => $product_id,
2515
					'posts_per_page' => -1,
2516
					'post_type'      => 'product_variation',
2517
					'fields'         => 'ids',
2518
					'post_status'    => array( 'publish', 'private' ),
2519
				)
2520
			);
2521
		}
2522
2523
		if ( method_exists( __CLASS__, "variation_bulk_action_$bulk_action" ) ) {
2524
			call_user_func( array( __CLASS__, "variation_bulk_action_$bulk_action" ), $variations, $data );
2525
		} else {
2526
			do_action( 'woocommerce_bulk_edit_variations_default', $bulk_action, $data, $product_id, $variations );
2527
		}
2528
2529
		do_action( 'woocommerce_bulk_edit_variations', $bulk_action, $data, $product_id, $variations );
2530
		WC_Product_Variable::sync( $product_id );
2531
		wc_delete_product_transients( $product_id );
2532
		wp_die();
2533
	}
2534
2535
	/**
2536
	 * Handle submissions from assets/js/settings-views-html-settings-tax.js Backbone model.
2537
	 */
2538
	public static function tax_rates_save_changes() {
2539
		// phpcs:disable WordPress.Security.NonceVerification.NoNonceVerification
2540
		if ( ! isset( $_POST['wc_tax_nonce'], $_POST['changes'] ) ) {
2541
			wp_send_json_error( 'missing_fields' );
2542
			wp_die();
2543
		}
2544
2545
		$current_class = ! empty( $_POST['current_class'] ) ? wp_unslash( $_POST['current_class'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
2546
2547
		if ( ! wp_verify_nonce( wp_unslash( $_POST['wc_tax_nonce'] ), 'wc_tax_nonce-class:' . $current_class ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
2548
			wp_send_json_error( 'bad_nonce' );
2549
			wp_die();
2550
		}
2551
2552
		$current_class = WC_Tax::format_tax_rate_class( $current_class );
2553
2554
		// Check User Caps.
2555
		if ( ! current_user_can( 'manage_woocommerce' ) ) {
2556
			wp_send_json_error( 'missing_capabilities' );
2557
			wp_die();
2558
		}
2559
2560
		$changes = wp_unslash( $_POST['changes'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
2561
		foreach ( $changes as $tax_rate_id => $data ) {
2562
			if ( isset( $data['deleted'] ) ) {
2563
				if ( isset( $data['newRow'] ) ) {
2564
					// So the user added and deleted a new row.
2565
					// That's fine, it's not in the database anyways. NEXT!
2566
					continue;
2567
				}
2568
				WC_Tax::_delete_tax_rate( $tax_rate_id );
2569
			}
2570
2571
			$tax_rate = array_intersect_key(
2572
				$data,
2573
				array(
2574
					'tax_rate_country'  => 1,
2575
					'tax_rate_state'    => 1,
2576
					'tax_rate'          => 1,
2577
					'tax_rate_name'     => 1,
2578
					'tax_rate_priority' => 1,
2579
					'tax_rate_compound' => 1,
2580
					'tax_rate_shipping' => 1,
2581
					'tax_rate_order'    => 1,
2582
				)
2583
			);
2584
2585
			if ( isset( $tax_rate['tax_rate'] ) ) {
2586
				$tax_rate['tax_rate'] = wc_format_decimal( $tax_rate['tax_rate'] );
2587
			}
2588
2589
			if ( isset( $data['newRow'] ) ) {
2590
				$tax_rate['tax_rate_class'] = $current_class;
2591
				$tax_rate_id                = WC_Tax::_insert_tax_rate( $tax_rate );
2592
			} elseif ( ! empty( $tax_rate ) ) {
2593
				WC_Tax::_update_tax_rate( $tax_rate_id, $tax_rate );
2594
			}
2595
2596
			if ( isset( $data['postcode'] ) ) {
2597
				$postcode = array_map( 'wc_clean', $data['postcode'] );
2598
				$postcode = array_map( 'wc_normalize_postcode', $postcode );
2599
				WC_Tax::_update_tax_rate_postcodes( $tax_rate_id, $postcode );
0 ignored issues
show
Documentation introduced by
$postcode is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2600
			}
2601
			if ( isset( $data['city'] ) ) {
2602
				WC_Tax::_update_tax_rate_cities( $tax_rate_id, array_map( 'wc_clean', array_map( 'wp_unslash', $data['city'] ) ) );
0 ignored issues
show
Documentation introduced by
array_map('wc_clean', ar...slash', $data['city'])) is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2603
			}
2604
		}
2605
2606
		WC_Cache_Helper::incr_cache_prefix( 'taxes' );
2607
		WC_Cache_Helper::get_transient_version( 'shipping', true );
2608
2609
		wp_send_json_success(
2610
			array(
2611
				'rates' => WC_Tax::get_rates_for_tax_class( $current_class ),
2612
			)
2613
		);
2614
		// phpcs:enable
2615
	}
2616
2617
	/**
2618
	 * Handle submissions from assets/js/wc-shipping-zones.js Backbone model.
2619
	 */
2620
	public static function shipping_zones_save_changes() {
2621
		if ( ! isset( $_POST['wc_shipping_zones_nonce'], $_POST['changes'] ) ) {
2622
			wp_send_json_error( 'missing_fields' );
2623
			wp_die();
2624
		}
2625
2626
		if ( ! wp_verify_nonce( wp_unslash( $_POST['wc_shipping_zones_nonce'] ), 'wc_shipping_zones_nonce' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
2627
			wp_send_json_error( 'bad_nonce' );
2628
			wp_die();
2629
		}
2630
2631
		// Check User Caps.
2632
		if ( ! current_user_can( 'manage_woocommerce' ) ) {
2633
			wp_send_json_error( 'missing_capabilities' );
2634
			wp_die();
2635
		}
2636
2637
		$changes = wp_unslash( $_POST['changes'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
2638
		foreach ( $changes as $zone_id => $data ) {
2639
			if ( isset( $data['deleted'] ) ) {
2640
				if ( isset( $data['newRow'] ) ) {
2641
					// So the user added and deleted a new row.
2642
					// That's fine, it's not in the database anyways. NEXT!
2643
					continue;
2644
				}
2645
				WC_Shipping_Zones::delete_zone( $zone_id );
2646
				continue;
2647
			}
2648
2649
			$zone_data = array_intersect_key(
2650
				$data,
2651
				array(
2652
					'zone_id'    => 1,
2653
					'zone_order' => 1,
2654
				)
2655
			);
2656
2657
			if ( isset( $zone_data['zone_id'] ) ) {
2658
				$zone = new WC_Shipping_Zone( $zone_data['zone_id'] );
2659
2660
				if ( isset( $zone_data['zone_order'] ) ) {
2661
					$zone->set_zone_order( $zone_data['zone_order'] );
2662
				}
2663
2664
				$zone->save();
2665
			}
2666
		}
2667
2668
		wp_send_json_success(
2669
			array(
2670
				'zones' => WC_Shipping_Zones::get_zones( 'json' ),
2671
			)
2672
		);
2673
	}
2674
2675
	/**
2676
	 * Handle submissions from assets/js/wc-shipping-zone-methods.js Backbone model.
2677
	 */
2678
	public static function shipping_zone_add_method() {
2679 View Code Duplication
		if ( ! isset( $_POST['wc_shipping_zones_nonce'], $_POST['zone_id'], $_POST['method_id'] ) ) {
2680
			wp_send_json_error( 'missing_fields' );
2681
			wp_die();
2682
		}
2683
2684
		if ( ! wp_verify_nonce( wp_unslash( $_POST['wc_shipping_zones_nonce'] ), 'wc_shipping_zones_nonce' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
2685
			wp_send_json_error( 'bad_nonce' );
2686
			wp_die();
2687
		}
2688
2689
		// Check User Caps.
2690
		if ( ! current_user_can( 'manage_woocommerce' ) ) {
2691
			wp_send_json_error( 'missing_capabilities' );
2692
			wp_die();
2693
		}
2694
2695
		$zone_id     = wc_clean( wp_unslash( $_POST['zone_id'] ) );
2696
		$zone        = new WC_Shipping_Zone( $zone_id );
2697
		$instance_id = $zone->add_shipping_method( wc_clean( wp_unslash( $_POST['method_id'] ) ) );
2698
2699
		wp_send_json_success(
2700
			array(
2701
				'instance_id' => $instance_id,
2702
				'zone_id'     => $zone->get_id(),
2703
				'zone_name'   => $zone->get_zone_name(),
2704
				'methods'     => $zone->get_shipping_methods( false, 'json' ),
2705
			)
2706
		);
2707
	}
2708
2709
	/**
2710
	 * Handle submissions from assets/js/wc-shipping-zone-methods.js Backbone model.
2711
	 */
2712
	public static function shipping_zone_methods_save_changes() {
2713 View Code Duplication
		if ( ! isset( $_POST['wc_shipping_zones_nonce'], $_POST['zone_id'], $_POST['changes'] ) ) {
2714
			wp_send_json_error( 'missing_fields' );
2715
			wp_die();
2716
		}
2717
2718
		if ( ! wp_verify_nonce( wp_unslash( $_POST['wc_shipping_zones_nonce'] ), 'wc_shipping_zones_nonce' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
2719
			wp_send_json_error( 'bad_nonce' );
2720
			wp_die();
2721
		}
2722
2723
		if ( ! current_user_can( 'manage_woocommerce' ) ) {
2724
			wp_send_json_error( 'missing_capabilities' );
2725
			wp_die();
2726
		}
2727
2728
		global $wpdb;
2729
2730
		$zone_id = wc_clean( wp_unslash( $_POST['zone_id'] ) );
2731
		$zone    = new WC_Shipping_Zone( $zone_id );
2732
		$changes = wp_unslash( $_POST['changes'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
2733
2734
		if ( isset( $changes['zone_name'] ) ) {
2735
			$zone->set_zone_name( wc_clean( $changes['zone_name'] ) );
2736
		}
2737
2738
		if ( isset( $changes['zone_locations'] ) ) {
2739
			$zone->clear_locations( array( 'state', 'country', 'continent' ) );
2740
			$locations = array_filter( array_map( 'wc_clean', (array) $changes['zone_locations'] ) );
2741
			foreach ( $locations as $location ) {
2742
				// Each posted location will be in the format type:code.
2743
				$location_parts = explode( ':', $location );
2744
				switch ( $location_parts[0] ) {
2745
					case 'state':
2746
						$zone->add_location( $location_parts[1] . ':' . $location_parts[2], 'state' );
2747
						break;
2748
					case 'country':
2749
						$zone->add_location( $location_parts[1], 'country' );
2750
						break;
2751
					case 'continent':
2752
						$zone->add_location( $location_parts[1], 'continent' );
2753
						break;
2754
				}
2755
			}
2756
		}
2757
2758
		if ( isset( $changes['zone_postcodes'] ) ) {
2759
			$zone->clear_locations( 'postcode' );
2760
			$postcodes = array_filter( array_map( 'strtoupper', array_map( 'wc_clean', explode( "\n", $changes['zone_postcodes'] ) ) ) );
2761
			foreach ( $postcodes as $postcode ) {
2762
				$zone->add_location( $postcode, 'postcode' );
2763
			}
2764
		}
2765
2766
		if ( isset( $changes['methods'] ) ) {
2767
			foreach ( $changes['methods'] as $instance_id => $data ) {
2768
				$method_id = $wpdb->get_var( $wpdb->prepare( "SELECT method_id FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE instance_id = %d", $instance_id ) );
2769
2770
				if ( isset( $data['deleted'] ) ) {
2771
					$shipping_method = WC_Shipping_Zones::get_shipping_method( $instance_id );
2772
					$option_key      = $shipping_method->get_instance_option_key();
2773
					if ( $wpdb->delete( "{$wpdb->prefix}woocommerce_shipping_zone_methods", array( 'instance_id' => $instance_id ) ) ) {
2774
						delete_option( $option_key );
2775
						do_action( 'woocommerce_shipping_zone_method_deleted', $instance_id, $method_id, $zone_id );
2776
					}
2777
					continue;
2778
				}
2779
2780
				$method_data = array_intersect_key(
2781
					$data,
2782
					array(
2783
						'method_order' => 1,
2784
						'enabled'      => 1,
2785
					)
2786
				);
2787
2788
				if ( isset( $method_data['method_order'] ) ) {
2789
					$wpdb->update( "{$wpdb->prefix}woocommerce_shipping_zone_methods", array( 'method_order' => absint( $method_data['method_order'] ) ), array( 'instance_id' => absint( $instance_id ) ) );
2790
				}
2791
2792
				if ( isset( $method_data['enabled'] ) ) {
2793
					$is_enabled = absint( 'yes' === $method_data['enabled'] );
2794
					if ( $wpdb->update( "{$wpdb->prefix}woocommerce_shipping_zone_methods", array( 'is_enabled' => $is_enabled ), array( 'instance_id' => absint( $instance_id ) ) ) ) {
2795
						do_action( 'woocommerce_shipping_zone_method_status_toggled', $instance_id, $method_id, $zone_id, $is_enabled );
2796
					}
2797
				}
2798
			}
2799
		}
2800
2801
		$zone->save();
2802
2803
		wp_send_json_success(
2804
			array(
2805
				'zone_id'   => $zone->get_id(),
2806
				'zone_name' => $zone->get_zone_name(),
2807
				'methods'   => $zone->get_shipping_methods( false, 'json' ),
2808
			)
2809
		);
2810
	}
2811
2812
	/**
2813
	 * Save method settings
2814
	 */
2815
	public static function shipping_zone_methods_save_settings() {
2816
		if ( ! isset( $_POST['wc_shipping_zones_nonce'], $_POST['instance_id'], $_POST['data'] ) ) {
2817
			wp_send_json_error( 'missing_fields' );
2818
			wp_die();
2819
		}
2820
2821
		if ( ! wp_verify_nonce( wp_unslash( $_POST['wc_shipping_zones_nonce'] ), 'wc_shipping_zones_nonce' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
2822
			wp_send_json_error( 'bad_nonce' );
2823
			wp_die();
2824
		}
2825
2826
		if ( ! current_user_can( 'manage_woocommerce' ) ) {
2827
			wp_send_json_error( 'missing_capabilities' );
2828
			wp_die();
2829
		}
2830
2831
		$instance_id     = absint( $_POST['instance_id'] );
2832
		$zone            = WC_Shipping_Zones::get_zone_by( 'instance_id', $instance_id );
2833
		$shipping_method = WC_Shipping_Zones::get_shipping_method( $instance_id );
2834
		$shipping_method->set_post_data( wp_unslash( $_POST['data'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
2835
		$shipping_method->process_admin_options();
2836
2837
		WC_Cache_Helper::get_transient_version( 'shipping', true );
2838
2839
		wp_send_json_success(
2840
			array(
2841
				'zone_id'   => $zone->get_id(),
2842
				'zone_name' => $zone->get_zone_name(),
2843
				'methods'   => $zone->get_shipping_methods( false, 'json' ),
2844
				'errors'    => $shipping_method->get_errors(),
2845
			)
2846
		);
2847
	}
2848
2849
	/**
2850
	 * Handle submissions from assets/js/wc-shipping-classes.js Backbone model.
2851
	 */
2852
	public static function shipping_classes_save_changes() {
2853
		if ( ! isset( $_POST['wc_shipping_classes_nonce'], $_POST['changes'] ) ) {
2854
			wp_send_json_error( 'missing_fields' );
2855
			wp_die();
2856
		}
2857
2858
		if ( ! wp_verify_nonce( wp_unslash( $_POST['wc_shipping_classes_nonce'] ), 'wc_shipping_classes_nonce' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
2859
			wp_send_json_error( 'bad_nonce' );
2860
			wp_die();
2861
		}
2862
2863
		if ( ! current_user_can( 'manage_woocommerce' ) ) {
2864
			wp_send_json_error( 'missing_capabilities' );
2865
			wp_die();
2866
		}
2867
2868
		$changes = wp_unslash( $_POST['changes'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
2869
2870
		foreach ( $changes as $term_id => $data ) {
2871
			$term_id = absint( $term_id );
2872
2873
			if ( isset( $data['deleted'] ) ) {
2874
				if ( isset( $data['newRow'] ) ) {
2875
					// So the user added and deleted a new row.
2876
					// That's fine, it's not in the database anyways. NEXT!
2877
					continue;
2878
				}
2879
				wp_delete_term( $term_id, 'product_shipping_class' );
2880
				continue;
2881
			}
2882
2883
			$update_args = array();
2884
2885
			if ( isset( $data['name'] ) ) {
2886
				$update_args['name'] = wc_clean( $data['name'] );
2887
			}
2888
2889
			if ( isset( $data['slug'] ) ) {
2890
				$update_args['slug'] = wc_clean( $data['slug'] );
2891
			}
2892
2893
			if ( isset( $data['description'] ) ) {
2894
				$update_args['description'] = wc_clean( $data['description'] );
2895
			}
2896
2897
			if ( isset( $data['newRow'] ) ) {
2898
				$update_args = array_filter( $update_args );
2899
				if ( empty( $update_args['name'] ) ) {
2900
					continue;
2901
				}
2902
				$inserted_term = wp_insert_term( $update_args['name'], 'product_shipping_class', $update_args );
2903
				$term_id       = is_wp_error( $inserted_term ) ? 0 : $inserted_term['term_id'];
2904
			} else {
2905
				wp_update_term( $term_id, 'product_shipping_class', $update_args );
2906
			}
2907
2908
			do_action( 'woocommerce_shipping_classes_save_class', $term_id, $data );
2909
		}
2910
2911
		$wc_shipping = WC_Shipping::instance();
2912
2913
		wp_send_json_success(
2914
			array(
2915
				'shipping_classes' => $wc_shipping->get_shipping_classes(),
2916
			)
2917
		);
2918
	}
2919
2920
	/**
2921
	 * Toggle payment gateway on or off via AJAX.
2922
	 *
2923
	 * @since 3.4.0
2924
	 */
2925
	public static function toggle_gateway_enabled() {
2926
		if ( current_user_can( 'manage_woocommerce' ) && check_ajax_referer( 'woocommerce-toggle-payment-gateway-enabled', 'security' ) && isset( $_POST['gateway_id'] ) ) {
2927
			// Load gateways.
2928
			$payment_gateways = WC()->payment_gateways->payment_gateways();
2929
2930
			// Get posted gateway.
2931
			$gateway_id = wc_clean( wp_unslash( $_POST['gateway_id'] ) );
2932
2933
			foreach ( $payment_gateways as $gateway ) {
2934
				if ( ! in_array( $gateway_id, array( $gateway->id, sanitize_title( get_class( $gateway ) ) ), true ) ) {
2935
					continue;
2936
				}
2937
				$enabled = $gateway->get_option( 'enabled', 'no' );
2938
2939
				if ( ! wc_string_to_bool( $enabled ) ) {
2940
					if ( $gateway->needs_setup() ) {
2941
						wp_send_json_error( 'needs_setup' );
2942
						wp_die();
2943
					} else {
2944
						$gateway->update_option( 'enabled', 'yes' );
2945
					}
2946
				} else {
2947
					// Disable the gateway.
2948
					$gateway->update_option( 'enabled', 'no' );
2949
				}
2950
2951
				wp_send_json_success( ! wc_string_to_bool( $enabled ) );
2952
				wp_die();
2953
			}
2954
		}
2955
2956
		wp_send_json_error( 'invalid_gateway_id' );
2957
		wp_die();
2958
	}
2959
}
2960
2961
WC_AJAX::init();
2962