Issues (850)

Security Analysis    4 potential vulnerabilities

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

  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.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  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.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  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.
  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.
  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.
  Code Injection (1)
Code Injection enables an attacker to execute arbitrary code on the server.
  Variable Injection (2)
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
  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.
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  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.
  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.
  Cross-Site Scripting (1)
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.
  Header Injection
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/wpinv-subscription.php (7 issues)

1
<?php
2
/**
3
 * Contains the subscription class.
4
 *
5
 * @since 1.0.19
6
 * @package Invoicing
7
 */
8
9
defined( 'ABSPATH' ) || exit;
10
11
/**
12
 * The Subscription Class
13
 *
14
 * @since  1.0.0
15
 */
16
class WPInv_Subscription extends GetPaid_Data {
17
18
	/**
19
	 * Which data store to load.
20
	 *
21
	 * @var string
22
	 */
23
	protected $data_store_name = 'subscription';
24
25
	/**
26
	 * This is the name of this object type.
27
	 *
28
	 * @var string
29
	 */
30
	protected $object_type = 'subscription';
31
32
	/**
33
	 * Item Data array. This is the core item data exposed in APIs.
34
	 *
35
	 * @since 1.0.19
36
	 * @var array
37
	 */
38
	protected $data = array(
39
		'customer_id'       => 0,
40
		'frequency'         => 1,
41
		'period'            => 'D',
42
		'initial_amount'    => null,
43
		'recurring_amount'  => null,
44
		'bill_times'        => 0,
45
		'transaction_id'    => '',
46
		'parent_payment_id' => null,
47
		'product_id'        => 0,
48
		'created'           => '0000-00-00 00:00:00',
49
		'expiration'        => '0000-00-00 00:00:00',
50
		'trial_period'      => '',
51
		'status'            => 'pending',
52
		'profile_id'        => '',
53
		'gateway'           => '',
54
		'customer'          => '',
55
	);
56
57
	/**
58
	 * Stores the status transition information.
59
	 *
60
	 * @since 1.0.19
61
	 * @var bool
62
	 */
63
	protected $status_transition = false;
64
65
	/**
66
	 * Get the subscription if ID is passed, otherwise the subscription is new and empty.
67
	 *
68
	 * @param  int|string|object|WPInv_Subscription $subscription Subscription id, profile_id, or object to read.
69
	 * @param  bool $deprecated
70
	 */
71
	function __construct( $subscription = 0, $deprecated = false ) {
72
73
		parent::__construct( $subscription );
74
75
		if ( ! $deprecated && ! empty( $subscription ) && is_numeric( $subscription ) ) {
76
			$this->set_id( $subscription );
0 ignored issues
show
It seems like $subscription can also be of type string; however, parameter $id of GetPaid_Data::set_id() 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

76
			$this->set_id( /** @scrutinizer ignore-type */ $subscription );
Loading history...
77
		} elseif ( $subscription instanceof self ) {
78
			$this->set_id( $subscription->get_id() );
79
		} elseif ( $deprecated && $subscription_id = self::get_subscription_id_by_field( $subscription, 'profile_id' ) ) {
80
			$this->set_id( $subscription_id );
81
		} elseif ( ! empty( $subscription->id ) ) {
82
			$this->set_id( $subscription->id );
83
		} else {
84
			$this->set_object_read( true );
85
		}
86
87
		// Load the datastore.
88
		$this->data_store = GetPaid_Data_Store::load( $this->data_store_name );
89
90
		if ( $this->get_id() > 0 ) {
91
			$this->data_store->read( $this );
92
		}
93
94
	}
95
96
	/**
97
	 * Given an invoice id, profile id, transaction id, it returns the subscription's id.
98
	 *
99
	 *
100
	 * @static
101
	 * @param string $value
102
	 * @param string $field Either invoice_id, transaction_id or profile_id.
103
	 * @since 1.0.19
104
	 * @return int
105
	 */
106
	public static function get_subscription_id_by_field( $value, $field = 'profile_id' ) {
107
        global $wpdb;
108
109
		// Trim the value.
110
		$value = trim( $value );
111
112
		if ( empty( $value ) ) {
113
			return 0;
114
		}
115
116
		if ( 'invoice_id' == $field ) {
117
			$field = 'parent_payment_id';
118
		}
119
120
        // Valid fields.
121
        $fields = array(
122
			'parent_payment_id',
123
			'transaction_id',
124
			'profile_id',
125
		);
126
127
		// Ensure a field has been passed.
128
		if ( empty( $field ) || ! in_array( $field, $fields ) ) {
129
			return 0;
130
		}
131
132
		// Maybe retrieve from the cache.
133
		$subscription_id   = wp_cache_get( $value, "getpaid_subscription_{$field}s_to_subscription_ids" );
134
		if ( ! empty( $subscription_id ) ) {
135
			return $subscription_id;
136
		}
137
138
        // Fetch from the db.
139
        $table            = $wpdb->prefix . 'wpinv_subscriptions';
140
        $subscription_id  = (int) $wpdb->get_var(
141
            $wpdb->prepare( "SELECT `id` FROM $table WHERE `$field`=%s LIMIT 1", $value )
142
        );
143
144
		if ( empty( $subscription_id ) ) {
145
			return 0;
146
		}
147
148
		// Update the cache with our data.
149
		wp_cache_set( $value, $subscription_id, "getpaid_subscription_{$field}s_to_subscription_ids" );
150
151
		return $subscription_id;
152
	}
153
154
	/**
155
     * Clears the subscription's cache.
156
     */
157
    public function clear_cache() {
158
		$caches = array(
159
			'getpaid_subscription_parent_payment_ids_to_subscription_ids' => $this->get_parent_payment_id(),
160
			'getpaid_subscription_transaction_ids_to_subscription_ids'    => $this->get_transaction_id(),
161
			'getpaid_subscription_profile_ids_to_subscription_ids'        => $this->get_profile_id(),
162
			'getpaid_subscriptions'                                       => $this->get_id(),
163
		);
164
165
		foreach ( $caches as $cache => $value ) {
166
			if ( '' !== $value && false !== $value ) {
167
				wp_cache_delete( $value, $cache );
168
			}
169
		}
170
	}
171
172
	/**
173
     * Checks if a subscription key is set.
174
     */
175
    public function _isset( $key ) {
176
        return isset( $this->data[ $key ] ) || method_exists( $this, "get_$key" );
177
	}
178
179
	/*
180
	|--------------------------------------------------------------------------
181
	| CRUD methods
182
	|--------------------------------------------------------------------------
183
	|
184
	| Methods which create, read, update and delete subscriptions from the database.
185
	|
186
    */
187
188
	/*
189
	|--------------------------------------------------------------------------
190
	| Getters
191
	|--------------------------------------------------------------------------
192
	*/
193
194
	/**
195
	 * Get customer id.
196
	 *
197
	 * @since 1.0.19
198
	 * @param  string $context View or edit context.
199
	 * @return int
200
	 */
201
	public function get_customer_id( $context = 'view' ) {
202
		return (int) $this->get_prop( 'customer_id', $context );
203
	}
204
205
	/**
206
	 * Get customer information.
207
	 *
208
	 * @since 1.0.19
209
	 * @param  string $context View or edit context.
210
	 * @return WP_User|false WP_User object on success, false on failure.
211
	 */
212
	public function get_customer( $context = 'view' ) {
213
		return get_userdata( $this->get_customer_id( $context ) );
214
	}
215
216
	/**
217
	 * Get parent invoice id.
218
	 *
219
	 * @since 1.0.19
220
	 * @param  string $context View or edit context.
221
	 * @return int
222
	 */
223
	public function get_parent_invoice_id( $context = 'view' ) {
224
		return (int) $this->get_prop( 'parent_payment_id', $context );
225
	}
226
227
	/**
228
	 * Alias for self::get_parent_invoice_id().
229
	 *
230
	 * @since 1.0.19
231
	 * @param  string $context View or edit context.
232
	 * @return int
233
	 */
234
    public function get_parent_payment_id( $context = 'view' ) {
235
        return $this->get_parent_invoice_id( $context );
236
	}
237
238
	/**
239
     * Alias for self::get_parent_invoice_id().
240
     *
241
     * @since  1.0.0
242
     * @return int
243
     */
244
    public function get_original_payment_id( $context = 'view' ) {
245
        return $this->get_parent_invoice_id( $context );
246
    }
247
248
	/**
249
	 * Get parent invoice.
250
	 *
251
	 * @since 1.0.19
252
	 * @param  string $context View or edit context.
253
	 * @return WPInv_Invoice
254
	 */
255
	public function get_parent_invoice( $context = 'view' ) {
256
		return new WPInv_Invoice( $this->get_parent_invoice_id( $context ) );
257
	}
258
259
	/**
260
	 * Alias for self::get_parent_invoice().
261
	 *
262
	 * @since 1.0.19
263
	 * @param  string $context View or edit context.
264
	 * @return WPInv_Invoice
265
	 */
266
    public function get_parent_payment( $context = 'view' ) {
267
        return $this->get_parent_invoice( $context );
268
	}
269
270
	/**
271
	 * Get subscription's product id.
272
	 *
273
	 * @since 1.0.19
274
	 * @param  string $context View or edit context.
275
	 * @return int
276
	 */
277
	public function get_product_id( $context = 'view' ) {
278
		return (int) $this->get_prop( 'product_id', $context );
279
	}
280
281
	/**
282
	 * Get the subscription product.
283
	 *
284
	 * @since 1.0.19
285
	 * @param  string $context View or edit context.
286
	 * @return WPInv_Item
287
	 */
288
	public function get_product( $context = 'view' ) {
289
		return new WPInv_Item( $this->get_product_id( $context ) );
290
	}
291
292
	/**
293
	 * Get parent invoice's gateway.
294
	 *
295
	 * Here for backwards compatibility.
296
	 *
297
	 * @since 1.0.19
298
	 * @param  string $context View or edit context.
299
	 * @return string
300
	 */
301
	public function get_gateway( $context = 'view' ) {
302
		return $this->get_parent_invoice( $context )->get_gateway();
303
	}
304
305
	/**
306
	 * Get the period of a renewal.
307
	 *
308
	 * @since 1.0.19
309
	 * @param  string $context View or edit context.
310
	 * @return string
311
	 */
312
	public function get_period( $context = 'view' ) {
313
		return $this->get_prop( 'period', $context );
314
	}
315
316
	/**
317
	 * Get number of periods each renewal is valid for.
318
	 *
319
	 * @since 1.0.19
320
	 * @param  string $context View or edit context.
321
	 * @return int
322
	 */
323
	public function get_frequency( $context = 'view' ) {
324
		return (int) $this->get_prop( 'frequency', $context );
325
	}
326
327
	/**
328
	 * Get the initial amount for the subscription.
329
	 *
330
	 * @since 1.0.19
331
	 * @param  string $context View or edit context.
332
	 * @return float
333
	 */
334
	public function get_initial_amount( $context = 'view' ) {
335
		return (float) wpinv_sanitize_amount( $this->get_prop( 'initial_amount', $context ) );
336
	}
337
338
	/**
339
	 * Get the recurring amount for the subscription.
340
	 *
341
	 * @since 1.0.19
342
	 * @param  string $context View or edit context.
343
	 * @return float
344
	 */
345
	public function get_recurring_amount( $context = 'view' ) {
346
		return (float) wpinv_sanitize_amount( $this->get_prop( 'recurring_amount', $context ) );
347
	}
348
349
	/**
350
	 * Get number of times that this subscription can be renewed.
351
	 *
352
	 * @since 1.0.19
353
	 * @param  string $context View or edit context.
354
	 * @return int
355
	 */
356
	public function get_bill_times( $context = 'view' ) {
357
		return (int) $this->get_prop( 'bill_times', $context );
358
	}
359
360
	/**
361
	 * Get transaction id of this subscription's parent invoice.
362
	 *
363
	 * @since 1.0.19
364
	 * @param  string $context View or edit context.
365
	 * @return string
366
	 */
367
	public function get_transaction_id( $context = 'view' ) {
368
		return $this->get_prop( 'transaction_id', $context );
369
	}
370
371
	/**
372
	 * Get the date that the subscription was created.
373
	 *
374
	 * @since 1.0.19
375
	 * @param  string $context View or edit context.
376
	 * @return string
377
	 */
378
	public function get_created( $context = 'view' ) {
379
		return $this->get_prop( 'created', $context );
380
	}
381
382
	/**
383
	 * Alias for self::get_created().
384
	 *
385
	 * @since 1.0.19
386
	 * @param  string $context View or edit context.
387
	 * @return string
388
	 */
389
	public function get_date_created( $context = 'view' ) {
390
		return $this->get_created( $context );
391
	}
392
393
	/**
394
	 * Retrieves the creation date in a timestamp
395
	 *
396
	 * @since  1.0.0
397
	 * @return int
398
	 */
399
	public function get_time_created() {
400
		$created = $this->get_date_created();
401
		return empty( $created ) ? current_time( 'timestamp' ) : strtotime( $created, current_time( 'timestamp' ) );
402
	}
403
404
	/**
405
	 * Get GMT date when the subscription was created.
406
	 *
407
	 * @since 1.0.19
408
	 * @param  string $context View or edit context.
409
	 * @return string
410
	 */
411
	public function get_date_created_gmt( $context = 'view' ) {
412
        $date = $this->get_date_created( $context );
413
414
        if ( $date ) {
415
            $date = get_gmt_from_date( $date );
416
        }
417
		return $date;
418
	}
419
420
	/**
421
	 * Get the date that the subscription will renew.
422
	 *
423
	 * @since 1.0.19
424
	 * @param  string $context View or edit context.
425
	 * @return string
426
	 */
427
	public function get_next_renewal_date( $context = 'view' ) {
428
		return $this->get_prop( 'expiration', $context );
429
	}
430
431
	/**
432
	 * Alias for self::get_next_renewal_date().
433
	 *
434
	 * @since 1.0.19
435
	 * @param  string $context View or edit context.
436
	 * @return string
437
	 */
438
	public function get_expiration( $context = 'view' ) {
439
		return $this->get_next_renewal_date( $context );
440
	}
441
442
	/**
443
	 * Retrieves the expiration date in a timestamp
444
	 *
445
	 * @since  1.0.0
446
	 * @return int
447
	 */
448
	public function get_expiration_time() {
449
		$expiration = $this->get_expiration();
450
451
		if ( empty( $expiration ) || '0000-00-00 00:00:00' == $expiration ) {
452
			return current_time( 'timestamp' );
453
		}
454
455
		$expiration = strtotime( $expiration, current_time( 'timestamp' ) );
456
		return $expiration < current_time( 'timestamp' ) ? current_time( 'timestamp' ) : $expiration;
457
	}
458
459
	/**
460
	 * Get GMT date when the subscription will renew.
461
	 *
462
	 * @since 1.0.19
463
	 * @param  string $context View or edit context.
464
	 * @return string
465
	 */
466
	public function get_next_renewal_date_gmt( $context = 'view' ) {
467
        $date = $this->get_next_renewal_date( $context );
468
469
        if ( $date ) {
470
            $date = get_gmt_from_date( $date );
471
        }
472
		return $date;
473
	}
474
475
	/**
476
	 * Get the subscription's trial period.
477
	 *
478
	 * @since 1.0.19
479
	 * @param  string $context View or edit context.
480
	 * @return string
481
	 */
482
	public function get_trial_period( $context = 'view' ) {
483
		return $this->get_prop( 'trial_period', $context );
484
	}
485
486
	/**
487
	 * Get the subscription's status.
488
	 *
489
	 * @since 1.0.19
490
	 * @param  string $context View or edit context.
491
	 * @return string
492
	 */
493
	public function get_status( $context = 'view' ) {
494
		return $this->get_prop( 'status', $context );
495
	}
496
497
	/**
498
	 * Get the subscription's profile id.
499
	 *
500
	 * @since 1.0.19
501
	 * @param  string $context View or edit context.
502
	 * @return string
503
	 */
504
	public function get_profile_id( $context = 'view' ) {
505
		return $this->get_prop( 'profile_id', $context );
506
	}
507
508
	/*
509
	|--------------------------------------------------------------------------
510
	| Setters
511
	|--------------------------------------------------------------------------
512
	*/
513
514
	/**
515
	 * Set customer id.
516
	 *
517
	 * @since 1.0.19
518
	 * @param  int $value The customer's id.
519
	 */
520
	public function set_customer_id( $value ) {
521
		$this->set_prop( 'customer_id', (int) $value );
522
	}
523
524
	/**
525
	 * Set parent invoice id.
526
	 *
527
	 * @since 1.0.19
528
	 * @param  int $value The parent invoice id.
529
	 */
530
	public function set_parent_invoice_id( $value ) {
531
		$this->set_prop( 'parent_payment_id', (int) $value );
532
	}
533
534
	/**
535
	 * Alias for self::set_parent_invoice_id().
536
	 *
537
	 * @since 1.0.19
538
	 * @param  int $value The parent invoice id.
539
	 */
540
    public function set_parent_payment_id( $value ) {
541
        $this->set_parent_invoice_id( $value );
542
	}
543
544
	/**
545
     * Alias for self::set_parent_invoice_id().
546
     *
547
     * @since 1.0.19
548
	 * @param  int $value The parent invoice id.
549
     */
550
    public function set_original_payment_id( $value ) {
551
        $this->set_parent_invoice_id( $value );
552
	}
553
554
	/**
555
	 * Set subscription's product id.
556
	 *
557
	 * @since 1.0.19
558
	 * @param  int $value The subscription product id.
559
	 */
560
	public function set_product_id( $value ) {
561
		$this->set_prop( 'product_id', (int) $value );
562
	}
563
564
	/**
565
	 * Set the period of a renewal.
566
	 *
567
	 * @since 1.0.19
568
	 * @param  string $value The renewal period.
569
	 */
570
	public function set_period( $value ) {
571
		$this->set_prop( 'period', $value );
572
	}
573
574
	/**
575
	 * Set number of periods each renewal is valid for.
576
	 *
577
	 * @since 1.0.19
578
	 * @param  int $value The subscription frequency.
579
	 */
580
	public function set_frequency( $value ) {
581
		$value = empty( $value ) ? 1 : (int) $value;
582
		$this->set_prop( 'frequency', absint( $value ) );
583
	}
584
585
	/**
586
	 * Set the initial amount for the subscription.
587
	 *
588
	 * @since 1.0.19
589
	 * @param  float $value The initial subcription amount.
590
	 */
591
	public function set_initial_amount( $value ) {
592
		$this->set_prop( 'initial_amount', wpinv_sanitize_amount( $value ) );
593
	}
594
595
	/**
596
	 * Set the recurring amount for the subscription.
597
	 *
598
	 * @since 1.0.19
599
	 * @param  float $value The recurring subcription amount.
600
	 */
601
	public function set_recurring_amount( $value ) {
602
		$this->set_prop( 'recurring_amount', wpinv_sanitize_amount( $value ) );
603
	}
604
605
	/**
606
	 * Set number of times that this subscription can be renewed.
607
	 *
608
	 * @since 1.0.19
609
	 * @param  int $value Bill times.
610
	 */
611
	public function set_bill_times( $value ) {
612
		$this->set_prop( 'bill_times', (int) $value );
613
	}
614
615
	/**
616
	 * Get transaction id of this subscription's parent invoice.
617
	 *
618
	 * @since 1.0.19
619
	 * @param string $value Bill times.
620
	 */
621
	public function set_transaction_id( $value ) {
622
		$this->set_prop( 'transaction_id', sanitize_text_field( $value ) );
623
	}
624
625
	/**
626
	 * Set date when this subscription started.
627
	 *
628
	 * @since 1.0.19
629
	 * @param string $value strtotime compliant date.
630
	 */
631
	public function set_created( $value ) {
632
        $date = strtotime( $value );
633
634
        if ( $date && $value !== '0000-00-00 00:00:00' ) {
635
            $this->set_prop( 'created', gmdate( 'Y-m-d H:i:s', $date ) );
636
            return;
637
        }
638
639
		$this->set_prop( 'created', '' );
640
641
	}
642
643
	/**
644
	 * Alias for self::set_created().
645
	 *
646
	 * @since 1.0.19
647
	 * @param string $value strtotime compliant date.
648
	 */
649
	public function set_date_created( $value ) {
650
		$this->set_created( $value );
651
    }
652
653
	/**
654
	 * Set the date that the subscription will renew.
655
	 *
656
	 * @since 1.0.19
657
	 * @param string $value strtotime compliant date.
658
	 */
659
	public function set_next_renewal_date( $value ) {
660
		$date = strtotime( $value );
661
662
        if ( $date && $value !== '0000-00-00 00:00:00' ) {
663
            $this->set_prop( 'expiration', gmdate( 'Y-m-d H:i:s', $date ) );
664
            return;
665
		}
666
667
		$this->set_prop( 'expiration', '' );
668
669
	}
670
671
	/**
672
	 * Alias for self::set_next_renewal_date().
673
	 *
674
	 * @since 1.0.19
675
	 * @param string $value strtotime compliant date.
676
	 */
677
	public function set_expiration( $value ) {
678
		$this->set_next_renewal_date( $value );
679
    }
680
681
	/**
682
	 * Set the subscription's trial period.
683
	 *
684
	 * @since 1.0.19
685
	 * @param string $value trial period e.g 1 year.
686
	 */
687
	public function set_trial_period( $value ) {
688
		$this->set_prop( 'trial_period', $value );
689
	}
690
691
	/**
692
	 * Set the subscription's status.
693
	 *
694
	 * @since 1.0.19
695
	 * @param string $new_status    New subscription status.
696
	 */
697
	public function set_status( $new_status ) {
698
699
		// Abort if this is not a valid status;
700
		if ( ! array_key_exists( $new_status, getpaid_get_subscription_statuses() ) ) {
701
			return;
702
		}
703
704
		$old_status = ! empty( $this->status_transition['from'] ) ? $this->status_transition['from'] : $this->get_status();
705
		if ( true === $this->object_read && $old_status !== $new_status ) {
706
			$this->status_transition = array(
0 ignored issues
show
Documentation Bug introduced by
It seems like array('from' => $old_status, 'to' => $new_status) of type array<string,mixed|string> is incompatible with the declared type boolean of property $status_transition.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
707
				'from' => $old_status,
708
				'to'   => $new_status,
709
			);
710
		}
711
712
		$this->set_prop( 'status', $new_status );
713
	}
714
715
	/**
716
	 * Set the subscription's (remote) profile id.
717
	 *
718
	 * @since 1.0.19
719
	 * @param  string $value the remote profile id.
720
	 */
721
	public function set_profile_id( $value ) {
722
		$this->set_prop( 'profile_id', sanitize_text_field( $value ) );
723
	}
724
725
	/*
726
	|--------------------------------------------------------------------------
727
	| Boolean methods
728
	|--------------------------------------------------------------------------
729
	|
730
	| Return true or false.
731
	|
732
	*/
733
734
	/**
735
     * Checks if the subscription has a given status.
736
	 *
737
	 * @param string|array String or array of strings to check for.
738
	 * @return bool
739
     */
740
    public function has_status( $status ) {
741
        return in_array( $this->get_status(), wpinv_clean( wpinv_parse_list( $status ) ) );
742
	}
743
744
	/**
745
     * Checks if the subscription has a trial period.
746
	 *
747
	 * @return bool
748
     */
749
    public function has_trial_period() {
750
		$period = $this->get_trial_period();
751
        return ! empty( $period );
752
	}
753
754
	/**
755
	 * Is the subscription active?
756
	 *
757
	 * @return bool
758
	 */
759
	public function is_active() {
760
		return $this->has_status( 'active trialling' ) && ! $this->is_expired();
761
	}
762
763
	/**
764
	 * Is the subscription expired?
765
	 *
766
	 * @return bool
767
	 */
768
	public function is_expired() {
769
		return $this->has_status( 'expired' ) || ( $this->has_status( 'active cancelled trialling' ) && $this->get_expiration_time() < current_time( 'timestamp' ) );
770
	}
771
772
	/**
773
	 * Is this the last renewals?
774
	 *
775
	 * @return bool
776
	 */
777
	public function is_last_renewal() {
778
		$max_bills = $this->get_bill_times();
779
		return ! empty( $max_bills ) && $max_bills <= $this->get_times_billed();
780
	}
781
782
	/*
783
	|--------------------------------------------------------------------------
784
	| Additional methods
785
	|--------------------------------------------------------------------------
786
	|
787
	| Calculating subscription details.
788
	|
789
	*/
790
791
	/**
792
	 * Backwards compatibilty.
793
	 */
794
	public function create( $data = array() ) {
795
796
		// Set the properties.
797
		if ( is_array( $data ) ) {
798
			$this->set_props( $data );
799
		}
800
801
		// Save the item.
802
		return $this->save();
803
804
	}
805
806
	/**
807
	 * Backwards compatibilty.
808
	 */
809
	public function update( $args = array() ) {
810
		return $this->create( $args );
811
	}
812
813
    /**
814
     * Retrieve renewal payments for a subscription
815
     *
816
     * @since  1.0.0
817
     * @return WP_Post[]
818
     */
819
    public function get_child_payments( $hide_pending = true ) {
820
821
		$statuses = array( 'publish', 'wpi-processing', 'wpi-renewal' );
822
823
		if ( ! $hide_pending ) {
824
			$statuses = array_keys( wpinv_get_invoice_statuses() );
825
		}
826
827
        return get_posts(
0 ignored issues
show
Bug Best Practice introduced by
The expression return get_posts(array('...ype' => 'wpi_invoice')) returns an array which contains values of type integer which are incompatible with the documented value type WP_Post.
Loading history...
828
			array(
829
            	'post_parent' => $this->get_parent_payment_id(),
830
            	'numberposts' => -1,
831
            	'post_status' => $statuses,
832
            	'orderby'     => 'ID',
833
            	'order'       => 'ASC',
834
            	'post_type'   => 'wpi_invoice',
835
			)
836
		);
837
    }
838
839
    /**
840
     * Counts the number of invoices generated for the subscription.
841
     *
842
     * @since  1.0.0
843
     * @return int
844
     */
845
    public function get_total_payments() {
846
		return getpaid_count_subscription_invoices( $this->get_parent_invoice_id(), $this->get_id() );
847
    }
848
849
    /**
850
     * Counts the number of payments for the subscription.
851
     *
852
     * @since  1.0.2
853
     * @return int
854
     */
855
    public function get_times_billed() {
856
        $times_billed = $this->get_total_payments();
857
858
        if ( (float) $this->get_initial_amount() == 0 && $times_billed > 0 ) {
859
            $times_billed--;
860
        }
861
862
        return (int) $times_billed;
863
    }
864
865
    /**
866
     * Records a new payment on the subscription
867
     *
868
     * @since  2.4
869
     * @param  array $args Array of values for the payment, including amount and transaction ID
870
	 * @param  WPInv_Invoice $invoice If adding an existing invoice.
871
     * @return bool
872
     */
873
    public function add_payment( $args = array(), $invoice = false ) {
874
875
		// Process each payment once.
876
        if ( ! empty( $args['transaction_id'] ) && $this->payment_exists( $args['transaction_id'] ) ) {
877
            return false;
878
        }
879
880
		// Are we creating a new invoice?
881
		if ( empty( $invoice ) ) {
882
			$invoice = $this->create_payment( false );
883
884
			if ( empty( $invoice ) ) {
885
				return false;
886
			}
887
		}
888
889
		// Maybe set a transaction id.
890
		if ( ! empty( $args['transaction_id'] ) ) {
891
			$invoice->set_transaction_id( $args['transaction_id'] );
892
		}
893
894
		// Set the completed date.
895
		$invoice->set_completed_date( current_time( 'mysql' ) );
896
897
		// And the gateway.
898
		if ( ! empty( $args['gateway'] ) ) {
899
			$invoice->set_gateway( $args['gateway'] );
900
		}
901
902
		$invoice->set_status( 'wpi-renewal' );
903
		$invoice->save();
904
905
		if ( ! $invoice->exists() ) {
906
			return false;
907
		}
908
909
		return $this->after_add_payment( $invoice );
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->after_add_payment($invoice) returns the type integer which is incompatible with the documented return type boolean.
Loading history...
910
	}
911
912
    public function after_add_payment( $invoice ) {
913
914
		do_action( 'getpaid_after_create_subscription_renewal_invoice', $invoice, $this );
915
		do_action( 'wpinv_recurring_add_subscription_payment', $invoice, $this );
916
        do_action( 'wpinv_recurring_record_payment', $invoice->get_id(), $this->get_parent_invoice_id(), $invoice->get_recurring_total(), $invoice->get_transaction_id() );
917
918
        update_post_meta( $invoice->get_id(), '_wpinv_subscription_id', $this->id );
919
920
        return $invoice->get_id();
921
	}
922
923
	/**
924
     * Creates a new invoice and returns it.
925
     *
926
     * @since  1.0.19
927
	 * @param bool $save Whether we should save the invoice.
928
     * @return WPInv_Invoice|bool
929
     */
930
    public function create_payment( $save = true ) {
931
932
		$parent_invoice = $this->get_parent_payment();
933
934
		if ( ! $parent_invoice->exists() ) {
935
			return false;
936
		}
937
938
		// Duplicate the parent invoice.
939
		$invoice = getpaid_duplicate_invoice( $parent_invoice );
940
		$invoice->set_parent_id( $parent_invoice->get_id() );
941
		$invoice->set_subscription_id( $this->get_id() );
942
		$invoice->set_remote_subscription_id( $this->get_profile_id() );
943
944
		// Set invoice items.
945
		$subscription_group = getpaid_get_invoice_subscription_group( $parent_invoice->get_id(), $this->get_id() );
946
		$allowed_items      = empty( $subscription_group ) ? array( $this->get_product_id() ) : array_keys( $subscription_group['items'] );
947
		$invoice_items      = array();
948
949
		foreach ( $invoice->get_items() as $item ) {
950
			if ( in_array( $item->get_id(), $allowed_items ) ) {
951
				$invoice_items[] = $item;
952
			}
953
		}
954
955
		$invoice->set_items( $invoice_items );
956
957
		if ( ! empty( $subscription_group['fees'] ) ) {
958
			$invoice->set_fees( $subscription_group['fees'] );
959
		}
960
961
		// Maybe recalculate discount (Pre-GetPaid Fix).
962
		$discount = new WPInv_Discount( $invoice->get_discount_code() );
963
964
		if ( $discount->exists() && $discount->is_recurring() ) {
965
			$invoice->add_discount( getpaid_calculate_invoice_discount( $invoice, $discount ) );
966
		}  else {
967
			// Unset discount code.
968
			$invoice->set_discount_code( '' );
969
970
			$invoice->remove_discount( 'discount_code' );
971
		}
972
973
		$invoice->recalculate_total();
974
		$invoice->set_status( 'wpi-pending' );
975
976
		if ( ! $save ) {
977
			return $invoice;
978
		}
979
980
		$invoice->save();
981
982
		return $invoice->exists() ? $invoice : false;
983
    }
984
985
	/**
986
	 * Renews or completes a subscription
987
	 *
988
	 * @since  1.0.0
989
	 * @return int The subscription's id
990
	 */
991
	public function renew( $calculate_from = null, $_new_expiration = null ) {
992
		// Complete subscription if applicable
993
		if ( $this->is_last_renewal() ) {
994
			return $this->complete();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->complete() could also return false which is incompatible with the documented return type integer. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
995
		}
996
997
		if ( ! empty( $_new_expiration ) ) {
998
			$new_expiration = $_new_expiration;
999
		} else {
1000
			// Calculate new expiration
1001
			$frequency      = $this->get_frequency();
1002
			$period         = $this->get_period();
1003
			$calculate_from = empty( $calculate_from ) ? $this->get_expiration_time() : $calculate_from;
1004
			$new_expiration = strtotime( "+ $frequency $period", $calculate_from );
1005
			$new_expiration = date( 'Y-m-d H:i:s', $new_expiration );
1006
		}
1007
1008
		$this->set_expiration( $new_expiration );
1009
		$this->set_status( 'active' );
1010
		$this->save();
1011
1012
		do_action( 'getpaid_subscription_renewed', $this );
1013
1014
		return $this->get_id();
1015
	}
1016
1017
	/**
1018
	 * Marks a subscription as completed
1019
	 *
1020
	 * Subscription is completed when the number of payments matches the billing_times field
1021
	 *
1022
	 * @since  1.0.0
1023
	 * @return int|bool Subscription id or false if the subscription is cancelled.
1024
	 */
1025
	public function complete() {
1026
1027
		// Only mark a subscription as complete if it's not already cancelled.
1028
		if ( $this->has_status( 'cancelled' ) ) {
1029
			return false;
1030
		}
1031
1032
		$this->set_status( 'completed' );
1033
		return $this->save();
1034
1035
	}
1036
1037
	/**
1038
	 * Marks a subscription as expired
1039
	 *
1040
	 * @since  1.0.0
1041
	 * @param  bool $check_expiration
1042
	 * @return int|bool Subscription id or false if $check_expiration is true and expiration date is in the future.
1043
	 */
1044
	public function expire( $check_expiration = false ) {
1045
1046
		if ( $check_expiration && $this->get_expiration_time() > current_time( 'timestamp' ) ) {
1047
			// Do not mark as expired since real expiration date is in the future
1048
			return false;
1049
		}
1050
1051
		$this->set_status( 'expired' );
1052
		return $this->save();
1053
1054
	}
1055
1056
	/**
1057
	 * Marks a subscription as failing
1058
	 *
1059
	 * @since  2.4.2
1060
	 * @return int Subscription id.
1061
	 */
1062
	public function failing() {
1063
		$this->set_status( 'failing' );
1064
		return $this->save();
1065
	}
1066
1067
    /**
1068
     * Marks a subscription as cancelled
1069
     *
1070
     * @since  1.0.0
1071
     * @return int Subscription id.
1072
     */
1073
    public function cancel() {
1074
		$this->set_status( 'cancelled' );
1075
		return $this->save();
1076
    }
1077
1078
	/**
1079
	 * Determines if a subscription can be cancelled both locally and with a payment processor.
1080
	 *
1081
	 * @since  1.0.0
1082
	 * @return bool
1083
	 */
1084
	public function can_cancel() {
1085
		return apply_filters( 'wpinv_subscription_can_cancel', $this->has_status( $this->get_cancellable_statuses() ), $this );
1086
	}
1087
1088
    /**
1089
     * Returns an array of subscription statuses that can be cancelled
1090
     *
1091
     * @access      public
1092
     * @since       1.0.0
1093
     * @return      array
1094
     */
1095
    public function get_cancellable_statuses() {
1096
        return apply_filters( 'wpinv_recurring_cancellable_statuses', array( 'active', 'trialling', 'failing' ) );
1097
    }
1098
1099
	/**
1100
	 * Retrieves the URL to cancel subscription
1101
	 *
1102
	 * @since  1.0.0
1103
	 * @return string
1104
	 */
1105
	public function get_cancel_url() {
1106
		$url = getpaid_get_authenticated_action_url( 'subscription_cancel', $this->get_view_url() );
1107
		return apply_filters( 'wpinv_subscription_cancel_url', $url, $this );
1108
	}
1109
1110
	/**
1111
	 * Retrieves the URL to view a subscription
1112
	 *
1113
	 * @since  1.0.19
1114
	 * @return string
1115
	 */
1116
	public function get_view_url() {
1117
1118
		$url = getpaid_get_tab_url( 'gp-subscriptions', get_permalink( (int) wpinv_get_option( 'invoice_subscription_page' ) ) );
0 ignored issues
show
It seems like get_permalink((int)wpinv...ce_subscription_page')) can also be of type false; however, parameter $default of getpaid_get_tab_url() does only seem to accept 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

1118
		$url = getpaid_get_tab_url( 'gp-subscriptions', /** @scrutinizer ignore-type */ get_permalink( (int) wpinv_get_option( 'invoice_subscription_page' ) ) );
Loading history...
1119
		$url = add_query_arg( 'subscription', $this->get_id(), $url );
1120
1121
		return apply_filters( 'getpaid_get_subscription_view_url', $url, $this );
1122
	}
1123
1124
	/**
1125
	 * Determines if subscription can be manually renewed
1126
	 *
1127
	 * This method is filtered by payment gateways in order to return true on subscriptions
1128
	 * that can be renewed manually
1129
	 *
1130
	 * @since  2.5
1131
	 * @return bool
1132
	 */
1133
	public function can_renew() {
1134
		return apply_filters( 'wpinv_subscription_can_renew', true, $this );
1135
	}
1136
1137
	/**
1138
	 * Retrieves the URL to renew a subscription
1139
	 *
1140
	 * @since  2.5
1141
	 * @return string
1142
	 */
1143
	public function get_renew_url() {
1144
		$url = wp_nonce_url(
1145
            add_query_arg(
1146
                array(
1147
					'getpaid-action' => 'renew_subscription',
1148
					'sub_id'         => $this->get_id,
0 ignored issues
show
Bug Best Practice introduced by
The property get_id does not exist on WPInv_Subscription. Since you implemented __get, consider adding a @property annotation.
Loading history...
1149
                )
1150
            ),
1151
            'getpaid-nonce'
1152
        );
1153
		return apply_filters( 'wpinv_subscription_renew_url', $url, $this );
1154
	}
1155
1156
	/**
1157
	 * Determines if subscription can have their payment method updated
1158
	 *
1159
	 * @since  1.0.0
1160
	 * @return bool
1161
	 */
1162
	public function can_update() {
1163
		return apply_filters( 'wpinv_subscription_can_update', false, $this );
1164
	}
1165
1166
	/**
1167
	 * Retrieves the URL to update subscription
1168
	 *
1169
	 * @since  1.0.0
1170
	 * @return string
1171
	 */
1172
	public function get_update_url() {
1173
		$url = add_query_arg(
1174
            array(
1175
				'action'          => 'update',
1176
				'subscription_id' => $this->get_id(),
1177
            )
1178
        );
1179
		return apply_filters( 'wpinv_subscription_update_url', $url, $this );
1180
	}
1181
1182
	/**
1183
	 * Retrieves the subscription status label
1184
	 *
1185
	 * @since  1.0.0
1186
	 * @return string
1187
	 */
1188
	public function get_status_label() {
1189
		return getpaid_get_subscription_status_label( $this->get_status() );
1190
	}
1191
1192
	/**
1193
	 * Retrieves the subscription status class
1194
	 *
1195
	 * @since  1.0.19
1196
	 * @return string
1197
	 */
1198
	public function get_status_class() {
1199
		$statuses = getpaid_get_subscription_status_classes();
1200
		return isset( $statuses[ $this->get_status() ] ) ? $statuses[ $this->get_status() ] : 'bg-dark';
1201
	}
1202
1203
    /**
1204
     * Retrieves the subscription status label
1205
     *
1206
     * @since  1.0.0
1207
     * @return string
1208
     */
1209
    public function get_status_label_html() {
1210
1211
		$status_label = sanitize_text_field( $this->get_status_label() );
1212
		$class        = esc_attr( $this->get_status_class() );
1213
		$status       = sanitize_html_class( $this->get_status() );
1214
1215
		return "<span class='bsui'><span class='badge $class $status text-white'>$status_label</span></span>";
1216
    }
1217
1218
    /**
1219
     * Determines if a payment exists with the specified transaction ID
1220
     *
1221
     * @since  2.4
1222
     * @param  string $txn_id The transaction ID from the merchant processor
1223
     * @return bool
1224
     */
1225
    public function payment_exists( $txn_id = '' ) {
1226
		$invoice_id = WPInv_Invoice::get_invoice_id_by_field( $txn_id, 'transaction_id' );
1227
        return ! empty( $invoice_id );
1228
	}
1229
1230
	/**
1231
	 * Handle the status transition.
1232
	 */
1233
	protected function status_transition() {
1234
		$status_transition = $this->status_transition;
1235
1236
		// Reset status transition variable.
1237
		$this->status_transition = false;
1238
1239
		if ( $status_transition ) {
1240
			try {
1241
1242
				// Fire a hook for the status change.
1243
				do_action( 'wpinv_subscription_' . $status_transition['to'], $this->get_id(), $this, $status_transition );
1244
				do_action( 'getpaid_subscription_' . $status_transition['to'], $this, $status_transition );
1245
1246
				if ( ! empty( $status_transition['from'] ) ) {
1247
1248
					/* translators: 1: old subscription status 2: new subscription status */
1249
					$transition_note = sprintf( __( 'Subscription status changed from %1$s to %2$s.', 'invoicing' ), getpaid_get_subscription_status_label( $status_transition['from'] ), getpaid_get_subscription_status_label( $status_transition['to'] ) );
1250
1251
					// Note the transition occurred.
1252
					$this->get_parent_payment()->add_note( $transition_note, false, false, true );
1253
1254
					// Fire another hook.
1255
					do_action( 'getpaid_subscription_status_' . $status_transition['from'] . '_to_' . $status_transition['to'], $this->get_id(), $this );
1256
					do_action( 'getpaid_subscription_status_changed', $this, $status_transition['from'], $status_transition['to'] );
1257
1258
				} else {
1259
					/* translators: %s: new invoice status */
1260
					$transition_note = sprintf( __( 'Subscription status set to %s.', 'invoicing' ), getpaid_get_subscription_status_label( $status_transition['to'] ) );
1261
1262
					// Note the transition occurred.
1263
					$this->get_parent_payment()->add_note( $transition_note, false, false, true );
1264
1265
				}
1266
			} catch ( Exception $e ) {
1267
				$this->get_parent_payment()->add_note( __( 'Error during subscription status transition.', 'invoicing' ) . ' ' . $e->getMessage() );
1268
			}
1269
		}
1270
1271
	}
1272
1273
	/**
1274
	 * Save data to the database.
1275
	 *
1276
	 * @since 1.0.19
1277
	 * @return int subscription ID
1278
	 */
1279
	public function save() {
1280
		parent::save();
1281
		$this->status_transition();
1282
		return $this->get_id();
1283
	}
1284
1285
	/**
1286
	 * Activates a subscription.
1287
	 *
1288
	 * @since 1.0.19
1289
	 * @return int subscription ID
1290
	 */
1291
	public function activate() {
1292
		$status = $this->has_trial_period() && 'trialling' === $this->get_status() ? 'trialling' : 'active';
1293
		$this->set_status( $status );
1294
		return $this->save();
1295
	}
1296
1297
}
1298