Completed
Push — master ( 48bc46...861ddb )
by Jonathan
04:46 queued 34s
created

Object_Sync_Sf_WordPress   F

Complexity

Total Complexity 298

Size/Duplication

Total Lines 2521
Duplicated Lines 0 %

Importance

Changes 13
Bugs 1 Features 1
Metric Value
eloc 1270
dl 0
loc 2521
rs 0.8
c 13
b 1
f 1
wmc 298

33 Methods

Rating   Name   Duplication   Size   Complexity  
D user_upsert() 0 118 19
A term_delete() 0 6 2
D attachment_upsert() 0 141 20
C get_wordpress_table_structure() 0 156 8
A get_wordpress_object_fields() 0 46 5
F post_upsert() 0 146 21
B user_create() 0 91 9
B object_delete() 0 52 9
A cache_expiration() 0 3 1
A comment_delete() 0 3 1
C post_create() 0 75 13
B term_update() 0 59 8
B object_upsert() 0 63 10
C attachment_update() 0 78 11
B post_update() 0 57 11
B object_create() 0 47 9
A post_delete() 0 3 1
A cache_set() 0 14 3
B attachment_create() 0 62 8
B comment_update() 0 60 8
A user_delete() 0 5 1
B get_wordpress_object_data() 0 53 10
C object_update() 0 75 13
B user_update() 0 60 11
B object_fields() 0 59 9
A get_object_types() 0 34 2
A __construct() 0 21 2
F comment_upsert() 0 153 26
A cache_get() 0 10 2
F comment_create() 0 75 12
B term_create() 0 68 11
A attachment_delete() 0 3 1
F term_upsert() 0 120 21

How to fix   Complexity   

Complex Class

Complex classes like Object_Sync_Sf_WordPress often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Object_Sync_Sf_WordPress, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Class file for Object_Sync_Sf_WordPress.
4
 *
5
 * @file
6
 */
7
8
if ( ! class_exists( 'Object_Sync_Salesforce' ) ) {
9
	die();
10
}
11
12
/**
13
 * Work with the WordPress $wpdb object. This class can make read and write calls to the WordPress database, and also cache the responses.
14
 */
