Passed
Push — master ( cb75a2...6929e7 )
by Brian
05:54 queued 11s
created

GetPaid_Data::meta_exists()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 3
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 4
rs 10
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 $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 ) {
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
Bug introduced by
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...
Bug introduced by
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.', 'getpaid' ), $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
Bug introduced by
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.', 'getpaid' ), $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.', 'getpaid' ), $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
509
	/**
510
	 * Update meta data by key or ID, if provided.
511
	 *
512
	 * @since  1.0.19
513
	 *
514
	 * @param  string       $key Meta key.
515
	 * @param  string|array $value Meta value.
516
	 * @param  int          $meta_id Meta ID.
517
	 */
518
	public function update_meta_data( $key, $value, $meta_id = 0 ) {
519
		if ( $this->is_internal_meta_key( $key ) ) {
520
			$function = 'set_' . $key;
521
522
			if ( is_callable( array( $this, $function ) ) ) {
523
				return $this->{$function}( $value );
524
			}
525
		}
526
527
		$this->maybe_read_meta_data();
528
529
		$array_key = false;
530
531
		if ( $meta_id ) {
532
			$array_keys = array_keys( wp_list_pluck( $this->meta_data, 'id' ), $meta_id, true );
533
			$array_key  = $array_keys ? current( $array_keys ) : false;
534
		} else {
535
			// Find matches by key.
536
			$matches = array();
537
			foreach ( $this->meta_data as $meta_data_array_key => $meta ) {
538
				if ( $meta->key === $key ) {
539
					$matches[] = $meta_data_array_key;
540
				}
541
			}
542
543
			if ( ! empty( $matches ) ) {
544
				// Set matches to null so only one key gets the new value.
545
				foreach ( $matches as $meta_data_array_key ) {
546
					$this->meta_data[ $meta_data_array_key ]->value = null;
547
				}
548
				$array_key = current( $matches );
549
			}
550
		}
551
552
		if ( false !== $array_key ) {
553
			$meta        = $this->meta_data[ $array_key ];
554
			$meta->key   = $key;
555
			$meta->value = $value;
556
		} else {
557
			$this->add_meta_data( $key, $value, true );
558
		}
559
	}
560
561
	/**
562
	 * Delete meta data.
563
	 *
564
	 * @since 1.0.19
565
	 * @param string $key Meta key.
566
	 */
567
	public function delete_meta_data( $key ) {
568
		$this->maybe_read_meta_data();
569
		$array_keys = array_keys( wp_list_pluck( $this->meta_data, 'key' ), $key, true );
570
571
		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...
572
			foreach ( $array_keys as $array_key ) {
573
				$this->meta_data[ $array_key ]->value = null;
574
			}
575
		}
576
	}
577
578
	/**
579
	 * Delete meta data.
580
	 *
581
	 * @since 1.0.19
582
	 * @param int $mid Meta ID.
583
	 */
584
	public function delete_meta_data_by_mid( $mid ) {
585
		$this->maybe_read_meta_data();
586
		$array_keys = array_keys( wp_list_pluck( $this->meta_data, 'id' ), (int) $mid, true );
587
588
		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...
589
			foreach ( $array_keys as $array_key ) {
590
				$this->meta_data[ $array_key ]->value = null;
591
			}
592
		}
593
	}
594
595
	/**
596
	 * Read meta data if null.
597
	 *
598
	 * @since 1.0.19
599
	 */
600
	protected function maybe_read_meta_data() {
601
		if ( is_null( $this->meta_data ) ) {
0 ignored issues
show
introduced by
The condition is_null($this->meta_data) is always false.
Loading history...
602
			$this->read_meta_data();
603
		}
604
	}
605
606
	/**
607
	 * Read Meta Data from the database. Ignore any internal properties.
608
	 * Uses it's own caches because get_metadata does not provide meta_ids.
609
	 *
610
	 * @since 1.0.19
611
	 * @param bool $force_read True to force a new DB read (and update cache).
612
	 */
