Issues (942)

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/abstracts/abstract-wc-data.php (2 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
 * Abstract Data.
4
 *
5
 * Handles generic data interaction which is implemented by
6
 * the different data store classes.
7
 *
8
 * @class       WC_Data
9
 * @version     3.0.0
10
 * @package     WooCommerce/Classes
11
 */
12
13
if ( ! defined( 'ABSPATH' ) ) {
14
	exit;
15
}
16
17
/**
18
 * Abstract WC Data Class
19
 *
20
 * Implemented by classes using the same CRUD(s) pattern.
21
 *
22
 * @version  2.6.0
23
 * @package  WooCommerce/Abstracts
24
 */
25
abstract class WC_Data {
26
27
	/**
28
	 * ID for this object.
29
	 *
30
	 * @since 3.0.0
31
	 * @var int
32
	 */
33
	protected $id = 0;
34
35
	/**
36
	 * Core data for this object. Name value pairs (name + default value).
37
	 *
38
	 * @since 3.0.0
39
	 * @var array
40
	 */
41
	protected $data = array();
42
43
	/**
44
	 * Core data changes for this object.
45
	 *
46
	 * @since 3.0.0
47
	 * @var array
48
	 */
49
	protected $changes = array();
50
51
	/**
52
	 * This is false until the object is read from the DB.
53
	 *
54
	 * @since 3.0.0
55
	 * @var bool
56
	 */
57
	protected $object_read = false;
58
59
	/**
60
	 * This is the name of this object type.
61
	 *
62
	 * @since 3.0.0
63
	 * @var string
64
	 */
65
	protected $object_type = 'data';
66
67
	/**
68
	 * Extra data for this object. Name value pairs (name + default value).
69
	 * Used as a standard way for sub classes (like product types) to add
70
	 * additional information to an inherited class.
71
	 *
72
	 * @since 3.0.0
73
	 * @var array
74
	 */
75
	protected $extra_data = array();
76
77
	/**
78
	 * Set to _data on construct so we can track and reset data if needed.
79
	 *
80
	 * @since 3.0.0
81
	 * @var array
82
	 */
83
	protected $default_data = array();
84
85
	/**
86
	 * Contains a reference to the data store for this class.
87
	 *
88
	 * @since 3.0.0
89
	 * @var object
90
	 */
91
	protected $data_store;
92
93
	/**
94
	 * Stores meta in cache for future reads.
95
	 * A group must be set to to enable caching.
96
	 *
97
	 * @since 3.0.0
98
	 * @var string
99
	 */
100
	protected $cache_group = '';
101
102
	/**
103
	 * Stores additional meta data.
104
	 *
105
	 * @since 3.0.0
106
	 * @var array
107
	 */
108
	protected $meta_data = null;
109
110
	/**
111
	 * Default constructor.
112
	 *
113
	 * @param int|object|array $read ID to load from the DB (optional) or already queried data.
114
	 */
115 546
	public function __construct( $read = 0 ) {
116 546
		$this->data         = array_merge( $this->data, $this->extra_data );
117 546
		$this->default_data = $this->data;
118
	}
119
120
	/**
121
	 * Only store the object ID to avoid serializing the data object instance.
122
	 *
123
	 * @return array
124
	 */
125
	public function __sleep() {
126
		return array( 'id' );
127
	}
128
129
	/**
130
	 * Re-run the constructor with the object ID.
131
	 *
132
	 * If the object no longer exists, remove the ID.
133
	 */
134
	public function __wakeup() {
135
		try {
136
			$this->__construct( absint( $this->id ) );
137
		} catch ( Exception $e ) {
138
			$this->set_id( 0 );
139
			$this->set_object_read( true );
140
		}
141
	}
142
143
	/**
144
	 * When the object is cloned, make sure meta is duplicated correctly.
145
	 *
146
	 * @since 3.0.2
147
	 */
148
	public function __clone() {
149
		$this->maybe_read_meta_data();
150
		if ( ! empty( $this->meta_data ) ) {
151
			foreach ( $this->meta_data as $array_key => $meta ) {
152
				$this->meta_data[ $array_key ] = clone $meta;
153
				if ( ! empty( $meta->id ) ) {
154
					$this->meta_data[ $array_key ]->id = null;
155
				}
156
			}
157
		}
158
	}
159
160
	/**
161
	 * Get the data store.
162
	 *
163
	 * @since  3.0.0
164
	 * @return object
165
	 */
166 24
	public function get_data_store() {
167 24
		return $this->data_store;
168
	}
169
170
	/**
171
	 * Returns the unique ID for this object.
172
	 *
173
	 * @since  2.6.0
174
	 * @return int
175
	 */
176 569
	public function get_id() {
177 569
		return $this->id;
178
	}
179
180
	/**
181
	 * Delete an object, set the ID to 0, and return result.
182
	 *
183
	 * @since  2.6.0
184
	 * @param  bool $force_delete Should the date be deleted permanently.
185
	 * @return bool result
186
	 */
187 52 View Code Duplication
	public function delete( $force_delete = false ) {
188 52
		if ( $this->data_store ) {
189 52
			$this->data_store->delete( $this, array( 'force_delete' => $force_delete ) );
190 52
			$this->set_id( 0 );
191 52
			return true;
192
		}
193
		return false;
194
	}
195
196
	/**
197
	 * Save should create or update based on object existence.
198
	 *
199
	 * @since  2.6.0
200
	 * @return int
201
	 */
202 179
	public function save() {
203 179
		if ( ! $this->data_store ) {
204
			return $this->get_id();
205
		}
206
207
		/**
208
		 * Trigger action before saving to the DB. Allows you to adjust object props before save.
209
		 *
210
		 * @param WC_Data          $this The object being saved.
211
		 * @param WC_Data_Store_WP $data_store THe data store persisting the data.
212
		 */
213 179
		do_action( 'woocommerce_before_' . $this->object_type . '_object_save', $this, $this->data_store );
214
215 179
		if ( $this->get_id() ) {
216 102
			$this->data_store->update( $this );
217
		} else {
218 171
			$this->data_store->create( $this );
219
		}
220
221
		/**
222
		 * Trigger action after saving to the DB.
223
		 *
224
		 * @param WC_Data          $this The object being saved.
225
		 * @param WC_Data_Store_WP $data_store THe data store persisting the data.
226
		 */
227 179
		do_action( 'woocommerce_after_' . $this->object_type . '_object_save', $this, $this->data_store );
228
229 179
		return $this->get_id();
230
	}
231
232
	/**
233
	 * Change data to JSON format.
234
	 *
235
	 * @since  2.6.0
236
	 * @return string Data in JSON format.
237
	 */
238 2
	public function __toString() {
239 2
		return wp_json_encode( $this->get_data() );
240
	}
241
242
	/**
243
	 * Returns all data for this object.
244
	 *
245
	 * @since  2.6.0
246
	 * @return array
247
	 */
248 6
	public function get_data() {
249 6
		return array_merge( array( 'id' => $this->get_id() ), $this->data, array( 'meta_data' => $this->get_meta_data() ) );
250
	}
251
252
	/**
253
	 * Returns array of expected data keys for this object.
254
	 *
255
	 * @since   3.0.0
256
	 * @return array
257
	 */
258 215
	public function get_data_keys() {
259 215
		return array_keys( $this->data );
260
	}
261
262
	/**
263
	 * Returns all "extra" data keys for an object (for sub objects like product types).
264
	 *
265
	 * @since  3.0.0
266
	 * @return array
267
	 */
268 307
	public function get_extra_data_keys() {
269 307
		return array_keys( $this->extra_data );
270
	}
271
272
	/**
273
	 * Filter null meta values from array.
274
	 *
275
	 * @since  3.0.0
276
	 * @param mixed $meta Meta value to check.
277
	 * @return bool
278
	 */
279 34
	protected function filter_null_meta( $meta ) {
280 34
		return ! is_null( $meta->value );
281
	}
282
283
	/**
284
	 * Get All Meta Data.
285
	 *
286
	 * @since 2.6.0
287
	 * @return array of objects.
288
	 */
289 74
	public function get_meta_data() {
290 74
		$this->maybe_read_meta_data();
291 74
		return array_values( array_filter( $this->meta_data, array( $this, 'filter_null_meta' ) ) );
292
	}
293
294
	/**
295
	 * Check if the key is an internal one.
296
	 *
297
	 * @since  3.2.0
298
	 * @param  string $key Key to check.
299
	 * @return bool   true if it's an internal key, false otherwise
300
	 */
301 69
	protected function is_internal_meta_key( $key ) {
302 69
		$internal_meta_key = ! empty( $key ) && $this->data_store && in_array( $key, $this->data_store->get_internal_meta_keys(), true );
303
304 69
		if ( ! $internal_meta_key ) {
305 69
			return false;
306
		}
307
308
		$has_setter_or_getter = is_callable( array( $this, 'set_' . $key ) ) || is_callable( array( $this, 'get_' . $key ) );
309
310
		if ( ! $has_setter_or_getter ) {
311
			return false;
312
		}
313
		/* translators: %s: $key Key to check */
314
		wc_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.', 'woocommerce' ), $key ), '3.2.0' );
315
316
		return true;
317
	}
318
319
	/**
320
	 * Get Meta Data by Key.
321
	 *
322
	 * @since  2.6.0
323
	 * @param  string $key Meta Key.
324
	 * @param  bool   $single return first found meta with key, or all with $key.
325
	 * @param  string $context What the value is for. Valid values are view and edit.
326
	 * @return mixed
327
	 */
328 52
	public function get_meta( $key = '', $single = true, $context = 'view' ) {
329 52
		if ( $this->is_internal_meta_key( $key ) ) {
330
			$function = 'get_' . $key;
331
332
			if ( is_callable( array( $this, $function ) ) ) {
333
				return $this->{$function}();
334
			}
335
		}
336
337 52
		$this->maybe_read_meta_data();
338 52
		$meta_data  = $this->get_meta_data();
339 52
		$array_keys = array_keys( wp_list_pluck( $meta_data, 'key' ), $key, true );
340 52
		$value      = $single ? '' : array();
341
342 52
		if ( ! empty( $array_keys ) ) {
343
			// 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()).
344 22
			if ( $single ) {
345 22
				$value = $meta_data[ current( $array_keys ) ]->value;
346
			} else {
347 1
				$value = array_intersect_key( $meta_data, array_flip( $array_keys ) );
348
			}
349
350 22
			if ( 'view' === $context ) {
351 22
				$value = apply_filters( $this->get_hook_prefix() . $key, $value, $this );
352
			}
353
		}
354
355 52
		return $value;
356
	}
357
358
	/**
359
	 * See if meta data exists, since get_meta always returns a '' or array().
360
	 *
361
	 * @since  3.0.0
362
	 * @param  string $key Meta Key.
363
	 * @return boolean
364
	 */
365 1
	public function meta_exists( $key = '' ) {
366 1
		$this->maybe_read_meta_data();
367 1
		$array_keys = wp_list_pluck( $this->get_meta_data(), 'key' );
368 1
		return in_array( $key, $array_keys, true );
369
	}
370
371
	/**
372
	 * Set all meta data from array.
373
	 *
374
	 * @since 2.6.0
375
	 * @param array $data Key/Value pairs.
376
	 */
377 1
	public function set_meta_data( $data ) {
378 1
		if ( ! empty( $data ) && is_array( $data ) ) {
379 1
			$this->maybe_read_meta_data();
380 1
			foreach ( $data as $meta ) {
381 1
				$meta = (array) $meta;
382 1
				if ( isset( $meta['key'], $meta['value'], $meta['id'] ) ) {
383 1
					$this->meta_data[] = new WC_Meta_Data(
384
						array(
385 1
							'id'    => $meta['id'],
386 1
							'key'   => $meta['key'],
387 1
							'value' => $meta['value'],
388
						)
389
					);
390
				}
391
			}
392
		}
393
	}
394
395
	/**
396
	 * Add meta data.
397
	 *
398
	 * @since 2.6.0
399
	 *
400
	 * @param string       $key Meta key.
401
	 * @param string|array $value Meta value.
402
	 * @param bool         $unique Should this be a unique key?.
403
	 */
404 36
	public function add_meta_data( $key, $value, $unique = false ) {
405 36 View Code Duplication
		if ( $this->is_internal_meta_key( $key ) ) {
406
			$function = 'set_' . $key;
407
408
			if ( is_callable( array( $this, $function ) ) ) {
409
				return $this->{$function}( $value );
410
			}
411
		}
412
413 36
		$this->maybe_read_meta_data();
414 36
		if ( $unique ) {
415 29
			$this->delete_meta_data( $key );
416
		}
417 36
		$this->meta_data[] = new WC_Meta_Data(
418
			array(
419 36
				'key'   => $key,
420 36
				'value' => $value,
421
			)
422
		);
423
	}
424
425
	/**
426
	 * Update meta data by key or ID, if provided.
427
	 *
428
	 * @since  2.6.0
429
	 *
430
	 * @param  string       $key Meta key.
431
	 * @param  string|array $value Meta value.
432
	 * @param  int          $meta_id Meta ID.
433
	 */
434 5
	public function update_meta_data( $key, $value, $meta_id = 0 ) {
435 5 View Code Duplication
		if ( $this->is_internal_meta_key( $key ) ) {
436
			$function = 'set_' . $key;
437
438
			if ( is_callable( array( $this, $function ) ) ) {
439
				return $this->{$function}( $value );
440
			}
441
		}
442
443 5
		$this->maybe_read_meta_data();
444
445 5
		$array_key = false;
446
447 5
		if ( $meta_id ) {
448 2
			$array_keys = array_keys( wp_list_pluck( $this->meta_data, 'id' ), $meta_id, true );
449 2
			$array_key  = $array_keys ? current( $array_keys ) : false;
450
		} else {
451
			// Find matches by key.
452 3
			$matches = array();
453 3
			foreach ( $this->meta_data as $meta_data_array_key => $meta ) {
454 2
				if ( $meta->key === $key ) {
455 1
					$matches[] = $meta_data_array_key;
456
				}
457
			}
458
459 3
			if ( ! empty( $matches ) ) {
460
				// Set matches to null so only one key gets the new value.
461 1
				foreach ( $matches as $meta_data_array_key ) {
462 1
					$this->meta_data[ $meta_data_array_key ]->value = null;
463
				}
464 1
				$array_key = current( $matches );
465
			}
466
		}
467
468 5
		if ( false !== $array_key ) {
469 1
			$meta        = $this->meta_data[ $array_key ];
470 1
			$meta->key   = $key;
471 1
			$meta->value = $value;
472
		} else {
473 4
			$this->add_meta_data( $key, $value, true );
474
		}
475
	}
476
477
	/**
478
	 * Delete meta data.
479
	 *
480
	 * @since 2.6.0
481
	 * @param string $key Meta key.
482
	 */
483 32 View Code Duplication
	public function delete_meta_data( $key ) {
484 32
		$this->maybe_read_meta_data();
485 32
		$array_keys = array_keys( wp_list_pluck( $this->meta_data, 'key' ), $key, true );
486
487 32
		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...
488 4
			foreach ( $array_keys as $array_key ) {
489 4
				$this->meta_data[ $array_key ]->value = null;
490
			}
491
		}
492
	}
493
494
	/**
495
	 * Delete meta data.
496
	 *
497
	 * @since 2.6.0
498
	 * @param int $mid Meta ID.
499
	 */
500 1 View Code Duplication
	public function delete_meta_data_by_mid( $mid ) {
501 1
		$this->maybe_read_meta_data();
502 1
		$array_keys = array_keys( wp_list_pluck( $this->meta_data, 'id' ), (int) $mid, true );
503
504 1
		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...
505 1
			foreach ( $array_keys as $array_key ) {
506 1
				$this->meta_data[ $array_key ]->value = null;
507
			}
508
		}
509
	}