15
class Object_Sync_Sf_WordPress {
16
17
	protected $wpdb;
18
	protected $version;
19
	protected $slug;
20
	protected $mappings;
21
	protected $logging;
22
	protected $option_prefix;
23
24
	/**
25
	 * Constructor which discovers objects in WordPress
26
	 *
27
	 * @param object $wpdb A wpdb object.
28
	 * @param string $version The plugin version.
29
	 * @param string $slug The plugin slug.
30
	 * @param object $mappings Mapping objects.
31
	 * @param object $logging a Object_Sync_Sf_Logging instance.
32
	 * @param string $option_prefix The plugin's option prefix
33
	 * @throws \Exception
34
	 */
35
	public function __construct( $wpdb, $version, $slug, $mappings, $logging, $option_prefix = '' ) {
36
		$this->wpdb          = $wpdb;
37
		$this->version       = $version;
38
		$this->slug          = $slug;
39
		$this->option_prefix = isset( $option_prefix ) ? $option_prefix : 'object_sync_for_salesforce_';
40
		$this->mappings      = $mappings;
41
		$this->logging       = $logging;
42
43
		add_action( 'admin_init', function() {
44
			$this->wordpress_objects = $this->get_object_types();
0 ignored issues
show
Bug Best Practice introduced by
The property wordpress_objects does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
45
		} );
46
47
		$this->options = array(
0 ignored issues
show
Bug Best Practice introduced by
The property options does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
48
			'cache'            => true,
49
			'cache_expiration' => $this->cache_expiration( 'wordpress_data_cache', 86400 ),
50
			'type'             => 'read',
51
		);
52
53
		$this->sfwp_transients = new Object_Sync_Sf_WordPress_Transient( 'sfwp_transients' );
0 ignored issues
show
Bug Best Practice introduced by
The property sfwp_transients does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
54
55
		$this->debug = get_option( $this->option_prefix . 'debug_mode', false );
0 ignored issues
show
Bug Best Practice introduced by
The property debug does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
56
57
	}
58
59
	/**
60
	 * Get WordPress object types
61
	 *
62
	 * @return array $wordpress_objects
63
	 */
64
	public function get_object_types() {
65
		/*
66
		 * Allow developers to specify, especially non-post, content types that should be included or ignored.
67
		 * Here's an example of filters to add/remove types:
68
		 *
69
			add_filter( 'object_sync_for_salesforce_add_more_wordpress_types', 'add_more_types', 10, 1 );
70
			function add_more_types( $wordpress_object_types ) {
71
				$wordpress_object_types[] = 'foo'; // this will add to the existing types.
72
				return $wordpress_object_types;
73
			}
74
75
			add_filter( 'object_sync_for_salesforce_remove_wordpress_types', 'wordpress_object_types', 10, 1 );
76
			function remove_types( $types_to_remove ) {
77
				$types_to_remove[] = 'revision'; // this adds to the array of types to ignore
78
				return $types_to_remove;
79
			}
80
		*/
81
82
		// this should include the available object types and send them to the hook
83
		$wordpress_types_not_posts_include = array( 'user', 'comment', 'category', 'tag' );
84
		$wordpress_objects                 = array_merge( get_post_types(), $wordpress_types_not_posts_include );
85
		// this should be all the objects
86
		$wordpress_objects = apply_filters( $this->option_prefix . 'add_more_wordpress_types', $wordpress_objects );
87
88
		// by default, only remove the revision, log, and scheduled-action types that we use in this plugin
89
		$types_to_remove = apply_filters( $this->option_prefix . 'remove_wordpress_types', array( 'wp_log', 'scheduled-action', 'revision' ) );
90
91
		// if the hook filters out any types, remove them from the visible list
92
		if ( ! empty( $types_to_remove ) ) {
93
			$wordpress_objects = array_diff( $wordpress_objects, $types_to_remove );
94
		}
95
96
		sort( $wordpress_objects );
97
		return $wordpress_objects;
98
	}
99
100
	/**
101
	 * Get WordPress table structure for an object
102
	 *
103
	 * @param string $object_type The type of object.
104
	 * @return array $object_table_structure The table structure.
105
	 */
106
	public function get_wordpress_table_structure( $object_type ) {
107
		if ( 'attachment' === $object_type ) {
108
			$object_table_structure = array(
109
				'object_name'     => 'post',
110
				'content_methods' => array(
111
					'create' => 'wp_insert_attachment',
112
					'read'   => 'get_posts',
113
					'update' => 'wp_insert_attachment',
114
					'delete' => 'wp_delete_attachment',
115
					'match'  => 'get_posts',
116
				),
117
				'meta_methods'    => array(
118
					'create' => 'wp_generate_attachment_metadata',
119
					'read'   => 'wp_get_attachment_metadata',
120
					'update' => 'wp_update_attachment_metadata',
121
					'delete' => '',
122
					'match'  => 'WP_Query',
123
				),
124
				'content_table'   => $this->wpdb->prefix . 'posts',
125
				'id_field'        => 'ID',
126
				'meta_table'      => $this->wpdb->prefix . 'postmeta',
127
				'meta_join_field' => 'post_id',
128
				'where'           => 'AND ' . $this->wpdb->prefix . 'posts.post_type = "' . $object_type . '"',
129
				'ignore_keys'     => array(),
130
			);
131
		} elseif ( 'user' === $object_type ) {
132
			// User meta fields need to use update_user_meta for create as well, otherwise it'll just get created twice because apparently when the post is created it's already there.
133
134
			$user_meta_methods = array(
135
				'create' => 'update_user_meta',
136
				'read'   => 'get_user_meta',
137
				'update' => 'update_user_meta',
138
				'delete' => 'delete_user_meta',
139
			);
140
141
			$object_table_structure = array(
142
				'object_name'     => 'user',
143
				'content_methods' => array(
144
					'create' => 'wp_insert_user',
145
					'read'   => 'get_user_by',
146
					'update' => 'wp_update_user',
147
					'delete' => 'wp_delete_user',
148
					'match'  => 'get_user_by',
149
				),
150
				'meta_methods'    => $user_meta_methods,
151
				'content_table'   => $this->wpdb->prefix . 'users',
152
				'id_field'        => 'ID',
153
				'meta_table'      => $this->wpdb->prefix . 'usermeta',
154
				'meta_join_field' => 'user_id',
155
				'where'           => '',
156
				'ignore_keys'     => array( // Keep it simple and avoid security risks.
157
					'user_pass',
158
					'user_activation_key',
159
					'session_tokens',
160
				),
161
			);
162
		} elseif ( 'post' === $object_type ) {
163
			$object_table_structure = array(
164
				'object_name'     => 'post',
165
				'content_methods' => array(
166
					'create' => 'wp_insert_post',
167
					'read'   => 'get_posts',
168
					'update' => 'wp_update_post',
169
					'delete' => 'wp_delete_post',
170
					'match'  => 'get_posts',
171
				),
172
				'meta_methods'    => array(
173
					'create' => 'add_post_meta',
174
					'read'   => 'get_post_meta',
175
					'update' => 'update_post_meta',
176
					'delete' => 'delete_post_meta',
177
					'match'  => 'WP_Query',
178
				),
179
				'content_table'   => $this->wpdb->prefix . 'posts',
180
				'id_field'        => 'ID',
181
				'meta_table'      => $this->wpdb->prefix . 'postmeta',
182
				'meta_join_field' => 'post_id',
183
				'where'           => 'AND ' . $this->wpdb->prefix . 'posts.post_type = "' . $object_type . '"',
184
				'ignore_keys'     => array(),
185
			);
186
		} elseif ( 'category' === $object_type || 'tag' === $object_type || 'post_tag' === $object_type ) {
187
			// I am unsure why post_tag wasn't here for so long, but i figure it probably needs to be there.
188
			$object_table_structure = array(
189
				'object_name'     => 'term',
190
				'content_methods' => array(
191
					'create' => 'wp_insert_term',
192
					'read'   => 'get_term_by',
193
					'update' => 'wp_update_term',
194
					'delete' => 'wp_delete_term',
195
					'match'  => 'get_term_by',
196
				),
197
				'meta_methods'    => array(
198
					'create' => 'add_term_meta',
199
					'read'   => 'get_term_meta',
200
					'update' => 'update_term_meta',
201
					'delete' => 'delete_metadata',
202
					'match'  => 'WP_Term_Query',
203
				),
204
				'content_table'   => $this->wpdb->prefix . 'terms',
205
				'id_field'        => 'term_id',
206
				'meta_table'      => array( $this->wpdb->prefix . 'termmeta', $this->wpdb->prefix . 'term_taxonomy' ),
207
				'meta_join_field' => 'term_id',
208
				'where'           => '',
209
				'ignore_keys'     => array(),
210
			);
211
		} elseif ( 'comment' === $object_type ) {
212
			$object_table_structure = array(
213
				'object_name'     => 'comment',
214
				'content_methods' => array(
215
					'create' => 'wp_new_comment',
216
					'read'   => 'get_comments',
217
					'update' => 'wp_update_comment',
218
					'delete' => 'wp_delete_comment',
219
					'match'  => 'get_comments',
220
				),
221
				'meta_methods'    => array(
222
					'create' => 'add_comment_meta',
223
					'read'   => 'get_comment_meta',
224
					'update' => 'update_comment_meta',
225
					'delete' => 'delete_comment_metadata',
226
					'match'  => 'WP_Comment_Query',
227
				),
228
				'content_table'   => $this->wpdb->prefix . 'comments',
229
				'id_field'        => 'comment_ID',
230
				'meta_table'      => $this->wpdb->prefix . 'commentmeta',
231
				'meta_join_field' => 'comment_id',
232
				'where'           => '',
233
				'ignore_keys'     => array(),
234
			);
235
		} else { // This is for custom post types.
236
			$object_table_structure = array(
237
				'object_name'     => 'post',
238
				'content_methods' => array(
239
					'create' => 'wp_insert_post',
240
					'read'   => 'get_posts',
241
					'update' => 'wp_update_post',
242
					'delete' => 'wp_delete_post',
243
					'match'  => 'get_posts',
244
				),
245
				'meta_methods'    => array(
246
					'create' => 'add_post_meta',
247
					'read'   => 'get_post_meta',
248
					'update' => 'update_post_meta',
249
					'delete' => 'delete_post_meta',
250
					'match'  => 'WP_Query',
251
				),
252
				'content_table'   => $this->wpdb->prefix . 'posts',
253
				'id_field'        => 'ID',
254
				'meta_table'      => $this->wpdb->prefix . 'postmeta',
255
				'meta_join_field' => 'post_id',
256
				'where'           => 'AND ' . $this->wpdb->prefix . 'posts.post_type = "' . $object_type . '"',
257
				'ignore_keys'     => array(),
258
			);
259
		} // End if().
260
261
		return $object_table_structure;
262
	}
263
264
	/**
265
	 * Get WordPress fields for an object
266
	 *
267
	 * @param string $wordpress_object The type of WordPress object.
268
	 * @param string $id_field The field of that object that corresponds with its ID in the database.
269
	 * @return array $object_fields
270
	 */
271
	public function get_wordpress_object_fields( $wordpress_object, $id_field = 'ID' ) {
272
273
		$object_table_structure = $this->get_wordpress_table_structure( $wordpress_object );
274
275
		$meta_table      = $object_table_structure['meta_table'];
276
		$meta_methods    = maybe_unserialize( $object_table_structure['meta_methods'] );
277
		$content_table   = $object_table_structure['content_table'];
278
		$content_methods = maybe_unserialize( $object_table_structure['content_methods'] );
279
		$id_field        = $object_table_structure['id_field'];
280
		$object_name     = $object_table_structure['object_name'];
281
		$where           = $object_table_structure['where'];
282
		$ignore_keys     = $object_table_structure['ignore_keys'];
283
284
		$object_fields = array();
285
286
		// Try to find the object fields in cache before acquiring it from other source.
287
		if ( true === $this->options['cache'] && 'write' !== $this->options['cache'] ) {
0 ignored issues
show
introduced by
The condition 'write' !== $this->options['cache'] is always true.
Loading history...
288
			$cached = $this->cache_get( $wordpress_object, array( 'data', 'meta' ) );
289
			if ( is_array( $cached ) ) {
290
				$object_fields['data']       = $cached;
291
				$object_fields['from_cache'] = true;
292
				$object_fields['cached']     = true;
293
			} else {
294
				$object_fields['data'] = $this->object_fields( $object_name, $id_field, $content_table, $content_methods, $meta_table, $meta_methods, $where, $ignore_keys );
295
				if ( ! empty( $object_fields['data'] ) ) {
296
					$object_fields['cached'] = $this->cache_set( $wordpress_object, array( 'data', 'meta' ), $object_fields['data'], $this->options['cache_expiration'] );
297
				} else {
298
					$object_fields['cached'] = false;
299
				}
300
				$object_fields['from_cache'] = false;
301
			}
302
		} else {
303
			$object_fields['data']       = $this->object_fields( $object_name, $id_field, $content_table, $content_methods, $meta_table, $meta_methods, $where, $ignore_keys );
304
			$object_fields['from_cache'] = false;
305
			$object_fields['cached']     = false;
306
		}
307
308
		/*
309
		 * Developers can use this hook to change the WordPress object field array.
310
		 * The returned $object_fields needs to be an array like is described earlier in this method:
311
		 *     $object_fields = array( 'data' => array(), 'from_cache' => bool, 'cached' => bool );
312
		 * This is useful for custom objects that do not use the normal metadata table structure.
313
		 */
314
		$object_fields = apply_filters( $this->option_prefix . 'wordpress_object_fields', $object_fields, $wordpress_object );
315
316
		return $object_fields['data'];
317
318
	}
319
320
	/**
321
	 * Get WordPress data based on what object it is
322
	 *
323
	 * @param string $object_type The type of object.
324
	 * @param string $object_id The ID of the object.
325
	 * @param bool $is_deleted Whether the WordPress object has been deleted
326
	 * @return array $wordpress_object
327
	 */
328
	public function get_wordpress_object_data( $object_type, $object_id, $is_deleted = false ) {
329
		$wordpress_object       = array();
330
		$object_table_structure = $this->get_wordpress_table_structure( $object_type );
331
332
		$meta_table      = $object_table_structure['meta_table'];
333
		$meta_methods    = maybe_unserialize( $object_table_structure['meta_methods'] );
334
		$content_table   = $object_table_structure['content_table'];
335
		$content_methods = maybe_unserialize( $object_table_structure['content_methods'] );
336
		$id_field        = $object_table_structure['id_field'];
337
		$object_name     = $object_table_structure['object_name'];
338
		$where           = $object_table_structure['where'];
339
		$ignore_keys     = $object_table_structure['ignore_keys'];
340
341
		if ( true === $is_deleted ) {
342
			$wordpress_object              = array();
343
			$wordpress_object[ $id_field ] = $object_id;
344
			return $wordpress_object;
345
		}
346
347
		if ( 'user' === $object_type ) {
348
			$data = get_userdata( $object_id );
0 ignored issues
show
Bug introduced by
$object_id of type string is incompatible with the type integer expected by parameter $user_id of get_userdata(). ( Ignorable by Annotation )

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

348
			$data = get_userdata( /** @scrutinizer ignore-type */ $object_id );
Loading history...
349
		} elseif ( 'post' === $object_type || 'attachment' === $object_type ) {
350
			$data = get_post( $object_id );
0 ignored issues
show
Bug introduced by
$object_id of type string is incompatible with the type WP_Post|integer|null expected by parameter $post of get_post(). ( Ignorable by Annotation )

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

350
			$data = get_post( /** @scrutinizer ignore-type */ $object_id );
Loading history...
351
		} elseif ( 'category' === $object_type || 'tag' === $object_type || 'post_tag' === $object_type ) {
352
			$data = get_term( $object_id );
0 ignored issues
show
Bug introduced by
$object_id of type string is incompatible with the type WP_Term|integer|object expected by parameter $term of get_term(). ( Ignorable by Annotation )

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

352
			$data = get_term( /** @scrutinizer ignore-type */ $object_id );
Loading history...
353
		} elseif ( 'comment' === $object_type ) {
354
			$data = get_comment( $object_id );
355
		} else { // This is for custom post types.
356
			$data = get_post( $object_id );
357
		}
358
359
		$fields = $this->get_wordpress_object_fields( $object_type );
360
		foreach ( $fields as $key => $value ) {
361
			$field                      = $value['key'];
362
			$wordpress_object[ $field ] = $data->{$field};
363
		}
364
365
		/*
366
		 * Allow developers to change the WordPress object, including any formatting that needs to happen to the data.
367
		 * The returned $wordpress_object needs to be an array like described above.
368
		 * This is useful for custom objects, hidden fields, or custom formatting.
369
		 * Here's an example of filters to add/modify data:
370
		 *
371
			add_filter( 'object_sync_for_salesforce_wordpress_object_data', 'modify_data', 10, 1 );
372
			function modify_data( $wordpress_object ) {
373
				$wordpress_object['field_a'] = 'i am a field value that salesforce wants to store but WordPress does not care about';
374
				return $wordpress_object;
375
			}
376
		*/
377
378
		$wordpress_object = apply_filters( $this->option_prefix . 'wordpress_object_data', $wordpress_object );
379
380
		return $wordpress_object;
381
382
	}
383
384
	/**
385
	 * Check to see if this API call exists in the cache
386
	 * if it does, return the transient for that key
387
	 *
388
	 * @param string $url The API call we'd like to make.
389
	 * @param array  $args The arguents of the API call.
390
	 * @return $this->sfwp_transients->get $cachekey
0 ignored issues
show
Documentation Bug introduced by
The doc comment $this->sfwp_transients->get at position 0 could not be parsed: Unknown type name '$this-' at position 0 in $this->sfwp_transients->get.
Loading history...
391
	 */
392
	public function cache_get( $url, $args ) {
393
		if ( is_array( $args ) ) {
0 ignored issues
show
introduced by
The condition is_array($args) is always true.
Loading history...
394
			$args[] = $url;
395
			array_multisort( $args );
396
		} else {
397
			$args .= $url;
398
		}
399
400
		$cachekey = md5( wp_json_encode( $args ) );
0 ignored issues
show
Bug introduced by
It seems like wp_json_encode($args) can also be of type false; however, parameter $str of md5() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

400
		$cachekey = md5( /** @scrutinizer ignore-type */ wp_json_encode( $args ) );
Loading history...
401
		return $this->sfwp_transients->get( $cachekey );
402
	}
403
404
	/**
405
	 * Create a cache entry for the current result, with the url and args as the key
406
	 *
407
	 * @param string $url The API query URL.
408
	 * @param array  $args The arguments passed on the API query.
409
	 * @param array  $data The data received.
410
	 * @param string $cache_expiration How long to keep the cache result around for.
411
	 * @return Bool whether or not the value was set
412
	 * @link https://wordpress.stackexchange.com/questions/174330/transient-storage-location-database-xcache-w3total-cache
413
	 */
414
	public function cache_set( $url, $args, $data, $cache_expiration = '' ) {
415
		if ( is_array( $args ) ) {
0 ignored issues
show
introduced by
The condition is_array($args) is always true.
Loading history...
416
			$args[] = $url;
417
			array_multisort( $args );
418
		} else {
419
			$args .= $url;
420
		}
421
		$cachekey = md5( wp_json_encode( $args ) );
0 ignored issues
show
Bug introduced by
It seems like wp_json_encode($args) can also be of type false; however, parameter $str of md5() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

421
		$cachekey = md5( /** @scrutinizer ignore-type */ wp_json_encode( $args ) );
Loading history...
422
		// Cache_expiration is how long it should be stored in the cache.
423
		// If we didn't give a custom one, use the default.
424
		if ( '' === $cache_expiration ) {
425
			$cache_expiration = $this->options['cache_expiration'];
426
		}
427
		return $this->sfwp_transients->set( $cachekey, $data, $cache_expiration );
428
	}
429
430
	/**
431
	 * If there is a WordPress setting for how long to keep this specific cache, return it and set the object property
432
	 * Otherwise, return seconds in 24 hours
433
	 *
434
	 * @param string $option_key The cache item to keep around.
435
	 * @param int    $expire The default time after which to expire the cache.
436
	 * @return The cache expiration saved in the database.
437
	 */
438
	public function cache_expiration( $option_key, $expire ) {
439
		$cache_expiration = get_option( $option_key, $expire );
440
		return $cache_expiration;
441
	}
442
443
	/**
444
	 * Get all the fields for an object
445
	 * The important thing here is returning the fields as an array:
446
	 * $all_fields = array( 'key' => 'key name', 'table' => 'table name', 'methods' => array( 'create' => '', 'read' => '', 'update' => '', 'delete' => '' ) );
447
	 * if there's a better way to do this than the mess of queries below, we should switch to that when we can
448
	 * we just need to make sure we get all applicable fields for the object itself, as well as its meta fields
449
	 *
450
	 * @param string $object_name THe name of the object type.
451
	 * @param string $id_field The database filed that contains its ID.
452
	 * @param string $content_table The table that normally contains such objects.
453
	 * @param array  $content_methods Unused, but included as part of the return.
454
	 * @param string $meta_table The table where meta values for this object type are contained.
455
	 * @param array  $meta_methods Unused, but included as part of the return.
456
	 * @param string $where SQL query.
457
	 * @param array  $ignore_keys Fields to ignore from the database.
458
	 * @return array $all_fields The fields for the object.
459
	 */
460
	private function object_fields( $object_name, $id_field, $content_table, $content_methods, $meta_table, $meta_methods, $where, $ignore_keys ) {
461
		// These two queries load all the fields from the specified object unless they have been specified as ignore fields.
462
		// They also load the fields that are meta_keys from the specified object's meta table.
463
		// Maybe a box for a custom query, since custom fields get done in so many ways.
464
		// Eventually this would be the kind of thing we could use fields api for, if it ever gets done.
465
		$data_fields      = $this->wpdb->get_col( "DESC {$content_table}", 0 );
466
		$data_field_types = $this->wpdb->get_col( "DESC {$content_table}", 1 ); // get the database field types
467
468
		if ( is_array( $meta_table ) ) {
0 ignored issues
show
introduced by
The condition is_array($meta_table) is always false.
Loading history...
469
			$tax_table  = $meta_table[1];
470
			$meta_table = $meta_table[0];
471
		}
472
		$select_meta = '
473
		SELECT DISTINCT ' . $meta_table . '.meta_key
474
		FROM ' . $content_table . '
475
		LEFT JOIN ' . $meta_table . '
476
		ON ' . $content_table . '.' . $id_field . ' = ' . $meta_table . '.' . $object_name . '_id
477
		WHERE ' . $meta_table . '.meta_key != ""
478
		' . $where . '
479
		';
480
		$meta_fields = $this->wpdb->get_results( $select_meta );
481
		$all_fields  = array();
482
483
		foreach ( $data_fields as $key => $value ) {
484
			if ( ! in_array( $value, $ignore_keys, true ) ) {
485
				$all_fields[] = array(
486
					'key'     => $value,
487
					'table'   => $content_table,
488
					'methods' => serialize( $content_methods ),
489
					'type'    => $data_field_types[ $key ],
490
				);
491
			}
492
		}
493
494
		foreach ( $meta_fields as $key => $value ) {
495
			if ( ! in_array( $value->meta_key, $ignore_keys, true ) ) {
496
				$all_fields[] = array(
497
					'key'     => $value->meta_key,
498
					'table'   => $meta_table,
499
					'methods' => serialize( $meta_methods ),
500
				);
501
			}
502
		}
503
504
		if ( 'term' === $object_name ) {
505
			$taxonomy = $this->wpdb->get_col( "DESC {$tax_table}", 0 );
506
			foreach ( $taxonomy as $key => $value ) {
507
				$exists = array_search( $value, array_column( $all_fields, 'key' ), true );
508
				if ( 0 !== $exists ) {
509
					$all_fields[] = array(
510
						'key'     => $value,
511
						'table'   => $tax_table,
512
						'methods' => serialize( $content_methods ),
513
					);
514
				}
515
			}
516
		}
517
518
		return $all_fields;
519
520
	}
521
522
	/**
523
	 * Create a new object of a given type.
524
	 *
525
	 * @param string $name Object type name, E.g., user, post, comment.
526
	 * @param array  $params Values of the fields to set for the object.
527
	 *
528
	 * @return array
529
	 *   data:
530
	 *     id : 123,
531
	 *     success : true
532
	 *   errors : [ ],
533
	 *   from_cache:
534
	 *   cached:
535
	 *   is_redo:
536
	 *
537
	 * part of CRUD for WordPress objects
538
	 */
539
	public function object_create( $name, $params ) {
540
541
		$structure = $this->get_wordpress_table_structure( $name );
542
		$id_field  = $structure['id_field'];
543
544
		switch ( $name ) {
545
			case 'user':
546
				$result = $this->user_create( $params, $id_field );
547
				break;
548
			case 'post':
549
				$result = $this->post_create( $params, $id_field );
550
				break;
551
			case 'attachment':
552
				$result = $this->attachment_create( $params, $id_field );
553
				break;
554
			case 'category':
555
			case 'tag':
556
			case 'post_tag':
557
				$result = $this->term_create( $params, $name, $id_field );
558
				break;
559
			case 'comment':
560
				$result = $this->comment_create( $params, $id_field );
561
				break;
562
			default:
563
				/*
564
				 * Developers can use this hook to create objects with their own methods.
565
				 * The returned $result needs to be an array like this.
566
				 * $id_field should be the database key; i.e. 'ID' and the value should be the new item's id, or whatever WordPress returns from the method (sometimes this can be an error)
567
				 * $success should be a boolean value
568
					$result = array( 'data' => array( $id_field => $post_id, 'success' => $success ), 'errors' => $errors );
569
				 * use hook like: add_filter( 'object_sync_for_salesforce_create_custom_wordpress_item', add_object, 10, 1 );
570
				 * the one param is: array( 'name' => objecttype, 'params' => array_of_params, 'id_field' => idfield )
571
				 */
572
				// Check to see if someone is calling the filter, and apply it if so.
573
				if ( ! has_filter( $this->option_prefix . 'create_custom_wordpress_item' ) ) {
574
					$result = $this->post_create( $params, $id_field, $name );
575
				} else {
576
					$result = apply_filters( $this->option_prefix . 'create_custom_wordpress_item', array(
577
						'params'   => $params,
578
						'name'     => $name,
579
						'id_field' => $id_field,
580
					) );
581
				}
582
				break;
583
		} // End switch().
584
585
		return $result;
586
587
	}
588
589
590
	/**
591
	 * Create new records or update existing records.
592
	 *
593
	 * The new records or updated records are based on the value of the specified
594
	 * field.  If the value is not unique, REST API returns a 300 response with
595
	 * the list of matching records.
596
	 *
597
	 * @param string $name Object type name, E.g., user, post, comment.
598
	 * @param string $key The field to check if this record should be created or updated.
599
	 * @param string $value The value for this record of the field specified for $key.
600
	 * @param array  $methods What WordPress methods do we use to get the data, if there are any. otherwise, maybe will have to do a wpdb query.
601
	 * @param array  $params Values of the fields to set for the object.
602
	 * @param bool   $pull_to_drafts Whether to save to WordPress drafts when pulling from Salesforce.
603
	 * @param bool   $check_only Allows this method to only check for matching records, instead of making any data changes.
604
	 *
605
	 * @return array
606
	 *   data:
607
	 *     "id" : 123,
608
	 *     "success" : true
609
	 *   "errors" : [ ],
610
	 *   from_cache:
611
	 *   cached:
612
	 *   is_redo:
613
	 *
614
	 * part of CRUD for WordPress objects
615
	 */
616
	public function object_upsert( $name, $key, $value, $methods = array(), $params, $pull_to_drafts = false, $check_only = false ) {
617
618
		$structure = $this->get_wordpress_table_structure( $name );
619
		$id_field  = $structure['id_field'];
620
621
		// If key is set, remove from $params to avoid SQL errors.
622
		if ( isset( $params[ $key ] ) ) {
623
			unset( $params[ $key ] );
624
		}
625
626
		// Allow developers to change both the key and value by which objects should be matched.
627
		$key   = apply_filters( $this->option_prefix . 'modify_upsert_key', $key );
628
		$value = apply_filters( $this->option_prefix . 'modify_upsert_value', $value );
629
630
		switch ( $name ) {
631
			case 'user':
632
				$result = $this->user_upsert( $key, $value, $methods, $params, $id_field, $pull_to_drafts, $check_only );
633
				break;
634
			case 'post':
635
				$result = $this->post_upsert( $key, $value, $methods, $params, $id_field, $pull_to_drafts, $name, $check_only );
636
				break;
637
			case 'attachment':
638
				$result = $this->attachment_upsert( $key, $value, $methods, $params, $id_field, $check_only );
639
				break;
640
			case 'category':
641
			case 'tag':
642
			case 'post_tag':
643
				$result = $this->term_upsert( $key, $value, $methods, $params, $name, $id_field, $check_only );
644
				break;
645
			case 'comment':
646
				$result = $this->comment_upsert( $key, $value, $methods, $params, $id_field, $pull_to_drafts, $check_only );
647
				break;
648
			default:
649
				/*
650
				 * Developers can use this hook to upsert objects with their own methods.
651
				 * The returned $result needs to be an array like this:
652
				 * $id_field should be the database key; i.e. 'ID' and the value should be the item's id, or whatever WordPress returns from the method (sometimes this can be an error)
653
				 * $success should be a boolean value
654
				 *     $result = array( 'data' => array( $id_field => $post_id, 'success' => $success ), 'errors' => $errors );
655
				 * Use hook like this:
656
				 *     add_filter( 'object_sync_for_salesforce_upsert_custom_wordpress_item', upsert_object, 10, 1 );
657
				 * The one param is:
658
				 *     array( 'key' => key, 'value' => value, 'methods' => methods, 'params' => array_of_params, 'id_field' => idfield, 'pull_to_drafts' => pulltodrafts, 'name' => name, 'check_only' => $check_only )
659
				*/
660
				// Check to see if someone is calling the filter, and apply it if so.
661
				if ( ! has_filter( $this->option_prefix . 'upsert_custom_wordpress_item' ) ) {
662
					$result = $this->post_upsert( $key, $value, $methods, $params, $id_field, $pull_to_drafts, $name, $check_only );
663
				} else {
664
					$result = apply_filters( $this->option_prefix . 'upsert_custom_wordpress_item', array(
665
						'key'            => $key,
666
						'value'          => $value,
667
						'methods'        => $methods,
668
						'params'         => $params,
669
						'id_field'       => $id_field,
670
						'pull_to_drafts' => $pull_to_drafts,
671
						'name'           => $name,
672
						'check_only'     => $check_only,
673
					) );
674
				}
675
				break;
676
		} // End switch().
677
678
		return $result;
679
680
	}
681
682
	/**
683
	 * Update an existing object.
684
	 *
685
	 * @param string $name Object type name, E.g., user, post, comment.
686
	 * @param string $id WordPress id of the object.
687
	 * @param array  $params Values of the fields to set for the object.
688
	 *
689
	 * part of CRUD for WordPress objects
690
	 *
691
	 * @return array
692
	 *   data:
693
	 *     success: 1
694
	 *   "errors" : [ ],
695
	 *   from_cache:
696
	 *   cached:
697
	 *   is_redo:
698
	 */
699
	public function object_update( $name, $id, $params ) {
700
701
		$structure = $this->get_wordpress_table_structure( $name );
702
		$id_field  = $structure['id_field'];
703
704
		switch ( $name ) {
705
			case 'user':
706
				// User id does not come through by default, but we need it to pass to wp method.
707
				$result = $this->user_update( $id, $params, $id_field );
708
				break;
709
			case 'post':
710
				$result = $this->post_update( $id, $params, $id_field );
711
				break;
712
			case 'attachment':
713
				$result = $this->attachment_update( $id, $params, $id_field );
714
				break;
715
			case 'category':
716
			case 'tag':
717
			case 'post_tag':
718
				$result = $this->term_update( $id, $params, $name, $id_field );
719
				break;
720
			case 'comment':
721
				$result = $this->comment_update( $id, $params, $id_field );
722
				break;
723
			default:
724
				/*
725
				 * Developers can use this hook to update objects with their own methods.
726
				 * The returned $result needs to be an array like this:
727
				 *     $id_field should be the database key; i.e. 'ID' and the value should be the updated item's id, or whatever WordPress returns from the method (sometimes this can be an error)
728
				 * $success should be a boolean value
729
				 *     $result = array( 'data' => array( $id_field => $post_id, 'success' => $success ), 'errors' => $errors );
730
				 * Use hook like this:
731
				 *     add_filter( 'object_sync_for_salesforce_update_custom_wordpress_item', update_object, 10, 1 );
732
				 * The one param is:
733
				 *     array( 'id' => id, 'params' => array_of_params, 'name' => objecttype, 'id_field' => idfield )
734
				 */
735
				// Check to see if someone is calling the filter, and apply it if so.
736
				if ( ! has_filter( $this->option_prefix . 'update_custom_wordpress_item' ) ) {
737
					$result = $this->post_update( $id, $params, $id_field, $name );
738
				} else {
739
					$result = apply_filters( $this->option_prefix . 'update_custom_wordpress_item', array(
740
						'id'       => $id,
741
						'params'   => $params,
742
						'name'     => $name,
743
						'id_field' => $id_field,
744
					) );
745
				}
746
				break;
747
		} // End switch().
748
749
		if ( isset( $result['errors'] ) && ! empty( $result['errors'] ) ) {
750
			$status = 'error';
751
			// translators: 1) is object type, 2) is id value
752
			$title = sprintf( esc_html__( 'Error: WordPress update for %1$s ID %2$s was unsuccessful with these errors:', 'object-sync-for-salesforce' ),
753
				esc_attr( $name ),
754
				esc_attr( $id )
755
			);
756
757
			if ( isset( $this->logging ) ) {
758
				$logging = $this->logging;
759
			} elseif ( class_exists( 'Object_Sync_Sf_Logging' ) ) {
760
				$logging = new Object_Sync_Sf_Logging( $this->wpdb, $this->version );
761
			}
762
763
			$error_log = array(
764
				'title'   => $title,
765
				'message' => esc_html( print_r( $result['errors'], true ) ),
0 ignored issues
show
introduced by
The use of function print_r() is discouraged
Loading history...
766
				'trigger' => 0,
767
				'parent'  => '',
768
				'status'  => $status,
769
			);
770
			$logging->setup( $error_log );
771
		}
772
773
		return $result;
774
	}
775
776
	/**
777
	 * Delete a WordPress object.
778
	 *
779
	 * @param string $name Object type name, E.g., user, post, comment.
780
	 * @param string $id WordPress id of the object.
781
	 *
782
	 * @return array
783
	 *   data:
784
	 *     success: 1
785
	 *   "errors" : [ ],
786
	 *
787
	 * part of CRUD for WordPress objects
788
	 */
789
	public function object_delete( $name, $id ) {
790
		$structure = $this->get_wordpress_table_structure( $name );
791
		$id_field  = $structure['id_field'];
792
793
		switch ( $name ) {
794
			case 'user':
795
				$success = $this->user_delete( $id );
0 ignored issues
show
Bug introduced by
$id of type string is incompatible with the type integer expected by parameter $id of Object_Sync_Sf_WordPress::user_delete(). ( Ignorable by Annotation )

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

795
				$success = $this->user_delete( /** @scrutinizer ignore-type */ $id );
Loading history...
796
				break;
797
			case 'post':
798
				$success = $this->post_delete( $id );
0 ignored issues
show
Bug introduced by
$id of type string is incompatible with the type integer expected by parameter $id of Object_Sync_Sf_WordPress::post_delete(). ( Ignorable by Annotation )

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

798
				$success = $this->post_delete( /** @scrutinizer ignore-type */ $id );
Loading history...
799
				break;
800
			case 'attachment':
801
				$success = $this->attachment_delete( $id );
0 ignored issues
show
Bug introduced by
$id of type string is incompatible with the type integer expected by parameter $id of Object_Sync_Sf_WordPress::attachment_delete(). ( Ignorable by Annotation )

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

801
				$success = $this->attachment_delete( /** @scrutinizer ignore-type */ $id );
Loading history...
802
				break;
803
			case 'category':
804
			case 'tag':
805
			case 'post_tag':
806
				$success = $this->term_delete( $id, $name );
807
				break;
808
			case 'comment':
809
				$success = $this->comment_delete( $id );
0 ignored issues
show
Bug introduced by
$id of type string is incompatible with the type integer expected by parameter $id of Object_Sync_Sf_WordPress::comment_delete(). ( Ignorable by Annotation )

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

809
				$success = $this->comment_delete( /** @scrutinizer ignore-type */ $id );
Loading history...
810
				break;
811
			default:
812
				/*
813
				 * Developers can use this hook to delete objects with their own methods.
814
				 * The returned $success is an object of the correct type, or a FALSE
815
				 * Use hook like:
816
				 *     add_filter( 'object_sync_for_salesforce_delete_custom_wordpress_item', delete_object, 10, 1 );
817
				 * The one param is:
818
				 *     array( 'id' => id, 'name' => objecttype )
819
				 */
820
				// Check to see if someone is calling the filter, and apply it if so.
821
				if ( ! has_filter( $this->option_prefix . 'delete_custom_wordpress_item' ) ) {
822
					$success = $this->post_delete( $id );
823
				} else {
824
					$success = apply_filters( $this->option_prefix . 'delete_custom_wordpress_item', array(
825
						'id'   => $id,
826
						'name' => $name,
827
					) );
828
				}
829
830
				$success = $this->post_delete( $id );
831
				break;
832
		} // End switch().
833
834
		$result = array(
835
			'data'   => array(
836
				'success' => $success,
837
			),
838
			'errors' => array(),
839
		);
840
		return $result;
841
	}
842
843
	/**
844
	 * Create a new WordPress user.
845
	 *
846
	 * @param array  $params array of user data params.
847
	 * @param string $id_field The column in the DB that holdes the user ID.
848
	 *
849
	 * @return array
850
	 *   data:
851
	 *     ID : 123,
852
	 *     success: 1
853
	 *   "errors" : [ ],
854
	 */
855
	private function user_create( $params, $id_field = 'ID' ) {
856
857
		// Allow username to be email address or username.
858
		// The username could be autogenerated before this point for the sake of URLs.
859
		$username      = $params['user_email']['value'];
860
		$email_address = $params['user_email']['value'];
861
		if ( isset( $params['user_login']['value'] ) ) { // User_login is used by username_exists.
862
			$username = $params['user_login']['value'];
863
		} else {
864
			$params['user_login'] = array(
865
				'value'         => $username,
866
				'method_modify' => 'wp_insert_user',
867
				'method_read'   => 'get_user_by',
868
			);
869
		}
870
871
		// This is a new user.
872
		if ( false === username_exists( $username ) ) {
873
874
			// Create the user
875
			// WordPress sends a password reset link so this password doesn't get used, but it does exist in the database, which is helpful to prevent access before the user uses their password reset email.
876
			$params['user_pass'] = array(
877
				'value'         => wp_generate_password( 12, false ),
878
				'method_modify' => 'wp_insert_user',
879
				'method_read'   => 'get_user_by',
880
			);
881
			// Load all params with a method_modify of the object structure's content_method into $content
882
			$content   = array();
883
			$structure = $this->get_wordpress_table_structure( 'user' );
884
			foreach ( $params as $key => $value ) {
885
				if ( in_array( $value['method_modify'], $structure['content_methods'] ) ) {
886
					$content[ $key ] = $value['value'];
887
					unset( $params[ $key ] );
888
				}
889
			}
890
891
			$user_id = wp_insert_user( $content );
892
893
			if ( is_wp_error( $user_id ) ) {
894
				$success = false;
895
				$errors  = $user_id;
896
			} else {
897
				$success = true;
898
				$errors  = array();
899
				foreach ( $params as $key => $value ) {
900
					$method = $value['method_modify'];
901
					// we need to provide a way for passing the values in a custom order here
902
					$meta_id = $method( $user_id, $key, $value['value'] );
903
					if ( false === $meta_id ) {
904
						$success  = false;
905
						$errors[] = array(
906
							'message' => sprintf(
907
								// translators: %1$s is a method name.
908
								esc_html__( 'Tried to upsert meta with method %1$s.', 'object-sync-for-salesforce' ),
909
								esc_html( $method )
910
							),
911
							'key'     => $key,
912
							'value'   => $value,
913
						);
914
					}
915
				}
916
917
				// Developers can use this hook to set any other user data - permissions, etc.
918
				do_action( $this->option_prefix . 'set_more_user_data', $user_id, $params, 'create' );
919
920
				// Send notification of new user.
921
				// todo: Figure out what permissions ought to get notifications for this and make sure it works the right way.
922
				wp_new_user_notification( $user_id, null, 'both' );
0 ignored issues
show
Bug introduced by
It seems like $user_id can also be of type WP_Error; however, parameter $user_id of wp_new_user_notification() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

922
				wp_new_user_notification( /** @scrutinizer ignore-type */ $user_id, null, 'both' );
Loading history...
923
924
			}
925
		} else {
926
			$user_id = username_exists( $username );
927
		} // End if().
928
929
		if ( is_wp_error( $user_id ) ) {
930
			$success = false;
931
			$errors  = $user_id;
932
		} else {
933
			$success = true;
934
			$errors  = array();
935
		}
936
937
		$result = array(
938
			'data'   => array(
939
				$id_field => $user_id,
940
				'success' => $success,
941
			),
942
			'errors' => $errors,
943
		);
944
945
		return $result;
946
947
	}
948
949
	/**
950
	 * Create a new WordPress user or update it if a match is found.
951
	 *
952
	 * @param string $key What key we are looking at for possible matches.
953
	 * @param string $value What value we are looking at for possible matches.
954
	 * @param array  $methods What WordPress methods do we use to get the data, if there are any. otherwise, maybe will have to do a wpdb query.
955
	 * @param array  $params Array of user data params. This is generated by Object_Sync_Sf_Mapping::map_params().
956
	 * @param string $id_field Optional string of what the ID field is, if it is ever not ID.
957
	 * @param bool   $pull_to_drafts Whether to save to WordPress drafts when pulling from Salesforce.
958
	 * @param bool   $check_only Allows this method to only check for matching records, instead of making any data changes.
959
	 *
960
	 * @return array
961
	 *   data:
962
	 *     ID : 123,
963
	 *     success: 1
964
	 *   "errors" : [ ],
965
	 */
966
	private function user_upsert( $key, $value, $methods = array(), $params, $id_field = 'ID', $pull_to_drafts = false, $check_only = false ) {
967
968
		// If the key is user_email, we need to make it just email because that is how the WordPress method reads it.
969
		$method = $methods['method_match'];
970
		if ( '' !== $method ) {
971
			// These methods should give us the user object if we are matching for one.
972
			// if we are trying to match to a meta field, the method is an object
973
			if ( class_exists( $method ) ) {
974
				$args        = array(
975
					'meta_query' => array(
0 ignored issues
show
introduced by
Detected usage of meta_query, possible slow query.
Loading history...
976
						array(
977
							'key'   => $key,
978
							'value' => $value,
979
						),
980
					),
981
				);
982
				$match_query = new $method( $args );
983
				$users       = $match_query->get_results();
984
				if ( ! empty( $users ) ) {
985
					$user = $users[0];
986
				}
987
			} else {
988
				$user = $method( str_replace( 'user_', '', $key ), $value );
989
			}
990
991
			if ( isset( $user ) && isset( $user->{$id_field} ) ) {
992
				// User does exist after checking the matching value. we want its id.
993
				$user_id = $user->{$id_field};
994
995
				if ( true === $check_only ) {
996
					// We are just checking to see if there is a match.
997
					return $user_id;
998
				}
999
1000
				// On the prematch fields, we specify the method_update param.
1001
				if ( isset( $methods['method_update'] ) ) {
1002
					$method = $methods['method_update'];
1003
				} else {
1004
					$method = $methods['method_modify'];
1005
				}
1006
				$params[ $key ] = array(
1007
					'value'         => $value,
1008
					'method_modify' => $method,
1009
					'method_read'   => $methods['method_read'],
1010
				);
1011
			} elseif ( false === $check_only ) {
1012
				// User does not exist after checking the matching value. create it.
1013
				// On the prematch fields, we specify the method_create param.
1014
				if ( isset( $methods['method_create'] ) ) {
1015
					$method = $methods['method_create'];
1016
				} else {
1017
					$method = $methods['method_modify'];
1018
				}
1019
				$params[ $key ] = array(
1020
					'value'         => $value,
1021
					'method_modify' => $method,
1022
					'method_read'   => $methods['method_read'],
1023
				);
1024
				$result         = $this->user_create( $params );
1025
				return $result;
1026
			} else {
1027
				// Check only is true but there's not a user yet.
1028
				return null;
1029
			} // End if().
1030
		} else {
1031
			// There is no method by which to check the user. we can check other ways here.
1032
			$params[ $key ] = array(
1033
				'value'         => $value,
1034
				'method_modify' => $methods['method_modify'],
1035
				'method_read'   => $methods['method_read'],
1036
			);
1037
1038
			// Allow username to be email address or username.
1039
			// The username could be autogenerated before this point for the sake of URLs.
1040
			if ( isset( $params['user_email']['value'] ) ) {
1041
				$username      = $params['user_email']['value'];
1042
				$email_address = $params['user_email']['value'];
1043
			}
1044
			if ( isset( $params['user_login']['value'] ) ) { // user_login is used by username_exists.
1045
				$username = $params['user_login']['value'];
1046
			}
1047
1048
			$existing_id = username_exists( $username ); // Returns an id if there is a result.
1049
1050
			// User does not exist after more checking. we want to create it.
1051
			if ( false === $existing_id && false === $check_only ) {
1052
				$result = $this->user_create( $params );
1053
				return $result;
1054
			} elseif ( true === $check_only ) {
1055
				// We are just checking to see if there is a match.
1056
				return $existing_id;
1057
			} else {
1058
				// User does exist based on username, and we aren't doing a check only. we want to update the wp user here.
1059
				$user_id = $existing_id;
1060
			}
1061
		} // End if().
1062
1063
		if ( isset( $user_id ) ) {
1064
			foreach ( $params as $key => $value ) {
1065
				$params[ $key ]['method_modify'] = $methods['method_update'];
1066
			}
1067
			$result = $this->user_update( $user_id, $params );
1068
			return $result;
1069
		}
1070
1071
		// Create log entry for lack of a user id.
1072
		if ( isset( $this->logging ) ) {
1073
			$logging = $this->logging;
1074
		} elseif ( class_exists( 'Object_Sync_Sf_Logging' ) ) {
1075
			$logging = new Object_Sync_Sf_Logging( $this->wpdb, $this->version );
1076
		}
1077
		$logging->setup(
1078
			// todo: can we get any more specific about this?
1079
			esc_html__( 'Error: Users: Tried to run user_upsert, and ended up without a user id', 'object-sync-for-salesforce' ),
1080
			'',
1081
			0,
1082
			0,
1083
			'error'
1084
		);
1085
1086
	}
1087
1088
	/**
1089
	 * Update a WordPress user.
1090
	 *
1091
	 * @param string $user_id The ID for the user to be updated. This value needs to be in the array that is sent to wp_update_user.
1092
	 * @param array  $params Array of user data params.
1093
	 * @param string $id_field Optional string of what the ID field is, if it is ever not ID.
1094
	 *
1095
	 * @return array
1096
	 *   data:
1097
	 *     success: 1
1098
	 *   "errors" : [ ],
1099
	 */
1100
	private function user_update( $user_id, $params, $id_field = 'ID' ) {
1101
		$content              = array();
1102
		$content[ $id_field ] = $user_id;
1103
		foreach ( $params as $key => $value ) {
1104
1105
			// if the update value for email already exists on another user, don't fail this update; keep the user's email address
1106
			if ( 'user_email' === $key && email_exists( $value['value'] ) ) {
1107
				unset( $params[ $key ] );
1108
				continue;
1109
			}
1110
1111
			// if the update value for login already exists on another user, don't fail this update; keep the user's login
1112
			if ( 'user_login' === $key && username_exists( $value['value'] ) ) {
1113
				unset( $params[ $key ] );
1114
				continue;
1115
			}
1116
1117
			if ( 'wp_update_user' === $value['method_modify'] ) {
1118
				$content[ $key ] = $value['value'];
1119
				unset( $params[ $key ] );
1120
			}
1121
		}
1122
1123
		$user_id = wp_update_user( $content );
1124
1125
		if ( is_wp_error( $user_id ) ) {
1126
			$success = false;
1127
			$errors  = $user_id;
1128
		} else {
1129
			$success = true;
1130
			$errors  = array();
1131
			foreach ( $params as $key => $value ) {
1132
				$method  = $value['method_modify'];
1133
				$meta_id = $method( $user_id, $key, $value['value'] );
1134
				if ( false === $meta_id ) {
1135
					$changed = false;
1136
					// Check and make sure the stored value matches $value['value'], otherwise it's an error.
1137
					if ( get_user_meta( $user_id, $key, true ) !== $value['value'] ) {
0 ignored issues
show
introduced by
get_user_meta() usage is highly discouraged, check VIP documentation on "Working with wp_users"
Loading history...
Bug introduced by
It seems like $user_id can also be of type WP_Error; however, parameter $user_id of get_user_meta() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

1137
					if ( get_user_meta( /** @scrutinizer ignore-type */ $user_id, $key, true ) !== $value['value'] ) {
Loading history...
1138
						$errors[] = array(
1139
							'key'          => $key,
1140
							'value'        => $value,
1141
							'actual_value' => get_user_meta( $user_id, $key, true ),
0 ignored issues
show
introduced by
get_user_meta() usage is highly discouraged, check VIP documentation on "Working with wp_users"
Loading history...
1142
						);
1143
					}
1144
				}
1145
			}
1146
1147
			// Developers can use this hook to set any other user data - permissions, etc.
1148
			do_action( $this->option_prefix . 'set_more_user_data', $user_id, $params, 'update' );
1149
1150
		}
1151
1152
		$result = array(
1153
			'data'   => array(
1154
				$id_field => $user_id,
1155
				'success' => $success,
1156
			),
1157
			'errors' => $errors,
1158
		);
1159
		return $result;
1160
	}
1161
1162
	/**
1163
	 * Delete a WordPress user.
1164
	 *
1165
	 * @param int $id User ID.
1166
	 * @param int $reassign If we should reassign any posts to other users. We don't change this from NULL anywhere in this plugin.
1167
	 *
1168
	 * @return boolean true if successful
1169
	 */
1170
	private function user_delete( $id, $reassign = null ) {
1171
		// According to https://codex.wordpress.org/Function_Reference/wp_delete_user we have to include user.php first; otherwise it throws undefined error.
1172
		require_once( ABSPATH . 'wp-admin/includes/user.php' );
1173
		$result = wp_delete_user( $id, $reassign );
1174
		return $result;
1175
	}
1176
1177
	/**
1178
	 * Create a new WordPress post.
1179
	 *
1180
	 * @param array  $params Array of post data params.
1181
	 * @param string $id_field Optional string of what the ID field is, if it is ever not ID.
1182
	 * @param string $post_type Optional string for custom post type, if applicable.
1183
	 *
1184
	 * @return array
1185
	 *   data:
1186
	 *     ID : 123,
1187
	 *     success: 1
1188
	 *   "errors" : [ ],
1189
	 */
1190
	private function post_create( $params, $id_field = 'ID', $post_type = 'post' ) {
1191
		// Load all params with a method_modify of the object structure's content_method into $content
1192
		$content   = array();
1193
		$structure = $this->get_wordpress_table_structure( $post_type );
1194
		foreach ( $params as $key => $value ) {
1195
			if ( in_array( $value['method_modify'], $structure['content_methods'] ) ) {
1196
				$content[ $key ] = $value['value'];
1197
				unset( $params[ $key ] );
1198
			}
1199
		}
1200
1201
		if ( '' !== $post_type ) {
1202
			$content['post_type'] = $post_type;
1203
		}
1204
1205
		// WordPress post creation will fail with an object of 0 if there is no title or content
1206
		// I think we should allow this to happen and not make users' data decisions, so
1207
		// if we're receiving nothing for either of these, create a blank one so it doesn't fail
1208
		// here we have to use $content because $params has already been unset
1209
		if ( ! isset( $content['post_title'] ) ) {
1210
			$content['post_title'] = ' ';
1211
		}
1212
		if ( ! isset( $content['post_content'] ) ) {
1213
			$content['post_content'] = ' ';
1214
		}
1215
1216
		$post_id = wp_insert_post( $content, true ); // return an error instead of a 0 id
1217
1218
		if ( is_wp_error( $post_id ) ) {
1219
			$success = false;
1220
			$errors  = $post_id;
1221
		} else {
1222
			$success = true;
1223
			$errors  = array();
1224
			// If it's a custom post type, fix the methods.
1225
			if ( isset( $params['RecordTypeId']['value'] ) ) {
1226
				$params['RecordTypeId']['method_modify'] = 'update_post_meta';
1227
				$params['RecordTypeId']['method_read']   = 'get_post_meta';
1228
			}
1229
			if ( is_array( $params ) && ! empty( $params ) ) {
1230
				foreach ( $params as $key => $value ) {
1231
					$method  = $value['method_modify'];
1232
					$meta_id = $method( $post_id, $key, $value['value'] );
1233
					if ( false === $meta_id ) {
1234
						$success  = false;
1235
						$errors[] = array(
1236
							'key'   => $key,
1237
							'value' => $value,
1238
						);
1239
					}
1240
				}
1241
			}
1242
1243
			// Developers can use this hook to set any other post data.
1244
			do_action( $this->option_prefix . 'set_more_post_data', $post_id, $params, 'create' );
1245
1246
		}
1247
1248
		if ( is_wp_error( $post_id ) ) {
1249
			$success = false;
1250
			$errors  = $post_id;
1251
		} else {
1252
			$success = true;
1253
			$errors  = array();
1254
		}
1255
1256
		$result = array(
1257
			'data'   => array(
1258
				$id_field => $post_id,
1259
				'success' => $success,
1260
			),
1261
			'errors' => $errors,
1262
		);
1263
1264
		return $result;
1265
1266
	}
1267
1268
	/**
1269
	 * Create a new WordPress post or update it if a match is found.
1270
	 *
1271
	 * @param string $key What key we are looking at for possible matches.
1272
	 * @param string $value What value we are looking at for possible matches.
1273
	 * @param array  $methods What WordPress methods do we use to get the data, if there are any. otherwise, maybe will have to do a wpdb query.
1274
	 * @param array  $params Array of post data params.
1275
	 * @param string $id_field optional string of what the ID field is, if it is ever not ID.
1276
	 * @param bool   $pull_to_drafts Whether to save to WordPress drafts when pulling from Salesforce.
1277
	 * @param string $post_type Optional string for custom post type, if applicable.
1278
	 * @param bool   $check_only Allows this method to only check for matching records, instead of making any data changes.
1279
	 *
1280
	 * @return array
1281
	 *   data:
1282
	 *     ID : 123,
1283
	 *     success: 1
1284
	 *   "errors" : [ ],
1285
	 */
1286
	private function post_upsert( $key, $value, $methods = array(), $params, $id_field = 'ID', $pull_to_drafts = false, $post_type = 'post', $check_only = false ) {
1287
1288
		$method = $methods['method_match'];
1289
1290
		if ( '' !== $method ) {
1291
			// By default, posts use get_posts as the method. args can be like this.
1292
			// The args don't really make sense, and are inconsistently documented.
1293
			// These methods should give us the post object.
1294
			$args = array();
1295
			if ( 'post_title' === $key ) {
1296
				$params['post_title'] = array(
1297
					'value'         => $value,
1298
					'method_modify' => $method,
1299
					'method_read'   => $methods['method_read'],
1300
				);
1301
				$args['name']         = sanitize_title( $value );
1302
			} else {
1303
				$args[ $key ] = $value;
1304
			}
1305
			$args['post_type'] = $post_type;
1306
			$post_statuses     = array( 'publish' );
1307
1308
			if ( true === filter_var( $pull_to_drafts, FILTER_VALIDATE_BOOLEAN ) ) {
1309
				$post_statuses[] = 'draft';
1310
			}
1311
			$args['post_status'] = $post_statuses;
1312
1313
			// if we are trying to match to a meta field, the method is an object
1314
			if ( class_exists( $method ) ) {
1315
				unset( $args[ $key ] );
1316
				$args['meta_query'] = array(
0 ignored issues
show
introduced by
Detected usage of meta_query, possible slow query.
Loading history...
1317
					array(
1318
						'key'   => $key,
1319
						'value' => $value,
1320
					),
1321
				);
1322
				$match_query        = new $method( $args );
1323
				$posts              = $match_query->get_results();
1324
			} else {
1325
				$posts = $method( $args );
1326
			}
1327
1328
			if ( isset( $posts ) && isset( $posts[0]->{$id_field} ) ) {
1329
				// Post does exist after checking the matching value. We want its id.
1330
				$post_id = $posts[0]->{$id_field};
1331
1332
				if ( true === $check_only ) {
1333
					// We are just checking to see if there is a match.
1334
					return $post_id;
1335
				}
1336
1337
				// On the prematch fields, we specify the method_update param.
1338
				if ( isset( $methods['method_update'] ) ) {
1339
					$method = $methods['method_update'];
1340
				} else {
1341
					$method = $methods['method_modify'];
1342
				}
1343
				$params[ $key ] = array(
1344
					'value'         => $value,
1345
					'method_modify' => $method,
1346
					'method_read'   => $methods['method_read'],
1347
				);
1348
			} elseif ( false === $check_only ) {
1349
				// Post does not exist after checking the matching value. create it.
1350
				// On the prematch fields, we specify the method_create param.
1351
				if ( isset( $methods['method_create'] ) ) {
1352
					$method = $methods['method_create'];
1353
				} else {
1354
					$method = $methods['method_modify'];
1355
				}
1356
				$params[ $key ] = array(
1357
					'value'         => $value,
1358
					'method_modify' => $method,
1359
					'method_read'   => $methods['method_read'],
1360
				);
1361
				$result         = $this->post_create( $params, $id_field, $post_type );
1362
				return $result;
1363
			} else {
1364
				// Check only is true but there's not a post yet.
1365
				return null;
1366
			} // End if().
1367
		} else {
1368
			// There is no method by which to check the post. we can check other ways here.
1369
			$params[ $key ] = array(
1370
				'value'         => $value,
1371
				'method_modify' => $methods['method_modify'],
1372
				'method_read'   => $methods['method_read'],
1373
			);
1374
1375
			// If we have a title, use it to check for existing post.
1376
			if ( isset( $params['post_title']['value'] ) ) {
1377
				$title = $params['post_title']['value'];
1378
			}
1379
1380
			// If we have content, use it to check for existing post.
1381
			if ( isset( $params['post_content']['value'] ) ) {
1382
				$content = $params['post_content']['value'];
1383
			} else {
1384
				$content = '';
1385
			}
1386
1387
			// If we have a date, use it to check for existing post.
1388
			if ( isset( $params['post_date']['value'] ) ) {
1389
				$date = $params['post_date']['value'];
1390
			} else {
1391
				$date = '';
1392
			}
1393
1394
			$existing_id = post_exists( $title, $content, $date ); // Returns an id if there is a result. Returns 0 if not.
1395
1396
			// Post does not exist after more checking. maybe we want to create it.
1397
			if ( 0 === $existing_id && false === $check_only ) {
1398
				$result = $this->post_create( $params, $id_field, $post_type );
1399
				return $result;
1400
			} elseif ( true === $check_only ) {
1401
				// We are just checking to see if there is a match.
1402
				return $existing_id;
1403
			} else {
1404
				// Post does exist based on fields, and we aren't doing a check only. we want to update the wp post here.
1405
				$post_id = $existing_id;
1406
			}
1407
1408
			return $result;
1409
1410
		} // End if().
1411
1412
		if ( isset( $post_id ) ) {
1413
			foreach ( $params as $key => $value ) {
1414
				$params[ $key ]['method_modify'] = $methods['method_update'];
1415
			}
1416
			$result = $this->post_update( $post_id, $params, $id_field, $post_type );
1417
			return $result;
1418
		}
1419
		// Create log entry for lack of a post id.
1420
		if ( isset( $this->logging ) ) {
1421
			$logging = $this->logging;
1422
		} elseif ( class_exists( 'Object_Sync_Sf_Logging' ) ) {
1423
			$logging = new Object_Sync_Sf_Logging( $this->wpdb, $this->version );
1424
		}
1425
		$logging->setup(
1426
			// todo: can we be more explicit here about what post upsert failed?
1427
			esc_html__( 'Error: Posts: Tried to run post_upsert, and ended up without a post id', 'object-sync-for-salesforce' ),
1428
			'',
1429
			0,
1430
			0,
1431
			'error'
1432
		);
1433
1434
	}
1435
1436
	/**
1437
	 * Update a WordPress post.
1438
	 *
1439
	 * @param string $post_id The ID for the post to be updated. This value needs to be in the array that is sent to wp_update_post.
1440
	 * @param array  $params Array of post data params.
1441
	 * @param string $id_field Optional string of what the ID field is, if it is ever not ID.
1442
	 * @param string $post_type Optional string for custom post type, if applicable.
1443
	 *
1444
	 * @return array
1445
	 *   data:
1446
	 *     success: 1
1447
	 *   "errors" : [ ],
1448
	 */
1449
	private function post_update( $post_id, $params, $id_field = 'ID', $post_type = '' ) {
1450
		$content              = array();
1451
		$content[ $id_field ] = $post_id;
1452
		foreach ( $params as $key => $value ) {
1453
			if ( 'wp_update_post' === $value['method_modify'] ) {
1454
				$content[ $key ] = $value['value'];
1455
				unset( $params[ $key ] );
1456
			}
1457
		}
1458
1459
		if ( '' !== $post_type ) {
1460
			$content['post_type'] = $post_type;
1461
		}
1462
1463
		$post_id = wp_update_post( $content, true ); // return an error instead of a 0 id
1464
1465
		if ( is_wp_error( $post_id ) ) {
1466
			$success = false;
1467
			$errors  = $post_id;
1468
		} else {
1469
			$success = true;
1470
			$errors  = array();
1471
			// If it's a custom post type, fix the methods.
1472
			if ( isset( $params['RecordTypeId']['value'] ) ) {
1473
				$params['RecordTypeId']['method_modify'] = 'update_post_meta';
1474
				$params['RecordTypeId']['method_read']   = 'get_post_meta';
1475
			}
1476
			if ( is_array( $params ) && ! empty( $params ) ) {
1477
				foreach ( $params as $key => $value ) {
1478
					$method  = $value['method_modify'];
1479
					$meta_id = $method( $post_id, $key, $value['value'] );
1480
					if ( false === $meta_id ) {
1481
						$changed = false;
1482
						// Check and make sure the stored value matches $value['value'], otherwise it's an error.
1483
						if ( get_post_meta( $post_id, $key, true ) !== $value['value'] ) {
0 ignored issues
show
Bug introduced by
It seems like $post_id can also be of type WP_Error; however, parameter $post_id of get_post_meta() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

1483
						if ( get_post_meta( /** @scrutinizer ignore-type */ $post_id, $key, true ) !== $value['value'] ) {
Loading history...
1484
							$errors[] = array(
1485
								'key'   => $key,
1486
								'value' => $value,
1487
							);
1488
						}
1489
					}
1490
				}
1491
			}
1492
1493
			// Developers can use this hook to set any other post data.
1494
			do_action( $this->option_prefix . 'set_more_post_data', $post_id, $params, 'update' );
1495
1496
		}
1497
1498
		$result = array(
1499
			'data'   => array(
1500
				$id_field => $post_id,
1501
				'success' => $success,
1502
			),
1503
			'errors' => $errors,
1504
		);
1505
		return $result;
1506
	}
1507
1508
	/**
1509
	 * Delete a WordPress post.
1510
	 *
1511
	 * @param int  $id Post ID.
1512
	 * @param bool $force_delete If we should bypass the trash. We don't change this from FALSE anywhere in this plugin.
1513
	 *
1514
	 * @return mixed post object if successful, false if failed
1515
	 */
1516
	private function post_delete( $id, $force_delete = false ) {
1517
		$result = wp_delete_post( $id, $force_delete );
1518
		return $result;
1519
	}
1520
1521
	/**
1522
	 * Create a new WordPress attachment.
1523
	 *
1524
	 * @param array  $params Array of attachment data params.
1525
	 * @param string $id_field Optional string of what the ID field is, if it is ever not ID.
1526
	 *
1527
	 * @return array
1528
	 *   data:
1529
	 *     ID : 123,
1530
	 *     success: 1
1531
	 *   "errors" : [ ],
1532
	 */
1533
	private function attachment_create( $params, $id_field = 'ID' ) {
1534
		// Load all params with a method_modify of the object structure's content_method into $content
1535
		$content   = array();
1536
		$structure = $this->get_wordpress_table_structure( 'attachment' );
1537
		// WP requires post_title, post_content (can be empty), post_status, and post_mime_type to create an attachment.
1538
		foreach ( $params as $key => $value ) {
1539
			if ( in_array( $value['method_modify'], $structure['content_methods'] ) ) {
1540
				$content[ $key ] = $value['value'];
1541
				unset( $params[ $key ] );
1542
			}
1543
		}
1544
1545
		// Developers can use this hook to pass filename and parent data for the attachment.
1546
		$params = apply_filters( $this->option_prefix . 'set_initial_attachment_data', $params );
1547
1548
		if ( isset( $params['filename']['value'] ) ) {
1549
			$filename = $params['filename']['value'];
1550
		} else {
1551
			$filename = false;
1552
		}
1553
1554
		if ( isset( $params['parent']['value'] ) ) {
1555
			$parent = $params['parent']['value'];
1556
		} else {
1557
			$parent = 0;
1558
		}
1559
1560
		$attachment_id = wp_insert_attachment( $content, $filename, $parent );
1561
1562
		if ( is_wp_error( $attachment_id ) ) {
1563
			$success = false;
1564
			$errors  = $attachment_id;
1565
		} else {
1566
			$success = true;
1567
			$errors  = array();
1568
1569
			if ( false !== $filename ) {
1570
				// According to https://codex.wordpress.org/Function_Reference/wp_insert_attachment we need this file.
1571
				require_once( ABSPATH . 'wp-admin/includes/image.php' );
1572
				// Generate metadata for the attachment.
1573
				$attach_data = wp_generate_attachment_metadata( $attachment_id, $filename );
0 ignored issues
show
Bug introduced by
It seems like $attachment_id can also be of type WP_Error; however, parameter $attachment_id of wp_generate_attachment_metadata() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

1573
				$attach_data = wp_generate_attachment_metadata( /** @scrutinizer ignore-type */ $attachment_id, $filename );
Loading history...
1574
				wp_update_attachment_metadata( $attachment_id, $attach_data );
0 ignored issues
show
Bug introduced by
It seems like $attachment_id can also be of type WP_Error; however, parameter $attachment_id of wp_update_attachment_metadata() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

1574
				wp_update_attachment_metadata( /** @scrutinizer ignore-type */ $attachment_id, $attach_data );
Loading history...
1575
			}
1576
1577
			if ( 0 !== $parent ) {
1578
				set_post_thumbnail( $parent_post_id, $attachment_id );
0 ignored issues
show
Bug introduced by
It seems like $attachment_id can also be of type WP_Error; however, parameter $thumbnail_id of set_post_thumbnail() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

1578
				set_post_thumbnail( $parent_post_id, /** @scrutinizer ignore-type */ $attachment_id );
Loading history...
1579
			}
1580
1581
			// Developers can use this hook to set any other attachment data.
1582
			do_action( $this->option_prefix . 'set_more_attachment_data', $attachment_id, $params, 'create' );
1583
1584
		}
1585
1586
		$result = array(
1587
			'data'   => array(
1588
				$id_field => $attachment_id,
1589
				'success' => $success,
1590
			),
1591
			'errors' => $errors,
1592
		);
1593
1594
		return $result;
1595
1596
	}
1597
1598
	/**
1599
	 * Create a new WordPress attachment or update it if a match is found.
1600
	 *
1601
	 * @param string $key What key we are looking at for possible matches.
1602
	 * @param string $value What value we are looking at for possible matches.
1603
	 * @param array  $methods What WordPress methods do we use to get the data, if there are any. otherwise, maybe will have to do a wpdb query.
1604
	 * @param array  $params Array of attachment data params.
1605
	 * @param string $id_field Optional string of what the ID field is, if it is ever not ID.
1606
	 * @param bool   $check_only Allows this method to only check for matching records, instead of making any data changes.
1607
	 *
1608
	 * @return array
1609
	 *   data:
1610
	 *     ID : 123,
1611
	 *     success: 1
1612
	 *   "errors" : [ ],
1613
	 */
1614
	private function attachment_upsert( $key, $value, $methods = array(), $params, $id_field = 'ID', $check_only = false ) {
1615
1616
		$method = $methods['method_match'];
1617
1618
		if ( '' !== $method ) {
1619
			// Get_posts is more helpful here, so that is the method attachment uses for 'read'.
1620
			// By default, posts use get_posts as the method. args can be like this.
1621
			// The args don't really make sense, and are inconsistently documented.
1622
			// These methods should give us the post object.
1623
			$args = array();
1624
			if ( 'post_title' === $key ) {
1625
				$params['post_title'] = array(
1626
					'value'         => $value,
1627
					'method_modify' => $method,
1628
					'method_read'   => $methods['method_read'],
1629
				);
1630
				$args['name']         = sanitize_title( $value );
1631
			} else {
1632
				$args[ $key ] = $value;
1633
			}
1634
			$args['post_type'] = 'attachment';
1635
1636
			// if we are trying to match to a meta field, the method is an object
1637
			if ( class_exists( $method ) ) {
1638
				unset( $args[ $key ] );
1639
				$args['meta_query'] = array(
0 ignored issues
show
introduced by
Detected usage of meta_query, possible slow query.
Loading history...
1640
					array(
1641
						'key'   => $key,
1642
						'value' => $value,
1643
					),
1644
				);
1645
				$match_query        = new $method( $args );
1646
				$posts              = $match_query->get_results();
1647
			} else {
1648
				$posts = $method( $args );
1649
			}
1650
1651
			if ( isset( $posts ) && isset( $posts[0]->{$id_field} ) ) {
1652
				// Attachment does exist after checking the matching value. we want its id.
1653
				$attachment_id = $posts[0]->{$id_field};
1654
1655
				if ( true === $check_only ) {
1656
					// We are just checking to see if there is a match.
1657
					return $attachment_id;
1658
				}
1659
1660
				// On the prematch fields, we specify the method_update param.
1661
				if ( isset( $methods['method_update'] ) ) {
1662
					$method = $methods['method_update'];
1663
				} else {
1664
					$method = $methods['method_modify'];
1665
				}
1666
				$params[ $key ] = array(
1667
					'value'         => $value,
1668
					'method_modify' => $method,
1669
					'method_read'   => $methods['method_read'],
1670
				);
1671
			} elseif ( false === $check_only ) {
1672
				// Attachment does not exist after checking the matching value. create it.
1673
				// On the prematch fields, we specify the method_create param.
1674
				if ( isset( $methods['method_create'] ) ) {
1675
					$method = $methods['method_create'];
1676
				} else {
1677
					$method = $methods['method_modify'];
1678
				}
1679
				$params[ $key ] = array(
1680
					'value'         => $value,
1681
					'method_modify' => $method,
1682
					'method_read'   => $methods['method_read'],
1683
				);
1684
				$result         = $this->attachment_create( $params );
1685
				return $result;
1686
			} else {
1687
				// Check only is true but there's not an attachment yet.
1688
				return null;
1689
			} // End if().
1690
		} else {
1691
			// There is no method by which to check the post. we can check other ways here.
1692
			$params[ $key ] = array(
1693
				'value'         => $value,
1694
				'method_modify' => $methods['method_modify'],
1695
				'method_read'   => $methods['method_read'],
1696
			);
1697
1698
			// If we have a title, use it to check for existing post.
1699
			if ( isset( $params['post_title']['value'] ) ) {
1700
				$title = $params['post_title']['value'];
1701
			}
1702
1703
			// If we have content, use it to check for existing post.
1704
			if ( isset( $params['post_content']['value'] ) ) {
1705
				$content = $params['post_content']['value'];
1706
			} else {
1707
				$content = '';
1708
			}
1709
1710
			// If we have a date, use it to check for existing post.
1711
			if ( isset( $params['post_date']['value'] ) ) {
1712
				$date = $params['post_date']['value'];
1713
			} else {
1714
				$date = '';
1715
			}
1716
1717
			$existing_id = post_exists( $title, $content, $date ); // Returns an id if there is a result. Returns 0 if not.
1718
1719
			// Attachment does not exist after more checking. maybe we want to create it.
1720
			if ( 0 === $existing_id && false === $check_only ) {
1721
				$result = $this->attachment_create( $params );
1722
				return $result;
1723
			} elseif ( true === $check_only ) {
1724
				// We are just checking to see if there is a match.
1725
				return $existing_id;
1726
			} else {
1727
				// Attachment does exist based on fields, and we aren't doing a check only. we want to update the wp attachment here.
1728
				$attachment_id = $existing_id;
1729
			}
1730
1731
			return $result;
1732
1733
		} // End if().
1734
1735
		if ( isset( $attachment_id ) ) {
1736
			foreach ( $params as $key => $value ) {
1737
				$params[ $key ]['method_modify'] = $methods['method_update'];
1738
			}
1739
			$result = $this->attachment_update( $attachment_id, $params );
1740
			return $result;
1741
		}
1742
1743
		// Create log entry for lack of an attachment id.
1744
		if ( isset( $this->logging ) ) {
1745
			$logging = $this->logging;
1746
		} elseif ( class_exists( 'Object_Sync_Sf_Logging' ) ) {
1747
			$logging = new Object_Sync_Sf_Logging( $this->wpdb, $this->version );
1748
		}
1749
		$logging->setup(
1750
			esc_html__( 'Error: Attachment: Tried to run attachment_upsert, and ended up without an attachment id', 'object-sync-for-salesforce' ),
1751
			'',
1752
			0,
1753
			0,
1754
			'error'
1755
		);
1756
1757
	}
1758
1759
	/**
1760
	 * Update a WordPress attachment.
1761
	 *
1762
	 * @param string $attachment_id The ID for the attachment to be updated. This value needs to be in the array that is sent to the update methods.
1763
	 * @param array  $params Array of attachment data params.
1764
	 * @param string $id_field Optional string of what the ID field is, if it is ever not ID.
1765
	 *
1766
	 * @return array
1767
	 *   data:
1768
	 *     success: 1
1769
	 *   "errors" : [ ],
1770
	 *
1771
	 * Note: this method uses wp_insert_attachment for core content fields as there isn't a corresponding method for updating these rows
1772
	 * it does use wp_update_attachment_metadata for the meta fields, though.
1773
	 * Developers should use hooks to change this, if it does not meet their needs.
1774
	 */
1775
	private function attachment_update( $attachment_id, $params, $id_field = 'ID' ) {
1776
		$content              = array();
1777
		$content[ $id_field ] = $attachment_id;
1778
		foreach ( $params as $key => $value ) {
1779
			if ( 'wp_insert_attachment' === $value['method_modify'] ) { // Should also be insert attachment maybe.
1780
				$content[ $key ] = $value['value'];
1781
				unset( $params[ $key ] );
1782
			}
1783
		}
1784
1785
		if ( isset( $params['filename']['value'] ) ) {
1786
			$filename = $params['filename']['value'];
1787
		} else {
1788
			$filename = false;
1789
		}
1790
1791
		if ( isset( $params['parent']['value'] ) ) {
1792
			$parent = $params['parent']['value'];
1793
		} else {
1794
			$parent = 0;
1795
		}
1796
1797
		$attachment_id = wp_insert_attachment( $content, $filename, $parent );
1798
1799
		if ( is_wp_error( $attachment_id ) ) {
1800
			$success = false;
1801
			$errors  = $attachment_id;
1802
		} else {
1803
			$success = true;
1804
			$errors  = array();
1805
1806
			if ( false !== $filename ) {
1807
				// According to https://codex.wordpress.org/Function_Reference/wp_insert_attachment we need this file.
1808
				require_once( ABSPATH . 'wp-admin/includes/image.php' );
1809
				// Generate metadata for the attachment.
1810
				$attach_data = wp_generate_attachment_metadata( $attachment_id, $filename );
0 ignored issues
show
Bug introduced by
It seems like $attachment_id can also be of type WP_Error; however, parameter $attachment_id of wp_generate_attachment_metadata() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

1810
				$attach_data = wp_generate_attachment_metadata( /** @scrutinizer ignore-type */ $attachment_id, $filename );
Loading history...
1811
			}
1812
1813
			// Put the data from salesforce into the meta array.
1814
			$attach_new_data = array();
1815
			foreach ( $params as $key => $value ) {
1816
				$method                  = $value['method_modify'];
1817
				$attach_new_data[ $key ] = $value['value'];
1818
			}
1819
1820
			if ( isset( $attach_data ) ) {
1821
				$attach_data = array_merge( $attach_data, $attach_new_data );
1822
			} else {
1823
				$attach_data = $attach_new_data;
1824
			}
1825
1826
			$meta_updated = wp_update_attachment_metadata( $attachment_id, $attach_data );
0 ignored issues
show
Bug introduced by
It seems like $attachment_id can also be of type WP_Error; however, parameter $attachment_id of wp_update_attachment_metadata() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

1826
			$meta_updated = wp_update_attachment_metadata( /** @scrutinizer ignore-type */ $attachment_id, $attach_data );
Loading history...
1827
1828
			if ( false === $meta_updated ) {
1829
				$success  = false;
1830
				$errors[] = array(
1831
					'key'   => $key,
1832
					'value' => $value,
1833
				);
1834
			}
1835
1836
			if ( 0 !== $parent ) {
1837
				set_post_thumbnail( $parent_post_id, $attachment_id );
0 ignored issues
show
Bug introduced by
It seems like $attachment_id can also be of type WP_Error; however, parameter $thumbnail_id of set_post_thumbnail() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

1837
				set_post_thumbnail( $parent_post_id, /** @scrutinizer ignore-type */ $attachment_id );
Loading history...
1838
			}
1839
1840
			// Developers can use this hook to set any other attachment data.
1841
			do_action( $this->option_prefix . 'set_more_attachment_data', $attachment_id, $params, 'update' );
1842
1843
		} // End if().
1844
1845
		$result = array(
1846
			'data'   => array(
1847
				$id_field => $attachment_id,
1848
				'success' => $success,
1849
			),
1850
			'errors' => $errors,
1851
		);
1852
		return $result;
1853
	}
1854
1855
	/**
1856
	 * Delete a WordPress attachment.
1857
	 *
1858
	 * @param int  $id Attachment ID.
1859
	 * @param bool $force_delete If we should bypass the trash. We don't change this from FALSE anywhere in this plugin.
1860
	 *
1861
	 * @return mixed
1862
	 *   attachment object if successful, false if failed
1863
	 */
1864
	private function attachment_delete( $id, $force_delete = false ) {
1865
		$result = wp_delete_attachment( $id, $force_delete );
1866
		return $result;
1867
	}
1868
1869
	/**
1870
	 * Create a new WordPress term.
1871
	 *
1872
	 * @param array  $params Array of term data params.
1873
	 * @param string $taxonomy The taxonomy to which to add the term. this is required.
1874
	 * @param string $id_field Optional string of what the ID field is, if it is ever not ID.
1875
	 *
1876
	 * @return array
1877
	 *   data:
1878
	 *     ID : 123,
1879
	 *     success: 1
1880
	 *   "errors" : [ ],
1881
	 */
1882
	private function term_create( $params, $taxonomy, $id_field = 'ID' ) {
1883
		if ( 'tag' === $taxonomy ) {
1884
			$taxonomy = 'post_tag';
1885
		}
1886
		// Load all params with a method_modify of the object structure's content_method into $content
1887
		$content   = array();
1888
		$structure = $this->get_wordpress_table_structure( $taxonomy );
1889
		$args      = array();
1890
		foreach ( $params as $key => $value ) {
1891
			if ( 'name' === $key ) {
1892
				$name = $value['value'];
1893
				unset( $params[ $key ] );
1894
			}
1895
			if ( in_array( $value['method_modify'], $structure['content_methods'] ) && 'name' !== $key ) {
1896
				$args[ $key ] = $value['value'];
1897
				unset( $params[ $key ] );
1898
			}
1899
		}
1900
		if ( isset( $name ) ) {
1901
			$term = wp_insert_term( $name, $taxonomy, $args );
1902
		}
1903
1904
		if ( is_wp_error( $term ) ) {
1905
			$success = false;
1906
			$errors  = $term;
1907
		} else {
1908
			$term_id = $term[ "$id_field" ];
1909
			$success = true;
1910
			$errors  = array();
1911
			foreach ( $params as $key => $value ) {
1912
				$method  = $value['method_modify'];
1913
				$meta_id = $method( $term_id, $key, $value['value'] );
1914
				if ( false === $meta_id ) {
1915
					$success  = false;
1916
					$errors[] = array(
1917
						'message' => sprintf(
1918
							// translators: %1$s is a method name.
1919
							esc_html__( 'Tried to upsert meta with method %1$s.', 'object-sync-for-salesforce' ),
1920
							esc_html( $method )
1921
						),
1922
						'key'     => $key,
1923
						'value'   => $value,
1924
					);
1925
				}
1926
			}
1927
1928
			// Developers can use this hook to set any other term data.
1929
			do_action( $this->option_prefix . 'set_more_term_data', $term_id, $params, 'create' );
1930
1931
		}
1932
1933
		if ( is_wp_error( $term ) ) {
1934
			$success = false;
1935
			$errors  = $term;
1936
		} else {
1937
			$success = true;
1938
			$errors  = array();
1939
		}
1940
1941
		$result = array(
1942
			'data'   => array(
1943
				$id_field => $term_id,
1944
				'success' => $success,
1945
			),
1946
			'errors' => $errors,
1947
		);
1948
1949
		return $result;
1950
1951
	}
1952
1953
	/**
1954
	 * Create a new WordPress term or update it if a match is found.
1955
	 *
1956
	 * @param string $key What key we are looking at for possible matches.
1957
	 * @param string $value What value we are looking at for possible matches.
1958
	 * @param array  $methods What WordPress methods do we use to get the data, if there are any. otherwise, maybe will have to do a wpdb query.
1959
	 * @param array  $params Array of term data params.
1960
	 * @param string $taxonomy The taxonomy to which to add the term. this is required..
1961
	 * @param string $id_field Optional string of what the ID field is, if it is ever not ID.
1962
	 * @param bool   $pull_to_drafts Whether to save to WordPress drafts when pulling from Salesforce.
1963
	 * @param bool   $check_only Allows this method to only check for matching records, instead of making any data changes.
1964
	 *
1965
	 * @return array
1966
	 *   data:
1967
	 *     ID : 123,
1968
	 *     success: 1
1969
	 *   "errors" : [ ],
1970
	 */
1971
	private function term_upsert( $key, $value, $methods = array(), $params, $taxonomy, $id_field = 'ID', $pull_to_drafts = false, $check_only = false ) {
1972
		if ( 'tag' === $taxonomy ) {
1973
			$taxonomy = 'post_tag';
1974
		}
1975
		$method = $methods['method_match'];
1976
		if ( '' !== $method ) {
1977
			// These methods should give us the term object if we are matching for one.
1978
			// if we are trying to match to a meta field, the method is an object
1979
			if ( class_exists( $method ) ) {
1980
				$args        = array(
1981
					'taxonomy'   => $taxonomy,
1982
					'meta_key'   => $key,
0 ignored issues
show
introduced by
Detected usage of meta_key, possible slow query.
Loading history...
1983
					'meta_value' => $value,
0 ignored issues
show
introduced by
Detected usage of meta_value, possible slow query.
Loading history...
1984
				);
1985
				$match_query = new $method( $args );
1986
				$terms       = $match_query->get_terms();
1987
				if ( ! empty( $terms ) ) {
1988
					$term = $terms[0];
1989
				}
1990
			} else {
1991
				$term = $method( $key, $value, $taxonomy ); // We need to put the taxonomy in there probably.
1992
			}
1993
1994
			if ( isset( $term ) && isset( $term->{$id_field} ) ) {
1995
				// Term does exist after checking the matching value. we want its id.
1996
				$term_id = $term->{$id_field};
1997
1998
				if ( true === $check_only ) {
1999
					// We are just checking to see if there is a match.
2000
					return $term_id;
2001
				}
2002
2003
				// On the prematch fields, we specify the method_update param.
2004
				if ( isset( $methods['method_update'] ) ) {
2005
					$method = $methods['method_update'];
2006
				} else {
2007
					$method = $methods['method_modify'];
2008
				}
2009
				$params[ $key ] = array(
2010
					'value'         => $value,
2011
					'method_modify' => $method,
2012
					'method_read'   => $methods['method_read'],
2013
				);
2014
			} elseif ( false === $check_only ) {
2015
				// Term does not exist after checking the matching value. Create it.
2016
				// On the prematch fields, we specify the method_create param.
2017
				if ( isset( $methods['method_create'] ) ) {
2018
					$method = $methods['method_create'];
2019
				} else {
2020
					$method = $methods['method_modify'];
2021
				}
2022
				$params[ $key ] = array(
2023
					'value'         => $value,
2024
					'method_modify' => $method,
2025
					'method_read'   => $methods['method_read'],
2026
				);
2027
				$result         = $this->term_create( $params, $taxonomy, $id_field );
2028
				return $result;
2029
			} else {
2030
				// Check only is true but there's not a term yet.
2031
				return null;
2032
			} // End if().
2033
		} else {
2034
			// There is no method by which to check the term. we can check other ways here.
2035
			$params[ $key ] = array(
2036
				'value'         => $value,
2037
				'method_modify' => $methods['method_modify'],
2038
				'method_read'   => $methods['method_read'],
2039
			);
2040
2041
			if ( isset( $params['name']['value'] ) ) {
2042
				$term = $params['name']['value'];
2043
			}
2044
2045
			if ( isset( $params['parent']['value'] ) ) {
2046
				$parent = $params['parent']['value'];
2047
			} else {
2048
				$parent = 0;
2049
			}
2050
2051
			// Returns an id if there is a result. Returns null if it does not exist.
2052
			// wpcom_vip_term_exists is cached, and therefore preferred.
2053
			if ( function_exists( 'wpcom_vip_term_exists' ) ) {
2054
				$existing_id = wpcom_vip_term_exists( $term, $taxonomy, $parent );
2055
			} else {
2056
				$existing_id = term_exists( $term, $taxonomy, $parent );
2057
			}
2058
2059
			// Term does not exist after more checking. maybe we want to create it.
2060
			if ( null === $existing_id && false === $check_only ) {
2061
				$result = $this->term_create( $params, $taxonomy, $id_field );
2062
				return $result;
2063
			} elseif ( true === $check_only ) {
2064
				// We are just checking to see if there is a match.
2065
				return $existing_id;
2066
			} else {
2067
				// Term does exist based on criteria, and we aren't doing a check only. we want to update the wp term here.
2068
				$term_id = $existing_id;
2069
			}
2070
		} // End if().
2071
2072
		if ( isset( $term_id ) ) {
2073
			foreach ( $params as $key => $value ) {
2074
				$params[ $key ]['method_modify'] = $methods['method_update'];
2075
			}
2076
			$result = $this->term_update( $term_id, $params, $taxonomy, $id_field );
2077
			return $result;
2078
		}
2079
		// Create log entry for lack of a term id.
2080
		if ( isset( $this->logging ) ) {
2081
			$logging = $this->logging;
2082
		} elseif ( class_exists( 'Object_Sync_Sf_Logging' ) ) {
2083
			$logging = new Object_Sync_Sf_Logging( $this->wpdb, $this->version );
2084
		}
2085
		$logging->setup(
2086
			esc_html__( 'Error: Terms: Tried to run term_upsert, and ended up without a term id', 'object-sync-for-salesforce' ),
2087
			'',
2088
			0,
2089
			0,
2090
			'error'
2091
		);
2092
2093
	}
2094
2095
	/**
2096
	 * Update a WordPress term.
2097
	 *
2098
	 * @param string $term_id The ID for the term to be updated. This value needs to be in the array that is sent to wp_update_term.
2099
	 * @param array  $params Array of term data params.
2100
	 * @param string $taxonomy The taxonomy to which to add the term. this is required.
2101
	 * @param string $id_field Optional string of what the ID field is, if it is ever not ID.
2102
	 *
2103
	 * @return array
2104
	 *   data:
2105
	 *     success: 1
2106
	 *   "errors" : [ ],
2107
	 */
2108
	private function term_update( $term_id, $params, $taxonomy, $id_field = 'ID' ) {
2109
		if ( 'tag' === $taxonomy ) {
2110
			$taxonomy = 'post_tag';
2111
		}
2112
		$args = array();
2113
		foreach ( $params as $key => $value ) {
2114
			if ( 'wp_update_term' === $value['method_modify'] ) {
2115
				$args[ $key ] = $value['value'];
2116
				unset( $params[ $key ] );
2117
			}
2118
		}
2119
		$term = wp_update_term( $term_id, $taxonomy, $args );
0 ignored issues
show
Bug introduced by
$term_id of type string is incompatible with the type integer expected by parameter $term_id of wp_update_term(). ( Ignorable by Annotation )

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

2119
		$term = wp_update_term( /** @scrutinizer ignore-type */ $term_id, $taxonomy, $args );
Loading history...
2120
2121
		if ( is_wp_error( $term ) ) {
2122
			$success = false;
2123
			$errors  = $term;
2124
		} else {
2125
			$term_id = $term[ "$id_field" ];
2126
			$success = true;
2127
			$errors  = array();
2128
			foreach ( $params as $key => $value ) {
2129
				$method  = $value['method_modify'];
2130
				$meta_id = $method( $term_id, $key, $value['value'] );
2131
				if ( false === $meta_id ) {
2132
					$success  = false;
2133
					$errors[] = array(
2134
						'message' => sprintf(
2135
							// translators: %1$s is a method name.
2136
							esc_html__( 'Tried to update meta with method %1$s.', 'object-sync-for-salesforce' ),
2137
							esc_html( $method )
2138
						),
2139
						'key'     => $key,
2140
						'value'   => $value,
2141
					);
2142
				}
2143
			}
2144
2145
			// Developers can use this hook to set any other term data.
2146
			do_action( $this->option_prefix . 'set_more_term_data', $term_id, $params, 'update' );
2147
2148
		}
2149
2150
		if ( is_wp_error( $term ) ) {
2151
			$success = false;
2152
			$errors  = $term;
2153
		} else {
2154
			$success = true;
2155
			$errors  = array();
2156
		}
2157
2158
		$result = array(
2159
			'data'   => array(
2160
				$id_field => $term_id,
2161
				'success' => $success,
2162
			),
2163
			'errors' => $errors,
2164
		);
2165
2166
		return $result;
2167
2168
	}
2169
2170
	/**
2171
	 * Delete a WordPress term.
2172
	 *
2173
	 * @param string $term_id The ID for the term to be updated. This value needs to be in the array that is sent to wp_update_term.
2174
	 * @param string $taxonomy The taxonomy from which to delete the term. this is required.
2175
	 *
2176
	 * @return bool True if successful, false if failed.
2177
	 */
2178
	private function term_delete( $term_id, $taxonomy ) {
2179
		if ( 'tag' === $taxonomy ) {
2180
			$taxonomy = 'post_tag';
2181
		}
2182
		$result = wp_delete_term( $term_id, $taxonomy );
0 ignored issues
show
Bug introduced by
$term_id of type string is incompatible with the type integer expected by parameter $term of wp_delete_term(). ( Ignorable by Annotation )

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

2182
		$result = wp_delete_term( /** @scrutinizer ignore-type */ $term_id, $taxonomy );
Loading history...
2183
		return $result;
2184
	}
2185
2186
	/**
2187
	 * Create a new WordPress comment.
2188
	 *
2189
	 * @param array  $params Array of comment data params.
2190
	 * @param string $id_field Optional string of what the ID field is, if it is ever not comment_ID.
2191
	 *
2192
	 * @return array
2193
	 *   data:
2194
	 *     ID : 123,
2195
	 *     success: 1
2196
	 *   "errors" : [ ],
2197
	 */
2198
	private function comment_create( $params, $id_field = 'comment_ID' ) {
2199
		// Load all params with a method_modify of the object structure's content_method into $content
2200
		$content   = array();
2201
		$structure = $this->get_wordpress_table_structure( 'comment' );
2202
		foreach ( $params as $key => $value ) {
2203
			if ( in_array( $value['method_modify'], $structure['content_methods'] ) ) {
2204
				$content[ $key ] = $value['value'];
2205
				unset( $params[ $key ] );
2206
			}
2207
		}
2208
2209
		// Fields that are required for comments, even if they are empty values.
2210
		if ( ! isset( $content['comment_author'] ) ) {
2211
			$content['comment_author'] = '';
2212
		}
2213
		if ( ! isset( $content['comment_author_IP'] ) ) {
2214
			$content['comment_author_IP'] = '';
2215
		}
2216
		if ( ! isset( $content['comment_author_email'] ) ) {
2217
			$content['comment_author_email'] = '';
2218
		}
2219
		if ( ! isset( $content['comment_author_url'] ) ) {
2220
			$content['comment_author_url'] = '';
2221
		}
2222
		if ( ! isset( $content['comment_type'] ) ) {
2223
			$content['comment_type'] = '';
2224
		}
2225
2226
		$comment_id = wp_new_comment( $content );
2227
2228
		if ( is_wp_error( $comment_id ) ) {
2229
			$success = false;
2230
			$errors  = $comment_id;
2231
		} else {
2232
			$success = true;
2233
			$errors  = array();
2234
			foreach ( $params as $key => $value ) {
2235
				$method  = $value['method_modify'];
2236
				$meta_id = $method( $comment_id, $key, $value['value'] );
2237
				if ( false === $meta_id ) {
2238
					$success  = false;
2239
					$errors[] = array(
2240
						'message' => sprintf(
2241
							// translators: %1$s is a method name.
2242
							esc_html__( 'Tried to add meta with method %1$s.', 'object-sync-for-salesforce' ),
2243
							esc_html( $method )
2244
						),
2245
						'key'     => $key,
2246
						'value'   => $value,
2247
					);
2248
				}
2249
			}
2250
2251
			// Developers can use this hook to set any other comment data.
2252
			do_action( $this->option_prefix . 'set_more_comment_data', $comment_id, $params, 'create' );
2253
2254
		}
2255
2256
		if ( is_wp_error( $comment_id ) ) {
2257
			$success = false;
2258
			$errors  = $comment_id;
2259
		} else {
2260
			$success = true;
2261
			$errors  = array();
2262
		}
2263
2264
		$result = array(
2265
			'data'   => array(
2266
				$id_field => $comment_id,
2267
				'success' => $success,
2268
			),
2269
			'errors' => $errors,
2270
		);
2271
2272
		return $result;
2273
2274
	}
2275
2276
	/**
2277
	 * Create a new WordPress comment or update it if a match is found.
2278
	 *
2279
	 * @param string $key What key we are looking at for possible matches.
2280
	 * @param string $value What value we are looking at for possible matches.
2281
	 * @param array  $methods What WordPress methods do we use to get the data, if there are any. otherwise, maybe will have to do a wpdb query.
2282
	 * @param array  $params Array of comment data params.
2283
	 * @param string $id_field Optional string of what the ID field is, if it is ever not comment_ID.
2284
	 * @param bool   $pull_to_drafts Whether to save to WordPress drafts when pulling from Salesforce.
2285
	 * @param bool   $check_only Allows this method to only check for matching records, instead of making any data changes.
2286
	 *
2287
	 * @return array
2288
	 *   data:
2289
	 *     ID : 123,
2290
	 *     success: 1
2291
	 *   "errors" : [ ],
2292
	 */
2293
	private function comment_upsert( $key, $value, $methods, $params, $id_field = 'comment_ID', $pull_to_drafts = false, $check_only = false ) {
2294
		$method = $methods['method_match'];
2295
		if ( 'get_comment' === $method ) {
2296
			$method = 'get_comments';
2297
		}
2298
		if ( '' !== $method ) {
2299
2300
			// These methods should give us the comment object if we are matching for one.
2301
			// if we are trying to match to a meta field, the method is an object
2302
			if ( class_exists( $method ) ) {
2303
				$args        = array(
2304
					'meta_query' => array(
0 ignored issues
show
introduced by
Detected usage of meta_query, possible slow query.
Loading history...
2305
						array(
2306
							'key'   => $key,
2307
							'value' => $value,
2308
						),
2309
					),
2310
				);
2311
				$match_query = new $method( $args );
2312
				$comments    = $match_query->get_comments();
2313
				if ( ! empty( $comments ) ) {
2314
					$comment = $users[0];
2315
				}
2316
			} else {
2317
				$match = array();
2318
				if ( 'comment_author' === $key ) {
2319
					$match['author__in'] = array( $value );
2320
				} else {
2321
					$key           = str_replace( 'comment_', '', $key );
2322
					$match[ $key ] = $value;
2323
				}
2324
				$comments = $method( $match );
2325
			}
2326
2327
			if ( 1 === count( $comments ) && isset( $comments ) && isset( $comments[0]->{$id_field} ) ) {
2328
				$comment = $comments[0];
2329
				// Comment does exist after checking the matching value. we want its id.
2330
				$comment_id = $comment->{$id_field};
2331
2332
				if ( true === $check_only ) {
2333
					// We are just checking to see if there is a match.
2334
					return $comment_id;
2335
				}
2336
2337
				// On the prematch fields, we specify the method_update param.
2338
				if ( isset( $methods['method_update'] ) ) {
2339
					$method = $methods['method_update'];
2340
				} else {
2341
					$method = $methods['method_modify'];
2342
				}
2343
				$params[ $key ] = array(
2344
					'value'         => $value,
2345
					'method_modify' => $method,
2346
					'method_read'   => $methods['method_read'],
2347
				);
2348
			} elseif ( count( $comments ) > 1 ) {
2349
				$status = 'error';
2350
				// Create log entry for multiple matches.
2351
				if ( isset( $this->logging ) ) {
2352
					$logging = $this->logging;
2353
				} elseif ( class_exists( 'Object_Sync_Sf_Logging' ) ) {
2354
					$logging = new Object_Sync_Sf_Logging( $this->wpdb, $this->version );
2355
				}
2356
				$logging->setup(
2357
					sprintf(
2358
						// translators: %1$s is a number. %2$s is a key. %3$s is the value of that key. %4$s is a var_export'd array of comments.
2359
						esc_html__( 'Error: Comments: there are %1$s comment matches for the Salesforce key %2$s with the value of %3$s. Here they are: %4$s', 'object-sync-for-salesforce' ),
2360
						absint( count( $comments ) ),
2361
						esc_html( $key ),
2362
						esc_html( $value ),
2363
						esc_html( var_export( $comments ) ) // Debugging code in production because having useful error messages is good.
0 ignored issues
show
Bug introduced by
Are you sure the usage of var_export($comments) is correct as it 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...
2364
					),
2365
					'',
2366
					0,
2367
					0,
2368
					$status
2369
				);
2370
			} elseif ( false === $check_only ) {
2371
				// Comment does not exist after checking the matching value. Create it.
2372
				// On the prematch fields, we specify the method_create param.
2373
				if ( isset( $methods['method_create'] ) ) {
2374
					$method = $methods['method_create'];
2375
				} else {
2376
					$method = $methods['method_modify'];
2377
				}
2378
				$params[ $key ] = array(
2379
					'value'         => $value,
2380
					'method_modify' => $method,
2381
					'method_read'   => $methods['method_read'],
2382
				);
2383
				$result         = $this->comment_create( $params, $id_field );
2384
				return $result;
2385
			} else {
2386
				// Check only is true but there's not a comment yet.
2387
				return null;
2388
			} // End if().
2389
		} else {
2390
			// There is no method by which to check the comment. We can check other ways here.
2391
			$params[ $key ] = array(
2392
				'value'         => $value,
2393
				'method_modify' => $methods['method_modify'],
2394
				'method_read'   => $methods['method_read'],
2395
			);
2396
2397
			if ( isset( $params['comment_author']['value'] ) ) {
2398
				$comment_author = $params['comment_author']['value'];
2399
			}
2400
2401
			if ( isset( $params['comment_date']['value'] ) ) {
2402
				$comment_date = $params['comment_date']['value'];
2403
			}
2404
2405
			if ( isset( $params['timezone']['value'] ) ) {
2406
				$timezone = $params['timezone']['value'];
2407
			} else {
2408
				$timezone = 'blog';
2409
			}
2410
2411
			$existing_id = comment_exists( $comment_author, $comment_date, $timezone ); // Returns an id if there is a result. Uses $wpdb->get_var, so it returns null if there is no value
2412
2413
			// Comment does not exist after more checking. We want to create it.
2414
			if ( null === $existing_id && false === $check_only ) {
2415
				$result = $this->comment_create( $params, $id_field );
2416
				return $result;
2417
			} elseif ( true === $check_only ) {
2418
				// We are just checking to see if there is a match.
2419
				return $existing_id;
2420
			} else {
2421
				// Comment does exist based on username, and we aren't doing a check only. we want to update the wp user here.
2422
				$comment_id = $existing_id;
2423
			}
2424
		} // End if() that sets up the parameters in the $params array.
2425
2426
		if ( isset( $comment_id ) ) {
2427
			foreach ( $params as $key => $value ) {
2428
				$params[ $key ]['method_modify'] = $methods['method_update'];
2429
			}
2430
			$result = $this->comment_update( $comment_id, $params, $id_field );
2431
			return $result;
2432
		}
2433
2434
		// Create log entry for lack of a comment id.
2435
		if ( isset( $this->logging ) ) {
2436
			$logging = $this->logging;
2437
		} elseif ( class_exists( 'Object_Sync_Sf_Logging' ) ) {
2438
			$logging = new Object_Sync_Sf_Logging( $this->wpdb, $this->version );
2439
		}
2440
		$logging->setup(
2441
			esc_html__( 'Error: Comments: Tried to run comment_upsert, and ended up without a comment id', 'object-sync-for-salesforce' ),
2442
			'',
2443
			0,
2444
			0,
2445
			'error'
2446
		);
2447
2448
	}
2449
2450
	/**
2451
	 * Update a WordPress comment.
2452
	 *
2453
	 * @param string $comment_id The ID for the comment to be updated. This value needs to be in the array that is sent to wp_update_comment.
2454
	 * @param array  $params Array of comment data params.
2455
	 * @param string $id_field Optional string of what the ID field is, if it is ever not ID.
2456
	 *
2457
	 * @return array
2458
	 *   data:
2459
	 *     success: 1
2460
	 *   "errors" : [ ],
2461
	 */
2462
	private function comment_update( $comment_id, $params, $id_field = 'comment_ID' ) {
2463
		$content              = array();
2464
		$content[ $id_field ] = $comment_id;
2465
		foreach ( $params as $key => $value ) {
2466
			if ( 'wp_update_comment' === $value['method_modify'] ) {
2467
				$content[ $key ] = $value['value'];
2468
				unset( $params[ $key ] );
2469
			}
2470
		}
2471
2472
		$updated = wp_update_comment( $content );
2473
2474
		if ( 0 === $updated ) {
2475
			$success = false;
2476
			$errors  = $updated;
2477
		} else {
2478
			$success = true;
2479
			$errors  = array();
2480
			foreach ( $params as $key => $value ) {
2481
				$method  = $value['method_modify'];
2482
				$meta_id = $method( $comment_id, $key, $value['value'] );
2483
				if ( false === $meta_id ) {
2484
					$changed = false;
2485
					// Check and make sure the stored value matches $value['value'], otherwise it's an error.
2486
					if ( get_comment_meta( $comment_id, $key, true ) !== $value['value'] ) {
0 ignored issues
show
Bug introduced by
$comment_id of type string is incompatible with the type integer expected by parameter $comment_id of get_comment_meta(). ( Ignorable by Annotation )

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

2486
					if ( get_comment_meta( /** @scrutinizer ignore-type */ $comment_id, $key, true ) !== $value['value'] ) {
Loading history...
2487
						$errors[] = array(
2488
							'message' => sprintf(
2489
								// Translators: %1$s is a method name.
2490
								esc_html__( 'Tried to update meta with method %1$s.', 'object-sync-for-salesforce' ),
2491
								esc_html( $method )
2492
							),
2493
							'key'     => $key,
2494
							'value'   => $value,
2495
						);
2496
					}
2497
				}
2498
			}
2499
2500
			// Developers can use this hook to set any other comment data.
2501
			do_action( $this->option_prefix . 'set_more_comment_data', $comment_id, $params, 'update' );
2502
2503
		}
2504
2505
		if ( is_wp_error( $updated ) ) {
2506
			$success = false;
2507
			$errors  = $updated;
2508
		} else {
2509
			$success = true;
2510
			$errors  = array();
2511
		}
2512
2513
		$result = array(
2514
			'data'   => array(
2515
				$id_field => $comment_id,
2516
				'success' => $success,
2517
			),
2518
			'errors' => $errors,
2519
		);
2520
2521
		return $result;
2522
2523
	}
2524
2525
	/**
2526
	 * Delete a WordPress comment.
2527
	 *
2528
	 * @param int  $id Comment ID.
2529
	 * @param bool $force_delete If we should bypass the trash. We don't change this from FALSE anywhere in this plugin.
2530
	 *
2531
	 * @return boolean true if successful, false if failed.
2532
	 */
2533
	private function comment_delete( $id, $force_delete = false ) {
2534
		$result = wp_delete_comment( $id, $force_delete );
2535
		return $result;
2536
	}
2537
2538
}
2539
2540
/**
2541
 * WordpressException is a placeholder class in the event that we want to modify Exception for our own purposes.
2542
 */
2543
class WordpressException extends Exception {
2544
}
2545
2546
/**
2547
 * Class to store all theme/plugin transients as an array in one WordPress transient
2548
 **/
2549
class Object_Sync_Sf_WordPress_Transient {
2550
2551
	protected $name;
2552
2553
	/**
2554
	 * Constructor which sets cache options and the name of the field that lists this plugin's cache keys.
2555
	 *
2556
	 * @param string $name The name of the field that lists all cache keys.
2557
	 */
2558
	public function __construct( $name ) {
2559
		$this->name         = $name;
2560
		$this->cache_prefix = esc_sql( 'sfwp_' );
0 ignored issues
show
Bug Best Practice introduced by
The property cache_prefix does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
2561
	}
2562
2563
	/**
2564
	 * Get the transient that lists all the other transients for this plugin.
2565
	 *
2566
	 * @return mixed value of transient. False of empty, otherwise array.
2567
	 */
2568
	public function all_keys() {
2569
		return get_transient( $this->name );
2570
	}
2571
2572
	/**
2573
	 * Set individual transient, and add its key to the list of this plugin's transients.
2574
	 *
2575
	 * @param string $cachekey the key for this cache item
2576
	 * @param mixed $value the value of the cache item
2577
	 * @param int $cache_expiration. How long the plugin key cache, and this individual item cache, should last before expiring.
2578
	 * @return mixed value of transient. False of empty, otherwise array.
2579
	 */
2580
	public function set( $cachekey, $value, $cache_expiration = 0 ) {
2581
2582
		$prefix   = $this->cache_prefix;
2583
		$cachekey = $prefix . $cachekey;
0 ignored issues
show
Bug introduced by
Are you sure $prefix of type array|string can be used in concatenation? ( Ignorable by Annotation )

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

2583
		$cachekey = /** @scrutinizer ignore-type */ $prefix . $cachekey;
Loading history...
2584
2585
		$keys   = $this->all_keys();
2586
		$keys[] = $cachekey;
2587
		set_transient( $this->name, $keys, $cache_expiration );
2588
2589
		return set_transient( $cachekey, $value, $cache_expiration );
2590
	}
2591
2592
	/**
2593
	 * Get the individual cache value
2594
	 *
2595
	 * @param string $cachekey the key for this cache item
2596
	 * @return mixed value of transient. False of empty, otherwise array.
2597
	 */
2598
	public function get( $cachekey ) {
2599
		$prefix   = $this->cache_prefix;
2600
		$cachekey = $prefix . $cachekey;
0 ignored issues
show
Bug introduced by
Are you sure $prefix of type array|string can be used in concatenation? ( Ignorable by Annotation )

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

2600
		$cachekey = /** @scrutinizer ignore-type */ $prefix . $cachekey;
Loading history...
2601
		return get_transient( $cachekey );
2602
	}
2603
2604
	/**
2605
	 * Delete the individual cache value
2606
	 *
2607
	 * @param string $cachekey the key for this cache item
2608
	 * @return bool True if successful, false otherwise.
2609
	 */
2610
	public function delete( $cachekey ) {
2611
		$prefix   = $this->cache_prefix;
2612
		$cachekey = $prefix . $cachekey;
0 ignored issues
show
Bug introduced by
Are you sure $prefix of type array|string can be used in concatenation? ( Ignorable by Annotation )

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

2612
		$cachekey = /** @scrutinizer ignore-type */ $prefix . $cachekey;
Loading history...
2613
		return delete_transient( $cachekey );
2614
	}
2615
2616
	/**
2617
	 * Delete the entire cache for this plugin
2618
	 *
2619
	 * @return bool True if successful, false otherwise.
2620
	 */
2621
	public function flush() {
2622
		$keys   = $this->all_keys();
2623
		$result = true;
2624
		if ( ! empty( $keys ) ) {
2625
			foreach ( $keys as $key ) {
2626
				$result = delete_transient( $key );
2627
			}
2628
		}
2629
		$result = delete_transient( $this->name );
2630
		return $result;
2631
	}
2632
2633
}
2634