613
	public function read_meta_data( $force_read = false ) {
614
		$this->meta_data = array();
615
		$cache_loaded    = false;
616
617
		if ( ! $this->get_id() ) {
618
			return;
619
		}
620
621
		if ( ! $this->data_store ) {
622
			return;
623
		}
624
625
		if ( ! empty( $this->cache_group ) ) {
626
			$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();
627
		}
628
629
		if ( ! $force_read ) {
630
			if ( ! empty( $this->cache_group ) ) {
631
				$cached_meta  = wp_cache_get( $cache_key, $this->cache_group );
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $cache_key does not seem to be defined for all execution paths leading up to this point.
Loading history...
632
				$cache_loaded = ! empty( $cached_meta );
633
			}
634
		}
635
636
		$raw_meta_data = $cache_loaded ? $cached_meta : $this->data_store->read_meta( $this );
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $cached_meta does not seem to be defined for all execution paths leading up to this point.
Loading history...
Bug introduced by
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

636
		$raw_meta_data = $cache_loaded ? $cached_meta : $this->data_store->/** @scrutinizer ignore-call */ read_meta( $this );
Loading history...
637
		if ( $raw_meta_data ) {
638
			foreach ( $raw_meta_data as $meta ) {
639
				$this->meta_data[] = new GetPaid_Meta_Data(
640
					array(
641
						'id'    => (int) $meta->meta_id,
642
						'key'   => $meta->meta_key,
643
						'value' => maybe_unserialize( $meta->meta_value ),
644
					)
645
				);
646
			}
647
648
			if ( ! $cache_loaded && ! empty( $this->cache_group ) ) {
649
				wp_cache_set( $cache_key, $raw_meta_data, $this->cache_group );
650
			}
651
		}
652
	}
653
654
	/**
655
	 * Update Meta Data in the database.
656
	 *
657
	 * @since 1.0.19
658
	 */
659
	public function save_meta_data() {
660
		if ( ! $this->data_store || is_null( $this->meta_data ) ) {
661
			return;
662
		}
663
		foreach ( $this->meta_data as $array_key => $meta ) {
664
			if ( is_null( $meta->value ) ) {
665
				if ( ! empty( $meta->id ) ) {
666
					$this->data_store->delete_meta( $this, $meta );
0 ignored issues
show
Bug introduced by
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

666
					$this->data_store->/** @scrutinizer ignore-call */ 
667
                        delete_meta( $this, $meta );
Loading history...
667
					unset( $this->meta_data[ $array_key ] );
668
				}
669
			} elseif ( empty( $meta->id ) ) {
670
				$meta->id = $this->data_store->add_meta( $this, $meta );
0 ignored issues
show
Bug introduced by
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

670
				/** @scrutinizer ignore-call */ 
671
    $meta->id = $this->data_store->add_meta( $this, $meta );
Loading history...
671
				$meta->apply_changes();
672
			} else {
673
				if ( $meta->get_changes() ) {
674
					$this->data_store->update_meta( $this, $meta );
0 ignored issues
show
Bug introduced by
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

674
					$this->data_store->/** @scrutinizer ignore-call */ 
675
                        update_meta( $this, $meta );
Loading history...
675
					$meta->apply_changes();
676
				}
677
			}
678
		}
679
		if ( ! empty( $this->cache_group ) ) {
680
			$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();
681
			wp_cache_delete( $cache_key, $this->cache_group );
682
		}
683
	}
684
685
	/**
686
	 * Set ID.
687
	 *
688
	 * @since 1.0.19
689
	 * @param int $id ID.
690
	 */
691
	public function set_id( $id ) {
692
		$this->id = absint( $id );
693
	}
694
695
	/**
696
	 * Sets item status.
697
	 *
698
	 * @since 1.0.19
699
	 * @param string $status New status.
700
	 * @return array details of change.
701
	 */
702
	public function set_status( $status ) {
703
        $old_status = $this->get_status();
704
705
		$this->set_prop( 'status', $status );
706
707
		return array(
708
			'from' => $old_status,
709
			'to'   => $status,
710
		);
711
    }
712
713
	/**
714
	 * Set all props to default values.
715
	 *
716
	 * @since 1.0.19
717
	 */