510
511
	/**
512
	 * Read meta data if null.
513
	 *
514
	 * @since 3.0.0
515
	 */
516 83
	protected function maybe_read_meta_data() {
517 83
		if ( is_null( $this->meta_data ) ) {
518 61
			$this->read_meta_data();
519
		}
520
	}
521
522
	/**
523
	 * Read Meta Data from the database. Ignore any internal properties.
524
	 * Uses it's own caches because get_metadata does not provide meta_ids.
525
	 *
526
	 * @since 2.6.0
527
	 * @param bool $force_read True to force a new DB read (and update cache).
528
	 */
529 243
	public function read_meta_data( $force_read = false ) {
530 243
		$this->meta_data = array();
531 243
		$cache_loaded    = false;
532
533 243
		if ( ! $this->get_id() ) {
534 51
			return;
535
		}
536
537 215
		if ( ! $this->data_store ) {
538
			return;
539
		}
540
541 215 View Code Duplication
		if ( ! empty( $this->cache_group ) ) {
542
			// Prefix by group allows invalidation by group until https://core.trac.wordpress.org/ticket/4476 is implemented.
543 158
			$cache_key = WC_Cache_Helper::get_cache_prefix( $this->cache_group ) . WC_Cache_Helper::get_cache_prefix( 'object_' . $this->get_id() ) . 'object_meta_' . $this->get_id();
544
		}
545
546 215
		if ( ! $force_read ) {
547 204
			if ( ! empty( $this->cache_group ) ) {
548 147
				$cached_meta  = wp_cache_get( $cache_key, $this->cache_group );
549 147
				$cache_loaded = ! empty( $cached_meta );
550
			}
551
		}
552
553 215
		$raw_meta_data = $cache_loaded ? $cached_meta : $this->data_store->read_meta( $this );
554 215
		if ( $raw_meta_data ) {
555 53
			foreach ( $raw_meta_data as $meta ) {
556 53
				$this->meta_data[] = new WC_Meta_Data(
557
					array(
558 53
						'id'    => (int) $meta->meta_id,
559 53
						'key'   => $meta->meta_key,
560 53
						'value' => maybe_unserialize( $meta->meta_value ),
561
					)
562
				);
563
			}
564
565 53
			if ( ! $cache_loaded && ! empty( $this->cache_group ) ) {
566 12
				wp_cache_set( $cache_key, $raw_meta_data, $this->cache_group );
567
			}
568
		}
569
	}
