Issues (1282)

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-give-donor.php (15 issues)

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
 * Donor
4
 *
5
 * @package     Give
6
 * @subpackage  Classes/Give_Donor
7
 * @copyright   Copyright (c) 2016, GiveWP
8
 * @license     https://opensource.org/licenses/gpl-license GNU Public License
9
 * @since       1.0
10
 */
11
12
// Exit if accessed directly.
13
if ( ! defined( 'ABSPATH' ) ) {
14
	exit;
15
}
16
17
/**
18
 * Give_Donor Class
19
 *
20
 * This class handles customers.
21
 *
22
 * @since 1.0
23
 */
24
class Give_Donor {
25
26
	/**
27
	 * The donor ID
28
	 *
29
	 * @since  1.0
30
	 * @access public
31
	 *
32
	 * @var    int
33
	 */
34
	public $id = 0;
35
36
	/**
37
	 * The donor's donation count.
38
	 *
39
	 * @since  1.0
40
	 * @access public
41
	 *
42
	 * @var    int
43
	 */
44
	public $purchase_count = 0;
45
46
	/**
47
	 * The donor's lifetime value.
48
	 *
49
	 * @since  1.0
50
	 * @access public
51
	 *
52
	 * @var    int
53
	 */
54
	public $purchase_value = 0;
55
56
	/**
57
	 * The donor's email.
58
	 *
59
	 * @since  1.0
60
	 * @access public
61
	 *
62
	 * @var    string
63
	 */
64
	public $email;
65
66
	/**
67
	 * The donor's emails.
68
	 *
69
	 * @since  1.7
70
	 * @access public
71
	 *
72
	 * @var    array
73
	 */
74
	public $emails;
75
76
	/**
77
	 * The donor's name.
78
	 *
79
	 * @since  1.0
80
	 * @access public
81
	 *
82
	 * @var    string
83
	 */
84
	public $name;
85
86
	/**
87
	 * The donor creation date.
88
	 *
89
	 * @since  1.0
90
	 * @access public
91
	 *
92
	 * @var    string
93
	 */
94
	public $date_created;
95
96
	/**
97
	 * The payment IDs associated with the donor.
98
	 *
99
	 * @since  1.0
100
	 * @access public
101
	 *
102
	 * @var    string
103
	 */
104
	public $payment_ids;
105
106
	/**
107
	 * The user ID associated with the donor.
108
	 *
109
	 * @since  1.0
110
	 * @access public
111
	 *
112
	 * @var    int
113
	 */
114
	public $user_id;
115
116
	/**
117
	 * Donor notes saved by admins.
118
	 *
119
	 * @since  1.0
120
	 * @access public
121
	 *
122
	 * @var    array
123
	 */
124
	protected $notes = null;
125
126
	/**
127
	 * Donor address.
128
	 *
129
	 * @since  1.0
130
	 * @access public
131
	 *
132
	 * @var    array
133
	 */
134
	public $address = array();
135
136
	/**
137
	 * The Database Abstraction
138
	 *
139
	 * @since  1.0
140
	 * @access protected
141
	 *
142
	 * @var    Give_DB_Donors
143
	 */
144
	protected $db;
145
146
	/**
147
	 * Give_Donor constructor.
148
	 *
149
	 * @param int|bool $_id_or_email
150
	 * @param bool     $by_user_id
151
	 */
152
	public function __construct( $_id_or_email = false, $by_user_id = false ) {
153
154
		$this->db = Give()->donors;
155
156
		if ( false === $_id_or_email || ( is_numeric( $_id_or_email ) && (int) $_id_or_email !== absint( $_id_or_email ) ) ) {
157
			return false;
0 ignored issues
show
Constructors do not have meaningful return values, anything that is returned from here is discarded. Are you sure this is correct?
Loading history...
158
		}
159
160
		$by_user_id = is_bool( $by_user_id ) ? $by_user_id : false;
161
162
		if ( is_numeric( $_id_or_email ) ) {
163
			$field = $by_user_id ? 'user_id' : 'id';
164
		} else {
165
			$field = 'email';
166
		}
167
168
		$donor = $this->db->get_donor_by( $field, $_id_or_email );
169
170
		if ( empty( $donor ) || ! is_object( $donor ) ) {
171
			return false;
0 ignored issues
show
Constructors do not have meaningful return values, anything that is returned from here is discarded. Are you sure this is correct?
Loading history...
172
		}
173
174
		$this->setup_donor( $donor );
175
176
	}
177
178
	/**
179
	 * Setup Donor
180
	 *
181
	 * Set donor variables.
182
	 *
183
	 * @since  1.0
184
	 * @access private
185
	 *
186
	 * @param  object $donor The Donor Object.
187
	 *
188
	 * @return bool             If the setup was successful or not.
189
	 */
190
	private function setup_donor( $donor ) {
191
192
		if ( ! is_object( $donor ) ) {
193
			return false;
194
		}
195
196
		// Get cached donors.
197
		$donor_vars = Give_Cache::get_group( $donor->id, 'give-donors' );
198
199
		if ( is_null( $donor_vars ) ) {
200
			foreach ( $donor as $key => $value ) {
201
202
				switch ( $key ) {
203
204
					// @todo We will remove this statement when we will remove notes column from donor table
205
					// https://github.com/impress-org/give/issues/3632
206
					case 'notes':
207
						break;
208
209
					default:
210
						$this->$key = $value;
211
						break;
212
213
				}
214
			}
215
216
			// Get donor's all email including primary email.
217
			$this->emails = (array) $this->get_meta( 'additional_email', false );
218
			$this->emails = array( 'primary' => $this->email ) + $this->emails;
219
220
			$this->setup_address();
221
222
			Give_Cache::set_group( $donor->id, get_object_vars( $this ), 'give-donors' );
223
		} else {
224
			foreach ( $donor_vars as $donor_var => $value ) {
0 ignored issues
show
The expression $donor_vars of type object|integer|double|string|array|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
225
				$this->$donor_var = $value;
226
			}
227
		}
228
229
		// Donor ID and email are the only things that are necessary, make sure they exist.
230
		if ( ! empty( $this->id ) && ! empty( $this->email ) ) {
231
			return true;
232
		}
233
234
		return false;
235
236
	}
237
238
239
	/**
240
	 * Setup donor address.
241
	 *
242
	 * @since  2.0
243
	 * @access public
244
	 */
245
	public function setup_address() {
246
		global $wpdb;
247
		$meta_type = Give()->donor_meta->meta_type;
248
249
		$addresses = $this->get_addresses_from_meta_cache();
250
251
		$addresses = ! empty( $addresses )
252
			? $addresses
253
			: $wpdb->get_results( $wpdb->prepare( "
254
				SELECT meta_key, meta_value FROM {$wpdb->donormeta}
255
				WHERE meta_key
256
				LIKE '%%%s%%'
257
				AND {$meta_type}_id=%d
258
				", 'give_donor_address', $this->id ), ARRAY_N );
259
260
		if ( empty( $addresses ) ) {
261
			return $this->address;
262
		}
263
264
		foreach ( $addresses as $address ) {
265
			$address[0] = str_replace( '_give_donor_address_', '', $address[0] );
266
			$address[0] = explode( '_', $address[0] );
267
268
			if ( 3 === count( $address[0] ) ) {
269
				$this->address[ $address[0][0] ][ $address[0][2] ][ $address[0][1] ] = $address[1];
270
			} else {
271
				$this->address[ $address[0][0] ][ $address[0][1] ] = $address[1];
272
			}
273
		}
274
	}
275
276
277
	/**
278
	 * Get addresses from meta cache
279
	 *
280
	 * @since 2.5.0
281
	 * @return array
282
	 */
283
	private function get_addresses_from_meta_cache() {
284
		$meta      = wp_cache_get( $this->id, 'donor_meta' );
285
		$addresses = array();
286
287
		if ( ! empty( $meta ) ) {
288
			foreach ( $meta as $meta_key => $meta_value ) {
289
				if ( false === strpos( $meta_key, 'give_donor_address' ) ) {
290
					continue;
291
				}
292
293
				$addresses[] = array( $meta_key, current( $meta_value ) );
294
			}
295
		}
296
297
		return $addresses;
298
	}
299
300
	/**
301
	 * Returns the saved address for a donor
302
	 *
303
	 * @access public
304
	 *
305
	 * @since  2.1.3
306
	 *
307
	 * @param array $args donor address.
308
	 *
309
	 * @return array The donor's address, if any
310
	 */
311
	public function get_donor_address( $args = array() ) {
312
		$args = wp_parse_args(
313
			$args,
314
			array(
315
				'address_type' => 'billing',
316
			)
317
		);
318
319
		$default_address = array(
320
			'line1'   => '',
321
			'line2'   => '',
322
			'city'    => '',
323
			'state'   => '',
324
			'country' => '',
325
			'zip'     => '',
326
		);
327
328
		// Backward compatibility.
329 View Code Duplication
		if ( ! give_has_upgrade_completed( 'v20_upgrades_user_address' ) ) {
0 ignored issues
show
This code seems to be duplicated across your project.

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

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

Loading history...
330
331
			// Backward compatibility for user id param.
332
			return wp_parse_args( (array) get_user_meta( $this->user_id, '_give_user_address', true ), $default_address );
333
334
		}
335
336
		if ( ! $this->id || empty( $this->address ) || ! array_key_exists( $args['address_type'], $this->address ) ) {
337
			return $default_address;
338
		}
339
340
		switch ( true ) {
341
			case is_string( end( $this->address[ $args['address_type'] ] ) ):
342
				$address = wp_parse_args( $this->address[ $args['address_type'] ], $default_address );
343
				break;
344
345 View Code Duplication
			case is_array( end( $this->address[ $args['address_type'] ] ) ):
0 ignored issues
show
This code seems to be duplicated across your project.

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

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

Loading history...
346
				$address = wp_parse_args( array_shift( $this->address[ $args['address_type'] ] ), $default_address );
347
				break;
348
		}
349
350
		return $address;
351
	}
352
353
	/**
354
	 * Magic __get function to dispatch a call to retrieve a private property.
355
	 *
356
	 * @since  1.0
357
	 * @access public
358
	 *
359
	 * @param $key
360
	 *
361
	 * @return mixed|\WP_Error
362
	 */
363
	public function __get( $key ) {
364
365
		if ( method_exists( $this, 'get_' . $key ) ) {
366
367
			return call_user_func( array( $this, 'get_' . $key ) );
368
369
		} else {
370
371
			/* translators: %s: property key */
372
			return new WP_Error( 'give-donor-invalid-property', sprintf( esc_html__( 'Can\'t get property %s.', 'give' ), $key ) );
373
374
		}
375
376
	}
377
378
	/**
379
	 * Creates a donor.
380
	 *
381
	 * @since  1.0
382
	 * @access public
383
	 *
384
	 * @param  array $data Array of attributes for a donor.
385
	 *
386
	 * @return bool|int    False if not a valid creation, donor ID if user is found or valid creation.
387
	 */
388
	public function create( $data = array() ) {
389
390
		if ( $this->id != 0 || empty( $data ) ) {
391
			return false;
392
		}
393
394
		$defaults = array(
395
			'payment_ids' => '',
396
		);
397
398
		$args = wp_parse_args( $data, $defaults );
399
		$args = $this->sanitize_columns( $args );
400
401
		if ( empty( $args['email'] ) || ! is_email( $args['email'] ) ) {
402
			return false;
403
		}
404
405 View Code Duplication
		if ( ! empty( $args['payment_ids'] ) && is_array( $args['payment_ids'] ) ) {
0 ignored issues
show
This code seems to be duplicated across your project.

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

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

Loading history...
406
			$args['payment_ids'] = implode( ',', array_unique( array_values( $args['payment_ids'] ) ) );
407
		}
408
409
		/**
410
		 * Fires before creating donors.
411
		 *
412
		 * @since 1.0
413
		 *
414
		 * @param array $args Donor attributes.
415
		 */
416
		do_action( 'give_donor_pre_create', $args );
417
418
		$created = false;
419
420
		// The DB class 'add' implies an update if the donor being asked to be created already exists
421 View Code Duplication
		if ( $this->db->add( $data ) ) {
0 ignored issues
show
This code seems to be duplicated across your project.

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

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

Loading history...
422
423
			// We've successfully added/updated the donor, reset the class vars with the new data
424
			$donor = $this->db->get_donor_by( 'email', $args['email'] );
425
426
			// Setup the donor data with the values from DB
427
			$this->setup_donor( $donor );
428
429
			$created = $this->id;
430
		}
431
432
		/**
433
		 * Fires after creating donors.
434
		 *
435
		 * @since 1.0
436
		 *
437
		 * @param bool|int $created False if not a valid creation, donor ID if user is found or valid creation.
438
		 * @param array    $args    Customer attributes.
439
		 */
440
		do_action( 'give_donor_post_create', $created, $args );
441
442
		return $created;
443
444
	}
445
446
	/**
447
	 * Updates a donor record.
448
	 *
449
	 * @since  1.0
450
	 * @access public
451
	 *
452
	 * @param  array $data Array of data attributes for a donor (checked via whitelist).
453
	 *
454
	 * @return bool        If the update was successful or not.
455
	 */
456
	public function update( $data = array() ) {
457
458
		if ( empty( $data ) ) {
459
			return false;
460
		}
461
462
		$data = $this->sanitize_columns( $data );
463
464
		/**
465
		 * Fires before updating donors.
466
		 *
467
		 * @since 1.0
468
		 *
469
		 * @param int   $donor_id Donor id.
470
		 * @param array $data     Donor attributes.
471
		 */
472
		do_action( 'give_donor_pre_update', $this->id, $data );
473
474
		$updated = false;
475
476 View Code Duplication
		if ( $this->db->update( $this->id, $data ) ) {
0 ignored issues
show
This code seems to be duplicated across your project.

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

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

Loading history...
477
478
			$donor = $this->db->get_donor_by( 'id', $this->id );
479
480
			$this->setup_donor( $donor );
481
482
			$updated = true;
483
		}
484
485
		/**
486
		 * Fires after updating donors.
487
		 *
488
		 * @since 1.0
489
		 *
490
		 * @param bool  $updated  If the update was successful or not.
491
		 * @param int   $donor_id Donor id.
492
		 * @param array $data     Donor attributes.
493
		 */
494
		do_action( 'give_donor_post_update', $updated, $this->id, $data );
495
496
		return $updated;
497
	}
498
499
	/**
500
	 * Attach Payment
501
	 *
502
	 * Attach payment to the donor then triggers increasing stats.
503
	 *
504
	 * @since  1.0
505
	 * @access public
506
	 *
507
	 * @param  int  $payment_id   The payment ID to attach to the donor.
508
	 * @param  bool $update_stats For backwards compatibility, if we should increase the stats or not.
509
	 *
510
	 * @return bool            If the attachment was successfully.
511
	 */
512
	public function attach_payment( $payment_id = 0, $update_stats = true ) {
513
514
		if ( empty( $payment_id ) ) {
515
			return false;
516
		}
517
518
		if ( empty( $this->payment_ids ) ) {
519
520
			$new_payment_ids = $payment_id;
521
522
		} else {
523
524
			$payment_ids = array_map( 'absint', explode( ',', $this->payment_ids ) );
525
526
			if ( in_array( $payment_id, $payment_ids ) ) {
527
				$update_stats = false;
528
			}
529
530
			$payment_ids[] = $payment_id;
531
532
			$new_payment_ids = implode( ',', array_unique( array_values( $payment_ids ) ) );
533
534
		}
535
536
		/**
537
		 * Fires before attaching payments to donors.
538
		 *
539
		 * @since 1.0
540
		 *
541
		 * @param int $payment_id Payment id.
542
		 * @param int $donor_id   Donor id.
543
		 */
544
		do_action( 'give_donor_pre_attach_payment', $payment_id, $this->id );
545
546
		$payment_added = $this->update( array( 'payment_ids' => $new_payment_ids ) );
547
548 View Code Duplication
		if ( $payment_added ) {
0 ignored issues
show
This code seems to be duplicated across your project.

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

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

Loading history...
549
550
			$this->payment_ids = $new_payment_ids;
0 ignored issues
show
Documentation Bug introduced by
It seems like $new_payment_ids can also be of type integer. However, the property $payment_ids is declared as type string. 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...
551
552
			// We added this payment successfully, increment the stats
553
			if ( $update_stats ) {
554
				$payment_amount = give_donation_amount( $payment_id, array( 'type' => 'stats' ) );
555
556
				if ( ! empty( $payment_amount ) ) {
557
					$this->increase_value( $payment_amount );
558
				}
559
560
				$this->increase_purchase_count();
561
			}
562
		}
563
564
		/**
565
		 * Fires after attaching payments to the donor.
566
		 *
567
		 * @since 1.0
568
		 *
569
		 * @param bool $payment_added If the attachment was successfully.
570
		 * @param int  $payment_id    Payment id.
571
		 * @param int  $donor_id      Donor id.
572
		 */
573
		do_action( 'give_donor_post_attach_payment', $payment_added, $payment_id, $this->id );
574
575
		return $payment_added;
576
	}
577
578
	/**
579
	 * Remove Payment
580
	 *
581
	 * Remove a payment from this donor, then triggers reducing stats.
582
	 *
583
	 * @since  1.0
584
	 * @access public
585
	 *
586
	 * @param  int  $payment_id   The Payment ID to remove.
587
	 * @param  bool $update_stats For backwards compatibility, if we should increase the stats or not.
588
	 *
589
	 * @return boolean               If the removal was successful.
590
	 */
591
	public function remove_payment( $payment_id = 0, $update_stats = true ) {
592
593
		if ( empty( $payment_id ) ) {
594
			return false;
595
		}
596
597
		$payment = new Give_Payment( $payment_id );
598
599
		if ( 'publish' !== $payment->status && 'revoked' !== $payment->status ) {
600
			$update_stats = false;
601
		}
602
603
		$new_payment_ids = '';
604
605
		if ( ! empty( $this->payment_ids ) ) {
606
607
			$payment_ids = array_map( 'absint', explode( ',', $this->payment_ids ) );
608
609
			$pos = array_search( $payment_id, $payment_ids );
610
			if ( false === $pos ) {
611
				return false;
612
			}
613
614
			unset( $payment_ids[ $pos ] );
615
			$payment_ids = array_filter( $payment_ids );
616
617
			$new_payment_ids = implode( ',', array_unique( array_values( $payment_ids ) ) );
618
619
		}
620
621
		/**
622
		 * Fires before removing payments from customers.
623
		 *
624
		 * @since 1.0
625
		 *
626
		 * @param int $payment_id Payment id.
627
		 * @param int $donor_id   Customer id.
628
		 */
629
		do_action( 'give_donor_pre_remove_payment', $payment_id, $this->id );
630
631
		$payment_removed = $this->update( array( 'payment_ids' => $new_payment_ids ) );
632
633 View Code Duplication
		if ( $payment_removed ) {
0 ignored issues
show
This code seems to be duplicated across your project.

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

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

Loading history...
634
635
			$this->payment_ids = $new_payment_ids;
636
637
			if ( $update_stats ) {
638
				// We removed this payment successfully, decrement the stats
639
				$payment_amount = give_donation_amount( $payment_id );
640
641
				if ( ! empty( $payment_amount ) ) {
642
					$this->decrease_value( $payment_amount );
643
				}
644
645
				$this->decrease_donation_count();
646
			}
647
		}
648
649
		/**
650
		 * Fires after removing payments from donors.
651
		 *
652
		 * @since 1.0
653
		 *
654
		 * @param bool $payment_removed If the removal was successfully.
655
		 * @param int  $payment_id      Payment id.
656
		 * @param int  $donor_id        Donor id.
657
		 */
658
		do_action( 'give_donor_post_remove_payment', $payment_removed, $payment_id, $this->id );
659
660
		return $payment_removed;
661
662
	}
663
664
	/**
665
	 * Increase the donation count of a donor.
666
	 *
667
	 * @since  1.0
668
	 * @access public
669
	 *
670
	 * @param  int $count The number to increase by.
671
	 *
672
	 * @return int        The donation count.
673
	 */
674
	public function increase_purchase_count( $count = 1 ) {
675
676
		// Make sure it's numeric and not negative.
677
		if ( ! is_numeric( $count ) || $count != absint( $count ) ) {
678
			return false;
679
		}
680
681
		$new_total = (int) $this->purchase_count + (int) $count;
682
683
		/**
684
		 * Fires before increasing the donor's donation count.
685
		 *
686
		 * @since 1.0
687
		 *
688
		 * @param int $count    The number to increase by.
689
		 * @param int $donor_id Donor id.
690
		 */
691
		do_action( 'give_donor_pre_increase_donation_count', $count, $this->id );
692
693
		if ( $this->update( array( 'purchase_count' => $new_total ) ) ) {
694
			$this->purchase_count = $new_total;
695
		}
696
697
		/**
698
		 * Fires after increasing the donor's donation count.
699
		 *
700
		 * @since 1.0
701
		 *
702
		 * @param int $purchase_count Donor donation count.
703
		 * @param int $count          The number increased by.
704
		 * @param int $donor_id       Donor id.
705
		 */
706
		do_action( 'give_donor_post_increase_donation_count', $this->purchase_count, $count, $this->id );
707
708
		return $this->purchase_count;
709
	}
710
711
	/**
712
	 * Decrease the donor donation count.
713
	 *
714
	 * @since  1.0
715
	 * @access public
716
	 *
717
	 * @param  int $count The amount to decrease by.
718
	 *
719
	 * @return mixed      If successful, the new count, otherwise false.
720
	 */
721
	public function decrease_donation_count( $count = 1 ) {
722
723
		// Make sure it's numeric and not negative
724
		if ( ! is_numeric( $count ) || $count != absint( $count ) ) {
725
			return false;
726
		}
727
728
		$new_total = (int) $this->purchase_count - (int) $count;
729
730
		if ( $new_total < 0 ) {
731
			$new_total = 0;
732
		}
733
734
		/**
735
		 * Fires before decreasing the donor's donation count.
736
		 *
737
		 * @since 1.0
738
		 *
739
		 * @param int $count    The number to decrease by.
740
		 * @param int $donor_id Customer id.
741
		 */
742
		do_action( 'give_donor_pre_decrease_donation_count', $count, $this->id );
743
744
		if ( $this->update( array( 'purchase_count' => $new_total ) ) ) {
745
			$this->purchase_count = $new_total;
746
		}
747
748
		/**
749
		 * Fires after decreasing the donor's donation count.
750
		 *
751
		 * @since 1.0
752
		 *
753
		 * @param int $purchase_count Donor's donation count.
754
		 * @param int $count          The number decreased by.
755
		 * @param int $donor_id       Donor id.
756
		 */
757
		do_action( 'give_donor_post_decrease_donation_count', $this->purchase_count, $count, $this->id );
758
759
		return $this->purchase_count;
760
	}
761
762
	/**
763
	 * Increase the donor's lifetime value.
764
	 *
765
	 * @since  1.0
766
	 * @access public
767
	 *
768
	 * @param  float $value The value to increase by.
769
	 *
770
	 * @return mixed        If successful, the new value, otherwise false.
771
	 */
772
	public function increase_value( $value = 0.00 ) {
773
774
		$new_value = floatval( $this->purchase_value ) + $value;
775
776
		/**
777
		 * Fires before increasing donor lifetime value.
778
		 *
779
		 * @since 1.0
780
		 *
781
		 * @param float $value    The value to increase by.
782
		 * @param int   $donor_id Customer id.
783
		 */
784
		do_action( 'give_donor_pre_increase_value', $value, $this->id );
785
786
		if ( $this->update( array( 'purchase_value' => $new_value ) ) ) {
787
			$this->purchase_value = $new_value;
0 ignored issues
show
Documentation Bug introduced by
The property $purchase_value was declared of type integer, but $new_value is of type double. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
788
		}
789
790
		/**
791
		 * Fires after increasing donor lifetime value.
792
		 *
793
		 * @since 1.0
794
		 *
795
		 * @param float $purchase_value Donor's lifetime value.
796
		 * @param float $value          The value increased by.
797
		 * @param int   $donor_id       Donor id.
798
		 */
799
		do_action( 'give_donor_post_increase_value', $this->purchase_value, $value, $this->id );
800
801
		return $this->purchase_value;
802
	}
803
804
	/**
805
	 * Decrease a donor's lifetime value.
806
	 *
807
	 * @since  1.0
808
	 * @access public
809
	 *
810
	 * @param  float $value The value to decrease by.
811
	 *
812
	 * @return mixed        If successful, the new value, otherwise false.
813
	 */
814
	public function decrease_value( $value = 0.00 ) {
815
816
		$new_value = floatval( $this->purchase_value ) - $value;
817
818
		if ( $new_value < 0 ) {
819
			$new_value = 0.00;
820
		}
821
822
		/**
823
		 * Fires before decreasing donor lifetime value.
824
		 *
825
		 * @since 1.0
826
		 *
827
		 * @param float $value    The value to decrease by.
828
		 * @param int   $donor_id Donor id.
829
		 */
830
		do_action( 'give_donor_pre_decrease_value', $value, $this->id );
831
832
		if ( $this->update( array( 'purchase_value' => $new_value ) ) ) {
833
			$this->purchase_value = $new_value;
0 ignored issues
show
Documentation Bug introduced by
The property $purchase_value was declared of type integer, but $new_value is of type double. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
834
		}
835
836
		/**
837
		 * Fires after decreasing donor lifetime value.
838
		 *
839
		 * @since 1.0
840
		 *
841
		 * @param float $purchase_value Donor lifetime value.
842
		 * @param float $value          The value decreased by.
843
		 * @param int   $donor_id       Donor id.
844
		 */
845
		do_action( 'give_donor_post_decrease_value', $this->purchase_value, $value, $this->id );
846
847
		return $this->purchase_value;
848
	}
849
850
	/**
851
	 * Decrease/Increase a donor's lifetime value.
852
	 *
853
	 * This function will update donation stat on basis of current amount and new amount donation difference.
854
	 * Difference value can positive or negative. Negative value will decrease user donation stat while positive value
855
	 * increase donation stat.
856
	 *
857
	 * @since  1.0
858
	 * @access public
859
	 *
860
	 * @param  float $curr_amount Current Donation amount.
861
	 * @param  float $new_amount  New (changed) Donation amount.
862
	 *
863
	 * @return mixed              If successful, the new donation stat value, otherwise false.
864
	 */
865
	public function update_donation_value( $curr_amount, $new_amount ) {
866
		/**
867
		 * Payment total difference value can be:
868
		 *  zero   (in case amount not change)
869
		 *  or -ve (in case amount decrease)
870
		 *  or +ve (in case amount increase)
871
		 */
872
		$payment_total_diff = $new_amount - $curr_amount;
873
874
		// We do not need to update donation stat if donation did not change.
875
		if ( ! $payment_total_diff ) {
876
			return false;
877
		}
878
879
		if ( $payment_total_diff > 0 ) {
880
			$this->increase_value( $payment_total_diff );
881
		} else {
882
			// Pass payment total difference as +ve value to decrease amount from user lifetime stat.
883
			$this->decrease_value( - $payment_total_diff );
884
		}
885
886
		return $this->purchase_value;
887
	}
888
889
	/**
890
	 * Get the parsed notes for a donor as an array.
891
	 *
892
	 * @since  1.0
893
	 * @access public
894
	 *
895
	 * @param  int $length The number of notes to get.
896
	 * @param  int $paged  What note to start at.
897
	 *
898
	 * @return array       The notes requested.
899
	 */
900
	public function get_notes( $length = 20, $paged = 1 ) {
901
902
		$length = is_numeric( $length ) ? $length : 20;
903
		$offset = is_numeric( $paged ) && $paged != 1 ? ( ( absint( $paged ) - 1 ) * $length ) : 0;
904
905
		$all_notes   = $this->get_raw_notes();
906
		$notes_array = array_reverse( array_filter( explode( "\n\n", $all_notes ) ) );
907
908
		$desired_notes = array_slice( $notes_array, $offset, $length );
909
910
		return $desired_notes;
911
912
	}
913
914
	/**
915
	 * Get the total number of notes we have after parsing.
916
	 *
917
	 * @since  1.0
918
	 * @access public
919
	 *
920
	 * @return int The number of notes for the donor.
921
	 */
922
	public function get_notes_count() {
923
924
		$all_notes   = $this->get_raw_notes();
925
		$notes_array = array_reverse( array_filter( explode( "\n\n", $all_notes ) ) );
926
927
		return count( $notes_array );
928
929
	}
930
931
	/**
932
	 * Get the total donation amount.
933
	 *
934
	 * @since 1.8.17
935
	 *
936
	 * @param array $args Pass any additional data.
937
	 *
938
	 * @return string|float
939
	 */
940
	public function get_total_donation_amount( $args = array() ) {
941
942
		/**
943
		 * Filter total donation amount.
944
		 *
945
		 * @since 1.8.17
946
		 *
947
		 * @param string|float $purchase_value Donor Purchase value.
948
		 * @param integer      $donor_id       Donor ID.
949
		 * @param array        $args           Pass additional data.
950
		 */
951
		return apply_filters( 'give_get_total_donation_amount', $this->purchase_value, $this->id, $args );
952
	}
953
954
	/**
955
	 * Add a note for the donor.
956
	 *
957
	 * @since  1.0
958
	 * @access public
959
	 *
960
	 * @param  string $note The note to add. Default is empty.
961
	 *
962
	 * @return string|boolean The new note if added successfully, false otherwise.
963
	 */
964
	public function add_note( $note = '' ) {
965
966
		$note = trim( $note );
967
		if ( empty( $note ) ) {
968
			return false;
969
		}
970
971
		$notes = $this->get_raw_notes();
972
973
		if ( empty( $notes ) ) {
974
			$notes = '';
975
		}
976
977
		// Backward compatibility.
978
		$note_string        = date_i18n( 'F j, Y H:i:s', current_time( 'timestamp' ) ) . ' - ' . $note;
979
		$formatted_new_note = apply_filters( 'give_customer_add_note_string', $note_string );
980
		$notes              .= "\n\n" . $formatted_new_note;
981
982
		/**
983
		 * Fires before donor note is added.
984
		 *
985
		 * @since 1.0
986
		 *
987
		 * @param string $formatted_new_note Formatted new note to add.
988
		 * @param int    $donor_id           Donor id.
989
		 */
990
		do_action( 'give_donor_pre_add_note', $formatted_new_note, $this->id );
991
992
		if ( ! give_has_upgrade_completed( 'v230_move_donor_note' ) ) {
993
			// Backward compatibility.
994
			$updated = $this->update( array( 'notes' => $notes ) );
995 View Code Duplication
		} else {
0 ignored issues
show
This code seems to be duplicated across your project.

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

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

Loading history...
996
			$updated = Give()->comment->db->add(
997
				array(
998
					'comment_content' => $note,
999
					'user_id'         => get_current_user_id(),
1000
					'comment_parent'  => $this->id,
1001
					'comment_type'    => 'donor',
1002
				)
1003
			);
1004
		}
1005
1006
		if ( $updated ) {
1007
			$this->notes = $this->get_notes();
1008
		}
1009
1010
		/**
1011
		 * Fires after donor note added.
1012
		 *
1013
		 * @since 1.0
1014
		 *
1015
		 * @param array  $donor_notes        Donor notes.
1016
		 * @param string $formatted_new_note Formatted new note added.
1017
		 * @param int    $donor_id           Donor id.
1018
		 */
1019
		do_action( 'give_donor_post_add_note', $this->notes, $formatted_new_note, $this->id );
1020
1021
		// Return the formatted note, so we can test, as well as update any displays
1022
		return $formatted_new_note;
1023
	}
1024
1025
	/**
1026
	 * Get the notes column for the donor
1027
	 *
1028
	 * @since  1.0
1029
	 * @access private
1030
	 *
1031
	 * @return string The Notes for the donor, non-parsed.
1032
	 */
1033
	private function get_raw_notes() {
1034
		$all_notes = '';
1035
		$comments = Give()->comment->db->get_results_by( array( 'comment_parent' => $this->id ) );
1036
1037
		// Generate notes output as we are doing before 2.3.0.
1038 View Code Duplication
		if( ! empty( $comments ) ) {
0 ignored issues
show
This code seems to be duplicated across your project.

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

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

Loading history...
1039
			/* @var stdClass $comment */
1040
			foreach ( $comments  as $comment ) {
1041
				$all_notes .= date_i18n( 'F j, Y H:i:s', strtotime( $comment->comment_date ) ) . " - {$comment->comment_content}\n\n";
1042
			}
1043
		}
1044
1045
		// Backward compatibility.
1046
		if( ! give_has_upgrade_completed('v230_move_donor_note') ) {
1047
			$all_notes = $this->db->get_column( 'notes', $this->id );
1048
		}
1049
1050
		return $all_notes;
1051
1052
	}
1053
1054
	/**
1055
	 * Retrieve a meta field for a donor.
1056
	 *
1057
	 * @since  1.6
1058
	 * @access public
1059
	 *
1060
	 * @param  string $meta_key The meta key to retrieve. Default is empty.
1061
	 * @param  bool   $single   Whether to return a single value. Default is true.
1062
	 *
1063
	 * @return mixed            Will be an array if $single is false. Will be value of meta data field if $single is
1064
	 *                          true.
1065
	 */
1066
	public function get_meta( $meta_key = '', $single = true ) {
1067
		return Give()->donor_meta->get_meta( $this->id, $meta_key, $single );
1068
	}
1069
1070
	/**
1071
	 * Add a meta data field to a donor.
1072
	 *
1073
	 * @since  1.6
1074
	 * @access public
1075
	 *
1076
	 * @param  string $meta_key   Metadata name. Default is empty.
1077
	 * @param  mixed  $meta_value Metadata value.
1078
	 * @param  bool   $unique     Optional. Whether the same key should not be added. Default is false.
1079
	 *
1080
	 * @return bool               False for failure. True for success.
1081
	 */
1082
	public function add_meta( $meta_key = '', $meta_value, $unique = false ) {
1083
		return Give()->donor_meta->add_meta( $this->id, $meta_key, $meta_value, $unique );
1084
	}
1085
1086
	/**
1087
	 * Update a meta field based on donor ID.
1088
	 *
1089
	 * @since  1.6
1090
	 * @access public
1091
	 *
1092
	 * @param  string $meta_key   Metadata key. Default is empty.
1093
	 * @param  mixed  $meta_value Metadata value.
1094
	 * @param  mixed  $prev_value Optional. Previous value to check before removing. Default is empty.
1095
	 *
1096
	 * @return bool               False on failure, true if success.
1097
	 */
1098
	public function update_meta( $meta_key = '', $meta_value, $prev_value = '' ) {
1099
		return Give()->donor_meta->update_meta( $this->id, $meta_key, $meta_value, $prev_value );
1100
	}
1101
1102
	/**
1103
	 * Remove metadata matching criteria from a donor.
1104
	 *
1105
	 * @since  1.6
1106
	 * @access public
1107
	 *
1108
	 * @param  string $meta_key   Metadata name. Default is empty.
1109
	 * @param  mixed  $meta_value Optional. Metadata value. Default is empty.
1110
	 *
1111
	 * @return bool               False for failure. True for success.
1112
	 */
1113
	public function delete_meta( $meta_key = '', $meta_value = '' ) {
1114
		return Give()->donor_meta->delete_meta( $this->id, $meta_key, $meta_value );
1115
	}
1116
1117
	/**
1118
	 * Sanitize the data for update/create
1119
	 *
1120
	 * @since  1.0
1121
	 * @access private
1122
	 *
1123
	 * @param  array $data The data to sanitize.
1124
	 *
1125
	 * @return array       The sanitized data, based off column defaults.
1126
	 */
1127
	private function sanitize_columns( $data ) {
1128
1129
		$columns        = $this->db->get_columns();
1130
		$default_values = $this->db->get_column_defaults();
1131
1132
		foreach ( $columns as $key => $type ) {
1133
1134
			// Only sanitize data that we were provided
1135
			if ( ! array_key_exists( $key, $data ) ) {
1136
				continue;
1137
			}
1138
1139
			switch ( $type ) {
1140
1141
				case '%s':
1142
					if ( 'email' == $key ) {
1143
						$data[ $key ] = sanitize_email( $data[ $key ] );
1144
					} elseif ( 'notes' == $key ) {
1145
						$data[ $key ] = strip_tags( $data[ $key ] );
1146
					} else {
1147
						$data[ $key ] = sanitize_text_field( $data[ $key ] );
1148
					}
1149
					break;
1150
1151
				case '%d':
1152
					if ( ! is_numeric( $data[ $key ] ) || (int) $data[ $key ] !== absint( $data[ $key ] ) ) {
1153
						$data[ $key ] = $default_values[ $key ];
1154
					} else {
1155
						$data[ $key ] = absint( $data[ $key ] );
1156
					}
1157
					break;
1158
1159
				case '%f':
1160
					// Convert what was given to a float
1161
					$value = floatval( $data[ $key ] );
1162
1163
					if ( ! is_float( $value ) ) {
1164
						$data[ $key ] = $default_values[ $key ];
1165
					} else {
1166
						$data[ $key ] = $value;
1167
					}
1168
					break;
1169
1170
				default:
1171
					$data[ $key ] = sanitize_text_field( $data[ $key ] );
1172
					break;
1173
1174
			}
1175
		}
1176
1177
		return $data;
1178
	}
1179
1180
	/**
1181
	 * Attach an email to the donor
1182
	 *
1183
	 * @since  1.7
1184
	 * @access public
1185
	 *
1186
	 * @param  string $email   The email address to attach to the donor
1187
	 * @param  bool   $primary Allows setting the email added as the primary
1188
	 *
1189
	 * @return bool            If the email was added successfully
1190
	 */
1191
	public function add_email( $email = '', $primary = false ) {
1192
		if ( ! is_email( $email ) ) {
1193
			return false;
1194
		}
1195
		$existing = new Give_Donor( $email );
1196
1197
		if ( $existing->id > 0 ) {
1198
			// Email address already belongs to another donor
1199
			return false;
1200
		}
1201
1202
		if ( email_exists( $email ) ) {
1203
			$user = get_user_by( 'email', $email );
1204
			if ( $user->ID != $this->user_id ) {
1205
				return false;
1206
			}
1207
		}
1208
1209
		do_action( 'give_donor_pre_add_email', $email, $this->id, $this );
1210
1211
		// Add is used to ensure duplicate emails are not added
1212
		$ret = (bool) $this->add_meta( 'additional_email', $email );
1213
1214
		do_action( 'give_donor_post_add_email', $email, $this->id, $this );
1215
1216
		if ( $ret && true === $primary ) {
1217
			$this->set_primary_email( $email );
1218
		}
1219
1220
		return $ret;
1221
	}
1222
1223
	/**
1224
	 * Remove an email from the donor.
1225
	 *
1226
	 * @since  1.7
1227
	 * @access public
1228
	 *
1229
	 * @param  string $email The email address to remove from the donor.
1230
	 *
1231
	 * @return bool          If the email was removed successfully.
1232
	 */
1233
	public function remove_email( $email = '' ) {
1234
		if ( ! is_email( $email ) ) {
1235
			return false;
1236
		}
1237
1238
		do_action( 'give_donor_pre_remove_email', $email, $this->id, $this );
1239
1240
		$ret = (bool) $this->delete_meta( 'additional_email', $email );
1241
1242
		do_action( 'give_donor_post_remove_email', $email, $this->id, $this );
1243
1244
		return $ret;
1245
	}
1246
1247
	/**
1248
	 * Set an email address as the donor's primary email.
1249
	 *
1250
	 * This will move the donor's previous primary email to an additional email.
1251
	 *
1252
	 * @since  1.7
1253
	 * @access public
1254
	 *
1255
	 * @param  string $new_primary_email The email address to remove from the donor.
1256
	 *
1257
	 * @return bool                      If the email was set as primary successfully.
1258
	 */
1259
	public function set_primary_email( $new_primary_email = '' ) {
1260
		if ( ! is_email( $new_primary_email ) ) {
1261
			return false;
1262
		}
1263
1264
		do_action( 'give_donor_pre_set_primary_email', $new_primary_email, $this->id, $this );
1265
1266
		$existing = new Give_Donor( $new_primary_email );
1267
1268
		if ( $existing->id > 0 && (int) $existing->id !== (int) $this->id ) {
1269
			// This email belongs to another donor.
1270
			return false;
1271
		}
1272
1273
		$old_email = $this->email;
1274
1275
		// Update donor record with new email.
1276
		$update = $this->update( array( 'email' => $new_primary_email ) );
1277
1278
		// Remove new primary from list of additional emails.
1279
		$remove = $this->remove_email( $new_primary_email );
1280
1281
		// Add old email to additional emails list.
1282
		$add = $this->add_email( $old_email );
1283
1284
		$ret = $update && $remove && $add;
1285
1286
		if ( $ret ) {
1287
			$this->email = $new_primary_email;
1288
		}
1289
1290
		do_action( 'give_donor_post_set_primary_email', $new_primary_email, $this->id, $this );
1291
1292
		return $ret;
1293
	}
1294
1295
	/**
1296
	 * Check if address valid or not.
1297
	 *
1298
	 * @since  2.0
1299
	 * @access private
1300
	 *
1301
	 * @param $address
1302
	 *
1303
	 * @return bool
1304
	 */
1305
	private function is_valid_address( $address ) {
1306
		$is_valid_address = true;
1307
1308
		// Address ready to process even if only one value set.
1309
		foreach ( $address as $address_type => $value ) {
1310
			// @todo: Handle state field validation on basis of country.
1311
			if ( in_array( $address_type, array( 'line2', 'state' ) ) ) {
1312
				continue;
1313
			}
1314
1315
			if ( empty( $value ) ) {
1316
				$is_valid_address = false;
1317
				break;
1318
			}
1319
		}
1320
1321
		return $is_valid_address;
1322
	}
1323
1324
	/**
1325
	 * Add donor address
1326
	 *
1327
	 * @since  2.0
1328
	 * @access public
1329
	 *
1330
	 * @param string $address_type
1331
	 * @param array  $address {
1332
	 *
1333
	 * @type string  $address2
1334
	 * @type string city
1335
	 * @type string zip
1336
	 * @type string state
1337
	 * @type string country
1338
	 * }
1339
	 *
1340
	 * @return bool
1341
	 */
1342
	public function add_address( $address_type, $address ) {
1343
		// Bailout.
1344
		if ( empty( $address_type ) || ! $this->is_valid_address( $address ) || ! $this->id ) {
1345
			return false;
1346
		}
1347
1348
		// Check if multiple address exist or not and set params.
1349
		$multi_address_id = null;
1350
		if ( $is_multi_address = ( false !== strpos( $address_type, '[]' ) ) ) {
1351
			$address_type = $is_multi_address ? str_replace( '[]', '', $address_type ) : $address_type;
1352
		} elseif ( $is_multi_address = ( false !== strpos( $address_type, '_' ) ) ) {
1353
			$exploded_address_type = explode( '_', $address_type );
1354
			$multi_address_id      = $is_multi_address ? array_pop( $exploded_address_type ) : $address_type;
1355
1356
			$address_type = $is_multi_address ? array_shift( $exploded_address_type ) : $address_type;
1357
		}
1358
1359
		// Bailout: do not save duplicate orders
1360
		if ( $this->does_address_exist( $address_type, $address ) ) {
1361
			return false;
1362
		}
1363
1364
		// Set default address.
1365
		$address = wp_parse_args( $address, array(
1366
			'line1'   => '',
1367
			'line2'   => '',
1368
			'city'    => '',
1369
			'state'   => '',
1370
			'country' => '',
1371
			'zip'     => '',
1372
		) );
1373
1374
		// Set meta key prefix.
1375
		global $wpdb;
1376
		$meta_key_prefix = "_give_donor_address_{$address_type}_{address_name}";
1377
		$meta_type       = Give()->donor_meta->meta_type;
1378
1379
		if ( $is_multi_address ) {
1380
			if ( is_null( $multi_address_id ) ) {
1381
				// Get latest address key to set multi address id.
1382
				$multi_address_id = $wpdb->get_var( $wpdb->prepare( "
1383
						SELECT meta_key FROM {$wpdb->donormeta}
1384
						WHERE meta_key
1385
						LIKE '%%%s%%'
1386
						AND {$meta_type}_id=%d
1387
						ORDER BY meta_id DESC
1388
						LIMIT 1
1389
						", "_give_donor_address_{$address_type}_line1", $this->id ) );
1390
1391
				if ( ! empty( $multi_address_id ) ) {
1392
					$multi_address_id = absint( substr( strrchr( $multi_address_id, '_' ), 1 ) );
1393
					$multi_address_id ++;
1394
				} else {
1395
					$multi_address_id = 0;
1396
				}
1397
			}
1398
1399
			$meta_key_prefix = "_give_donor_address_{$address_type}_{address_name}_{$multi_address_id}";
1400
		}
1401
1402
		// Save donor address.
1403
		foreach ( $address as $type => $value ) {
1404
			$meta_key = str_replace( '{address_name}', $type, $meta_key_prefix );
1405
			Give()->donor_meta->update_meta( $this->id, $meta_key, $value );
1406
		}
1407
1408
		$this->setup_address();
1409
1410
		return true;
1411
	}
1412
1413
	/**
1414
	 * Remove donor address
1415
	 *
1416
	 * @since  2.0
1417
	 * @access public
1418
	 * @global wpdb  $wpdb
1419
	 *
1420
	 * @param string $address_id
1421
	 *
1422
	 * @return bool
1423
	 */
1424
	public function remove_address( $address_id ) {
1425
		global $wpdb;
1426
1427
		// Get address type.
1428
		$is_multi_address = false !== strpos( $address_id, '_' ) ? true : false;
1429
1430
		$address_key_arr = explode( '_', $address_id );
1431
1432
		$address_type  = false !== strpos( $address_id, '_' ) ? array_shift( $address_key_arr ) : $address_id;
1433
		$address_count = false !== strpos( $address_id, '_' ) ? array_pop( $address_key_arr ) : null;
1434
1435
		// Set meta key prefix.
1436
		$meta_key_prefix = "_give_donor_address_{$address_type}_%";
1437
		if ( $is_multi_address && is_numeric( $address_count ) ) {
1438
			$meta_key_prefix .= "_{$address_count}";
1439
		}
1440
1441
		$meta_type = Give()->donor_meta->meta_type;
1442
1443
		// Process query.
1444
		$row_affected = $wpdb->query( $wpdb->prepare( "
1445
				DELETE FROM {$wpdb->donormeta}
1446
				WHERE meta_key
1447
				LIKE '%s'
1448
				AND {$meta_type}_id=%d
1449
				", $meta_key_prefix, $this->id ) );
1450
1451
		// Delete cache.
1452
		Give_Cache::delete_group( $this->id, 'give-donors' );
1453
		wp_cache_delete( $this->id,  "{$meta_type}_meta" );
1454
1455
		$this->setup_address();
1456
1457
		return (bool) $row_affected;
1458
	}
1459
1460
	/**
1461
	 * Update donor address
1462
	 *
1463
	 * @since  2.0
1464
	 * @access public
1465
	 * @global wpdb  $wpdb
1466
	 *
1467
	 * @param string $address_id
1468
	 * @param array  $address
1469
	 *
1470
	 * @return bool
1471
	 */
1472
	public function update_address( $address_id, $address ) {
1473
		global $wpdb;
1474
1475
		// Get address type.
1476
		$is_multi_address = false !== strpos( $address_id, '_' ) ? true : false;
1477
		$exploded_address_id = explode( '_', $address_id );
1478
1479
		$address_type = false !== strpos( $address_id, '_' ) ? array_shift( $exploded_address_id ) : $address_id;
1480
1481
		$address_count = false !== strpos( $address_id, '_' ) ? array_pop( $exploded_address_id ) : null;
1482
1483
		// Set meta key prefix.
1484
		$meta_key_prefix = "_give_donor_address_{$address_type}_%";
1485
		if ( $is_multi_address && is_numeric( $address_count ) ) {
1486
			$meta_key_prefix .= "_{$address_count}";
1487
		}
1488
1489
		$meta_type = Give()->donor_meta->meta_type;
1490
1491
		// Process query.
1492
		$row_affected = $wpdb->get_results( $wpdb->prepare( "
1493
				SELECT meta_key FROM {$wpdb->donormeta}
1494
				WHERE meta_key
1495
				LIKE '%s'
1496
				AND {$meta_type}_id=%d
1497
				", $meta_key_prefix, $this->id ) );
1498
1499
		// Return result.
1500
		if ( ! count( $row_affected ) ) {
1501
			return false;
1502
		}
1503
1504
		// Update address.
1505
		if ( ! $this->add_address( $address_id, $address ) ) {
1506
			return false;
1507
		}
1508
1509
		return true;
1510
	}
1511
1512
1513
	/**
1514
	 * Check if donor already has current address
1515
	 *
1516
	 * @since  2.0
1517
	 * @access public
1518
	 *
1519
	 * @param string $current_address_type
1520
	 * @param array  $current_address
1521
	 *
1522
	 * @return bool|null
1523
	 */
1524
	public function does_address_exist( $current_address_type, $current_address ) {
1525
		$status = false;
1526
1527
		// Bailout.
1528
		if ( empty( $current_address_type ) || empty( $current_address ) ) {
1529
			return null;
1530
		}
1531
1532
		// Bailout.
1533
		if ( empty( $this->address ) || empty( $this->address[ $current_address_type ] ) ) {
1534
			return $status;
1535
		}
1536
1537
		// Get address.
1538
		$address = $this->address[ $current_address_type ];
1539
1540
		switch ( true ) {
1541
1542
			// Single address.
1543
			case is_string( end( $address ) ) :
1544
				$status = $this->is_address_match( $current_address, $address );
1545
				break;
1546
1547
			// Multi address.
1548
			case is_array( end( $address ) ):
1549
				// Compare address.
1550
				foreach ( $address as $saved_address ) {
1551
					if ( empty( $saved_address ) ) {
1552
						continue;
1553
					}
1554
1555
					// Exit loop immediately if address exist.
1556
					if ( $status = $this->is_address_match( $current_address, $saved_address ) ) {
1557
						break;
1558
					}
1559
				}
1560
				break;
1561
		}
1562
1563
		return $status;
1564
	}
1565
1566
	/**
1567
	 * Compare address.
1568
	 *
1569
	 * @since  2.0
1570
	 * @access private
1571
	 *
1572
	 * @param array $address_1
1573
	 * @param array $address_2
1574
	 *
1575
	 * @return bool
1576
	 */
1577
	private function is_address_match( $address_1, $address_2 ) {
1578
		$result = array_diff_assoc( $address_1, $address_2 );
1579
1580
		return empty( $result );
1581
	}
1582
1583
	/**
1584
	 * Split donor name into first name and last name
1585
	 *
1586
	 * @param   int $id Donor ID
1587
	 *
1588
	 * @since   2.0
1589
	 * @return  object
1590
	 */
1591
	public function split_donor_name( $id ) {
1592
		$first_name = $last_name = '';
1593
		$donor      = new Give_Donor( $id );
1594
1595
		$split_donor_name = explode( ' ', $donor->name, 2 );
1596
1597
		// Check for existence of first name after split of donor name.
1598
		if ( is_array( $split_donor_name ) && ! empty( $split_donor_name[0] ) ) {
1599
			$first_name = $split_donor_name[0];
1600
		}
1601
1602
		// Check for existence of last name after split of donor name.
1603
		if ( is_array( $split_donor_name ) && ! empty( $split_donor_name[1] ) ) {
1604
			$last_name = $split_donor_name[1];
1605
		}
1606
1607
		return (object) array( 'first_name' => $first_name, 'last_name' => $last_name );
1608
	}
1609
1610
	/**
1611
	 * Retrieves first name of donor with backward compatibility
1612
	 *
1613
	 * @since   2.0
1614
	 * @return  string
1615
	 */
1616
	public function get_first_name() {
1617
		$first_name = $this->get_meta( '_give_donor_first_name' );
1618
		if ( ! $first_name ) {
1619
			$first_name = $this->split_donor_name( $this->id )->first_name;
1620
		}
1621
1622
		return $first_name;
1623
	}
1624
1625
	/**
1626
	 * Retrieves last name of donor with backward compatibility
1627
	 *
1628
	 * @since   2.0
1629
	 * @return  string
1630
	 */
1631
	public function get_last_name() {
1632
		$first_name = $this->get_meta( '_give_donor_first_name' );
1633
		$last_name  = $this->get_meta( '_give_donor_last_name' );
1634
1635
		// This condition will prevent unnecessary splitting of donor name to fetch last name.
1636
		if ( ! $first_name && ! $last_name ) {
1637
			$last_name = $this->split_donor_name( $this->id )->last_name;
1638
		}
1639
1640
		return ( $last_name ) ? $last_name : '';
1641
	}
1642
1643
	/**
1644
	 * Retrieves company name of donor
1645
	 *
1646
	 * @since   2.1
1647
	 *
1648
	 * @return  string $company_name Donor Company Name
1649
	 */
1650
	public function get_company_name() {
1651
		$company_name = $this->get_meta( '_give_donor_company' );
1652
1653
		return $company_name;
1654
	}
1655
1656
	/**
1657
	 * Retrieves last donation for the donor.
1658
	 *
1659
	 * @since   2.1
1660
	 *
1661
	 * @return  string $company_name Donor Company Name
1662
	 */
1663
	public function get_last_donation() {
1664
1665
		$payments = array_unique( array_values( explode( ',', $this->payment_ids ) ) );
1666
1667
		return end( $payments );
1668
1669
	}
1670
1671
	/**
1672
	 * Retrieves last donation for the donor.
1673
	 *
1674
	 * @since   2.1
1675
	 *
1676
	 * @param bool $formatted Whether to return with the date format or not.
1677
	 *
1678
	 * @return string The date of the last donation.
1679
	 */
1680
	public function get_last_donation_date( $formatted = false ) {
1681
		$completed_data = '';
1682
1683
		// Return if donation id is invalid.
1684
		if( ! ( $last_donation = absint( $this->get_last_donation() ) ) ) {
1685
			return $completed_data;
1686
		}
1687
1688
		$completed_data = give_get_payment_completed_date( $last_donation );
1689
1690
		if ( $formatted ) {
1691
			return date_i18n( give_date_format(), strtotime( $completed_data ) );
1692
		}
1693
1694
		return $completed_data;
1695
1696
	}
1697
1698
	/**
1699
	 * Retrieves a donor's initials (first name and last name).
1700
	 *
1701
	 * @since   2.1
1702
	 *
1703
	 * @return string The donor's two initials (no middle).
1704
	 */
1705
	public function get_donor_initals() {
1706
		/**
1707
		 * Filter the donor name initials
1708
		 *
1709
		 * @since 2.1.0
1710
		 */
1711
		return apply_filters(
1712
			'get_donor_initals',
1713
			give_get_name_initial( array(
1714
				'firstname' =>  $this->get_first_name(),
1715
				'lastname' =>  $this->get_last_name()
1716
			) )
1717
		);
1718
1719
	}
1720
1721
}
1722