718
	public function set_defaults() {
719
		$this->data    = $this->default_data;
720
		$this->changes = array();
721
		$this->set_object_read( false );
722
	}
723
724
	/**
725
	 * Set object read property.
726
	 *
727
	 * @since 1.0.19
728
	 * @param boolean $read Should read?.
729
	 */
730
	public function set_object_read( $read = true ) {
731
		$this->object_read = (bool) $read;
732
	}
733
734
	/**
735
	 * Get object read property.
736
	 *
737
	 * @since  1.0.19
738
	 * @return boolean
739
	 */
740
	public function get_object_read() {
741
		return (bool) $this->object_read;
742
	}
743
744
	/**
745
	 * Set a collection of props in one go, collect any errors, and return the result.
746
	 * Only sets using public methods.
747
	 *
748
	 * @since  1.0.19
749
	 *
750
	 * @param array  $props Key value pairs to set. Key is the prop and should map to a setter function name.
751
	 * @param string $context In what context to run this.
752
	 *
753
	 * @return bool|WP_Error
754
	 */
755
	public function set_props( $props, $context = 'set' ) {
756
		$errors = false;
757
758
		foreach ( $props as $prop => $value ) {
759
			try {
760
				/**
761
				 * Checks if the prop being set is allowed, and the value is not null.
762
				 */
763
				if ( is_null( $value ) || in_array( $prop, array( 'prop', 'date_prop', 'meta_data' ), true ) ) {
764
					continue;
765
				}
766
				$setter = "set_$prop";
767
768
				if ( is_callable( array( $this, $setter ) ) ) {
769
					$this->{$setter}( $value );
770
				}
771
			} catch ( Exception $e ) {
772
				if ( ! $errors ) {
773
					$errors = new WP_Error();
774
				}
775
				$errors->add( $e->getCode(), $e->getMessage() );
776
				$this->last_error = $e->getMessage();
777
			}
778
		}
779
780
		return $errors && count( $errors->get_error_codes() ) ? $errors : true;
781
	}
782
783
	/**
784
	 * Sets a prop for a setter method.
785
	 *
786
	 * This stores changes in a special array so we can track what needs saving
787
	 * the the DB later.
788
	 *
789
	 * @since 1.0.19
790
	 * @param string $prop Name of prop to set.
791
	 * @param mixed  $value Value of the prop.
792
	 */