570
571
	/**
572
	 * Update Meta Data in the database.
573
	 *
574
	 * @since 2.6.0
575
	 */
576 397
	public function save_meta_data() {
577 397
		if ( ! $this->data_store || is_null( $this->meta_data ) ) {
578 378
			return;
579
		}
580 68
		foreach ( $this->meta_data as $array_key => $meta ) {
581 41
			if ( is_null( $meta->value ) ) {
582 2
				if ( ! empty( $meta->id ) ) {
583 2
					$this->data_store->delete_meta( $this, $meta );
584 2
					unset( $this->meta_data[ $array_key ] );
585
				}
586 41
			} elseif ( empty( $meta->id ) ) {
587 30
				$meta->id = $this->data_store->add_meta( $this, $meta );
588 30
				$meta->apply_changes();
589
			} else {
590 20
				if ( $meta->get_changes() ) {
591 2
					$this->data_store->update_meta( $this, $meta );
592 2
					$meta->apply_changes();
593
				}
594
			}
595
		}
596 68 View Code Duplication
		if ( ! empty( $this->cache_group ) ) {
597 43
			$cache_key = WC_Cache_Helper::get_cache_prefix( $this->cache_group ) . WC_Cache_Helper::get_cache_prefix( 'object_' . $this->get_id() ) . 'object_meta_' . $this->get_id();
598 43
			wp_cache_delete( $cache_key, $this->cache_group );
599
		}
600
	}
