Passed
Pull Request — master (#257)
by
unknown
04:31
created

WPInv_Discount::is_expired()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 2
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 3
rs 10
1
<?php
2
/**
3
 * Contains Discount calculation class
4
 *
5
 * @since   1.0.14
6
 */
7
8
defined( 'ABSPATH' ) || exit;
9
10
/**
11
 * Discount class.
12
 * 
13
 * @since 1.0.14
14
 * @property string $code
15
 * @property string $description
16
 * @property string $type
17
 * @property string $expiration
18
 * @property string $start
19
 * @property string $status
20
 * @property string $date_modified
21
 * @property string $date_created
22
 * @property array $items
23
 * @property array $excluded_items
24
 * @property int $uses
25
 * @property int $max_uses
26
 * @property bool $is_recurring
27
 * @property bool $is_single_use
28
 * @property float $min_total
29
 * @property float $max_total
30
 * @property float $amount
31
 *
32
 */
33
class WPInv_Discount {
34
	
35
	/**
36
	 * Discount ID.
37
	 *
38
	 * @since 1.0.14
39
	 * @var integer|null
40
	 */
41
	public $ID = null;
42
43
	/**
44
	 * Old discount status.
45
	 *
46
	 * @since 1.0.14
47
	 * @var string
48
	 */
49
	public $old_status = 'draft';
50
	
51
	/**
52
	 * Data array, with defaults.
53
	 *
54
	 * @since 1.0.14
55
	 * @var array
56
	 */
57
	protected $data = array();
58
59
	/**
60
	 * Discount constructor.
61
	 *
62
	 * @param int|array|string|WPInv_Discount $discount discount data, object, ID or code.
63
	 * @since 1.0.14
64
	 */
65
	public function __construct( $discount = array() ) {
66
        
67
        // If the discount is an instance of this class...
68
		if ( $discount instanceof WPInv_Discount ) {
69
			$this->init( $discount->data );
70
			return;
71
        }
72
        
73
        // If the discount is an array of discount details...
74
        if ( is_array( $discount ) ) {
75
			$this->init( $discount );
76
			return;
77
		}
78
		
79
		// Try fetching the discount by its post id.
80
		$data = false;
81
		
82
		if ( ! empty( $discount ) && is_numeric( $discount ) ) {
83
			$discount = absint( $discount );
84
			$data = self::get_data_by( 'id', $discount );
85
		}
86
87
		if ( $data ) {
88
			$this->init( $data );
0 ignored issues
show
Bug introduced by
It seems like $data can also be of type true; however, parameter $data of WPInv_Discount::init() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

88
			$this->init( /** @scrutinizer ignore-type */ $data );
Loading history...
89
			return;
90
		}
91
		
92
		// Try fetching the discount by its discount code.
93
		if ( ! empty( $discount ) && is_string( $discount ) ) {
94
			$data = self::get_data_by( 'discount_code', $discount );
95
		}
96
97
		if ( $data ) {
98
			$this->init( $data );
99
			return;
100
		} 
101
		
102
		// If we are here then the discount does not exist.
103
		$this->init( array() );
104
	}
105
	
106
	/**
107
	 * Sets up object properties
108
	 *
109
	 * @since 1.0.14
110
	 * @param array $data An array containing the discount's data
111
	 */
112
	public function init( $data ) {
113
		$data       	  = self::sanitize_discount_data( $data );
114
		$this->data 	  = $data;
115
		$this->old_status = $data['status'];
116
		$this->ID   	  = $data['ID'];
117
	}
118
	
119
	/**
120
	 * Fetch a discount from the db/cache
121
	 *
122
	 *
123
	 * @static
124
	 *
125
	 *
126
	 * @param string $field The field to query against: 'ID', 'discount_code'
127
	 * @param string|int $value The field value
128
	 * @since 1.0.14
129
	 * @return array|bool array of discount details on success. False otherwise.
130
	 */
131
	public static function get_data_by( $field, $value ) {
132
133
		// 'ID' is an alias of 'id'.
134
		if ( 'ID' === $field ) {
135
			$field = 'id';
136
		}
137
138
		if ( 'id' == $field ) {
139
			// Make sure the value is numeric to avoid casting objects, for example,
140
			// to int 1.
141
			if ( ! is_numeric( $value ) )
142
				return false;
143
			$value = intval( $value );
144
			if ( $value < 1 )
145
				return false;
146
		} else {
147
			$value = trim( $value );
148
		}
149
150
		if ( !$value || ! is_string( $field ) )
151
			return false;
152
153
		// prepare query args
154
		switch ( strtolower( $field ) ) {
155
			case 'id':
156
				$discount_id = $value;
157
				$args		 = array( 'include' => array( $value ) );
158
				break;
159
			case 'discount_code':
160
			case 'code':
161
				$discount_id = wp_cache_get( $value, 'WPInv_Discount_Codes' );
162
				$args		 = array( 'meta_key' => '_wpi_discount_code', 'meta_value' => $value );
163
				break;
164
			case 'name':
165
				$discount_id = 0;
166
				$args		 = array( 'name' => $value );
167
				break;
168
			default:
169
				return false;
170
		}
171
172
		// Check if there is a cached value.
173
		if ( ! empty( $discount_id ) && $discount = wp_cache_get( (int) $discount_id, 'WPInv_Discounts' ) ) {
174
			return $discount;
175
		}
176
177
		$args = wp_parse_args(
178
			$args,
0 ignored issues
show
Security Variable Injection introduced by
$args can contain request data and is used in variable name context(s) leading to a potential security vulnerability.

1 path for user data to reach this point

  1. Read from $_POST, and wpinv_store_discount() is called
    in includes/admin/admin-meta-boxes.php on line 379
  2. Enters via parameter $data
    in includes/wpinv-discount-functions.php on line 252
  3. array('code' => IssetNode ? sanitize_text_field($data['code']) : $existing_data['code'], 'type' => IssetNode ? sanitize_text_field($data['type']) : $existing_data['type'], 'amount' => IssetNode ? wpinv_sanitize_amount($data['amount']) : $existing_data['amount'], 'start' => IssetNode ? sanitize_text_field($data['start']) : $existing_data['start'], 'expiration' => IssetNode ? sanitize_text_field($data['expiration']) : $existing_data['expiration'], 'min_total' => IssetNode ? wpinv_sanitize_amount($data['min_total']) : $existing_data['min_total'], 'max_total' => IssetNode ? wpinv_sanitize_amount($data['max_total']) : $existing_data['max_total'], 'max_uses' => IssetNode ? absint($data['max_uses']) : $existing_data['max_uses'], 'items' => IssetNode ? $data['items'] : $existing_data['items'], 'excluded_items' => IssetNode ? $data['excluded_items'] : $existing_data['excluded_items'], 'is_recurring' => IssetNode ? (bool)$data['recurring'] : $existing_data['is_recurring'], 'is_single_use' => IssetNode ? (bool)$data['single_use'] : false, 'uses' => IssetNode ? (int)$data['uses'] : $existing_data['uses']) is assigned to $meta
    in includes/wpinv-discount-functions.php on line 260
  4. Data is passed through array_merge(), and array_merge($existing_data, $meta) is assigned to $data
    in includes/wpinv-discount-functions.php on line 277
  5. wpinv_get_discount_obj() is called
    in includes/wpinv-discount-functions.php on line 278
  6. Enters via parameter $discount
    in includes/wpinv-discount-functions.php on line 215
  7. WPInv_Discount::__construct() is called
    in includes/wpinv-discount-functions.php on line 216
  8. Enters via parameter $discount
    in includes/class-wpinv-discount.php on line 65
  9. WPInv_Discount::get_data_by() is called
    in includes/class-wpinv-discount.php on line 94
  10. Enters via parameter $value
    in includes/class-wpinv-discount.php on line 131
  11. Data is passed through trim(), and trim($value) is assigned to $value
    in includes/class-wpinv-discount.php on line 147
  12. array('include' => array($value)) is assigned to $args
    in includes/class-wpinv-discount.php on line 157

Used in variable context

  1. wp_parse_args() is called
    in includes/class-wpinv-discount.php on line 178
  2. Enters via parameter $args
    in wordpress/wp-includes/functions.php on line 4294
  3. wp_parse_str() is called
    in wordpress/wp-includes/functions.php on line 4300
  4. Enters via parameter $string
    in wordpress/wp-includes/formatting.php on line 4865
  5. parse_str() is called
    in wordpress/wp-includes/formatting.php on line 4866

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
179
			array(
180
				'post_type'      => 'wpi_discount',
181
				'posts_per_page' => 1,
182
				'post_status'    => array( 'publish', 'pending', 'draft', 'expired' )
183
			)
184
		);
185
186
		$discount = get_posts( $args );
0 ignored issues
show
Security Variable Injection introduced by
$args can contain request data and is used in variable name context(s) leading to a potential security vulnerability.

1 path for user data to reach this point

  1. Read from $_POST, and wpinv_store_discount() is called
    in includes/admin/admin-meta-boxes.php on line 379
  2. Enters via parameter $data
    in includes/wpinv-discount-functions.php on line 252
  3. array('code' => IssetNode ? sanitize_text_field($data['code']) : $existing_data['code'], 'type' => IssetNode ? sanitize_text_field($data['type']) : $existing_data['type'], 'amount' => IssetNode ? wpinv_sanitize_amount($data['amount']) : $existing_data['amount'], 'start' => IssetNode ? sanitize_text_field($data['start']) : $existing_data['start'], 'expiration' => IssetNode ? sanitize_text_field($data['expiration']) : $existing_data['expiration'], 'min_total' => IssetNode ? wpinv_sanitize_amount($data['min_total']) : $existing_data['min_total'], 'max_total' => IssetNode ? wpinv_sanitize_amount($data['max_total']) : $existing_data['max_total'], 'max_uses' => IssetNode ? absint($data['max_uses']) : $existing_data['max_uses'], 'items' => IssetNode ? $data['items'] : $existing_data['items'], 'excluded_items' => IssetNode ? $data['excluded_items'] : $existing_data['excluded_items'], 'is_recurring' => IssetNode ? (bool)$data['recurring'] : $existing_data['is_recurring'], 'is_single_use' => IssetNode ? (bool)$data['single_use'] : false, 'uses' => IssetNode ? (int)$data['uses'] : $existing_data['uses']) is assigned to $meta
    in includes/wpinv-discount-functions.php on line 260
  4. Data is passed through array_merge(), and array_merge($existing_data, $meta) is assigned to $data
    in includes/wpinv-discount-functions.php on line 277
  5. wpinv_get_discount_obj() is called
    in includes/wpinv-discount-functions.php on line 278
  6. Enters via parameter $discount
    in includes/wpinv-discount-functions.php on line 215
  7. WPInv_Discount::__construct() is called
    in includes/wpinv-discount-functions.php on line 216
  8. Enters via parameter $discount
    in includes/class-wpinv-discount.php on line 65
  9. WPInv_Discount::get_data_by() is called
    in includes/class-wpinv-discount.php on line 94
  10. Enters via parameter $value
    in includes/class-wpinv-discount.php on line 131
  11. Data is passed through trim(), and trim($value) is assigned to $value
    in includes/class-wpinv-discount.php on line 147
  12. array('include' => array($value)) is assigned to $args
    in includes/class-wpinv-discount.php on line 157
  13. Data is passed through wp_parse_args(), and wp_parse_args($args, array('post_type' => 'wpi_discount', 'posts_per_page' => 1, 'post_status' => array('publish', 'pending', 'draft', 'expired'))) is assigned to $args
    in includes/class-wpinv-discount.php on line 177

Used in variable context

  1. get_posts() is called
    in includes/class-wpinv-discount.php on line 186
  2. Enters via parameter $args
    in wordpress/wp-includes/post.php on line 2009
  3. wp_parse_args() is called
    in wordpress/wp-includes/post.php on line 2023
  4. Enters via parameter $args
    in wordpress/wp-includes/functions.php on line 4294
  5. wp_parse_str() is called
    in wordpress/wp-includes/functions.php on line 4300
  6. Enters via parameter $string
    in wordpress/wp-includes/formatting.php on line 4865
  7. parse_str() is called
    in wordpress/wp-includes/formatting.php on line 4866

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
187
				
188
		if( empty( $discount ) ) {
189
			return false;
190
		}
191
192
		$discount = $discount[0];
193
		
194
		// Prepare the return data.
195
		$return = array(
196
            'ID'                          => $discount->ID,
197
            'code'                        => get_post_meta( $discount->ID, '_wpi_discount_code', true ),
198
            'amount'                      => get_post_meta( $discount->ID, '_wpi_discount_amount', true ),
199
            'date_created'                => $discount->post_date,
200
			'date_modified'               => $discount->post_modified,
201
			'status'               		  => $discount->post_status,
202
			'start'                  	  => get_post_meta( $discount->ID, '_wpi_discount_start', true ),
203
            'expiration'                  => get_post_meta( $discount->ID, '_wpi_discount_expiration', true ),
204
            'type'               		  => get_post_meta( $discount->ID, '_wpi_discount_type', true ),
205
            'description'                 => $discount->post_excerpt,
206
            'uses'                 		  => get_post_meta( $discount->ID, '_wpi_discount_uses', true ),
207
            'is_single_use'               => get_post_meta( $discount->ID, '_wpi_discount_is_single_use', true ),
208
            'items'              	      => get_post_meta( $discount->ID, '_wpi_discount_items', true ),
209
            'excluded_items'              => get_post_meta( $discount->ID, '_wpi_discount_excluded_items', true ),
210
            'max_uses'                    => get_post_meta( $discount->ID, '_wpi_discount_max_uses', true ),
211
            'is_recurring'                => get_post_meta( $discount->ID, '_wpi_discount_is_recurring', true ),
212
            'min_total'                   => get_post_meta( $discount->ID, '_wpi_discount_min_total', true ),
213
            'max_total'                   => get_post_meta( $discount->ID, '_wpi_discount_max_total', true ),
214
        );
215
		
216
		$return = self::sanitize_discount_data( $return );
217
		$return = apply_filters( 'wpinv_discount_properties', $return );
218
219
		// Update the cache with our data
220
		wp_cache_add( $discount->ID, $return, 'WPInv_Discounts' );
221
		wp_cache_add( $return['code'], $discount->ID, 'WPInv_Discount_Codes' );
222
223
		return $return;
224
	}
225
	
226
	/**
227
	 * Sanitizes discount data
228
	 *
229
	 * @static
230
	 * @since 1.0.14
231
	 * @access public
232
	 *
233
	 * @return array the sanitized data
234
	 */
235
	public static function sanitize_discount_data( $data ) {
236
		
237
		$allowed_discount_types = array_keys( wpinv_get_discount_types() );
238
		
239
		$return = array(
240
            'ID'                          => null,
241
            'code'                        => '',
242
            'amount'                      => 0,
243
            'date_created'                => current_time('mysql'),
244
            'date_modified'               => current_time('mysql'),
245
			'expiration'                  => null,
246
			'start'                  	  => current_time('mysql'),
247
			'status'                  	  => 'draft',
248
            'type'               		  => 'percent',
249
            'description'                 => '',
250
            'uses'                        => 0,
251
            'is_single_use'               => false,
252
            'items'              		  => array(),
253
            'excluded_items'              => array(),
254
            'max_uses'                    => 0,
255
            'is_recurring'                => false,
256
            'min_total'                   => '',
257
			'max_total'              	  => '',
258
		);
259
		
260
				
261
		// Arrays only please.
262
		if (! is_array( $data ) ) {
263
            return $return;
264
        }
265
266
		// If an id is provided, ensure it is a valid discount.
267
        if (! empty( $data['ID'] ) && is_numeric( $data['ID'] ) && 'wpi_discount' !== get_post_type( $data['ID'] ) ) {
268
            return $return;
269
		}
270
271
        $return = wp_parse_args( $data, $return );
0 ignored issues
show
Security Variable Injection introduced by
$data can contain request data and is used in variable name context(s) leading to a potential security vulnerability.

1 path for user data to reach this point

  1. Read from $_POST, and wpinv_store_discount() is called
    in includes/admin/admin-meta-boxes.php on line 379
  2. Enters via parameter $data
    in includes/wpinv-discount-functions.php on line 252
  3. array('code' => IssetNode ? sanitize_text_field($data['code']) : $existing_data['code'], 'type' => IssetNode ? sanitize_text_field($data['type']) : $existing_data['type'], 'amount' => IssetNode ? wpinv_sanitize_amount($data['amount']) : $existing_data['amount'], 'start' => IssetNode ? sanitize_text_field($data['start']) : $existing_data['start'], 'expiration' => IssetNode ? sanitize_text_field($data['expiration']) : $existing_data['expiration'], 'min_total' => IssetNode ? wpinv_sanitize_amount($data['min_total']) : $existing_data['min_total'], 'max_total' => IssetNode ? wpinv_sanitize_amount($data['max_total']) : $existing_data['max_total'], 'max_uses' => IssetNode ? absint($data['max_uses']) : $existing_data['max_uses'], 'items' => IssetNode ? $data['items'] : $existing_data['items'], 'excluded_items' => IssetNode ? $data['excluded_items'] : $existing_data['excluded_items'], 'is_recurring' => IssetNode ? (bool)$data['recurring'] : $existing_data['is_recurring'], 'is_single_use' => IssetNode ? (bool)$data['single_use'] : false, 'uses' => IssetNode ? (int)$data['uses'] : $existing_data['uses']) is assigned to $meta
    in includes/wpinv-discount-functions.php on line 260
  4. Data is passed through array_merge(), and array_merge($existing_data, $meta) is assigned to $data
    in includes/wpinv-discount-functions.php on line 277
  5. wpinv_get_discount_obj() is called
    in includes/wpinv-discount-functions.php on line 278
  6. Enters via parameter $discount
    in includes/wpinv-discount-functions.php on line 215
  7. WPInv_Discount::__construct() is called
    in includes/wpinv-discount-functions.php on line 216
  8. Enters via parameter $discount
    in includes/class-wpinv-discount.php on line 65
  9. WPInv_Discount::init() is called
    in includes/class-wpinv-discount.php on line 75
  10. Enters via parameter $data
    in includes/class-wpinv-discount.php on line 112
  11. WPInv_Discount::sanitize_discount_data() is called
    in includes/class-wpinv-discount.php on line 113
  12. Enters via parameter $data
    in includes/class-wpinv-discount.php on line 235

Used in variable context

  1. wp_parse_args() is called
    in includes/class-wpinv-discount.php on line 271
  2. Enters via parameter $args
    in wordpress/wp-includes/functions.php on line 4294
  3. wp_parse_str() is called
    in wordpress/wp-includes/functions.php on line 4300
  4. Enters via parameter $string
    in wordpress/wp-includes/formatting.php on line 4865
  5. parse_str() is called
    in wordpress/wp-includes/formatting.php on line 4866

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
272
273
        // Sanitize some keys.
274
        $return['amount']         = wpinv_sanitize_amount( $return['amount'] );
275
		$return['is_single_use']  = (bool) $return['is_single_use'];
276
		$return['is_recurring']   = (bool) $return['is_recurring'];
277
		$return['uses']	          = (int) $return['uses'];
278
		$return['max_uses']	      = (int) $return['max_uses'];
279
		$return['min_total'] 	  = wpinv_sanitize_amount( $return['min_total'] );
280
        $return['max_total'] 	  = wpinv_sanitize_amount( $return['max_total'] );
281
282
		// Trim all values.
283
		$return = wpinv_clean( $return );
284
		
285
		// Ensure the discount type is supported.
286
        if ( ! in_array( $return['type'], $allowed_discount_types, true ) ) {
287
            $return['type'] = 'percent';
288
		}
289
		$return['type_name'] = wpinv_get_discount_type_name( $return['type'] );
290
		
291
		// Do not offer more than a 100% discount.
292
		if ( $return['type'] == 'percent' && (float) $return['amount'] > 100 ) {
293
			$return['amount'] = 100;
294
		}
295
296
		// Format dates.
297
		foreach( wpinv_parse_list( 'date_created date_modified expiration start') as $prop ) {
298
			if( ! empty( $return[$prop] ) ) {
299
				$return[$prop]      = date_i18n( 'Y-m-d H:i:s', strtotime( $return[$prop] ) );
300
			}
301
		}
302
303
		// Formart items.
304
		foreach( wpinv_parse_list( 'excluded_items items') as $prop ) {
305
306
			if( ! empty( $return[$prop] ) ) {
307
				// Ensure that the property is an array of non-empty integers.
308
				$return[$prop]      = array_filter( array_map( 'intval', wpinv_parse_list( $return[$prop] ) ) );
309
			} else {
310
				$return[$prop]      = array();
311
			}
312
313
		}
314
		
315
		return apply_filters( 'sanitize_discount_data', $return, $data );
316
	}
317
	
318
	/**
319
	 * Magic method for checking the existence of a certain custom field.
320
	 *
321
	 * @since 1.0.14
322
	 * @access public
323
	 *
324
	 * @return bool Whether the given discount field is set.
325
	 */
326
	public function __isset( $key ){
327
		return isset( $this->data[$key] );
328
	}
329
	
330
	/**
331
	 * Magic method for accessing discount properties.
332
	 *
333
	 * @since 1.0.14
334
	 * @access public
335
	 *
336
	 * @param string $key Discount data to retrieve
337
	 * @return mixed Value of the given discount property (if set).
338
	 */
339
	public function __get( $key ) {
340
		
341
		if ( $key == 'id' ) {
342
			$key = 'ID';
343
		}
344
		
345
		if( method_exists( $this, "get_$key") ) {
346
			$value 	= call_user_func( array( $this, "get_$key" ) );
347
		} else if( isset( $this->data[$key] ) ) {
348
			$value 	= $this->data[$key];
349
		} else {
350
			$value = null;
351
		}
352
		
353
		return apply_filters( "wpinv_get_discount_{$key}", $value, $this->ID, $this, $this->data['code'], $this->data );
354
355
	}
356
	
357
	/**
358
	 * Magic method for setting discount fields.
359
	 *
360
	 * This method does not update custom fields in the database.
361
	 *
362
	 * @since 1.0.14
363
	 * @access public
364
	 *
365
	 */
366
	public function __set( $key, $value ) {
367
		
368
		if ( 'id' == strtolower( $key ) ) {
369
			
370
			$this->ID = $value;
371
			$this->data['ID'] = $value;
372
			return;
373
			
374
		}
375
		
376
		$value = apply_filters( "wpinv_set_discount_{$key}", $value, $this->ID, $this, $this->code, $this->data );
377
		$this->data[$key] = $value;
378
		
379
	}
380
	
381
	/**
382
	 * Saves (or updates) a discount to the database
383
	 *
384
	 * @since 1.0.14
385
	 * @access public
386
	 * @return bool
387
	 *
388
	 */
389
	public function save(){
390
		
391
		$data = self::sanitize_discount_data( $this->data );
392
393
		// Should we create a new post?
394
		if(! $data[ 'ID' ] ) {
395
396
			$id = wp_insert_post( array(
397
				'post_status'           => $data['status'],
398
				'post_type'             => 'wpi_discount',
399
				'post_excerpt'          => $data['description'],
400
			) );
401
402
			if( empty( $id ) ) {
403
				return false;
404
			}
405
406
			$data[ 'ID' ] = $id;
407
			$this->ID = $id;
0 ignored issues
show
Documentation Bug introduced by
It seems like $id can also be of type WP_Error. However, the property $ID is declared as type integer|null. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
408
			$this->data['ID'] = $id;
409
410
		} else {
411
			$this->update_status( $data['post_status'] );
412
		}
413
414
		$meta = apply_filters( 'wpinv_update_discount', $data, $this->ID, $this );
415
416
		do_action( 'wpinv_pre_update_discount', $meta, $this->ID, $this );
417
418
		foreach( wpinv_parse_list( 'ID date_created date_modified status description type_name' ) as $prop ) {
419
			unset( $meta[$prop] );
420
		}
421
422
		if( empty( $meta['uses'] ) ) {
423
			unset( $meta['uses'] );
424
		}
425
426
		// Save the metadata
427
		foreach( $meta as $key => $value ) {
428
			update_post_meta( $this->ID, "_wpi_discount_$key", $value );
0 ignored issues
show
Bug introduced by
It seems like $this->ID can also be of type WP_Error; however, parameter $post_id of update_post_meta() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

428
			update_post_meta( /** @scrutinizer ignore-type */ $this->ID, "_wpi_discount_$key", $value );
Loading history...
429
		}
430
		
431
		// Empty the cache for this discount.
432
		wp_cache_delete( $this->ID, 'WPInv_Discounts' );
0 ignored issues
show
Bug introduced by
It seems like $this->ID can also be of type WP_Error; however, parameter $key of wp_cache_delete() does only seem to accept integer|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

432
		wp_cache_delete( /** @scrutinizer ignore-type */ $this->ID, 'WPInv_Discounts' );
Loading history...
433
		wp_cache_delete( $data['code'], 'WPInv_Discount_Codes' );
434
435
		do_action( 'wpinv_post_update_discount', $meta, $this->ID, $this );
436
437
		$data = self::get_data_by( 'id', $this->ID );
0 ignored issues
show
Bug introduced by
It seems like $this->ID can also be of type WP_Error; however, parameter $value of WPInv_Discount::get_data_by() does only seem to accept integer|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

437
		$data = self::get_data_by( 'id', /** @scrutinizer ignore-type */ $this->ID );
Loading history...
438
		if( is_array( $data ) ) {
439
			$this->init( $data );
440
		} else {
441
			$this->init( array() );
442
		}
443
444
		return true;		
445
	}
446
447
	/**
448
	 * Saves (or updates) a discount to the database
449
	 *
450
	 * @since 1.0.14
451
	 * @access public
452
	 * @return bool
453
	 *
454
	 */
455
	public function update_status( $status = 'publish' ){
456
457
458
		if( $this->exists() && $this->old_status != $status ) {
459
460
			do_action( 'wpinv_pre_update_discount_status', $this->ID, $this->old_status, $status );
461
        	$updated = wp_update_post( array( 'ID' => $this->ID, 'post_status' => $status ) );
462
			do_action( 'wpinv_post_update_discount_status', $this->ID, $this->old_status, $status );
463
464
			wp_cache_delete( $this->ID, 'WPInv_Discounts' );
465
			wp_cache_delete( $this->code, 'WPInv_Discount_Codes' );
466
467
			return $updated !== 0;
468
			
469
		}
470
471
		return false;		
472
	}
473
	
474
	
475
	/**
476
	 * Checks whether a discount exists in the database or not
477
	 * 
478
	 * @since 1.0.14
479
	 */
480
	public function exists(){
481
		return ! empty( $this->ID );
482
	}
483
	
484
	// Boolean methods
485
	
486
	/**
487
	 * Checks the discount type.
488
	 * 
489
	 * 
490
	 * @param  string $type the discount type to check against
491
	 * @since 1.0.14
492
	 * @return bool
493
	 */
494
	public function is_type( $type ) {
495
		return $this->type == $type;
496
	}
497
	
498
	/**
499
	 * Checks whether the discount is published or not
500
	 * 
501
	 * @since 1.0.14
502
	 * @return bool
503
	 */
504
	public function is_active() {
505
		return $this->status == 'publish';
506
	}
507
	
508
	/**
509
	 * Checks whether the discount is has exided the usage limit or not
510
	 * 
511
	 * @since 1.0.14
512
	 * @return bool
513
	 */
514
	public function has_exceeded_limit() {
515
		if( empty( $this->max_uses ) || empty( $this->uses ) ) { 
516
			return false ;
517
		}
518
		
519
		$exceeded =  $this->uses >= $this->max_uses;
520
		return apply_filters( 'wpinv_is_discount_maxed_out', $exceeded, $this->ID, $this, $this->code );
521
	}
522
	
523
	/**
524
	 * Checks if the discount is expired
525
	 * 
526
	 * @since 1.0.14
527
	 * @return bool
528
	 */
529
	public function is_expired() {
530
		$expired = empty ( $this->expires ) ? false : current_time( 'timestamp' ) > strtotime( $this->expires );
0 ignored issues
show
Bug Best Practice introduced by
The property expires does not exist on WPInv_Discount. Since you implemented __get, consider adding a @property annotation.
Loading history...
531
		return apply_filters( 'wpinv_is_discount_expired', $expired, $this->ID, $this, $this->code );
532
	}
533
534
	/**
535
	 * Checks the discount start date.
536
	 * 
537
	 * @since 1.0.14
538
	 * @return bool
539
	 */
540
	public function has_started() {
541
		$started = empty ( $this->start ) ? true : current_time( 'timestamp' ) > strtotime( $this->start );
542
		return apply_filters( 'wpinv_is_discount_started', $started, $this->ID, $this, $this->code );		
543
	}
544
	
545
	/**
546
	 * Check if a discount is valid for a given item id.
547
	 *
548
	 * @param  int|array  $item_ids
549
	 * @since 1.0.14
550
	 * @return boolean
551
	 */
552
	public function is_valid_for_items( $item_ids ) {
553
		 
554
		$item_ids = wpinv_parse_list( $item_ids );
555
		$included = array_intersect( $item_ids, $this->items );
556
		$excluded = array_intersect( $item_ids, $this->excluded_items );
557
558
		if( ! empty( $this->excluded_items ) && ! empty( $excluded ) ) {
559
			return false;
560
		}
561
562
		if( ! empty( $this->items ) && empty( $included ) ) {
563
			return false;
564
		}
565
		return true;
566
	}
567
	
568
	/**
569
	 * Check if a discount is valid for the given amount
570
	 *
571
	 * @param  float  $amount The amount to check against
572
	 * @since 1.0.14
573
	 * @return boolean
574
	 */
575
	public function is_valid_for_amount( $amount ) {
576
577
		$amount = floatval( $amount );
578
579
		// check if it meets the minimum amount valid.
580
		if( $this->min_total > 0 && $amount < $this->min_total ) {
581
			return false;
582
		}
583
584
		// check if it meets the maximum amount valid.
585
		if( $this->max_total > 0 && $amount > $this->max_total ) {
586
			return false;
587
		}
588
589
		return true;
590
	}
591
592
	/**
593
	 * Checks if the minimum amount is met
594
	 *
595
	 * @param  float  $amount The amount to check against
596
	 * @since 1.0.14
597
	 * @return boolean
598
	 */
599
	public function is_minimum_amount_met( $amount ) {
600
		$amount = floatval( $amount );
601
		$min_met= ! ( $this->min_total > 0 && $amount < $this->min_total );
602
		return apply_filters( 'wpinv_is_discount_min_met', $min_met, $this->ID, $this, $this->code, $amount );
603
	}
604
605
	/**
606
	 * Checks if the maximum amount is met
607
	 *
608
	 * @param  float  $amount The amount to check against
609
	 * @since 1.0.14
610
	 * @return boolean
611
	 */
612
	public function is_maximum_amount_met( $amount ) {
613
		$amount = floatval( $amount );
614
		$max_met= ! ( $this->max_total > 0 && $amount > $this->max_total );
615
		return apply_filters( 'wpinv_is_discount_max_met', $max_met, $this->ID, $this, $this->code, $amount );
616
	}
617
618
	/**
619
	 * Check if a discount is valid for the given user
620
	 *
621
	 * @param  int|string  $user
622
	 * @since 1.0.14
623
	 * @return boolean
624
	 */
625
	public function is_valid_for_user( $user ) {
626
		global $wpi_checkout_id;
627
628
		if( empty( $user ) || empty( $this->is_single_use ) ) {
629
			return true;
630
		}
631
632
		$user_id = 0;
633
        if ( is_int( $user ) ) {
634
            $user_id = absint( $user );
635
        } else if ( is_email( $user ) && $user_data = get_user_by( 'email', $user ) ) {
636
            $user_id = $user_data->ID;
637
        } else if ( $user_data = get_user_by( 'login', $user ) ) {
638
            $user_id = $user_data->ID;
639
        } else if ( absint( $user ) > 0 ) {
640
            $user_id = absint( $user );
641
		}
642
643
		if( empty( $user_id ) ) {
644
			return true;
645
		}
646
		
647
		// Get all payments with matching user id
648
        $payments = wpinv_get_invoices( array( 'user' => $user_id, 'limit' => false ) ); 
649
		$code     = strtolower( $this->code );
650
651
		foreach ( $payments as $payment ) {
652
653
			// Don't count discount used for current invoice checkout.
654
			if ( !empty( $wpi_checkout_id ) && $wpi_checkout_id == $payment->ID ) {
655
				continue;
656
			}
657
			
658
			if ( $payment->has_status( array( 'wpi-cancelled', 'wpi-failed' ) ) ) {
659
				continue;
660
			}
661
662
			$discounts = $payment->get_discounts( true );
663
			if ( empty( $discounts ) ) {
664
				continue;
665
			}
666
667
			$discounts = array_map( 'strtolower', wpinv_parse_list( $discounts ) );
668
			if ( ! empty( $discounts ) && in_array( $code, $discounts ) ) {
669
				return false;
670
			}
671
		}
672
673
		return true;
674
	}
675
676
	/**
677
	 * Deletes the discount from the database
678
	 *
679
	 * @since 1.0.14
680
	 * @return boolean
681
	 */
682
	public function remove() {
683
684
		if( empty( $this->ID ) ) {
685
			return true;
686
		}
687
688
		do_action( 'wpinv_pre_delete_discount', $this->ID );
689
		wp_cache_delete( $this->ID, 'WPInv_Discounts' );
690
    	wp_delete_post( $this->ID, true );
691
		wp_cache_delete( $this->code, 'WPInv_Discount_Codes' );
692
    	do_action( 'wpinv_post_delete_discount', $this->ID );
693
694
		$this->ID = null;
695
		$this->data['id'] = null;
696
		return true;
697
	}
698
699
	/**
700
	 * Increases a discount's usage.
701
	 *
702
	 * @since 1.0.14
703
	 * @param int $by The number of usages to increas by.
704
	 * @return bool
705
	 */
706
	public function increase_usage( $by = 1 ) {
707
708
		$this->uses = $this->uses + $by;
709
710
		if( $this->uses  < 0 ) {
711
			$this->uses = 0;
712
		}
713
714
		$this->save();
715
716
		if( $by > 0 ) {
717
			do_action( 'wpinv_discount_increase_use_count', $this->uses, $this->ID, $this->code, $by );
718
		} else {
719
			do_action( 'wpinv_discount_decrease_use_count', $this->uses, $this->ID, $this->code, absint( $by ) );
720
		}
721
		
722
		return $this->uses;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->uses returns the type integer which is incompatible with the documented return type boolean.
Loading history...
723
	}
724
725
	/**
726
	 * Retrieves discount data
727
	 *
728
	 * @since 1.0.14
729
	 * @return array
730
	 */
731
	public function get_data() {
732
		$return = array();
733
		foreach( array_keys( $this->data ) as $key ) {
734
			$return[ $key ] = $this->$key;
735
		}
736
		return $return;
737
	}
738
739
	/**
740
	 * Retrieves discount data as json
741
	 *
742
	 * @since 1.0.14
743
	 * @return string|false
744
	 */
745
	public function get_data_as_json() {
746
		return wp_json_encode( $this->get_data() );
747
	}
748
749
	/**
750
	 * Checks if a discount can only be used once per user.
751
	 *
752
	 * @since 1.0.14
753
	 * @return bool
754
	 */
755
	public function get_is_single_use() {
756
		return (bool) apply_filters( 'wpinv_is_discount_single_use', $this->data['is_single_use'], $this->ID, $this, $this->code );
757
	}
758
759
	/**
760
	 * Checks if a discount is recurring.
761
	 *
762
	 * @since 1.0.14
763
	 * @return bool
764
	 */
765
	public function get_is_recurring() {
766
		return (bool) apply_filters( 'wpinv_is_discount_recurring', $this->data['is_recurring'], $this->ID, $this->code, $this );
767
	}
768
769
	/**
770
	 * Returns a discount's included items.
771
	 *
772
	 * @since 1.0.14
773
	 * @return array
774
	 */
775
	public function get_items() {
776
		return wpinv_parse_list( apply_filters( 'wpinv_get_discount_item_reqs', $this->data['items'], $this->ID, $this, $this->code ) );
777
	}
778
779
	/**
780
	 * Returns a discount's discounted amount.
781
	 *
782
	 * @since 1.0.14
783
	 * @return float
784
	 */
785
	public function get_discounted_amount( $amount ) {
786
787
		if ( $this->type == 'flat' ) {
788
            $amount = $amount - $this->amount;
789
		} else {
790
            $amount = $amount - ( $amount * ( $this->amount / 100 ) );
791
		}
792
793
		if ( $amount < 0 ) {
794
			$amount = 0;
795
		}
796
797
		return apply_filters( 'wpinv_discounted_amount', $amount, $this->ID, $this, $this->code, $this->amount );
798
	}
799
	
800
}
801