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/data-stores/class-getpaid-data.php (12 issues)

1
<?php
2
3
/**
4
 * Abstract Data.
5
 *
6
 * Handles generic data interaction which is implemented by
7
 * the different data store classes.
8
 *
9
 */
10
11
if ( ! defined( 'ABSPATH' ) ) {
12
	exit;
13
}
14
15
/**
16
 * Abstract GetPaid Data Class
17
 *
18
 * Implemented by classes using the same CRUD(s) pattern.
19
 *
20
 * @version  1.0.19
21
 */
22
abstract class GetPaid_Data {
23
24
	/**
25
	 * ID for this object.
26
	 *
27
	 * @since 1.0.19
28
	 * @var int
29
	 */
30
	protected $id = 0;
31
32
	/**
33
	 * Core data for this object. Name value pairs (name + default value).
34
	 *
35
	 * @since 1.0.19
36
	 * @var array
37
	 */
38
	protected $data = array();
39
40
	/**
41
	 * Core data changes for this object.
42
	 *
43
	 * @since 1.0.19
44
	 * @var array
45
	 */
46
	protected $changes = array();
47
48
	/**
49
	 * This is false until the object is read from the DB.
50
	 *
51
	 * @since 1.0.19
52
	 * @var bool
53
	 */
54
	protected $object_read = false;
55
56
	/**
57
	 * This is the name of this object type.
58
	 *
59
	 * @since 1.0.19
60
	 * @var string
61
	 */
62
	protected $object_type = 'data';
63
64
	/**
65
	 * Extra data for this object. Name value pairs (name + default value).
66
	 * Used as a standard way for sub classes (like item types) to add
67
	 * additional information to an inherited class.
68
	 *
69
	 * @since 1.0.19
70
	 * @var array
71
	 */
72
	protected $extra_data = array();
73
74
	/**
75
	 * Set to _data on construct so we can track and reset data if needed.
76
	 *
77
	 * @since 1.0.19
78
	 * @var array
79
	 */
80
	protected $default_data = array();
81
82
	/**
83
	 * Contains a reference to the data store for this class.
84
	 *
85
	 * @since 1.0.19
86
	 * @var GetPaid_Data_Store
87
	 */
88
	protected $data_store;
89
90
	/**
91
	 * Stores meta in cache for future reads.
92
	 * A group must be set to to enable caching.
93
	 *
94
	 * @since 1.0.19
95
	 * @var string
96
	 */
97
	protected $cache_group = '';
98
99
	/**
100
	 * Stores the last error.
101
	 *
102
	 * @since 1.0.19
103
	 * @var string
104
	 */
105
	public $last_error = '';
106
107
	/**
108
	 * Stores additional meta data.
109
	 *
110
	 * @since 1.0.19
111
	 * @var array
112
	 */
113
	protected $meta_data = null;
114
115
	/**
116
	 * Default constructor.
117
	 *
118
	 * @param int|object|array|string $read ID to load from the DB (optional) or already queried data.
119
	 */
120
	public function __construct( $read = 0 ) {
121
		$this->data         = array_merge( $this->data, $this->extra_data );
122
		$this->default_data = $this->data;
123
	}
124
125
	/**
126
	 * Only store the object ID to avoid serializing the data object instance.
127
	 *
128
	 * @return array
129
	 */
130
	public function __sleep() {
131
		return array( 'id' );
132
	}
133
134
	/**
135
	 * Re-run the constructor with the object ID.
136
	 *
137
	 * If the object no longer exists, remove the ID.
138
	 */
139
	public function __wakeup() {
140
		$this->__construct( absint( $this->id ) );
141
142
		if ( ! empty( $this->last_error ) ) {
143
			$this->set_id( 0 );
144
		}
145
146
	}
147
148
	/**
149
	 * When the object is cloned, make sure meta is duplicated correctly.
150
	 *
151
	 * @since 1.0.19
152
	 */
153
	public function __clone() {
154
		$this->maybe_read_meta_data();
155
		if ( ! empty( $this->meta_data ) ) {
156
			foreach ( $this->meta_data as $array_key => $meta ) {
157
				$this->meta_data[ $array_key ] = clone $meta;
158
				if ( ! empty( $meta->id ) ) {
159
					$this->meta_data[ $array_key ]->id = null;
160
				}
161
			}
162
		}
163
	}
164
165
	/**
166
	 * Get the data store.
167
	 *
168
	 * @since  1.0.19
169
	 * @return object
170
	 */
171
	public function get_data_store() {
172
		return $this->data_store;
173
	}
174
175
	/**
176
	 * Get the object type.
177
	 *
178
	 * @since  1.0.19
179
	 * @return string
180
	 */
181
	public function get_object_type() {
182
		return $this->object_type;
183
	}
184
185
	/**
186
	 * Returns the unique ID for this object.
187
	 *
188
	 * @since  1.0.19
189
	 * @return int
190
	 */
191
	public function get_id() {
192
		return $this->id;
193
	}
194
195
	/**
196
	 * Get form status.
197
	 *
198
	 * @since 1.0.19
199
	 * @param  string $context View or edit context.
200
	 * @return string
201
	 */
202
	public function get_status( $context = 'view' ) {
203
		return $this->get_prop( 'status', $context );
204
    }
205
206
	/**
207
	 * Delete an object, set the ID to 0, and return result.
208
	 *
209
	 * @since  1.0.19
210
	 * @param  bool $force_delete Should the data be deleted permanently.
211
	 * @return bool result
212
	 */
213
	public function delete( $force_delete = false ) {
214
		if ( $this->data_store && $this->exists() ) {
215
			$this->data_store->delete( $this, array( 'force_delete' => $force_delete ) );
216
			$this->set_id( 0 );
217
			return true;
218
		}
219
		return false;
220
	}
221
222
	/**
223
	 * Save should create or update based on object existence.
224
	 *
225
	 * @since  1.0.19
226
	 * @return int
227
	 */
228
	public function save() {
229
		if ( ! $this->data_store ) {
230
			return $this->get_id();
231
		}
232
233
		/**
234
		 * Trigger action before saving to the DB. Allows you to adjust object props before save.
235
		 *
236
		 * @param GetPaid_Data          $this The object being saved.
237
		 * @param GetPaid_Data_Store_WP $data_store The data store persisting the data.
238
		 */
239
		do_action( 'getpaid_before_' . $this->object_type . '_object_save', $this, $this->data_store );
240
241
		if ( $this->get_id() ) {
242
			$this->data_store->update( $this );
243
		} else {
244
			$this->data_store->create( $this );
245
		}
246
247
		/**
248
		 * Trigger action after saving to the DB.
249
		 *
250
		 * @param GetPaid_Data          $this The object being saved.
251
		 * @param GetPaid_Data_Store_WP $data_store The data store persisting the data.
252
		 */
253
		do_action( 'getpaid_after_' . $this->object_type . '_object_save', $this, $this->data_store );
254
255
		return $this->get_id();
256
	}
257
258
	/**
259
	 * Change data to JSON format.
260
	 *
261
	 * @since  1.0.19
262
	 * @return string Data in JSON format.
263
	 */
264
	public function __toString() {
265
		return wp_json_encode( $this->get_data() );
0 ignored issues
show
Bug Best Practice introduced by
The expression return wp_json_encode($this->get_data()) could also return false which is incompatible with the documented return type string. 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...
266
	}
267
268
	/**
269
	 * Returns all data for this object.
270
	 *
271
	 * @since  1.0.19
272
	 * @return array
273
	 */
274
	public function get_data() {
275
		return array_merge( array( 'id' => $this->get_id() ), $this->data, array( 'meta_data' => $this->get_meta_data() ) );
276
	}
277
278
	/**
279
	 * Returns array of expected data keys for this object.
280
	 *
281
	 * @since   1.0.19
282
	 * @return array
283
	 */
284
	public function get_data_keys() {
285
		return array_keys( $this->data );
286
	}
287
288
	/**
289
	 * Returns all "extra" data keys for an object (for sub objects like item types).
290
	 *
291
	 * @since  1.0.19
292
	 * @return array
293
	 */
294
	public function get_extra_data_keys() {
295
		return array_keys( $this->extra_data );
296
	}
297
298
	/**
299
	 * Filter null meta values from array.
300
	 *
301
	 * @since  1.0.19
302
	 * @param mixed $meta Meta value to check.
303
	 * @return bool
304
	 */
305
	protected function filter_null_meta( $meta ) {
306
		return ! is_null( $meta->value );
307
	}
308
309
	/**
310
	 * Get All Meta Data.
311
	 *
312
	 * @since 1.0.19
313
	 * @return array of objects.
314
	 */
315
	public function get_meta_data() {
316
		$this->maybe_read_meta_data();
317
		return array_values( array_filter( $this->meta_data, array( $this, 'filter_null_meta' ) ) );
318
	}
319
320
	/**
321
	 * Check if the key is an internal one.
322
	 *
323
	 * @since  1.0.19
324
	 * @param  string $key Key to check.
325
	 * @return bool   true if it's an internal key, false otherwise
326
	 */
327
	protected function is_internal_meta_key( $key ) {
328
		$internal_meta_key = ! empty( $key ) && $this->data_store && in_array( $key, $this->data_store->get_internal_meta_keys(), true );
0 ignored issues
show
The method get_internal_meta_keys() does not exist on GetPaid_Data_Store. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

328
		$internal_meta_key = ! empty( $key ) && $this->data_store && in_array( $key, $this->data_store->/** @scrutinizer ignore-call */ get_internal_meta_keys(), true );
Loading history...
It seems like $this->data_store->get_internal_meta_keys() can also be of type null; however, parameter $haystack of in_array() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

328
		$internal_meta_key = ! empty( $key ) && $this->data_store && in_array( $key, /** @scrutinizer ignore-type */ $this->data_store->get_internal_meta_keys(), true );
Loading history...
329
330
		if ( ! $internal_meta_key ) {
331
			return false;
332
		}
333
334
		$has_setter_or_getter = is_callable( array( $this, 'set_' . $key ) ) || is_callable( array( $this, 'get_' . $key ) );
335
336
		if ( ! $has_setter_or_getter ) {
337
			return false;
338
		}
339
340
		/* translators: %s: $key Key to check */
341
		getpaid_doing_it_wrong( __FUNCTION__, sprintf( __( 'Generic add/update/get meta methods should not be used for internal meta data, including "%s". Use getters and setters.', 'invoicing' ), $key ), '1.0.19' );
342
343
		return true;
344
	}
345
346
	/**
347
	 * Magic method for setting data fields.
348
	 *
349
	 * This method does not update custom fields in the database.
350
	 *
351
	 * @since 1.0.19
352
	 * @access public
353
	 *
354
	 */
355
	public function __set( $key, $value ) {
356
357
		if ( 'id' == strtolower( $key ) ) {
358
			return $this->set_id( $value );
0 ignored issues
show
Are you sure the usage of $this->set_id($value) targeting GetPaid_Data::set_id() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
359
		}
360
361
		if ( method_exists( $this, "set_$key" ) ) {
362
363
			/* translators: %s: $key Key to set */
364
			getpaid_doing_it_wrong( __FUNCTION__, sprintf( __( 'Object data such as "%s" should not be accessed directly. Use getters and setters.', 'invoicing' ), $key ), '1.0.19' );
365
366
			call_user_func( array( $this, "set_$key" ), $value );
367
		} else {
368
			$this->set_prop( $key, $value );
369
		}
370
371
	}
372
373
	/**
374
     * Margic method for retrieving a property.
375
     */
376
    public function __get( $key ) {
377
378
        // Check if we have a helper method for that.
379
        if ( method_exists( $this, 'get_' . $key ) ) {
380
381
			if ( 'post_type' != $key ) {
382
				/* translators: %s: $key Key to set */
383
				getpaid_doing_it_wrong( __FUNCTION__, sprintf( __( 'Object data such as "%s" should not be accessed directly. Use getters and setters.', 'invoicing' ), $key ), '1.0.19' );
384
			}
385
386
            return call_user_func( array( $this, 'get_' . $key ) );
387
        }
388
389
        // Check if the key is in the associated $post object.
390
        if ( ! empty( $this->post ) && isset( $this->post->$key ) ) {
0 ignored issues
show
Bug Best Practice introduced by
The property post does not exist on GetPaid_Data. Since you implemented __get, consider adding a @property annotation.
Loading history...
391
            return $this->post->$key;
392
        }
393
394
		return $this->get_prop( $key );
395
396
    }
397
398
	/**
399
	 * Get Meta Data by Key.
400
	 *
401
	 * @since  1.0.19
402
	 * @param  string $key Meta Key.
403
	 * @param  bool   $single return first found meta with key, or all with $key.
404
	 * @param  string $context What the value is for. Valid values are view and edit.
405
	 * @return mixed
406
	 */
407
	public function get_meta( $key = '', $single = true, $context = 'view' ) {
408
409
		// Check if this is an internal meta key.
410
		$_key = str_replace( '_wpinv', '', $key );
411
		$_key = str_replace( 'wpinv', '', $_key );
412
		if ( $this->is_internal_meta_key( $key ) ) {
413
			$function = 'get_' . $_key;
414
415
			if ( is_callable( array( $this, $function ) ) ) {
416
				return $this->{$function}();
417
			}
418
		}
419
420
		// Read the meta data if not yet read.
421
		$this->maybe_read_meta_data();
422
		$meta_data  = $this->get_meta_data();
423
		$array_keys = array_keys( wp_list_pluck( $meta_data, 'key' ), $key, true );
424
		$value      = $single ? '' : array();
425
426
		if ( ! empty( $array_keys ) ) {
427
			// We don't use the $this->meta_data property directly here because we don't want meta with a null value (i.e. meta which has been deleted via $this->delete_meta_data()).
428
			if ( $single ) {
429
				$value = $meta_data[ current( $array_keys ) ]->value;
430
			} else {
431
				$value = array_intersect_key( $meta_data, array_flip( $array_keys ) );
432
			}
433
		}
434
435
		if ( 'view' === $context ) {
436
			$value = apply_filters( $this->get_hook_prefix() . $key, $value, $this );
437
		}
438
439
		return $value;
440
	}
441
442
	/**
443
	 * See if meta data exists, since get_meta always returns a '' or array().
444
	 *
445
	 * @since  1.0.19
446
	 * @param  string $key Meta Key.
447
	 * @return boolean
448
	 */
449
	public function meta_exists( $key = '' ) {
450
		$this->maybe_read_meta_data();
451
		$array_keys = wp_list_pluck( $this->get_meta_data(), 'key' );
452
		return in_array( $key, $array_keys, true );
453
	}
454
455
	/**
456
	 * Set all meta data from array.
457
	 *
458
	 * @since 1.0.19
459
	 * @param array $data Key/Value pairs.
460
	 */
461
	public function set_meta_data( $data ) {
462
		if ( ! empty( $data ) && is_array( $data ) ) {
463
			$this->maybe_read_meta_data();
464
			foreach ( $data as $meta ) {
465
				$meta = (array) $meta;
466
				if ( isset( $meta['key'], $meta['value'], $meta['id'] ) ) {
467
					$this->meta_data[] = new GetPaid_Meta_Data(
468
						array(
469
							'id'    => $meta['id'],
470
							'key'   => $meta['key'],
471
							'value' => $meta['value'],
472
						)
473
					);
474
				}
475
			}
476
		}
477
	}
478
479
	/**
480
	 * Add meta data.
481
	 *
482
	 * @since 1.0.19
483
	 *
484
	 * @param string       $key Meta key.
485
	 * @param string|array $value Meta value.
486
	 * @param bool         $unique Should this be a unique key?.
487
	 */
488
	public function add_meta_data( $key, $value, $unique = false ) {
489
		if ( $this->is_internal_meta_key( $key ) ) {
490
			$function = 'set_' . $key;
491
492
			if ( is_callable( array( $this, $function ) ) ) {
493
				return $this->{$function}( $value );
494
			}
495
		}
496
497
		$this->maybe_read_meta_data();
498
		if ( $unique ) {
499
			$this->delete_meta_data( $key );
500
		}
501
		$this->meta_data[] = new GetPaid_Meta_Data(
502
			array(
503
				'key'   => $key,
504
				'value' => $value,
505
			)
506
		);
507
508
		$this->save();
509
	}
510
511
	/**
512
	 * Update meta data by key or ID, if provided.
513
	 *
514
	 * @since  1.0.19
515
	 *
516
	 * @param  string       $key Meta key.
517
	 * @param  string|array $value Meta value.
518
	 * @param  int          $meta_id Meta ID.
519
	 */
520
	public function update_meta_data( $key, $value, $meta_id = 0 ) {
521
		if ( $this->is_internal_meta_key( $key ) ) {
522
			$function = 'set_' . $key;
523
524
			if ( is_callable( array( $this, $function ) ) ) {
525
				return $this->{$function}( $value );
526
			}
527
		}
528
529
		$this->maybe_read_meta_data();
530
531
		$array_key = false;
532
533
		if ( $meta_id ) {
534
			$array_keys = array_keys( wp_list_pluck( $this->meta_data, 'id' ), $meta_id, true );
535
			$array_key  = $array_keys ? current( $array_keys ) : false;
536
		} else {
537
			// Find matches by key.
538
			$matches = array();
539
			foreach ( $this->meta_data as $meta_data_array_key => $meta ) {
540
				if ( $meta->key === $key ) {
541
					$matches[] = $meta_data_array_key;
542
				}
543
			}
544
545
			if ( ! empty( $matches ) ) {
546
				// Set matches to null so only one key gets the new value.
547
				foreach ( $matches as $meta_data_array_key ) {
548
					$this->meta_data[ $meta_data_array_key ]->value = null;
549
				}
550
				$array_key = current( $matches );
551
			}
552
		}
553
554
		if ( false !== $array_key ) {
555
			$meta        = $this->meta_data[ $array_key ];
556
			$meta->key   = $key;
557
			$meta->value = $value;
558
		} else {
559
			$this->add_meta_data( $key, $value, true );
560
		}
561
	}
562
563
	/**
564
	 * Delete meta data.
565
	 *
566
	 * @since 1.0.19
567
	 * @param string $key Meta key.
568
	 */
569
	public function delete_meta_data( $key ) {
570
		$this->maybe_read_meta_data();
571
		$array_keys = array_keys( wp_list_pluck( $this->meta_data, 'key' ), $key, true );
572
573
		if ( $array_keys ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $array_keys of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
574
			foreach ( $array_keys as $array_key ) {
575
				$this->meta_data[ $array_key ]->value = null;
576
			}
577
		}
578
	}
579
580
	/**
581
	 * Delete meta data.
582
	 *
583
	 * @since 1.0.19
584
	 * @param int $mid Meta ID.
585
	 */
586
	public function delete_meta_data_by_mid( $mid ) {
587
		$this->maybe_read_meta_data();
588
		$array_keys = array_keys( wp_list_pluck( $this->meta_data, 'id' ), (int) $mid, true );
589
590
		if ( $array_keys ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $array_keys of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
591
			foreach ( $array_keys as $array_key ) {
592
				$this->meta_data[ $array_key ]->value = null;
593
			}
594
		}
595
	}
596
597
	/**
598
	 * Read meta data if null.
599
	 *
600
	 * @since 1.0.19
601
	 */
602
	protected function maybe_read_meta_data() {
603
		if ( is_null( $this->meta_data ) ) {
0 ignored issues
show
The condition is_null($this->meta_data) is always false.
Loading history...
604
			$this->read_meta_data();
605
		}
606
	}
607
608
	/**
609
	 * Read Meta Data from the database. Ignore any internal properties.
610
	 * Uses it's own caches because get_metadata does not provide meta_ids.
611
	 *
612
	 * @since 1.0.19
613
	 * @param bool $force_read True to force a new DB read (and update cache).
614
	 */
615
	public function read_meta_data( $force_read = false ) {
616
617
		// Reset meta data.
618
		$this->meta_data = array();
619
620
		// Maybe abort early.
621
		if ( ! $this->get_id() || ! $this->data_store ) {
622
			return;
623
		}
624
625
		// Only read from cache if the cache key is set.
626
		$cache_key = null;
627
		if ( ! $force_read && ! empty( $this->cache_group ) ) {
628
			$cache_key     = GetPaid_Cache_Helper::get_cache_prefix( $this->cache_group ) . GetPaid_Cache_Helper::get_cache_prefix( 'object_' . $this->get_id() ) . 'object_meta_' . $this->get_id();
629
			$raw_meta_data = wp_cache_get( $cache_key, $this->cache_group );
630
		}
631
632
		// Should we force read?
633
		if ( empty( $raw_meta_data ) ) {
634
			$raw_meta_data = $this->data_store->read_meta( $this );
0 ignored issues
show
The method read_meta() does not exist on GetPaid_Data_Store. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

634
			/** @scrutinizer ignore-call */ 
635
   $raw_meta_data = $this->data_store->read_meta( $this );
Loading history...
635
636
			if ( ! empty( $cache_key ) ) {
637
				wp_cache_set( $cache_key, $raw_meta_data, $this->cache_group );
638
			}
639
}
640
641
		// Set meta data.
642
		if ( is_array( $raw_meta_data ) ) {
643
644
			foreach ( $raw_meta_data as $meta ) {
645
				$this->meta_data[] = new GetPaid_Meta_Data(
646
					array(
647
						'id'    => (int) $meta->meta_id,
648
						'key'   => $meta->meta_key,
649
						'value' => maybe_unserialize( $meta->meta_value ),
650
					)
651
				);
652
			}
653
}
654
655
	}
656
657
	/**
658
	 * Update Meta Data in the database.
659
	 *
660
	 * @since 1.0.19
661
	 */
662
	public function save_meta_data() {
663
		if ( ! $this->data_store || is_null( $this->meta_data ) ) {
664
			return;
665
		}
666
		foreach ( $this->meta_data as $array_key => $meta ) {
667
			if ( is_null( $meta->value ) ) {
668
				if ( ! empty( $meta->id ) ) {
669
					$this->data_store->delete_meta( $this, $meta );
0 ignored issues
show
The method delete_meta() does not exist on GetPaid_Data_Store. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

669
					$this->data_store->/** @scrutinizer ignore-call */ 
670
                        delete_meta( $this, $meta );
Loading history...
670
					unset( $this->meta_data[ $array_key ] );
671
				}
672
			} elseif ( empty( $meta->id ) ) {
673
				$meta->id = $this->data_store->add_meta( $this, $meta );
0 ignored issues
show
The method add_meta() does not exist on GetPaid_Data_Store. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

673
				/** @scrutinizer ignore-call */ 
674
    $meta->id = $this->data_store->add_meta( $this, $meta );
Loading history...
674
				$meta->apply_changes();
675
			} else {
676
				if ( $meta->get_changes() ) {
677
					$this->data_store->update_meta( $this, $meta );
0 ignored issues
show
The method update_meta() does not exist on GetPaid_Data_Store. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

677
					$this->data_store->/** @scrutinizer ignore-call */ 
678
                        update_meta( $this, $meta );
Loading history...
678
					$meta->apply_changes();
679
				}
680
			}
681
		}
682
		if ( ! empty( $this->cache_group ) ) {
683
			$cache_key = GetPaid_Cache_Helper::get_cache_prefix( $this->cache_group ) . GetPaid_Cache_Helper::get_cache_prefix( 'object_' . $this->get_id() ) . 'object_meta_' . $this->get_id();
684
			wp_cache_delete( $cache_key, $this->cache_group );
685
		}
686
	}
687
688
	/**
689
	 * Set ID.
690
	 *
691
	 * @since 1.0.19
692
	 * @param int $id ID.
693
	 */
694
	public function set_id( $id ) {
695
		$this->id = absint( $id );
696
	}
697
698
	/**
699
	 * Sets item status.
700
	 *
701
	 * @since 1.0.19
702
	 * @param string $status New status.
703
	 * @return array details of change.
704
	 */
705
	public function set_status( $status ) {
706
        $old_status = $this->get_status();
707
708
		$this->set_prop( 'status', $status );
709
710
		return array(
711
			'from' => $old_status,
712
			'to'   => $status,
713
		);
714
    }
715
716
	/**
717
	 * Set all props to default values.
718
	 *
719
	 * @since 1.0.19
720
	 */
721
	public function set_defaults() {
722
		$this->data    = $this->default_data;
723
		$this->changes = array();
724
		$this->set_object_read( false );
725
	}
726
727
	/**
728
	 * Set object read property.
729
	 *
730
	 * @since 1.0.19
731
	 * @param boolean $read Should read?.
732
	 */
733
	public function set_object_read( $read = true ) {
734
		$this->object_read = (bool) $read;
735
	}
736
737
	/**
738
	 * Get object read property.
739
	 *
740
	 * @since  1.0.19
741
	 * @return boolean
742
	 */
743
	public function get_object_read() {
744
		return (bool) $this->object_read;
745
	}
746
747
	/**
748
	 * Set a collection of props in one go, collect any errors, and return the result.
749
	 * Only sets using public methods.
750
	 *
751
	 * @since  1.0.19
752
	 *
753
	 * @param array  $props Key value pairs to set. Key is the prop and should map to a setter function name.
754
	 * @param string $context In what context to run this.
755
	 *
756
	 * @return bool|WP_Error
757
	 */
758
	public function set_props( $props, $context = 'set' ) {
759
		$errors = false;
760
761
		$props = wp_unslash( $props );
762
		foreach ( $props as $prop => $value ) {
763
			try {
764
				/**
765
				 * Checks if the prop being set is allowed, and the value is not null.
766
				 */
767
				if ( is_null( $value ) || in_array( $prop, array( 'prop', 'date_prop', 'meta_data' ), true ) ) {
768
					continue;
769
				}
770
				$setter = "set_$prop";
771
772
				if ( is_callable( array( $this, $setter ) ) ) {
773
					$this->{$setter}( $value );
774
				}
775
			} catch ( Exception $e ) {
776
				if ( ! $errors ) {
777
					$errors = new WP_Error();
778
				}
779
				$errors->add( $e->getCode(), $e->getMessage() );
780
				$this->last_error = $e->getMessage();
781
			}
782
		}
783
784
		return $errors && count( $errors->get_error_codes() ) ? $errors : true;
785
	}
786
787
	/**
788
	 * Sets a prop for a setter method.
789
	 *
790
	 * This stores changes in a special array so we can track what needs saving
791
	 * the the DB later.
792
	 *
793
	 * @since 1.0.19
794
	 * @param string $prop Name of prop to set.
795
	 * @param mixed  $value Value of the prop.
796
	 */
797
	protected function set_prop( $prop, $value ) {
798
		if ( array_key_exists( $prop, $this->data ) ) {
799
			if ( true === $this->object_read ) {
800
				if ( $value !== $this->data[ $prop ] || array_key_exists( $prop, $this->changes ) ) {
801
					$this->changes[ $prop ] = $value;
802
				}
803
			} else {
804
				$this->data[ $prop ] = $value;
805
			}
806
		}
807
	}
808
809
	/**
810
	 * Return data changes only.
811
	 *
812
	 * @since 1.0.19
813
	 * @return array
814
	 */
815
	public function get_changes() {
816
		return $this->changes;
817
	}
818
819
	/**
820
	 * Merge changes with data and clear.
821
	 *
822
	 * @since 1.0.19
823
	 */
824
	public function apply_changes() {
825
		$this->data    = array_replace( $this->data, $this->changes );
826
		$this->changes = array();
827
	}
828
829
	/**
830
	 * Prefix for action and filter hooks on data.
831
	 *
832
	 * @since  1.0.19
833
	 * @return string
834
	 */
835
	protected function get_hook_prefix() {
836
		return 'wpinv_get_' . $this->object_type . '_';
837
	}
838
839
	/**
840
	 * Gets a prop for a getter method.
841
	 *
842
	 * Gets the value from either current pending changes, or the data itself.
843
	 * Context controls what happens to the value before it's returned.
844
	 *
845
	 * @since  1.0.19
846
	 * @param  string $prop Name of prop to get.
847
	 * @param  string $context What the value is for. Valid values are view and edit.
848
	 * @return mixed
849
	 */
850
	protected function get_prop( $prop, $context = 'view' ) {
851
		$value = null;
852
853
		if ( array_key_exists( $prop, $this->data ) ) {
854
			$value = array_key_exists( $prop, $this->changes ) ? $this->changes[ $prop ] : $this->data[ $prop ];
855
856
			if ( 'view' === $context ) {
857
				$value = apply_filters( $this->get_hook_prefix() . $prop, $value, $this );
858
			}
859
		}
860
861
		return $value;
862
	}
863
864
	/**
865
	 * Sets a date prop whilst handling formatting and datetime objects.
866
	 *
867
	 * @since 1.0.19
868
	 * @param string         $prop Name of prop to set.
869
	 * @param string|integer $value Value of the prop.
870
	 */
871
	protected function set_date_prop( $prop, $value ) {
872
873
		if ( empty( $value ) ) {
874
			$this->set_prop( $prop, null );
875
			return;
876
		}
877
		$this->set_prop( $prop, $value );
878
879
	}
880
881
	/**
882
	 * When invalid data is found, throw an exception unless reading from the DB.
883
	 *
884
	 * @since 1.0.19
885
	 * @param string $code             Error code.
886
	 * @param string $message          Error message.
887
	 */
888
	protected function error( $code, $message ) {
889
		$this->last_error = $message;
890
	}
891
892
	/**
893
	 * Checks if the object is saved in the database
894
	 *
895
	 * @since 1.0.19
896
	 * @return bool
897
	 */
898
	public function exists() {
899
		$id = $this->get_id();
900
		return ! empty( $id );
901
	}
902
903
}
904