601
602
	/**
603
	 * Set ID.
604
	 *
605
	 * @since 3.0.0
606
	 * @param int $id ID.
607
	 */
608 450
	public function set_id( $id ) {
609 450
		$this->id = absint( $id );
610
	}
611
612
	/**
613
	 * Set all props to default values.
614
	 *
615
	 * @since 3.0.0
616
	 */
617 303
	public function set_defaults() {
618 303
		$this->data    = $this->default_data;
619 303
		$this->changes = array();
620 303
		$this->set_object_read( false );
621
	}
622
623
	/**
624
	 * Set object read property.
625
	 *
626
	 * @since 3.0.0
627
	 * @param boolean $read Should read?.
628
	 */
629 568
	public function set_object_read( $read = true ) {
630 568
		$this->object_read = (bool) $read;
631
	}
632
633
	/**
634
	 * Get object read property.
635
	 *
636
	 * @since  3.0.0
637
	 * @return boolean
638
	 */
639 245
	public function get_object_read() {
640 245
		return (bool) $this->object_read;
641
	}
642
643
	/**
644
	 * Set a collection of props in one go, collect any errors, and return the result.
645
	 * Only sets using public methods.
646
	 *
647
	 * @since  3.0.0
648
	 *
649
	 * @param array  $props Key value pairs to set. Key is the prop and should map to a setter function name.
650
	 * @param string $context In what context to run this.
651
	 *
652
	 * @return bool|WP_Error
653
	 */
