Issues (942)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

includes/class-wc-discounts.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * Discount calculation
4
 *
5
 * @package WooCommerce/Classes
6
 * @since   3.2.0
7
 */
8
9
defined( 'ABSPATH' ) || exit;
10
11
/**
12
 * Discounts class.
13
 */
14
class WC_Discounts {
15
16
	/**
17
	 * Reference to cart or order object.
18
	 *
19
	 * @since 3.2.0
20
	 * @var WC_Cart|WC_Order
21
	 */
22
	protected $object;
23
24
	/**
25
	 * An array of items to discount.
26
	 *
27
	 * @var array
28
	 */
29
	protected $items = array();
30
31
	/**
32
	 * An array of discounts which have been applied to items.
33
	 *
34
	 * @var array[] Code => Item Key => Value
35
	 */
36
	protected $discounts = array();
37
38
	/**
39
	 * WC_Discounts Constructor.
40
	 *
41
	 * @param WC_Cart|WC_Order $object Cart or order object.
42
	 */
43 103
	public function __construct( $object = null ) {
44 103
		if ( is_a( $object, 'WC_Cart' ) ) {
45 90
			$this->set_items_from_cart( $object );
46 45
		} elseif ( is_a( $object, 'WC_Order' ) ) {
47 12
			$this->set_items_from_order( $object );
48
		}
49
	}
50
51
	/**
52
	 * Set items directly. Used by WC_Cart_Totals.
53
	 *
54
	 * @since 3.2.3
55
	 * @param array $items Items to set.
56
	 */
57 82
	public function set_items( $items ) {
58 82
		$this->items     = $items;
59 82
		$this->discounts = array();
60 82
		uasort( $this->items, array( $this, 'sort_by_price' ) );
61
	}
62
63
	/**
64
	 * Normalise cart items which will be discounted.
65
	 *
66
	 * @since 3.2.0
67
	 * @param WC_Cart $cart Cart object.
68
	 */
69 90
	public function set_items_from_cart( $cart ) {
70 90
		$this->items     = array();
71 90
		$this->discounts = array();
72
73 90
		if ( ! is_a( $cart, 'WC_Cart' ) ) {
74 1
			return;
75
		}
76
77 90
		$this->object = $cart;
78
79 90
		foreach ( $cart->get_cart() as $key => $cart_item ) {
80 82
			$item                = new stdClass();
81 82
			$item->key           = $key;
82 82
			$item->object        = $cart_item;
83 82
			$item->product       = $cart_item['data'];
84 82
			$item->quantity      = $cart_item['quantity'];
85 82
			$item->price         = wc_add_number_precision_deep( $item->product->get_price() * $item->quantity );
86 82
			$this->items[ $key ] = $item;
87
		}
88
89 90
		uasort( $this->items, array( $this, 'sort_by_price' ) );
90
	}
91
92
	/**
93
	 * Normalise order items which will be discounted.
94
	 *
95
	 * @since 3.2.0
96
	 * @param WC_Order $order Order object.
97
	 */
98 12
	public function set_items_from_order( $order ) {
99 12
		$this->items     = array();
100 12
		$this->discounts = array();
101
102 12
		if ( ! is_a( $order, 'WC_Order' ) ) {
103
			return;
104
		}
105
106 12
		$this->object = $order;
107
108 12
		foreach ( $order->get_items() as $order_item ) {
109 12
			$item           = new stdClass();
110 12
			$item->key      = $order_item->get_id();
111 12
			$item->object   = $order_item;
112 12
			$item->product  = $order_item->get_product();
113 12
			$item->quantity = $order_item->get_quantity();
114 12
			$item->price    = wc_add_number_precision_deep( $order_item->get_subtotal() );
115
116 12
			if ( $order->get_prices_include_tax() ) {
117 4
				$item->price += wc_add_number_precision_deep( $order_item->get_subtotal_tax() );
118
			}
119
120 12
			$this->items[ $order_item->get_id() ] = $item;
121
		}
122
123 12
		uasort( $this->items, array( $this, 'sort_by_price' ) );
124
	}
125
126
	/**
127
	 * Get the object concerned.
128
	 *
129
	 * @since  3.3.2
130
	 * @return object
131
	 */
132
	public function get_object() {
133
		return $this->object;
134
	}
135
136
	/**
137
	 * Get items.
138
	 *
139
	 * @since  3.2.0
140
	 * @return object[]
141
	 */
142 72
	public function get_items() {
143 72
		return $this->items;
144
	}
145
146
	/**
147
	 * Get items to validate.
148
	 *
149
	 * @since  3.3.2
150
	 * @return object[]
151
	 */
152 71
	public function get_items_to_validate() {
153 71
		return apply_filters( 'woocommerce_coupon_get_items_to_validate', $this->get_items(), $this );
154
	}
155
156
	/**
157
	 * Get discount by key with or without precision.
158
	 *
159
	 * @since  3.2.0
160
	 * @param  string $key name of discount row to return.
161
	 * @param  bool   $in_cents Should the totals be returned in cents, or without precision.
162
	 * @return array
163
	 */
164 62
	public function get_discount( $key, $in_cents = false ) {
165 62
		$item_discount_totals = $this->get_discounts_by_item( $in_cents );
166 62
		return isset( $item_discount_totals[ $key ] ) ? $item_discount_totals[ $key ] : 0;
167
	}
168
169
	/**
170
	 * Get all discount totals.
171
	 *
172
	 * @since  3.2.0
173
	 * @param  bool $in_cents Should the totals be returned in cents, or without precision.
174
	 * @return array
175
	 */
176 65
	public function get_discounts( $in_cents = false ) {
177 65
		$discounts = $this->discounts;
178 65
		return $in_cents ? $discounts : wc_remove_number_precision_deep( $discounts );
179
	}
180
181
	/**
182
	 * Get all discount totals per item.
183
	 *
184
	 * @since  3.2.0
185
	 * @param  bool $in_cents Should the totals be returned in cents, or without precision.
186
	 * @return array
187
	 */
188 94
	public function get_discounts_by_item( $in_cents = false ) {
189 94
		$discounts            = $this->discounts;
190 94
		$item_discount_totals = (array) array_shift( $discounts );
191
192 94
		foreach ( $discounts as $item_discounts ) {
193 12
			foreach ( $item_discounts as $item_key => $item_discount ) {
194 12
				$item_discount_totals[ $item_key ] += $item_discount;
195
			}
196
		}
197
198 94
		return $in_cents ? $item_discount_totals : wc_remove_number_precision_deep( $item_discount_totals );
199
	}
200
201
	/**
202
	 * Get all discount totals per coupon.
203
	 *
204
	 * @since  3.2.0
205
	 * @param  bool $in_cents Should the totals be returned in cents, or without precision.
206
	 * @return array
207
	 */
208 94
	public function get_discounts_by_coupon( $in_cents = false ) {
209 94
		$coupon_discount_totals = array_map( 'array_sum', $this->discounts );
210
211 94
		return $in_cents ? $coupon_discount_totals : wc_remove_number_precision_deep( $coupon_discount_totals );
212
	}
213
214
	/**
215
	 * Get discounted price of an item without precision.
216
	 *
217
	 * @since  3.2.0
218
	 * @param  object $item Get data for this item.
219
	 * @return float
220
	 */
221 1
	public function get_discounted_price( $item ) {
222 1
		return wc_remove_number_precision_deep( $this->get_discounted_price_in_cents( $item ) );
223
	}
224
225
	/**
226
	 * Get discounted price of an item to precision (in cents).
227
	 *
228
	 * @since  3.2.0
229
	 * @param  object $item Get data for this item.
230
	 * @return int
231
	 */
232 62
	public function get_discounted_price_in_cents( $item ) {
233 62
		return absint( round( $item->price - $this->get_discount( $item->key, true ) ) );
234
	}
235
236
	/**
237
	 * Apply a discount to all items using a coupon.
238
	 *
239
	 * @since  3.2.0
240
	 * @param  WC_Coupon $coupon Coupon object being applied to the items.
241
	 * @param  bool      $validate Set to false to skip coupon validation.
242
	 * @throws Exception Error message when coupon isn't valid.
243
	 * @return bool|WP_Error True if applied or WP_Error instance in failure.
244
	 */
245 63
	public function apply_coupon( $coupon, $validate = true ) {
246 63
		if ( ! is_a( $coupon, 'WC_Coupon' ) ) {
247
			return new WP_Error( 'invalid_coupon', __( 'Invalid coupon', 'woocommerce' ) );
248
		}
249
250 63
		$is_coupon_valid = $validate ? $this->is_coupon_valid( $coupon ) : true;
251
252 63
		if ( is_wp_error( $is_coupon_valid ) ) {
253
			return $is_coupon_valid;
254
		}
255
256 63
		if ( ! isset( $this->discounts[ $coupon->get_code() ] ) ) {
257 63
			$this->discounts[ $coupon->get_code() ] = array_fill_keys( array_keys( $this->items ), 0 );
258
		}
259
260 63
		$items_to_apply = $this->get_items_to_apply_coupon( $coupon );
261
262
		// Core discounts are handled here as of 3.2.
263 63
		switch ( $coupon->get_discount_type() ) {
264
			case 'percent':
265 33
				$this->apply_coupon_percent( $coupon, $items_to_apply );
266 33
				break;
267
			case 'fixed_product':
268 16
				$this->apply_coupon_fixed_product( $coupon, $items_to_apply );
269 16
				break;
270
			case 'fixed_cart':
271 20
				$this->apply_coupon_fixed_cart( $coupon, $items_to_apply );
272 20
				break;
273
			default:
274 1
				$this->apply_coupon_custom( $coupon, $items_to_apply );
275 1
				break;
276
		}
277
278 63
		return true;
279
	}
280
281
	/**
282
	 * Sort by price.
283
	 *
284
	 * @since  3.2.0
285
	 * @param  array $a First element.
286
	 * @param  array $b Second element.
287
	 * @return int
288
	 */
289 32
	protected function sort_by_price( $a, $b ) {
290 32
		$price_1 = $a->price * $a->quantity;
291 32
		$price_2 = $b->price * $b->quantity;
292 32
		if ( $price_1 === $price_2 ) {
293 12
			return 0;
294
		}
295 26
		return ( $price_1 < $price_2 ) ? 1 : -1;
296
	}
297
298
	/**
299
	 * Filter out all products which have been fully discounted to 0.
300
	 * Used as array_filter callback.
301
	 *
302
	 * @since  3.2.0
303
	 * @param  object $item Get data for this item.
304
	 * @return bool
305
	 */
306 20
	protected function filter_products_with_price( $item ) {
307 20
		return $this->get_discounted_price_in_cents( $item ) > 0;
308
	}
309
310
	/**
311
	 * Get items which the coupon should be applied to.
312
	 *
313
	 * @since  3.2.0
314
	 * @param  object $coupon Coupon object.
315
	 * @return array
316
	 */
317 63
	protected function get_items_to_apply_coupon( $coupon ) {
318 63
		$items_to_apply = array();
319
320 63
		foreach ( $this->get_items_to_validate() as $item ) {
321 62
			$item_to_apply = clone $item; // Clone the item so changes to this item do not affect the originals.
322
323 62
			if ( 0 === $this->get_discounted_price_in_cents( $item_to_apply ) || 0 >= $item_to_apply->quantity ) {
324 1
				continue;
325
			}
326
327 62
			if ( ! $coupon->is_valid_for_product( $item_to_apply->product, $item_to_apply->object ) && ! $coupon->is_valid_for_cart() ) {
328 2
				continue;
329
			}
330
331 62
			$items_to_apply[] = $item_to_apply;
332
		}
333 63
		return $items_to_apply;
334
	}
335
336
	/**
337
	 * Apply percent discount to items and return an array of discounts granted.
338
	 *
339
	 * @since  3.2.0
340
	 * @param  WC_Coupon $coupon Coupon object. Passed through filters.
341
	 * @param  array     $items_to_apply Array of items to apply the coupon to.
342
	 * @return int Total discounted.
343
	 */
344 33
	protected function apply_coupon_percent( $coupon, $items_to_apply ) {
345 33
		$total_discount        = 0;
346 33
		$cart_total            = 0;
347 33
		$limit_usage_qty       = 0;
348 33
		$applied_count         = 0;
349 33
		$adjust_final_discount = true;
350
351 33
		if ( null !== $coupon->get_limit_usage_to_x_items() ) {
352 8
			$limit_usage_qty = $coupon->get_limit_usage_to_x_items();
353
		}
354
355 33
		$coupon_amount = $coupon->get_amount();
356
357 33
		foreach ( $items_to_apply as $item ) {
358
			// Find out how much price is available to discount for the item.
359 32
			$discounted_price = $this->get_discounted_price_in_cents( $item );
360
361
			// Get the price we actually want to discount, based on settings.
362 32
			$price_to_discount = ( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ) ? $discounted_price : round( $item->price );
363
364
			// See how many and what price to apply to.
365 32
			$apply_quantity    = $limit_usage_qty && ( $limit_usage_qty - $applied_count ) < $item->quantity ? $limit_usage_qty - $applied_count : $item->quantity;
366 32
			$apply_quantity    = max( 0, apply_filters( 'woocommerce_coupon_get_apply_quantity', $apply_quantity, $item, $coupon, $this ) );
367 32
			$price_to_discount = ( $price_to_discount / $item->quantity ) * $apply_quantity;
368
369
			// Run coupon calculations.
370 32
			$discount = floor( $price_to_discount * ( $coupon_amount / 100 ) );
371
372 32
			if ( is_a( $this->object, 'WC_Cart' ) && has_filter( 'woocommerce_coupon_get_discount_amount' ) ) {
373
				// Send through the legacy filter, but not as cents.
374 1
				$filtered_discount = wc_add_number_precision( apply_filters( 'woocommerce_coupon_get_discount_amount', wc_remove_number_precision( $discount ), wc_remove_number_precision( $price_to_discount ), $item->object, false, $coupon ) );
375
376 1
				if ( $filtered_discount !== $discount ) {
377 1
					$discount              = $filtered_discount;
378 1
					$adjust_final_discount = false;
379
				}
380
			}
381
382 32
			$discount       = wc_round_discount( min( $discounted_price, $discount ), 0 );
383 32
			$cart_total     = $cart_total + $price_to_discount;
384 32
			$total_discount = $total_discount + $discount;
385 32
			$applied_count  = $applied_count + $apply_quantity;
386
387
			// Store code and discount amount per item.
388 32
			$this->discounts[ $coupon->get_code() ][ $item->key ] += $discount;
389
		}
390
391
		// Work out how much discount would have been given to the cart as a whole and compare to what was discounted on all line items.
392 33
		$cart_total_discount = wc_round_discount( $cart_total * ( $coupon_amount / 100 ), 0 );
393
394 33
		if ( $total_discount < $cart_total_discount && $adjust_final_discount ) {
395 2
			$total_discount += $this->apply_coupon_remainder( $coupon, $items_to_apply, $cart_total_discount - $total_discount );
396
		}
397
398 33
		return $total_discount;
399
	}
400
401
	/**
402
	 * Apply fixed product discount to items.
403
	 *
404
	 * @since  3.2.0
405
	 * @param  WC_Coupon $coupon Coupon object. Passed through filters.
406
	 * @param  array     $items_to_apply Array of items to apply the coupon to.
407
	 * @param  int       $amount Fixed discount amount to apply in cents. Leave blank to pull from coupon.
408
	 * @return int Total discounted.
409
	 */
410 35
	protected function apply_coupon_fixed_product( $coupon, $items_to_apply, $amount = null ) {
411 35
		$total_discount  = 0;
412 35
		$amount          = $amount ? $amount : wc_add_number_precision( $coupon->get_amount() );
413 35
		$limit_usage_qty = 0;
414 35
		$applied_count   = 0;
415
416 35
		if ( null !== $coupon->get_limit_usage_to_x_items() ) {
417 9
			$limit_usage_qty = $coupon->get_limit_usage_to_x_items();
418
		}
419
420 35
		foreach ( $items_to_apply as $item ) {
421
			// Find out how much price is available to discount for the item.
422 35
			$discounted_price = $this->get_discounted_price_in_cents( $item );
423
424
			// Get the price we actually want to discount, based on settings.
425 35
			$price_to_discount = ( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ) ? $discounted_price : $item->price;
426
427
			// Run coupon calculations.
428 35
			if ( $limit_usage_qty ) {
429 9
				$apply_quantity = $limit_usage_qty - $applied_count < $item->quantity ? $limit_usage_qty - $applied_count : $item->quantity;
430 9
				$apply_quantity = max( 0, apply_filters( 'woocommerce_coupon_get_apply_quantity', $apply_quantity, $item, $coupon, $this ) );
431 9
				$discount       = min( $amount, $item->price / $item->quantity ) * $apply_quantity;
432
			} else {
433 26
				$apply_quantity = apply_filters( 'woocommerce_coupon_get_apply_quantity', $item->quantity, $item, $coupon, $this );
434 26
				$discount       = $amount * $apply_quantity;
435
			}
436
437 35
			if ( is_a( $this->object, 'WC_Cart' ) && has_filter( 'woocommerce_coupon_get_discount_amount' ) ) {
438
				// Send through the legacy filter, but not as cents.
439
				$discount = wc_add_number_precision( apply_filters( 'woocommerce_coupon_get_discount_amount', wc_remove_number_precision( $discount ), wc_remove_number_precision( $price_to_discount ), $item->object, false, $coupon ) );
440
			}
441
442 35
			$discount       = min( $discounted_price, $discount );
443 35
			$total_discount = $total_discount + $discount;
444 35
			$applied_count  = $applied_count + $apply_quantity;
445
446
			// Store code and discount amount per item.
447 35
			$this->discounts[ $coupon->get_code() ][ $item->key ] += $discount;
448
		}
449 35
		return $total_discount;
450
	}
451
452
	/**
453
	 * Apply fixed cart discount to items.
454
	 *
455
	 * @since  3.2.0
456
	 * @param  WC_Coupon $coupon Coupon object. Passed through filters.
457
	 * @param  array     $items_to_apply Array of items to apply the coupon to.
458
	 * @param  int       $amount Fixed discount amount to apply in cents. Leave blank to pull from coupon.
459
	 * @return int Total discounted.
460
	 */
461 20
	protected function apply_coupon_fixed_cart( $coupon, $items_to_apply, $amount = null ) {
462 20
		$total_discount = 0;
463 20
		$amount         = $amount ? $amount : wc_add_number_precision( $coupon->get_amount() );
464 20
		$items_to_apply = array_filter( $items_to_apply, array( $this, 'filter_products_with_price' ) );
465 20
		$item_count     = array_sum( wp_list_pluck( $items_to_apply, 'quantity' ) );
466
467 20
		if ( ! $item_count ) {
468
			return $total_discount;
469
		}
470
471 20
		if ( ! $amount ) {
472
			// If there is no amount we still send it through so filters are fired.
473
			$total_discount = $this->apply_coupon_fixed_product( $coupon, $items_to_apply, 0 );
474
		} else {
475 20
			$per_item_discount = absint( $amount / $item_count ); // round it down to the nearest cent.
476
477 20
			if ( $per_item_discount > 0 ) {
478 20
				$total_discount = $this->apply_coupon_fixed_product( $coupon, $items_to_apply, $per_item_discount );
479
480
				/**
481
				 * If there is still discount remaining, repeat the process.
482
				 */
483 20
				if ( $total_discount > 0 && $total_discount < $amount ) {
484 20
					$total_discount += $this->apply_coupon_fixed_cart( $coupon, $items_to_apply, $amount - $total_discount );
485
				}
486 5
			} elseif ( $amount > 0 ) {
487 5
				$total_discount += $this->apply_coupon_remainder( $coupon, $items_to_apply, $amount );
488
			}
489
		}
490 20
		return $total_discount;
491
	}
492
493
	/**
494
	 * Apply custom coupon discount to items.
495
	 *
496
	 * @since  3.3
497
	 * @param  WC_Coupon $coupon Coupon object. Passed through filters.
498
	 * @param  array     $items_to_apply Array of items to apply the coupon to.
499
	 * @return int Total discounted.
500
	 */
501 1
	protected function apply_coupon_custom( $coupon, $items_to_apply ) {
502 1
		$limit_usage_qty = 0;
503 1
		$applied_count   = 0;
504
505 1
		if ( null !== $coupon->get_limit_usage_to_x_items() ) {
506 1
			$limit_usage_qty = $coupon->get_limit_usage_to_x_items();
507
		}
508
509
		// Apply the coupon to each item.
510 1
		foreach ( $items_to_apply as $item ) {
511
			// Find out how much price is available to discount for the item.
512 1
			$discounted_price = $this->get_discounted_price_in_cents( $item );
513
514
			// Get the price we actually want to discount, based on settings.
515 1
			$price_to_discount = wc_remove_number_precision( ( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ) ? $discounted_price : $item->price );
516
517
			// See how many and what price to apply to.
518 1
			$apply_quantity = $limit_usage_qty && ( $limit_usage_qty - $applied_count ) < $item->quantity ? $limit_usage_qty - $applied_count : $item->quantity;
519 1
			$apply_quantity = max( 0, apply_filters( 'woocommerce_coupon_get_apply_quantity', $apply_quantity, $item, $coupon, $this ) );
520
521
			// Run coupon calculations.
522 1
			$discount      = wc_add_number_precision( $coupon->get_discount_amount( $price_to_discount / $item->quantity, $item->object, true ) ) * $apply_quantity;
523 1
			$discount      = wc_round_discount( min( $discounted_price, $discount ), 0 );
524 1
			$applied_count = $applied_count + $apply_quantity;
525
526
			// Store code and discount amount per item.
527 1
			$this->discounts[ $coupon->get_code() ][ $item->key ] += $discount;
528
		}
529
530
		// Allow post-processing for custom coupon types (e.g. calculating discrepancy, etc).
531 1
		$this->discounts[ $coupon->get_code() ] = apply_filters( 'woocommerce_coupon_custom_discounts_array', $this->discounts[ $coupon->get_code() ], $coupon );
532
533 1
		return array_sum( $this->discounts[ $coupon->get_code() ] );
534
	}
535
536
	/**
537
	 * Deal with remaining fractional discounts by splitting it over items
538
	 * until the amount is expired, discounting 1 cent at a time.
539
	 *
540
	 * @since 3.2.0
541
	 * @param  WC_Coupon $coupon Coupon object if appliable. Passed through filters.
542
	 * @param  array     $items_to_apply Array of items to apply the coupon to.
543
	 * @param  int       $amount Fixed discount amount to apply.
544
	 * @return int Total discounted.
545
	 */
546 7
	protected function apply_coupon_remainder( $coupon, $items_to_apply, $amount ) {
547 7
		$total_discount = 0;
548
549 7
		foreach ( $items_to_apply as $item ) {
550 7
			for ( $i = 0; $i < $item->quantity; $i ++ ) {
551
				// Find out how much price is available to discount for the item.
552 7
				$discounted_price = $this->get_discounted_price_in_cents( $item );
553
554
				// Get the price we actually want to discount, based on settings.
555 7
				$price_to_discount = ( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ) ? $discounted_price : $item->price;
556
557
				// Run coupon calculations.
558 7
				$discount = min( $price_to_discount, 1 );
559
560
				// Store totals.
561 7
				$total_discount += $discount;
562
563
				// Store code and discount amount per item.
564 7
				$this->discounts[ $coupon->get_code() ][ $item->key ] += $discount;
565
566 7
				if ( $total_discount >= $amount ) {
567 7
					break 2;
568
				}
569
			}
570 3
			if ( $total_discount >= $amount ) {
571
				break;
572
			}
573
		}
574 7
		return $total_discount;
575
	}
576
577
	/**
578
	 * Ensure coupon exists or throw exception.
579
	 *
580
	 * @since  3.2.0
581
	 * @throws Exception Error message.
582
	 * @param  WC_Coupon $coupon Coupon data.
583
	 * @return bool
584
	 */
585 67
	protected function validate_coupon_exists( $coupon ) {
586 67
		if ( ! $coupon->get_id() && ! $coupon->get_virtual() ) {
587
			/* translators: %s: coupon code */
588
			throw new Exception( sprintf( __( 'Coupon "%s" does not exist!', 'woocommerce' ), $coupon->get_code() ), 105 );
589
		}
590
591 67
		return true;
592
	}
593
594
	/**
595
	 * Ensure coupon usage limit is valid or throw exception.
596
	 *
597
	 * @since  3.2.0
598
	 * @throws Exception Error message.
599
	 * @param  WC_Coupon $coupon Coupon data.
600
	 * @return bool
601
	 */
602 67
	protected function validate_coupon_usage_limit( $coupon ) {
603 67
		if ( $coupon->get_usage_limit() > 0 && $coupon->get_usage_count() >= $coupon->get_usage_limit() ) {
604
			throw new Exception( __( 'Coupon usage limit has been reached.', 'woocommerce' ), 106 );
605
		}
606
607 67
		return true;
608
	}
609
610
	/**
611
	 * Ensure coupon user usage limit is valid or throw exception.
612
	 *
613
	 * Per user usage limit - check here if user is logged in (against user IDs).
614
	 * Checked again for emails later on in WC_Cart::check_customer_coupons().
615
	 *
616
	 * @since  3.2.0
617
	 * @throws Exception Error message.
618
	 * @param  WC_Coupon $coupon  Coupon data.
619
	 * @param  int       $user_id User ID.
620
	 * @return bool
621
	 */
622 67
	protected function validate_coupon_user_usage_limit( $coupon, $user_id = 0 ) {
623 67
		if ( empty( $user_id ) ) {
624 67
			if ( $this->object instanceof WC_Order ) {
625 8
				$user_id = $this->object->get_customer_id();
626
			} else {
627 59
				$user_id = get_current_user_id();
628
			}
629
		}
630
631 67
		if ( $coupon && $user_id && apply_filters( 'woocommerce_coupon_validate_user_usage_limit', $coupon->get_usage_limit_per_user() > 0, $user_id, $coupon, $this ) && $coupon->get_id() && $coupon->get_data_store() ) {
632
			$date_store  = $coupon->get_data_store();
633
			$usage_count = $date_store->get_usage_by_user_id( $coupon, $user_id );
634
			if ( $usage_count >= $coupon->get_usage_limit_per_user() ) {
635
				throw new Exception( __( 'Coupon usage limit has been reached.', 'woocommerce' ), 106 );
636
			}
637
		}
638
639 67
		return true;
640
	}
641
642
	/**
643
	 * Ensure coupon date is valid or throw exception.
644
	 *
645
	 * @since  3.2.0
646
	 * @throws Exception Error message.
647
	 * @param  WC_Coupon $coupon Coupon data.
648
	 * @return bool
649
	 */
650 67
	protected function validate_coupon_expiry_date( $coupon ) {
651 67
		if ( $coupon->get_date_expires() && apply_filters( 'woocommerce_coupon_validate_expiry_date', current_time( 'timestamp', true ) > $coupon->get_date_expires()->getTimestamp(), $coupon, $this ) ) {
652 1
			throw new Exception( __( 'This coupon has expired.', 'woocommerce' ), 107 );
653
		}
654
655 67
		return true;
656
	}
657
658
	/**
659
	 * Ensure coupon amount is valid or throw exception.
660
	 *
661
	 * @since  3.2.0
662
	 * @throws Exception Error message.
663
	 * @param  WC_Coupon $coupon   Coupon data.
664
	 * @return bool
665
	 */
666 67 View Code Duplication
	protected function validate_coupon_minimum_amount( $coupon ) {
667 67
		$subtotal = wc_remove_number_precision( $this->get_object_subtotal() );
668
669 67
		if ( $coupon->get_minimum_amount() > 0 && apply_filters( 'woocommerce_coupon_validate_minimum_amount', $coupon->get_minimum_amount() > $subtotal, $coupon, $subtotal ) ) {
670
			/* translators: %s: coupon minimum amount */
671
			throw new Exception( sprintf( __( 'The minimum spend for this coupon is %s.', 'woocommerce' ), wc_price( $coupon->get_minimum_amount() ) ), 108 );
672
		}
673
674 67
		return true;
675
	}
676
677
	/**
678
	 * Ensure coupon amount is valid or throw exception.
679
	 *
680
	 * @since  3.2.0
681
	 * @throws Exception Error message.
682
	 * @param  WC_Coupon $coupon   Coupon data.
683
	 * @return bool
684
	 */
685 67 View Code Duplication
	protected function validate_coupon_maximum_amount( $coupon ) {
686 67
		$subtotal = wc_remove_number_precision( $this->get_object_subtotal() );
687
688 67
		if ( $coupon->get_maximum_amount() > 0 && apply_filters( 'woocommerce_coupon_validate_maximum_amount', $coupon->get_maximum_amount() < $subtotal, $coupon ) ) {
689
			/* translators: %s: coupon maximum amount */
690
			throw new Exception( sprintf( __( 'The maximum spend for this coupon is %s.', 'woocommerce' ), wc_price( $coupon->get_maximum_amount() ) ), 112 );
691
		}
692
693 67
		return true;
694
	}
695
696
	/**
697
	 * Ensure coupon is valid for products in the list is valid or throw exception.
698
	 *
699
	 * @since  3.2.0
700
	 * @throws Exception Error message.
701
	 * @param  WC_Coupon $coupon Coupon data.
702
	 * @return bool
703
	 */
704 67
	protected function validate_coupon_product_ids( $coupon ) {
705 67
		if ( count( $coupon->get_product_ids() ) > 0 ) {
706
			$valid = false;
707
708 View Code Duplication
			foreach ( $this->get_items_to_validate() as $item ) {
709
				if ( $item->product && in_array( $item->product->get_id(), $coupon->get_product_ids(), true ) || in_array( $item->product->get_parent_id(), $coupon->get_product_ids(), true ) ) {
710
					$valid = true;
711
					break;
712
				}
713
			}
714
715
			if ( ! $valid ) {
716
				throw new Exception( __( 'Sorry, this coupon is not applicable to selected products.', 'woocommerce' ), 109 );
717
			}
718
		}
719
720 67
		return true;
721
	}
722
723
	/**
724
	 * Ensure coupon is valid for product categories in the list is valid or throw exception.
725
	 *
726
	 * @since  3.2.0
727
	 * @throws Exception Error message.
728
	 * @param  WC_Coupon $coupon Coupon data.
729
	 * @return bool
730
	 */
731 67
	protected function validate_coupon_product_categories( $coupon ) {
732 67
		if ( count( $coupon->get_product_categories() ) > 0 ) {
733
			$valid = false;
734
735
			foreach ( $this->get_items_to_validate() as $item ) {
736
				if ( $coupon->get_exclude_sale_items() && $item->product && $item->product->is_on_sale() ) {
737
					continue;
738
				}
739
740
				$product_cats = wc_get_product_cat_ids( $item->product->get_id() );
741
742
				if ( $item->product->get_parent_id() ) {
743
					$product_cats = array_merge( $product_cats, wc_get_product_cat_ids( $item->product->get_parent_id() ) );
744
				}
745
746
				// If we find an item with a cat in our allowed cat list, the coupon is valid.
747
				if ( count( array_intersect( $product_cats, $coupon->get_product_categories() ) ) > 0 ) {
748
					$valid = true;
749
					break;
750
				}
751
			}
752
753
			if ( ! $valid ) {
754
				throw new Exception( __( 'Sorry, this coupon is not applicable to selected products.', 'woocommerce' ), 109 );
755
			}
756
		}
757
758 67
		return true;
759
	}
760
761
	/**
762
	 * Ensure coupon is valid for sale items in the list is valid or throw exception.
763
	 *
764
	 * @since  3.2.0
765
	 * @throws Exception Error message.
766
	 * @param  WC_Coupon $coupon Coupon data.
767
	 * @return bool
768
	 */
769 21
	protected function validate_coupon_sale_items( $coupon ) {
770 21
		if ( $coupon->get_exclude_sale_items() ) {
771 1
			$valid = true;
772
773 1
			foreach ( $this->get_items_to_validate() as $item ) {
774 1
				if ( $item->product && $item->product->is_on_sale() ) {
775 1
					$valid = false;
776 1
					break;
777
				}
778
			}
779
780 1
			if ( ! $valid ) {
781 1
				throw new Exception( __( 'Sorry, this coupon is not valid for sale items.', 'woocommerce' ), 110 );
782
			}
783
		}
784
785 21
		return true;
786
	}
787
788
	/**
789
	 * All exclusion rules must pass at the same time for a product coupon to be valid.
790
	 *
791
	 * @since  3.2.0
792
	 * @throws Exception Error message.
793
	 * @param  WC_Coupon $coupon Coupon data.
794
	 * @return bool
795
	 */
796 67
	protected function validate_coupon_excluded_items( $coupon ) {
797 67
		$items = $this->get_items_to_validate();
798 67
		if ( ! empty( $items ) && $coupon->is_type( wc_get_product_coupon_types() ) ) {
799 46
			$valid = false;
800
801 46
			foreach ( $items as $item ) {
802 46
				if ( $item->product && $coupon->is_valid_for_product( $item->product, $item->object ) ) {
803 46
					$valid = true;
804 46
					break;
805
				}
806
			}
807
808 46
			if ( ! $valid ) {
809
				throw new Exception( __( 'Sorry, this coupon is not applicable to selected products.', 'woocommerce' ), 109 );
810
			}
811
		}
812
813 67
		return true;
814
	}
815
816
	/**
817
	 * Cart discounts cannot be added if non-eligible product is found.
818
	 *
819
	 * @since  3.2.0
820
	 * @throws Exception Error message.
821
	 * @param  WC_Coupon $coupon Coupon data.
822
	 * @return bool
823
	 */
824 67
	protected function validate_coupon_eligible_items( $coupon ) {
825 67
		if ( ! $coupon->is_type( wc_get_product_coupon_types() ) ) {
826 21
			$this->validate_coupon_sale_items( $coupon );
827 21
			$this->validate_coupon_excluded_product_ids( $coupon );
828 21
			$this->validate_coupon_excluded_product_categories( $coupon );
829
		}
830
831 67
		return true;
832
	}
833
834
	/**
835
	 * Exclude products.
836
	 *
837
	 * @since  3.2.0
838
	 * @throws Exception Error message.
839
	 * @param  WC_Coupon $coupon Coupon data.
840
	 * @return bool
841
	 */
842 21
	protected function validate_coupon_excluded_product_ids( $coupon ) {
843
		// Exclude Products.
844 21
		if ( count( $coupon->get_excluded_product_ids() ) > 0 ) {
845
			$products = array();
846
847 View Code Duplication
			foreach ( $this->get_items_to_validate() as $item ) {
848
				if ( $item->product && in_array( $item->product->get_id(), $coupon->get_excluded_product_ids(), true ) || in_array( $item->product->get_parent_id(), $coupon->get_excluded_product_ids(), true ) ) {
849
					$products[] = $item->product->get_name();
850
				}
851
			}
852
853 View Code Duplication
			if ( ! empty( $products ) ) {
854
				/* translators: %s: products list */
855
				throw new Exception( sprintf( __( 'Sorry, this coupon is not applicable to the products: %s.', 'woocommerce' ), implode( ', ', $products ) ), 113 );
856
			}
857
		}
858
859 21
		return true;
860
	}
861
862
	/**
863
	 * Exclude categories from product list.
864
	 *
865
	 * @since  3.2.0
866
	 * @throws Exception Error message.
867
	 * @param  WC_Coupon $coupon Coupon data.
868
	 * @return bool
869
	 */
870 21
	protected function validate_coupon_excluded_product_categories( $coupon ) {
871 21
		if ( count( $coupon->get_excluded_product_categories() ) > 0 ) {
872
			$categories = array();
873
874
			foreach ( $this->get_items_to_validate() as $item ) {
875
				if ( ! $item->product ) {
876
					continue;
877
				}
878
879
				$product_cats = wc_get_product_cat_ids( $item->product->get_id() );
880
881
				if ( $item->product->get_parent_id() ) {
882
					$product_cats = array_merge( $product_cats, wc_get_product_cat_ids( $item->product->get_parent_id() ) );
883
				}
884
885
				$cat_id_list = array_intersect( $product_cats, $coupon->get_excluded_product_categories() );
886 View Code Duplication
				if ( count( $cat_id_list ) > 0 ) {
887
					foreach ( $cat_id_list as $cat_id ) {
888
						$cat          = get_term( $cat_id, 'product_cat' );
889
						$categories[] = $cat->name;
890
					}
891
				}
892
			}
893
894 View Code Duplication
			if ( ! empty( $categories ) ) {
895
				/* translators: %s: categories list */
896
				throw new Exception( sprintf( __( 'Sorry, this coupon is not applicable to the categories: %s.', 'woocommerce' ), implode( ', ', array_unique( $categories ) ) ), 114 );
897
			}
898
		}
899
900 21
		return true;
901
	}
902
903
	/**
904
	 * Get the object subtotal
905
	 *
906
	 * @return int
907
	 */
908 67
	protected function get_object_subtotal() {
909 67
		if ( is_a( $this->object, 'WC_Cart' ) ) {
910 58
			return wc_add_number_precision( $this->object->get_displayed_subtotal() );
911 9
		} elseif ( is_a( $this->object, 'WC_Order' ) ) {
912 8
			$subtotal = wc_add_number_precision( $this->object->get_subtotal() );
913
914 8
			if ( $this->object->get_prices_include_tax() ) {
0 ignored issues
show
The method get_prices_include_tax does only exist in WC_Order, but not in WC_Cart.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
915
				// Add tax to tax-exclusive subtotal.
916 2
				$subtotal = $subtotal + wc_add_number_precision( round( $this->object->get_total_tax(), wc_get_price_decimals() ) );
917
			}
918
919 8
			return $subtotal;
920
		} else {
921 1
			return array_sum( wp_list_pluck( $this->items, 'price' ) );
922
		}
923
	}
924
925
	/**
926
	 * Check if a coupon is valid.
927
	 *
928
	 * Error Codes:
929
	 * - 100: Invalid filtered.
930
	 * - 101: Invalid removed.
931
	 * - 102: Not yours removed.
932
	 * - 103: Already applied.
933
	 * - 104: Individual use only.
934
	 * - 105: Not exists.
935
	 * - 106: Usage limit reached.
936
	 * - 107: Expired.
937
	 * - 108: Minimum spend limit not met.
938
	 * - 109: Not applicable.
939
	 * - 110: Not valid for sale items.
940
	 * - 111: Missing coupon code.
941
	 * - 112: Maximum spend limit met.
942
	 * - 113: Excluded products.
943
	 * - 114: Excluded categories.
944
	 *
945
	 * @since  3.2.0
946
	 * @throws Exception Error message.
947
	 * @param  WC_Coupon $coupon Coupon data.
948
	 * @return bool|WP_Error
949
	 */
950 67
	public function is_coupon_valid( $coupon ) {
951
		try {
952 67
			$this->validate_coupon_exists( $coupon );
953 67
			$this->validate_coupon_usage_limit( $coupon );
954 67
			$this->validate_coupon_user_usage_limit( $coupon );
955 67
			$this->validate_coupon_expiry_date( $coupon );
956 67
			$this->validate_coupon_minimum_amount( $coupon );
957 67
			$this->validate_coupon_maximum_amount( $coupon );
958 67
			$this->validate_coupon_product_ids( $coupon );
959 67
			$this->validate_coupon_product_categories( $coupon );
960 67
			$this->validate_coupon_excluded_items( $coupon );
961 67
			$this->validate_coupon_eligible_items( $coupon );
962
963 67
			if ( ! apply_filters( 'woocommerce_coupon_is_valid', true, $coupon, $this ) ) {
964 67
				throw new Exception( __( 'Coupon is not valid.', 'woocommerce' ), 100 );
965
			}
966 2
		} catch ( Exception $e ) {
967
			/**
968
			 * Filter the coupon error message.
969
			 *
970
			 * @param string    $error_message Error message.
971
			 * @param int       $error_code    Error code.
972
			 * @param WC_Coupon $coupon        Coupon data.
973
			 */
974 2
			$message = apply_filters( 'woocommerce_coupon_error', is_numeric( $e->getMessage() ) ? $coupon->get_coupon_error( $e->getMessage() ) : $e->getMessage(), $e->getCode(), $coupon );
975
976 2
			return new WP_Error(
977 2
				'invalid_coupon',
978
				$message,
979
				array(
980 2
					'status' => 400,
981
				)
982
			);
983
		}
984 67
		return true;
985
	}
986
}
987