793
	protected function set_prop( $prop, $value ) {
794
		if ( array_key_exists( $prop, $this->data ) ) {
795
			if ( true === $this->object_read ) {
796
				if ( $value !== $this->data[ $prop ] || array_key_exists( $prop, $this->changes ) ) {
797
					$this->changes[ $prop ] = maybe_unserialize( $value );
0 ignored issues
show
Security Object Injection introduced by
$value can contain request data and is used in unserialized context(s) leading to a potential security vulnerability.

2 paths for user data to reach this point

  1. Path: Read from $_POST, and Data is passed through wp_unslash(), and Data is passed through json_decode(), and json_decode(wp_unslash($_POST['wpinv_form_items']), true) is assigned to $form_items in includes/admin/meta-boxes/class-getpaid-meta-box-payment-form.php on line 95
  1. Read from $_POST, and Data is passed through wp_unslash(), and Data is passed through json_decode(), and json_decode(wp_unslash($_POST['wpinv_form_items']), true) is assigned to $form_items
    in includes/admin/meta-boxes/class-getpaid-meta-box-payment-form.php on line 95
  2. Data is passed through maybe_save_items(), and self::maybe_save_items($form_items) is assigned to $form_items
    in includes/admin/meta-boxes/class-getpaid-meta-box-payment-form.php on line 103
  3. GetPaid_Payment_Form::set_items() is called
    in includes/admin/meta-boxes/class-getpaid-meta-box-payment-form.php on line 106
  4. Enters via parameter $value
    in includes/class-getpaid-payment-form.php on line 538
  5. GetPaid_Data::set_prop() is called
    in includes/class-getpaid-payment-form.php on line 540
  6. Enters via parameter $value
    in includes/data-stores/class-getpaid-data.php on line 793
  2. Path: Read from $_POST, and Data is passed through wp_unslash(), and Data is passed through json_decode(), and json_decode(wp_unslash($_POST['wpinv_form_elements']), true) is assigned to $form_elements in includes/admin/meta-boxes/class-getpaid-meta-box-payment-form.php on line 109
  1. Read from $_POST, and Data is passed through wp_unslash(), and Data is passed through json_decode(), and json_decode(wp_unslash($_POST['wpinv_form_elements']), true) is assigned to $form_elements
    in includes/admin/meta-boxes/class-getpaid-meta-box-payment-form.php on line 109
  2. GetPaid_Payment_Form::set_elements() is called
    in includes/admin/meta-boxes/class-getpaid-meta-box-payment-form.php on line 114
  3. Enters via parameter $value
    in includes/class-getpaid-payment-form.php on line 526
  4. GetPaid_Data::set_prop() is called
    in includes/class-getpaid-payment-form.php on line 528
  5. Enters via parameter $value
    in includes/data-stores/class-getpaid-data.php on line 793

Used in unserialized context

  1. maybe_unserialize() is called
    in includes/data-stores/class-getpaid-data.php on line 795
  2. Enters via parameter $data
    in wordpress/wp-includes/functions.php on line 622
  3. Data is passed through trim()
    in wordpress/wp-includes/functions.php on line 624
  4. unserialize() is called
    in wordpress/wp-includes/functions.php on line 624

Preventing Object Injection Attacks

If you pass raw user-data to unserialize() for example, this can be used to create an object of any class that is available in your local filesystem. For an attacker, classes that have magic methods like __destruct or __wakeup are particularly interesting in such a case, as they can be exploited very easily.

We recommend to not pass user data to such a function. In case of unserialize, better use JSON to transfer data.

General Strategies to prevent injection

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

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

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

$sanitized = (integer) $tainted;
Loading history...
798
				}
799
			} else {
800
				$this->data[ $prop ] = maybe_unserialize( $value );
0 ignored issues
show
Security Object Injection introduced by
$value can contain request data and is used in unserialized context(s) leading to a potential security vulnerability.

2 paths for user data to reach this point

  1. Path: Read from $_POST, and Data is passed through wp_unslash(), and Data is passed through json_decode(), and json_decode(wp_unslash($_POST['wpinv_form_items']), true) is assigned to $form_items in includes/admin/meta-boxes/class-getpaid-meta-box-payment-form.php on line 95
  1. Read from $_POST, and Data is passed through wp_unslash(), and Data is passed through json_decode(), and json_decode(wp_unslash($_POST['wpinv_form_items']), true) is assigned to $form_items
    in includes/admin/meta-boxes/class-getpaid-meta-box-payment-form.php on line 95
  2. Data is passed through maybe_save_items(), and self::maybe_save_items($form_items) is assigned to $form_items
    in includes/admin/meta-boxes/class-getpaid-meta-box-payment-form.php on line 103
  3. GetPaid_Payment_Form::set_items() is called
    in includes/admin/meta-boxes/class-getpaid-meta-box-payment-form.php on line 106
  4. Enters via parameter $value
    in includes/class-getpaid-payment-form.php on line 538
  5. GetPaid_Data::set_prop() is called
    in includes/class-getpaid-payment-form.php on line 540
  6. Enters via parameter $value
    in includes/data-stores/class-getpaid-data.php on line 793
  2. Path: Read from $_POST, and Data is passed through wp_unslash(), and Data is passed through json_decode(), and json_decode(wp_unslash($_POST['wpinv_form_elements']), true) is assigned to $form_elements in includes/admin/meta-boxes/class-getpaid-meta-box-payment-form.php on line 109
  1. Read from $_POST, and Data is passed through wp_unslash(), and Data is passed through json_decode(), and json_decode(wp_unslash($_POST['wpinv_form_elements']), true) is assigned to $form_elements
    in includes/admin/meta-boxes/class-getpaid-meta-box-payment-form.php on line 109
  2. GetPaid_Payment_Form::set_elements() is called
    in includes/admin/meta-boxes/class-getpaid-meta-box-payment-form.php on line 114
  3. Enters via parameter $value
    in includes/class-getpaid-payment-form.php on line 526
  4. GetPaid_Data::set_prop() is called
    in includes/class-getpaid-payment-form.php on line 528
  5. Enters via parameter $value
    in includes/data-stores/class-getpaid-data.php on line 793

Used in unserialized context

  1. maybe_unserialize() is called
    in includes/data-stores/class-getpaid-data.php on line 798
  2. Enters via parameter $data
    in wordpress/wp-includes/functions.php on line 622
  3. Data is passed through trim()
    in wordpress/wp-includes/functions.php on line 624
  4. unserialize() is called
    in wordpress/wp-includes/functions.php on line 624

Preventing Object Injection Attacks

If you pass raw user-data to unserialize() for example, this can be used to create an object of any class that is available in your local filesystem. For an attacker, classes that have magic methods like __destruct or __wakeup are particularly interesting in such a case, as they can be exploited very easily.

We recommend to not pass user data to such a function. In case of unserialize, better use JSON to transfer data.

General Strategies to prevent injection

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

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

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

$sanitized = (integer) $tainted;
Loading history...
801
			}
802
		}
803
	}