654 333
	public function set_props( $props, $context = 'set' ) {
655 333
		$errors = false;
656
657 333
		foreach ( $props as $prop => $value ) {
658
			try {
659
				/**
660
				 * Checks if the prop being set is allowed, and the value is not null.
661
				 */
662 333
				if ( is_null( $value ) || in_array( $prop, array( 'prop', 'date_prop', 'meta_data' ), true ) ) {
663 271
					continue;
664
				}
665 333
				$setter = "set_$prop";
666
667 333
				if ( is_callable( array( $this, $setter ) ) ) {
668 333
					$this->{$setter}( $value );
669
				}
670 68
			} catch ( WC_Data_Exception $e ) {
671 68
				if ( ! $errors ) {
672 68
					$errors = new WP_Error();
673
				}
674 68
				$errors->add( $e->getErrorCode(), $e->getMessage() );
675
			}
676
		}
677
678 333
		return $errors && count( $errors->get_error_codes() ) ? $errors : true;
679
	}
680
681
	/**
682
	 * Sets a prop for a setter method.
683
	 *
684
	 * This stores changes in a special array so we can track what needs saving
685
	 * the the DB later.
686
	 *
687
	 * @since 3.0.0
688
	 * @param string $prop Name of prop to set.
689
	 * @param mixed  $value Value of the prop.
690
	 */
691 509
	protected function set_prop( $prop, $value ) {
692 509
		if ( array_key_exists( $prop, $this->data ) ) {
693 509
			if ( true === $this->object_read ) {
694 495 View Code Duplication
				if ( $value !== $this->data[ $prop ] || array_key_exists( $prop, $this->changes ) ) {
695 495
					$this->changes[ $prop ] = $value;
696
				}
697
			} else {
698 353
				$this->data[ $prop ] = $value;
699
			}
700
		}
701
	}
702
703
	/**
704
	 * Return data changes only.
705
	 *
706
	 * @since 3.0.0
707
	 * @return array
708
	 */
709 381
	public function get_changes() {
710 381
		return $this->changes;
711
	}
712
713
	/**
714
	 * Merge changes with data and clear.
715
	 *
716
	 * @since 3.0.0
717
	 */
718 411
	public function apply_changes() {
719 411
		$this->data    = array_replace_recursive( $this->data, $this->changes ); // @codingStandardsIgnoreLine
720 411
		$this->changes = array();
721
	}
722
723
	/**
724
	 * Prefix for action and filter hooks on data.
725
	 *
726
	 * @since  3.0.0
727
	 * @return string
728
	 */
729 469
	protected function get_hook_prefix() {
730 469
		return 'woocommerce_' . $this->object_type . '_get_';
731
	}