804
805
	/**
806
	 * Return data changes only.
807
	 *
808
	 * @since 1.0.19
809
	 * @return array
810
	 */
811
	public function get_changes() {
812
		return $this->changes;
813
	}
814
815
	/**
816
	 * Merge changes with data and clear.
817
	 *
818
	 * @since 1.0.19
819
	 */
820
	public function apply_changes() {
821
		$this->data    = array_replace_recursive( $this->data, $this->changes );
822
		$this->changes = array();
823
	}
824
825
	/**
826
	 * Prefix for action and filter hooks on data.
827
	 *
828
	 * @since  1.0.19
829
	 * @return string
830
	 */
831
	protected function get_hook_prefix() {
832
		return 'wpinv_get_' . $this->object_type . '_';
833
	}
834
835
	/**
836
	 * Gets a prop for a getter method.
837
	 *
838
	 * Gets the value from either current pending changes, or the data itself.
839
	 * Context controls what happens to the value before it's returned.
840
	 *
841
	 * @since  1.0.19
842
	 * @param  string $prop Name of prop to get.
843
	 * @param  string $context What the value is for. Valid values are view and edit.
844
	 * @return mixed
845
	 */
846
	protected function get_prop( $prop, $context = 'view' ) {
847
		$value = null;
848
849
		if ( array_key_exists( $prop, $this->data ) ) {
850
			$value = array_key_exists( $prop, $this->changes ) ? $this->changes[ $prop ] : $this->data[ $prop ];
851
852
			if ( 'view' === $context ) {
853
				$value = apply_filters( $this->get_hook_prefix() . $prop, $value, $this );
854
			}
855
		}
856
857
		return $value;
858
	}
859
860
	/**
861
	 * Sets a date prop whilst handling formatting and datetime objects.
862
	 *
863
	 * @since 1.0.19
864
	 * @param string         $prop Name of prop to set.
865
	 * @param string|integer $value Value of the prop.
866
	 */
867
	protected function set_date_prop( $prop, $value ) {
868
869
		if ( empty( $value ) ) {
870
			$this->set_prop( $prop, null );
871
			return;
872
		}
873
		$this->set_prop( $prop, $value );
874
875
	}
876
877
	/**
878
	 * When invalid data is found, throw an exception unless reading from the DB.
879
	 *
880
	 * @since 1.0.19
881
	 * @param string $code             Error code.
882
	 * @param string $message          Error message.
883
	 */
884
	protected function error( $code, $message ) {
885
		$this->last_error = $message;
886
	}
887
888
	/**
889
	 * Checks if the object is saved in the database
890
	 *
891
	 * @since 1.0.19
892
	 * @return bool
893
	 */
894
	public function exists() {
895
		$id = $this->get_id();
896
		return ! empty( $id );
897
	}
898
899
}
900