732
733
	/**
734
	 * Gets a prop for a getter method.
735
	 *
736
	 * Gets the value from either current pending changes, or the data itself.
737
	 * Context controls what happens to the value before it's returned.
738
	 *
739
	 * @since  3.0.0
740
	 * @param  string $prop Name of prop to get.
741
	 * @param  string $context What the value is for. Valid values are view and edit.
742
	 * @return mixed
743
	 */
744 524
	protected function get_prop( $prop, $context = 'view' ) {
745 524
		$value = null;
746
747 524
		if ( array_key_exists( $prop, $this->data ) ) {
748 524
			$value = array_key_exists( $prop, $this->changes ) ? $this->changes[ $prop ] : $this->data[ $prop ];
749
750 524
			if ( 'view' === $context ) {
751 512
				$value = apply_filters( $this->get_hook_prefix() . $prop, $value, $this );
752
			}
753
		}
754
755 524
		return $value;
756
	}
757
758
	/**
759
	 * Sets a date prop whilst handling formatting and datetime objects.
760
	 *
761
	 * @since 3.0.0
762
	 * @param string         $prop Name of prop to set.
763
	 * @param string|integer $value Value of the prop.
764
	 */
765 382
	protected function set_date_prop( $prop, $value ) {
766
		try {
767 382
			if ( empty( $value ) ) {
768 145
				$this->set_prop( $prop, null );
769 145
				return;
770
			}
771
772 381
			if ( is_a( $value, 'WC_DateTime' ) ) {
773
				$datetime = $value;
774 381
			} elseif ( is_numeric( $value ) ) {
775
				// Timestamps are handled as UTC timestamps in all cases.
776 369
				$datetime = new WC_DateTime( "@{$value}", new DateTimeZone( 'UTC' ) );
777
			} else {
778
				// Strings are defined in local WP timezone. Convert to UTC.
779 65 View Code Duplication
				if ( 1 === preg_match( '/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(Z|((-|\+)\d{2}:\d{2}))$/', $value, $date_bits ) ) {
780 1
					$offset    = ! empty( $date_bits[7] ) ? iso8601_timezone_to_offset( $date_bits[7] ) : wc_timezone_offset();
781 1
					$timestamp = gmmktime( $date_bits[4], $date_bits[5], $date_bits[6], $date_bits[2], $date_bits[3], $date_bits[1] ) - $offset;
782
				} else {
783 65
					$timestamp = wc_string_to_timestamp( get_gmt_from_date( gmdate( 'Y-m-d H:i:s', wc_string_to_timestamp( $value ) ) ) );
784
				}
785 65
				$datetime = new WC_DateTime( "@{$timestamp}", new DateTimeZone( 'UTC' ) );
786
			}
787
788
			// Set local timezone or offset.
789 381
			if ( get_option( 'timezone_string' ) ) {
790 1
				$datetime->setTimezone( new DateTimeZone( wc_timezone_string() ) );
791
			} else {
792 381
				$datetime->set_utc_offset( wc_timezone_offset() );
793
			}
794
795 381
			$this->set_prop( $prop, $datetime );
796
		} catch ( Exception $e ) {} // @codingStandardsIgnoreLine.
797
	}
798
799
	/**
800
	 * When invalid data is found, throw an exception unless reading from the DB.
801
	 *
802
	 * @throws WC_Data_Exception Data Exception.
803
	 * @since 3.0.0
804
	 * @param string $code             Error code.
805
	 * @param string $message          Error message.
806
	 * @param int    $http_status_code HTTP status code.
807
	 * @param array  $data             Extra error data.
808
	 */
809 69
	protected function error( $code, $message, $http_status_code = 400, $data = array() ) {
810 69
		throw new WC_Data_Exception( $code, $message, $http_status_code, $data );
811
	}
812
}
813