Object_Sync_Sf_Salesforce_Pull   F
last analyzed

Complexity

Total Complexity 228

Size/Duplication

Total Lines 2276
Duplicated Lines 0 %

Importance

Changes 27
Bugs 5 Features 1
Metric Value
eloc 1076
c 27
b 5
f 1
dl 0
loc 2276
rs 1.296
wmc 228

29 Methods

Rating   Name   Duplication   Size   Complexity  
A manual_pull() 0 25 5
A deprecated_pull_option_names() 0 10 4
A salesforce_pull_webhook() 0 27 2
A add_actions() 0 8 1
A batch_soql_queries() 0 8 2
A deprecated_actions() 0 3 1
A __construct() 0 39 1
A check_offset_query() 0 22 3
A clear_current_type_query() 0 7 1
A create_object_map() 0 16 1
A increment_current_type_datetime() 0 8 2
A get_synced_object() 0 14 1
A check_for_upsert() 0 7 3
B update_called_from_salesforce() 0 109 5
F salesforce_pull_process_records() 0 319 46
C get_next_record_batch() 0 45 12
F get_updated_records() 0 275 32
A parse_error_message() 0 15 3
A is_pull_allowed() 0 30 3
B delete_called_from_salesforce() 0 130 8
A get_pull_date_value() 0 31 2
A get_pull_offset() 0 7 4
F get_pull_query() 0 125 13
D get_deleted_records() 0 130 21
C get_merged_records() 0 75 13
A respond_to_salesforce_merge() 0 35 5
A salesforce_pull() 0 16 3
A check_throttle() 0 8 2
F create_called_from_salesforce() 0 299 29

How to fix   Complexity   

Complex Class

Complex classes like Object_Sync_Sf_Salesforce_Pull 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_Salesforce_Pull, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Pull data from Salesforce into WordPress
4
 *
5
 * @class   Object_Sync_Sf_Salesforce_Pull
6
 * @package Object_Sync_Salesforce
7
 */
8
9
defined( 'ABSPATH' ) || exit;
10
11
/**
12
 * Object_Sync_Sf_Salesforce_Pull class.
13
 */
14
class Object_Sync_Sf_Salesforce_Pull {
15
16
	/**
17
	 * Current version of the plugin
18
	 *
19
	 * @var string
20
	 */
21
	public $version;
22
23
	/**
24
	 * The main plugin file
25
	 *
26
	 * @var string
27
	 */
28
	public $file;
29
30
	/**
31
	 * Global object of `$wpdb`, the WordPress database
32
	 *
33
	 * @var object
34
	 */
35
	public $wpdb;
36
37
	/**
38
	 * The plugin's slug so we can include it when necessary
39
	 *
40
	 * @var string
41
	 */
42
	public $slug;
43
44
	/**
45
	 * The plugin's prefix when saving options to the database
46
	 *
47
	 * @var string
48
	 */
49
	public $option_prefix;
50
51
	/**
52
	 * Login credentials for the Salesforce API; comes from wp-config or from the plugin settings
53
	 *
54
	 * @var array
55
	 */
56
	public $login_credentials;
57
58
	/**
59
	 * Array of what classes in the plugin can be scheduled to occur with `wp_cron` events
60
	 *
61
	 * @var array
62
	 */
63
	public $schedulable_classes;
64
65
	/**
66
	 * Object_Sync_Sf_Queue class
67
	 *
68
	 * @var object
69
	 */
70
	public $queue;
71
72
	/**
73
	 * Object_Sync_Sf_Logging class
74
	 *
75
	 * @var object
76
	 */
77
	public $logging;
78
79
	/**
80
	 * Object_Sync_Sf_Mapping class
81
	 *
82
	 * @var object
83
	 */
84
	public $mappings;
85
86
	/**
87
	 * Object_Sync_Sf_WordPress class
88
	 *
89
	 * @var object
90
	 */
91
	public $wordpress;
92
93
	/**
94
	 * Object_Sync_Sf_Salesforce class
95
	 * This contains Salesforce API methods
96
	 *
97
	 * @var array
98
	 */
99
	public $salesforce;
100
101
	/**
102
	 * Object_Sync_Sf_Pull_Options class
103
	 *
104
	 * @var object
105
	 */
106
	public $pull_options;
107
108
	/**
109
	 * Object_Sync_Sf_Sync_Transients class
110
	 *
111
	 * @var object
112
	 */
113
	public $sync_transients;
114
115
	/**
116
	 * The name of the ActionScheduler queue
117
	 *
118
	 * @var string
119
	 */
120
	public $schedule_name;
121
122
	/**
123
	 * Whether to batch SOQL queries
124
	 *
125
	 * @var bool
126
	 */
127
	private $batch_soql_queries;
128
129
	/**
130
	 * Minimum size of a SOQL batch
131
	 *
132
	 * @var int
133
	 */
134
	private $min_soql_batch_size;
135
136
	/**
137
	 * Maximum size of a SOQL batch
138
	 *
139
	 * @var int
140
	 */
141
	private $max_soql_size;
142
143
	/**
144
	 * Types of Salesforce records that can be merged
145
	 *
146
	 * @var array
147
	 */
148
	public $mergeable_record_types;
149
150
	/**
151
	 * Whether the plugin is in debug mode
152
	 *
153
	 * @var bool
154
	 */
155
	public $debug;
156
157
	/**
158
	 * Constructor for pull class
159
	 */
160
	public function __construct() {
161
		$this->version             = object_sync_for_salesforce()->version;
162
		$this->file                = object_sync_for_salesforce()->file;
163
		$this->wpdb                = object_sync_for_salesforce()->wpdb;
164
		$this->slug                = object_sync_for_salesforce()->slug;
165
		$this->option_prefix       = object_sync_for_salesforce()->option_prefix;
166
		$this->login_credentials   = object_sync_for_salesforce()->login_credentials;
167
		$this->schedulable_classes = object_sync_for_salesforce()->schedulable_classes;
168
169
		$this->queue      = object_sync_for_salesforce()->queue;
170
		$this->logging    = object_sync_for_salesforce()->logging;
171
		$this->mappings   = object_sync_for_salesforce()->mappings;
172
		$this->wordpress  = object_sync_for_salesforce()->wordpress;
173
		$this->salesforce = object_sync_for_salesforce()->salesforce;
174
175
		$this->pull_options    = new Object_Sync_Sf_Pull_Options();
176
		$this->sync_transients = new Object_Sync_Sf_Sync_Transients();
177
178
		$this->schedule_name = 'salesforce_pull';
179
180
		// To be clear: we should only ever set this to true if Salesforce actually starts to reliably support it instead of generally ignoring it.
181
		$this->batch_soql_queries = $this->batch_soql_queries( false );
182
183
		// Maximum offset size for a SOQL query to Salesforce
184
		// See: https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql_select_offset.htm
185
		// From Salesforce: "The maximum offset is 2,000 rows. Requesting an offset greater than 2,000 results in a NUMBER_OUTSIDE_VALID_RANGE error.".
186
		$this->min_soql_batch_size = 200; // batches cannot be smaller than 200 records.
187
		$this->max_soql_size       = 2000;
188
189
		$this->mergeable_record_types = array( 'Lead', 'Contact', 'Account' );
190
191
		// Create action hooks for WordPress objects. We run this after plugins are loaded in case something depends on another plugin.
192
		add_action( 'plugins_loaded', array( $this, 'add_actions' ) );
0 ignored issues
show
Bug introduced by
The function add_action was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

192
		/** @scrutinizer ignore-call */ 
193
  add_action( 'plugins_loaded', array( $this, 'add_actions' ) );
Loading history...
193
194
		// deprecated actions.
195
		$this->deprecated_actions();
196
197
		// use the option value for whether we're in debug mode.
198
		$this->debug = filter_var( get_option( $this->option_prefix . 'debug_mode', false ), FILTER_VALIDATE_BOOLEAN );
0 ignored issues
show
Bug introduced by
The function get_option was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

198
		$this->debug = filter_var( /** @scrutinizer ignore-call */ get_option( $this->option_prefix . 'debug_mode', false ), FILTER_VALIDATE_BOOLEAN );
Loading history...
199
	}
200
201
	/**
202
	 * Whether to use the batchSize parameter on SOQL queries
203
	 *
204
	 * @param bool $batch_soql_queries whether to batch the queries.
205
	 * @return bool $batch_soql_queries
206
	 */
207
	private function batch_soql_queries( $batch_soql_queries ) {
208
		// as of version 34.0, the Salesforce REST API accepts a batchSize option on the Sforce-Call-Options header.
209
		if ( version_compare( $this->login_credentials['rest_api_version'], '34.0', '<' ) ) {
210
			$batch_soql_queries = false;
211
		}
212
		// otherwise, return whatever the plugin's default value is.
213
		// this allows us to decide to support query batching if it is ever not absurdly bad.
214
		return $batch_soql_queries;
215
	}
216
217
	/**
218
	 * Create the action hooks based on what object maps exist from the admin settings
219
	 * route is http://example.com/wp-json/salesforce-rest-api/pull/ plus params we decide to accept.
220
	 */
221
	public function add_actions() {
222
223
		// ajax hook.
224
		add_action( 'wp_ajax_salesforce_pull_webhook', array( $this, 'salesforce_pull_webhook' ) );
0 ignored issues
show
Bug introduced by
The function add_action was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

224
		/** @scrutinizer ignore-call */ 
225
  add_action( 'wp_ajax_salesforce_pull_webhook', array( $this, 'salesforce_pull_webhook' ) );
Loading history...
225
226
		// action-scheduler needs two hooks: one to check for records, and one to process them.
227
		add_action( $this->option_prefix . 'pull_check_records', array( $this, 'salesforce_pull' ), 10 );
228
		add_action( $this->option_prefix . 'pull_process_records', array( $this, 'salesforce_pull_process_records' ), 10, 3 );
229
	}
230
231
	/**
232
	 * Deprecated actions
233
	 */
234
	public function deprecated_actions() {
235
		// previous option names.
236
		add_filter( 'object_sync_for_salesforce_pull_option_legacy_key', array( $this, 'deprecated_pull_option_names' ), 10, 2 );
0 ignored issues
show
Bug introduced by
The function add_filter was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

236
		/** @scrutinizer ignore-call */ 
237
  add_filter( 'object_sync_for_salesforce_pull_option_legacy_key', array( $this, 'deprecated_pull_option_names' ), 10, 2 );
Loading history...
237
	}
238
239
	/**
240
	 * Modify legacy named individual option records for sync operations
241
	 *
242
	 * @param string $key the plugin's suggested key name.
243
	 * @param array  $params the array of parameters to make the key.
244
	 * @return mixed $value the value of the item. False if it's empty.
245
	 * @deprecated   this was added in 2.1.0 to upgrade old option keys, but will be removed in a future version.
246
	 */
247
	public function deprecated_pull_option_names( $key, $params ) {
248
		if ( $this->option_prefix . 'pull_last_id' === $key ) {
249
			$key = $this->option_prefix . 'last_pull_id';
250
		}
251
		if ( isset( $params['object_type'] ) ) {
252
			if ( $this->option_prefix . 'pull_current_query_' . $params['object_type'] === $key ) {
253
				$key = $this->option_prefix . 'currently_pulling_query_' . $params['object_type'];
254
			}
255
		}
256
		return $key;
257
	}
258
259
	/**
260
	 * REST API callback for salesforce pull. Returns status of 200 for successful
261
	 * attempt or 403 for a failed pull attempt (SF not authorized, threshhold
262
	 * reached, etc.
263
	 *
264
	 * @param WP_REST_Request $request This is a merged object of all the arguments from the API request.
265
	 * @return array
266
	 * code: 201
267
	 * data:
268
	 *   success : true
269
	 */
270
	public function salesforce_pull_webhook( WP_REST_Request $request ) {
0 ignored issues
show
Bug introduced by
The type WP_REST_Request was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
271
272
		// run a pull request and then run the schedule if anything is in there.
273
		$data = $this->salesforce_pull();
274
275
		// salesforce_pull currently returns true if it runs successfully.
276
		if ( true === $data ) {
277
			$code = '201';
278
			// check to see if anything is in the queue and handle it if it is
279
			// single task for action-scheduler to check for data.
280
			$this->queue->add(
281
				$this->schedulable_classes[ $this->schedule_name ]['initializer'],
282
				array(),
283
				$this->schedule_name
284
			);
285
		} else {
286
			$code = '403';
287
		}
288
289
		$result = array(
290
			'code' => $code,
291
			'data' => array(
292
				'success' => $data,
293
			),
294
		);
295
296
		return $result;
297
	}
298
299
	/**
300
	 * Callback for the standard pull process used by webhooks and cron.
301
	 */
302
	public function salesforce_pull() {
303
		$sfapi = $this->salesforce['sfapi'];
304
305
		if ( true === $this->salesforce['is_authorized'] && true === $this->check_throttle() ) {
306
307
			$this->get_updated_records();
308
			$this->get_merged_records();
309
			$this->get_deleted_records();
310
311
			// Store this request time for the throttle check.
312
			$this->pull_options->set( 'last_sync', '', '', time() );
313
			return true;
314
315
		} else {
316
			// No pull happened.
317
			return false;
318
		}
319
	}
320
321
	/**
322
	 * Determines if the Salesforce pull should be allowed, or throttled.
323
	 * Prevents too many pull processes from running at once.
324
	 *
325
	 * @return bool Returns false if the time elapsed between recent pulls is too short.
326
	 */
327
	private function check_throttle() {
328
		$pull_throttle = $this->pull_options->get( 'throttle', '', '', 5 );
329
		$last_sync     = $this->pull_options->get( 'last_sync', '', '', 0 );
330
331
		if ( time() > ( $last_sync + $pull_throttle ) ) {
332
			return true;
333
		} else {
334
			return false;
335
		}
336
	}
337
338
	/**
339
	 * Pull updated records from Salesforce and place them in the queue.
340
	 * Executes a SOQL query based on defined mappings, loops through the results,
341
	 * and places each updated SF object into the queue for later processing.
342
	 * We copy the convention from the Drupal module here, and run a separate SOQL query for each type of object in SF
343
	 * If we return something here, it's because there is an error.
344
	 */
345
	private function get_updated_records() {
346
		$sfapi = $this->salesforce['sfapi'];
347
		foreach ( $this->mappings->get_fieldmaps( null, $this->mappings->active_fieldmap_conditions ) as $salesforce_mapping ) {
348
			$map_sync_triggers = (array) $salesforce_mapping['sync_triggers']; // this sets which Salesforce triggers are allowed for the mapping.
349
			$type              = $salesforce_mapping['salesforce_object']; // this sets the Salesforce object type for the SOQL query.
350
351
			// Setting true as third parameter so we do not change the offset.
352
			$soql = $this->get_pull_query( $type, $salesforce_mapping, true );
353
354
			// get_pull_query returns null if it has no matching fields.
355
			if ( null === $soql ) {
356
				continue;
357
			}
358
359
			// we don't want to cache this because timestamps.
360
			$query_options = array(
361
				'cache' => false,
362
			);
363
364
			// if we are batching soql queries, let's do it.
365
			if ( true === $this->batch_soql_queries ) {
366
				// pull query batch size option name.
367
				$pull_query_batch_size = $this->pull_options->get( 'query_batch_size', '', '', '' );
368
				if ( '' !== $pull_query_batch_size ) {
369
					$batch_size = filter_var( $this->pull_options->get( 'query_batch_size', '', '', $this->min_soql_batch_size ), FILTER_VALIDATE_INT );
370
				} else {
371
					// old limit value.
372
					// Deprecated in 2.0.3.
373
					$batch_size = filter_var( $this->pull_options->get( 'query_limit', '', '', $this->min_soql_batch_size ), FILTER_VALIDATE_INT );
374
				}
375
				$batch_size = filter_var(
376
					$batch_size,
377
					FILTER_VALIDATE_INT,
378
					array(
379
						'options' => array(
380
							'min_range' => $this->min_soql_batch_size,
381
							'max_range' => $this->max_soql_size,
382
						),
383
					)
384
				);
385
				if ( false !== $batch_size ) {
386
					// the Sforce-Query-Options header is a comma delimited string.
387
					$query_options['headers']['Sforce-Query-Options'] = 'batchSize=' . $batch_size;
388
				}
389
			}
390
391
			// run the __toString method to convert the SOQL query object to a string.
392
			$soql_string = (string) $soql;
393
394
			// Execute query.
395
			$results = $sfapi->query(
396
				$soql_string,
397
				$query_options
398
			);
399
400
			$response     = $results['data'];
401
			$version_path = wp_parse_url( $sfapi->get_api_endpoint(), PHP_URL_PATH );
0 ignored issues
show
Bug introduced by
The function wp_parse_url was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

401
			$version_path = /** @scrutinizer ignore-call */ wp_parse_url( $sfapi->get_api_endpoint(), PHP_URL_PATH );
Loading history...
402
403
			$sf_last_sync = $this->pull_options->get( 'last_sync', $type, $salesforce_mapping['id'], null );
404
			$last_sync    = gmdate( 'Y-m-d\TH:i:s\Z', $sf_last_sync );
405
406
			if ( ! isset( $response['errorCode'] ) && 0 < count( $response['records'] ) ) {
407
				// Write items to the queue.
408
				foreach ( $response['records'] as $key => $result ) {
409
					// if we've already pulled, or tried to pull, the current ID, don't do it again.
410
					$last_id = $this->pull_options->get( 'last_id', '', $salesforce_mapping['id'], '' );
411
					if ( $last_id === $result['Id'] ) {
412
						if ( true === $this->debug ) {
413
							// create log entry for failed pull.
414
							$status = 'debug';
415
							$title  = sprintf(
416
								// translators: placeholders are: 1) the log status, 2) the Salesforce ID.
417
								esc_html__( '%1$s: Salesforce ID %2$s has already been attempted.', 'object-sync-for-salesforce' ),
0 ignored issues
show
Bug introduced by
The function esc_html__ was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

417
								/** @scrutinizer ignore-call */ 
418
        esc_html__( '%1$s: Salesforce ID %2$s has already been attempted.', 'object-sync-for-salesforce' ),
Loading history...
418
								ucfirst( esc_attr( $status ) ),
0 ignored issues
show
Bug introduced by
The function esc_attr was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

418
								ucfirst( /** @scrutinizer ignore-call */ esc_attr( $status ) ),
Loading history...
419
								esc_attr( $result['Id'] )
420
							);
421
							$debug = array(
422
								'title'   => $title,
423
								'message' => esc_html__( 'This ID has already been attempted so it was not pulled again.', 'object-sync-for-salesforce' ),
424
								'trigger' => 0,
425
								'parent'  => '',
426
								'status'  => $status,
427
							);
428
							$this->logging->setup( $debug );
429
						}
430
431
						continue;
432
					}
433
434
					// if this record is new as of the last sync, use the create trigger.
435
					if ( isset( $result['CreatedDate'] ) && $result['CreatedDate'] > $last_sync ) {
436
						$sf_sync_trigger = $this->mappings->sync_sf_create;
437
					} else {
438
						$sf_sync_trigger = $this->mappings->sync_sf_update;
439
					}
440
441
					// Only queue when the record's trigger is configured for the mapping.
442
					if ( isset( $map_sync_triggers ) && isset( $sf_sync_trigger ) && in_array( $sf_sync_trigger, $map_sync_triggers, true ) ) { // wp or sf crud event.
443
						$data = array(
444
							'object_type'     => $type,
445
							'object'          => $result,
446
							'mapping'         => $salesforce_mapping,
447
							'sf_sync_trigger' => $sf_sync_trigger, // use the appropriate trigger based on when this was created.
448
						);
449
450
						$pull_allowed = $this->is_pull_allowed( $type, $result, $sf_sync_trigger, $salesforce_mapping, $map_sync_triggers );
451
452
						if ( false === $pull_allowed ) {
453
							// update the current state so we don't end up on the same record again if the loop fails.
454
							$this->pull_options->set( 'last_id', '', $salesforce_mapping['id'], $result['Id'] );
455
							if ( true === $this->debug ) {
456
								// create log entry for failed pull.
457
								$status = 'debug';
458
								$title  = sprintf(
459
									// translators: placeholders are: 1) the log status, 2) the Salesforce ID.
460
									esc_html__( '%1$s: Salesforce ID %2$s is not allowed.', 'object-sync-for-salesforce' ),
461
									ucfirst( esc_attr( $status ) ),
462
									esc_attr( $result['Id'] )
463
								);
464
								$debug = array(
465
									'title'   => $title,
466
									'message' => esc_html__( 'This ID is not pullable so it was skipped.', 'object-sync-for-salesforce' ),
467
									'trigger' => $sf_sync_trigger,
468
									'parent'  => '',
469
									'status'  => $status,
470
								);
471
								$this->logging->setup( $debug );
472
							}
473
							continue;
474
						}
475
476
						if ( true === $this->debug ) {
477
							// create log entry for queue addition.
478
							$status = 'debug';
479
							$title  = sprintf(
480
								// translators: placeholders are: 1) the log status, 2) the Salesforce ID.
481
								esc_html__( '%1$s: Start to add Salesforce ID %2$s to the queue', 'object-sync-for-salesforce' ),
482
								ucfirst( esc_attr( $status ) ),
483
								esc_attr( $result['Id'] )
484
							);
485
							$message = sprintf(
486
								// translators: 1) is the name of the hook that was called, 2) is the Salesforce object type, 3) is the ID for the object map, 4) is the event trigger that is running, and 5) is the name of the schedule that is running.
487
								esc_html__( 'This record is being sent to the queue. The hook name is %1$s. The arguments for the hook are: object type %2$s, object map ID %3$s, sync trigger %4$s. The schedule name is %5$s.', 'object-sync-for-salesforce' ),
488
								esc_attr( $this->schedulable_classes[ $this->schedule_name ]['callback'] ),
489
								esc_attr( $type ),
490
								absint( $salesforce_mapping['id'] ),
0 ignored issues
show
Bug introduced by
The function absint was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

490
								/** @scrutinizer ignore-call */ 
491
        absint( $salesforce_mapping['id'] ),
Loading history...
491
								$sf_sync_trigger,
492
								$this->schedule_name
493
							);
494
							$debug = array(
495
								'title'   => $title,
496
								'message' => $message,
497
								'trigger' => $sf_sync_trigger,
498
								'parent'  => '',
499
								'status'  => $status,
500
							);
501
							$this->logging->setup( $debug );
502
						}
503
504
						// add a queue action to save data from salesforce.
505
						$this->queue->add(
506
							$this->schedulable_classes[ $this->schedule_name ]['callback'],
507
							array(
508
								'object_type'     => $type,
509
								'object'          => $result['Id'],
510
								'sf_sync_trigger' => $sf_sync_trigger,
511
							),
512
							$this->schedule_name
513
						);
514
						// update the current state so we don't end up on the same record again if the loop fails.
515
						$this->pull_options->set( 'last_id', '', $salesforce_mapping['id'], $result['Id'] );
516
						if ( true === $this->debug ) {
517
							// create log entry for successful pull.
518
							$status = 'debug';
519
							$title  = sprintf(
520
								// translators: placeholders are: 1) the log status, 2) the Salesforce ID.
521
								esc_html__( '%1$s: Salesforce ID %2$s has been successfully pulled into the queue.', 'object-sync-for-salesforce' ),
522
								ucfirst( esc_attr( $status ) ),
523
								esc_attr( $result['Id'] )
524
							);
525
							$debug = array(
526
								'title'   => $title,
527
								'message' => esc_html__( 'This ID has been successfully pulled and added to the queue for processing. It cannot be pulled again without being modified again.', 'object-sync-for-salesforce' ),
528
								'trigger' => $sf_sync_trigger,
529
								'parent'  => '',
530
								'status'  => $status,
531
							);
532
							$this->logging->setup( $debug );
533
						} // end of debug
534
					} // end if
535
				} // end foreach
536
537
				// we're done with the foreach. save  the LastModifiedDate of the last item processed, we will store it if the query has results with an additional offset.
538
				$last_date_for_query = isset( $result['LastModifiedDate'] ) ? $result['LastModifiedDate'] : '';
539
540
				if ( true === $this->batch_soql_queries ) {
541
					// if applicable, process the next batch of records.
542
					$this->get_next_record_batch( $last_sync, $salesforce_mapping, $map_sync_triggers, $type, $version_path, $query_options, $response );
0 ignored issues
show
Bug introduced by
$last_sync of type string is incompatible with the type datetime expected by parameter $last_sync of Object_Sync_Sf_Salesforc...get_next_record_batch(). ( Ignorable by Annotation )

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

542
					$this->get_next_record_batch( /** @scrutinizer ignore-type */ $last_sync, $salesforce_mapping, $map_sync_triggers, $type, $version_path, $query_options, $response );
Loading history...
543
				} else {
544
					// Here, we check and see if the query has results with an additional offset.
545
					// If it does, we regenerate the query so it will have an offset next time it runs.
546
					// If it does not, we clear the query if we've just processed the last row.
547
					// this allows us to run an offset on the stored query instead of clearing it.
548
					$does_next_offset_have_results = $this->check_offset_query( $type, $salesforce_mapping, $query_options );
549
					end( $response['records'] );
550
					$last_record_key = key( $response['records'] );
551
					if ( true === $does_next_offset_have_results ) {
552
						// store the LastModifiedDate of the last item processed, or the current time if it isn't there.
553
						$this->increment_current_type_datetime( $type, $last_date_for_query, $salesforce_mapping['id'] );
0 ignored issues
show
Bug introduced by
It seems like $last_date_for_query can also be of type string; however, parameter $next_query_modified_date of Object_Sync_Sf_Salesforc...current_type_datetime() does only seem to accept timestamp, 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

553
						$this->increment_current_type_datetime( $type, /** @scrutinizer ignore-type */ $last_date_for_query, $salesforce_mapping['id'] );
Loading history...
554
						// increment SOQL query to run.
555
						$soql = $this->get_pull_query( $type, $salesforce_mapping );
556
					} elseif ( $last_record_key === $key ) {
557
						// clear the stored query. we don't need to offset and we've finished the loop.
558
						$this->clear_current_type_query( $type, $salesforce_mapping['id'] );
559
					}
560
				} // end if
561
			} elseif ( ! isset( $response['errorCode'] ) && 0 === count( $response['records'] ) && false === $this->batch_soql_queries ) {
562
				// only update/clear these option values if we are currently still processing a query.
563
				$current_query = $this->pull_options->get( 'current_query', $type, $salesforce_mapping['id'], '' );
564
				if ( '' !== $current_query ) {
565
					$this->clear_current_type_query( $type, $salesforce_mapping['id'] );
566
				}
567
				// try to check for specific error codes from Salesforce.
568
			} elseif ( isset( $response['errorCode'] ) && 'INVALID_FIELD' === $response['errorCode'] ) {
569
				// set up log entry.
570
				$status    = 'error';
571
				$log_title = sprintf(
572
					// translators: placeholders are: 1) the log status, 2) the server error code, and 3) the name of the Salesforce object.
573
					esc_html__( '%1$s: %2$s when pulling %3$s data from Salesforce. Check and resave the fieldmap.', 'object-sync-for-salesforce' ),
574
					ucfirst( esc_attr( $status ) ),
575
					esc_attr( $response['errorCode'] ),
576
					esc_attr( $salesforce_mapping['salesforce_object'] )
577
				);
578
				$log_body = '<p>' . esc_html__( 'A field may have been deleted from Salesforce, or it has otherwise become invalid. You may need to check and resave your fieldmap.', 'object-sync-for-salesforce' ) . '</p>';
579
580
				// if it's an invalid field, try to clear the cached query so it can try again next time.
581
				$current_query = $this->pull_options->get( 'current_query', $type, $salesforce_mapping['id'], '' );
582
				if ( '' !== $current_query ) {
583
					$this->clear_current_type_query( $type, $salesforce_mapping['id'] );
584
					$log_title .= esc_html__( ' The stored query has been cleared.', 'object-sync-for-salesforce' );
585
					$log_body  .= '<p>' . esc_html__( 'The currently stored query for this object type has been deleted.', 'object-sync-for-salesforce' ) . '</p>';
586
				}
587
588
				$log_body .= sprintf(
589
					// translators: placeholders are: 1) the Salesforce API response message.
590
					'<p>' . esc_html__( 'Salesforce API Response: %1$s', 'object-sync-for-salesforce' ) . '</p>',
591
					$response['message']
592
				);
593
				$result = array(
594
					'title'   => $log_title,
595
					'message' => $log_body,
596
					'trigger' => 0,
597
					'parent'  => '',
598
					'status'  => $status,
599
				);
600
				$this->logging->setup( $result );
601
			} elseif ( isset( $response['errorCode'] ) ) {
602
				// create log entry for failed pull.
603
				$status = 'error';
604
				$title  = sprintf(
605
					// translators: placeholders are: 1) the log status, 2) the server error code, and 3) the name of the Salesforce object.
606
					esc_html__( '%1$s: %2$s when pulling %3$s data from Salesforce', 'object-sync-for-salesforce' ),
607
					ucfirst( esc_attr( $status ) ),
608
					esc_attr( $response['errorCode'] ),
609
					esc_attr( $salesforce_mapping['salesforce_object'] )
610
				);
611
				$result = array(
612
					'title'   => $title,
613
					'message' => $response['message'],
614
					'trigger' => 0,
615
					'parent'  => '',
616
					'status'  => $status,
617
				);
618
				$this->logging->setup( $result );
619
				return $result;
620
621
			} // End if() statement.
622
		} // End foreach() loop.
623
	}
624
625
	/**
626
	 * Pull the next batch of records from the Salesforce API, if applicable
627
	 * Executes a nextRecordsUrl SOQL query based on the previous result,
628
	 * and places each updated SF object into the queue for later processing.
629
	 *
630
	 * @param datetime $last_sync when it was last synced.
631
	 * @param array    $salesforce_mapping the fieldmap.
632
	 * @param array    $map_sync_triggers the sync trigger bit values.
633
	 * @param string   $type the Salesforce object type.
634
	 * @param string   $version_path the API path for the Salesforce version.
635
	 * @param array    $query_options the SOQL query options.
636
	 * @param array    $response the previous response.
637
	 */
638
	private function get_next_record_batch( $last_sync, $salesforce_mapping, $map_sync_triggers, $type, $version_path, $query_options, $response ) {
639
		// Handle next batch of records if it exists.
640
		$next_records_url = isset( $response['nextRecordsUrl'] ) ? str_replace( $version_path, '', $response['nextRecordsUrl'] ) : false;
641
		while ( $next_records_url ) {
642
			// shouldn't cache this either. it's going into the queue if it exists anyway.
643
			$new_results  = $sfapi->api_call(
644
				$next_records_url,
645
				array(),
646
				'GET',
647
				$query_options
648
			);
649
			$new_response = $new_results['data'];
650
			if ( ! isset( $new_response['errorCode'] ) ) {
651
				// Write items to the queue.
652
				foreach ( $new_response['records'] as $result ) {
653
					// if this record is new as of the last sync, use the create trigger.
654
					if ( isset( $result['CreatedDate'] ) && $result['CreatedDate'] > $last_sync ) {
655
						$sf_sync_trigger = $this->mappings->sync_sf_create;
656
					} else {
657
						$sf_sync_trigger = $this->mappings->sync_sf_update;
658
					}
659
					// Only queue when the record's trigger is configured for the mapping.
660
					if ( isset( $map_sync_triggers ) && is_array( $map_sync_triggers ) && isset( $sf_sync_trigger ) && in_array( $sf_sync_trigger, $map_sync_triggers, true ) ) { // wp or sf crud event.
661
						$data = array(
662
							'object_type'     => $type,
663
							'object'          => $result,
664
							'mapping'         => $salesforce_mapping,
665
							'sf_sync_trigger' => $sf_sync_trigger, // use the appropriate trigger based on when this was created.
666
						);
667
						// add a queue action to save data from salesforce.
668
						$this->queue->add(
669
							$this->schedulable_classes[ $this->schedule_name ]['callback'],
670
							array(
671
								'object_type'     => $type,
672
								'object'          => $result['Id'],
673
								'sf_sync_trigger' => $sf_sync_trigger,
674
							),
675
							$this->schedule_name
676
						);
677
						// Update the last pull sync timestamp for this record type to avoid re-processing in case of error.
678
						$last_sync_pull_trigger = DateTime::createFromFormat( 'Y-m-d\TH:i:s+', $result[ $salesforce_mapping['pull_trigger_field'] ], new DateTimeZone( 'UTC' ) );
679
					}
680
				}
681
			}
682
			$next_records_url = isset( $new_response['nextRecordsUrl'] ) ? str_replace( $version_path, '', $new_response['nextRecordsUrl'] ) : false;
683
		} // end while loop
684
	}
685
686
	/**
687
	 * Get the next offset query. If check is true, only see if that query would have results. Otherwise, return the SOQL object.
688
	 * When batchSize is not in use, run a check with an offset.
689
	 *
690
	 * @param string $type the Salesforce object type.
691
	 * @param array  $salesforce_mapping the map between object types.
692
	 * @param array  $query_options the options for the SOQL query.
693
	 * @return object|bool $soql|$does_next_offset_have_results
694
	 */
695
	private function check_offset_query( $type, $salesforce_mapping, $query_options ) {
696
697
		$soql                          = $this->get_pull_query( $type, $salesforce_mapping );
698
		$does_next_offset_have_results = false;
699
700
		// run the __toString method to convert the SOQL query object to a string.
701
		$soql_string = (string) $soql;
702
703
		$sfapi = $this->salesforce['sfapi'];
704
		// Execute query
705
		// have to cast it to string to make sure it uses the magic method
706
		// we don't want to cache this because timestamps.
707
		$results = $sfapi->query(
708
			$soql_string,
709
			$query_options
710
		);
711
712
		$response = $results['data'];
713
		if ( ! isset( $response['errorCode'] ) && 0 < count( $response['records'] ) ) {
714
			$does_next_offset_have_results = true;
715
		}
716
		return $does_next_offset_have_results;
717
	}
718
719
720
	/**
721
	 * Given a SObject type name, build an SOQL query to include all fields for all
722
	 * fieldmaps mapped to that SObject.
723
	 *
724
	 * @param string $type e.g. "Contact", "Account", etc.
725
	 * @param array  $salesforce_mapping the fieldmap that maps the two object types.
726
	 * @param bool   $force_current_offset if true, the will offset not be recalculated and will use the offset of the current query saved on the db, if present.
727
	 * @return Object_Sync_Sf_Salesforce_Select_Query or null if no mappings or no mapped fields were found.
728
	 * @see Object_Sync_Sf_Mapping::get_mapped_fields
729
	 * @see Object_Sync_Sf_Mapping::get_mapped_record_types
730
	 */
731
	private function get_pull_query( $type, $salesforce_mapping = array(), $force_current_offset = false ) {
732
		// we need to determine what to do with saved queries. this is what we currently do but it doesn't work.
733
		// check if we have a stored next query to run for this type. if so, unserialize it so we have an object.
734
		$pull_query_running = $this->pull_options->get( 'current_query', $type, $salesforce_mapping['id'], '' );
735
		if ( '' !== $pull_query_running ) {
736
			$saved_query = maybe_unserialize( $pull_query_running );
0 ignored issues
show
Bug introduced by
The function maybe_unserialize was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

736
			$saved_query = /** @scrutinizer ignore-call */ maybe_unserialize( $pull_query_running );
Loading history...
737
		}
738
739
		$mapped_fields       = array();
740
		$mapped_record_types = array();
741
742
		// only use fields that come from Salesforce to WordPress, or that sync.
743
		$mapped_fields = array_merge(
744
			$mapped_fields,
745
			$this->mappings->get_mapped_fields(
746
				$salesforce_mapping,
747
				array( $this->mappings->direction_sync, $this->mappings->direction_sf_wordpress )
748
			)
749
		);
750
751
		// If Record Type is specified, restrict query.
752
		$mapping_record_types = $this->mappings->get_mapped_record_types( $salesforce_mapping );
753
754
		// If Record Type is not specified for a given mapping, ensure query is unrestricted.
755
		if ( empty( $mapping_record_types ) ) {
756
			$mapped_record_types = false;
757
		} elseif ( is_array( $mapped_record_types ) ) {
0 ignored issues
show
introduced by
The condition is_array($mapped_record_types) is always true.
Loading history...
758
			$mapped_record_types = array_merge( $mapped_record_types, $mapping_record_types );
759
		}
760
761
		// There are no field mappings configured to pull data from Salesforce so
762
		// move on to the next mapped object. Prevents querying unmapped data.
763
		if ( empty( $mapped_fields ) ) {
764
			return null;
765
		}
766
767
		if ( ! isset( $saved_query ) ) {
768
			$soql = new Object_Sync_Sf_Salesforce_Select_Query( $type );
769
770
			// Convert field mappings to SOQL.
771
			$soql->fields = array_merge(
772
				$mapped_fields,
773
				array(
774
					'Id' => 'Id',
775
					$salesforce_mapping['pull_trigger_field'] => $salesforce_mapping['pull_trigger_field'],
776
				)
777
			);
778
779
			// check if this is a Salesforce create event.
780
			if ( in_array( $this->mappings->sync_sf_create, (array) $salesforce_mapping['sync_triggers'], true ) ) {
781
				$soql->fields['CreatedDate'] = 'CreatedDate';
782
			}
783
784
			// Order by the trigger field, requesting the oldest records first.
785
			$soql->order = array(
786
				$salesforce_mapping['pull_trigger_field'] => 'ASC',
787
			);
788
789
			// Set a limit on the number of records that can be retrieved from the API at one time.
790
			$soql->limit = filter_var( $this->pull_options->get( 'query_limit', '', '', 25 ), FILTER_VALIDATE_INT );
791
792
			// if there are record types on this fieldmap, use them as a conditional.
793
			if ( ! empty( $mapped_record_types ) ) {
794
				$soql->add_condition( 'RecordTypeId', array_values( $mapped_record_types ), 'IN' );
795
			}
796
		} else {
797
			$soql = $saved_query;
798
		}
799
800
		// Get the value for the pull trigger field. Often this will LastModifiedDate. It needs to change when the query gets regenerated after the max offset has been reached.
801
		$pull_trigger_field_value = $this->get_pull_date_value( $type, $soql, $salesforce_mapping['id'] );
802
803
		// we check to see if the stored date is the same as the new one. if it is not, we will want to reset the offset.
804
		$reset_offset = false;
805
		$has_date     = false;
806
		$key          = array_search( $salesforce_mapping['pull_trigger_field'], array_column( $soql->conditions, 'field' ), true );
807
		if ( false !== $key ) {
808
			$has_date = true;
809
			if ( $soql->conditions[ $key ]['value'] !== $pull_trigger_field_value ) {
810
				$reset_offset = true;
811
			}
812
		}
813
814
		if ( false === $has_date ) {
815
			$reset_offset = true;
816
			$soql->add_condition( $salesforce_mapping['pull_trigger_field'], $pull_trigger_field_value, '>' );
817
		} else {
818
			$soql->conditions[ $key ]['value'] = $pull_trigger_field_value;
819
		}
820
821
		// We don't want to change the offset on the first call of get_updated_records function. Otherwise the offset may be added two times and we would lose records...
822
		if ( false === $force_current_offset ) {
823
			// Get the value for the SOQL offset. If max has already been reached, it is zero.
824
			$soql->offset = $this->get_pull_offset( $type, $soql, $reset_offset );
825
		}
826
827
		// add a filter here to modify the query
828
		// Hook to allow other plugins to modify the SOQL query before it is sent to Salesforce.
829
		$soql = apply_filters( $this->option_prefix . 'pull_query_modify', $soql, $type, $salesforce_mapping, $mapped_fields );
0 ignored issues
show
Bug introduced by
The function apply_filters was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

829
		$soql = /** @scrutinizer ignore-call */ apply_filters( $this->option_prefix . 'pull_query_modify', $soql, $type, $salesforce_mapping, $mapped_fields );
Loading history...
830
831
		// quick example to change the order to descending.
832
		/* // phpcs:ignore Squiz.PHP.CommentedOutCode.Found
833
		add_filter( 'object_sync_for_salesforce_pull_query_modify', 'change_pull_query', 10, 4 );
834
		// can always reduce this number if all the arguments are not necessary
835
		function change_pull_query( $soql, $type, $salesforce_mapping, $mapped_fields ) {
836
			$soql->order = 'DESC';
837
			return $soql;
838
		}
839
		*/
840
841
		// Make sure our SOQL object properties that are arrays are unique. This prevents values added via developer hook from being added repeatedly when a query is cached.
842
		if ( version_compare( PHP_VERSION, '7.0.8', '>=' ) ) {
843
			$soql->fields     = array_unique( $soql->fields, SORT_REGULAR );
844
			$soql->order      = array_unique( $soql->order, SORT_REGULAR );
845
			$soql->conditions = array_unique( $soql->conditions, SORT_REGULAR );
846
		} else {
847
			$soql->fields     = array_map( 'unserialize', array_unique( array_map( 'serialize', $soql->fields ) ) );
848
			$soql->order      = array_map( 'unserialize', array_unique( array_map( 'serialize', $soql->order ) ) );
849
			$soql->conditions = array_map( 'unserialize', array_unique( array_map( 'serialize', $soql->conditions ) ) );
850
		}
851
852
		// serialize the currently running SOQL query and store it for this type.
853
		$serialized_current_query = maybe_serialize( $soql );
0 ignored issues
show
Bug introduced by
The function maybe_serialize was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

853
		$serialized_current_query = /** @scrutinizer ignore-call */ maybe_serialize( $soql );
Loading history...
854
		$this->pull_options->set( 'current_query', $type, $salesforce_mapping['id'], $serialized_current_query, false );
855
		return $soql;
856
	}
857
858
859
	/**
860
	 * Determine the offset for the SOQL query to run
861
	 *
862
	 * @param string $type e.g. "Contact", "Account", etc.
863
	 * @param object $soql the SOQL object.
864
	 * @param bool   $reset whether to reset the offset.
865
	 */
866
	private function get_pull_offset( $type, $soql, $reset = false ) {
867
		// set an offset. if there is a saved offset, add the limit to it and move on. otherwise, use the limit.
868
		$offset = isset( $soql->offset ) ? $soql->offset + $soql->limit : $soql->limit;
869
		if ( true === $reset || $offset > $this->max_soql_size ) {
870
			$offset = 0;
871
		}
872
		return $offset;
873
	}
874
875
	/**
876
	 * Given a SObject type name, determine the datetime value the SOQL object should use to filter results. Often this will be LastModifiedDate.
877
	 *
878
	 * @param string $type e.g. "Contact", "Account", etc.
879
	 * @param object $soql the SOQL object.
880
	 * @param int    $fieldmap_id is the fieldmap ID that goes with this query.
881
	 * @return timestamp $pull_trigger_field_value
0 ignored issues
show
Bug introduced by
The type timestamp was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
882
	 */
883
	private function get_pull_date_value( $type, $soql, $fieldmap_id = '' ) {
884
		// If no lastupdate, get all records, else get records since last pull.
885
		// this should be what keeps it from getting all the records, whether or not they've ever been updated
886
		// we also use the option for when the plugin was installed, and don't go back further than that by default.
887
888
		$sf_activate_time = get_option( $this->option_prefix . 'activate_time', '' );
0 ignored issues
show
Bug introduced by
The function get_option was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

888
		$sf_activate_time = /** @scrutinizer ignore-call */ get_option( $this->option_prefix . 'activate_time', '' );
Loading history...
889
		$sf_last_sync     = $this->pull_options->get( 'last_sync', $type, $fieldmap_id, null );
890
		if ( $sf_last_sync ) {
891
			$pull_trigger_field_value = gmdate( 'Y-m-d\TH:i:s\Z', $sf_last_sync );
892
		} else {
893
			$pull_trigger_field_value = gmdate( 'Y-m-d\TH:i:s\Z', $sf_activate_time );
894
		}
895
896
		// Hook to allow other plugins to set the "last pull date" to whenever they want, including a datetime from before the plugin was activated.
897
		$pull_trigger_field_value = apply_filters( $this->option_prefix . 'change_pull_date_value', $pull_trigger_field_value, $type, $soql, $fieldmap_id );
0 ignored issues
show
Bug introduced by
The function apply_filters was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

897
		$pull_trigger_field_value = /** @scrutinizer ignore-call */ apply_filters( $this->option_prefix . 'change_pull_date_value', $pull_trigger_field_value, $type, $soql, $fieldmap_id );
Loading history...
898
899
		// example to use another datetime value.
900
		// the value needs to be a gmdate, formatted for Salesforce: 'Y-m-d\TH:i:s\Z'.
901
		/* // phpcs:ignore Squiz.PHP.CommentedOutCode.Found
902
		add_filter( 'object_sync_for_salesforce_change_pull_date_value', 'change_pull_date_value', 10, 5 );
903
		// can always reduce this number if all the arguments are not necessary
904
		function change_pull_date_value( $pull_trigger_field_value, $object_type, $soql, $fieldmap_id ) {
905
			if ( 'Contact' === $object_type  ) {
906
				// example: go back to 2006-01-01T23:01:01+01:00, which is 1136152861.
907
				$pull_trigger_field_value = gmdate( 'Y-m-d\TH:i:s\Z', 1136152861 );
908
			}
909
			return $pull_trigger_field_value;
910
		}
911
		*/
912
913
		return $pull_trigger_field_value;
914
	}
915
916
	/**
917
	 * Get merged records from Salesforce.
918
	 * Note that merges can currently only work if the Soap API is enabled.
919
	 */
920
	private function get_merged_records() {
921
922
		$use_soap = $this->salesforce['soap_loaded'];
923
		if ( true === $use_soap ) {
924
			$soap = new Object_Sync_Sf_Salesforce_Soap_Partner();
925
		} else {
926
			return; // if the REST API ever supports merging, we can do something else in this case.
927
		}
928
		$seconds = 60;
929
930
		$merged_records = array();
931
932
		// Load fieldmaps for mergeable types.
933
		foreach ( $this->mergeable_record_types as $type ) {
934
			$mappings = $this->mappings->get_fieldmaps(
935
				null,
936
				array_merge(
937
					$this->mappings->active_fieldmap_conditions,
938
					array(
939
						'salesforce_object' => $type,
940
					)
941
				),
942
			);
943
944
			// Iterate over each field mapping to determine our query parameters.
945
			foreach ( $mappings as $salesforce_mapping ) {
946
				$last_merge_sync = $this->pull_options->get( 'merge_last', $salesforce_mapping['salesforce_object'], $salesforce_mapping['id'], time() );
947
				$now             = time();
948
				$this->pull_options->set( 'merge_last', $salesforce_mapping['salesforce_object'], $salesforce_mapping['id'], $now );
949
950
				// get_deleted() constraint: startDate cannot be more than 30 days ago
951
				// (using an incompatible date may lead to exceptions).
952
				$last_merge_sync = $last_merge_sync > ( time() - 2505600 ) ? $last_merge_sync : ( time() - 2505600 );
953
954
				// get_deleted() constraint: startDate must be at least one minute greater
955
				// than endDate.
956
				$now = $now > ( $last_merge_sync + 60 ) ? $now : $now + 60;
957
958
				// need to be using gmdate for Salesforce call.
959
				$last_merge_sync_sf = gmdate( 'Y-m-d\TH:i:s\Z', $last_merge_sync );
960
				$merged             = array();
961
				// there doesn't appear to be a way to do this in the rest api; for now we'll do soap.
962
				// the soap query only works immediately following a merge and shortly after.
963
				if ( true === $use_soap ) {
964
					$type   = $salesforce_mapping['salesforce_object'];
965
					$query  = "SELECT Id, isDeleted, masterRecordId FROM $type WHERE masterRecordId != '' AND SystemModStamp > $last_merge_sync_sf";
966
					$merged = $soap->try_soap( 'queryAll', $query );
967
					if ( ! empty( $merged->records ) ) {
968
						$merged = json_decode( wp_json_encode( $merged->records ), true );
0 ignored issues
show
Bug introduced by
The function wp_json_encode was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

968
						$merged = json_decode( /** @scrutinizer ignore-call */ wp_json_encode( $merged->records ), true );
Loading history...
969
					} else {
970
						continue;
971
					}
972
973
					foreach ( $merged as $result ) {
974
						$record = array();
975
						if ( is_array( array_unique( $result['Id'] ) ) ) {
976
							$record['Id'] = array_unique( $result['Id'] )[0];
977
						} else {
978
							$record['Id'] = $result['Id'];
979
						}
980
						if ( isset( $result['any'] ) ) {
981
							libxml_use_internal_errors( true );
982
							$any = simplexml_load_string( '<?xml version="1.0" standalone="yes"?><root>' . $result['any'] . '</root>' );
983
							if ( $any ) {
984
								$json   = wp_json_encode( $any );
985
								$array  = json_decode( $json, true );
986
								$record = array_merge( $record, $array );
987
							}
988
						}
989
						$merged_records[] = $record;
990
						$this->respond_to_salesforce_merge( $type, $record );
991
					} // End foreach on merged
992
				} // End if on soap
993
				if ( ! empty( $merged_records ) ) {
994
					$this->sync_transients->set( 'salesforce_merged', $type, $salesforce_mapping['id'], $merged_records, $seconds );
995
				}
996
			} // End foreach on mappings
997
		} // end foreach on types
998
	}
999
1000
	/**
1001
	 * Respond to Salesforce merge events
1002
	 * This means we update the mapping object to contain the new Salesforce Id, and pull its data
1003
	 *
1004
	 * @param string $object_type what type of Salesforce object it is.
1005
	 * @param array  $merged_record the record that was merged.
1006
	 */
1007
	private function respond_to_salesforce_merge( $object_type, $merged_record ) {
1008
		$op = 'Merge';
1009
		if ( isset( $merged_record['Id'] ) && true === filter_var( $merged_record['sf:IsDeleted'], FILTER_VALIDATE_BOOLEAN ) && '' !== $merged_record['sf:MasterRecordId'] ) {
1010
			$previous_sf_id  = $merged_record['Id'];
1011
			$new_sf_id       = $merged_record['sf:MasterRecordId'];
1012
			$mapping_objects = $this->mappings->load_object_maps_by_salesforce_id( $previous_sf_id );
1013
			foreach ( $mapping_objects as $mapping_object ) {
1014
				$wordpress_type                  = $mapping_object['wordpress_object'];
1015
				$wordpress_id                    = $mapping_object['wordpress_id'];
1016
				$mapping_object['salesforce_id'] = $new_sf_id;
1017
				$mapping_object                  = $this->mappings->update_object_map( $mapping_object, $mapping_object['id'] );
1018
1019
				$status = 'success';
1020
				$title  = sprintf(
1021
					// translators: placeholders are: 1) the log status, 2) what operation is happening, 3) the name of the Salesforce object type, 4) the previous Salesforce Id value, 5) the new Salesforce Id value, 6) the name of the WordPress object, 7) the WordPress id value.
1022
					esc_html__( '%1$s: %2$s Salesforce %3$s objects with Ids %4$s and %5$s were merged (%5$s is the remaining ID. It is mapped to WordPress %6$s with %7$s.)', 'object-sync-for-salesforce' ),
0 ignored issues
show
Bug introduced by
The function esc_html__ was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1022
					/** @scrutinizer ignore-call */ 
1023
     esc_html__( '%1$s: %2$s Salesforce %3$s objects with Ids %4$s and %5$s were merged (%5$s is the remaining ID. It is mapped to WordPress %6$s with %7$s.)', 'object-sync-for-salesforce' ),
Loading history...
1023
					ucfirst( esc_attr( $status ) ),
0 ignored issues
show
Bug introduced by
The function esc_attr was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1023
					ucfirst( /** @scrutinizer ignore-call */ esc_attr( $status ) ),
Loading history...
1024
					esc_attr( $op ),
1025
					esc_attr( $object_type ),
1026
					esc_attr( $previous_sf_id ),
1027
					esc_attr( $new_sf_id ),
1028
					esc_attr( $wordpress_type ),
1029
					esc_attr( $wordpress_id )
1030
				);
1031
				$result = array(
1032
					'title'   => $title,
1033
					'message' => '',
1034
					'trigger' => 0,
1035
					'parent'  => $wordpress_id,
1036
					'status'  => $status,
1037
				);
1038
				$this->logging->setup( $result );
1039
1040
				// after it has been merged, pull it.
1041
				$result = $this->manual_pull( $object_type, $new_sf_id );
1042
1043
			}
1044
		}
1045
	}
1046
1047
	/**
1048
	 * Get deleted records from salesforce.
1049
	 * Note that deletions can only be queried via REST with an API version >= 29.0.
1050
	 */
1051
	private function get_deleted_records() {
1052
1053
		$sfapi = $this->salesforce['sfapi'];
1054
1055
		// Load all unique SF record types that we have mappings for. This results in a double loop.
1056
		foreach ( $this->mappings->get_fieldmaps( null, $this->mappings->active_fieldmap_conditions ) as $salesforce_mapping ) {
1057
1058
			$map_sync_triggers = (array) $salesforce_mapping['sync_triggers']; // this sets which Salesforce triggers are allowed for the mapping.
1059
			$type              = $salesforce_mapping['salesforce_object']; // this sets the Salesforce object type for the SOQL query.
1060
1061
			$mappings = $this->mappings->get_fieldmaps(
1062
				null,
1063
				array_merge(
1064
					$this->mappings->active_fieldmap_conditions,
1065
					array(
1066
						'salesforce_object' => $type,
1067
					)
1068
				),
1069
			);
1070
1071
			// Iterate over each field mapping to determine our query parameters.
1072
			foreach ( $mappings as $salesforce_mapping ) {
1073
1074
				$last_delete_sync = $this->pull_options->get( 'delete_last', $type, $salesforce_mapping['id'], time() );
1075
				$now              = time();
1076
				$this->pull_options->set( 'delete_last', $type, $salesforce_mapping['id'], $now );
1077
1078
				// get_deleted() constraint: startDate cannot be more than 30 days ago
1079
				// (using an incompatible date may lead to exceptions).
1080
				$last_delete_sync = $last_delete_sync > ( time() - 2505600 ) ? $last_delete_sync : ( time() - 2505600 );
1081
1082
				// get_deleted() constraint: startDate must be at least one minute greater
1083
				// than endDate.
1084
				$now = $now > ( $last_delete_sync + 60 ) ? $now : $now + 60;
1085
1086
				// need to be using gmdate for Salesforce call.
1087
				$last_delete_sync_sf = gmdate( 'Y-m-d\TH:i:s\Z', $last_delete_sync );
1088
				$now_sf              = gmdate( 'Y-m-d\TH:i:s\Z', $now );
1089
1090
				// Salesforce call.
1091
				$deleted = $sfapi->get_deleted( $type, $last_delete_sync_sf, $now_sf );
1092
				$merged  = $this->sync_transients->get( 'salesforce_merged', $type, $salesforce_mapping['id'] );
1093
				if ( false !== $merged && isset( $deleted['data']['deletedRecords'] ) ) {
1094
					foreach ( $merged as $key ) {
1095
						$deleted['data']['deletedRecords'] = array_filter(
1096
							$deleted['data']['deletedRecords'],
1097
							function ( $x ) use ( $key ) {
1098
								if ( ! isset( $x['Id'] ) && isset( $x['id'] ) ) {
1099
									$x['Id'] = $x['id'];
1100
								}
1101
								return $x['Id'] !== $key;
1102
							}
1103
						);
1104
					}
1105
				}
1106
1107
				if ( empty( $deleted['data']['deletedRecords'] ) ) {
1108
					continue;
1109
				}
1110
1111
				foreach ( $deleted['data']['deletedRecords'] as $result ) {
1112
1113
					$sf_sync_trigger = $this->mappings->sync_sf_delete;
1114
1115
					// Salesforce seriously returns Id for update requests and id for delete requests and this makes no sense but maybe one day they might change it somehow?
1116
					if ( ! isset( $result['Id'] ) && isset( $result['id'] ) ) {
1117
						$result['Id'] = $result['id'];
1118
					}
1119
					$data = array(
1120
						'object_type'     => $type,
1121
						'object'          => $result,
1122
						'mapping'         => $salesforce_mapping,
1123
						'sf_sync_trigger' => $sf_sync_trigger, // sf delete trigger.
1124
					);
1125
1126
					// default is pull is allowed.
1127
					$pull_allowed = true;
1128
1129
					// if the current fieldmap does not allow delete, set pull_allowed to false.
1130
					if ( isset( $map_sync_triggers ) && ! in_array( $this->mappings->sync_sf_delete, $map_sync_triggers, true ) ) {
1131
						$pull_allowed = false;
1132
					}
1133
1134
					// Hook to allow other plugins to prevent a pull per-mapping.
1135
					// Putting the pull_allowed hook here will keep the queue from deleting a WordPress record when it is not supposed to delete it.
1136
					$pull_allowed = apply_filters( $this->option_prefix . 'pull_object_allowed', $pull_allowed, $type, $result, $sf_sync_trigger, $salesforce_mapping );
0 ignored issues
show
Bug introduced by
The function apply_filters was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1136
					$pull_allowed = /** @scrutinizer ignore-call */ apply_filters( $this->option_prefix . 'pull_object_allowed', $pull_allowed, $type, $result, $sf_sync_trigger, $salesforce_mapping );
Loading history...
1137
1138
					// example to keep from deleting the WordPress record mapped to the Contact with Id of abcdef.
1139
					/* // phpcs:ignore Squiz.PHP.CommentedOutCode.Found
1140
					add_filter( 'object_sync_for_salesforce_pull_object_allowed', 'check_user', 10, 5 );
1141
					// can always reduce this number if all the arguments are not necessary
1142
					function check_user( $pull_allowed, $object_type, $object, $sf_sync_trigger, $salesforce_mapping ) {
1143
						if ( 'Contact' === $object_type && 'abcdef' === $object['Id'] ) {
1144
							return false;
1145
						}
1146
					}
1147
					*/
1148
1149
					if ( false === $pull_allowed ) {
1150
						if ( isset( $salesforce_mapping['always_delete_object_maps_on_delete'] ) && ( '1' === $salesforce_mapping['always_delete_object_maps_on_delete'] ) ) {
1151
							$mapping_objects = $this->mappings->load_object_maps_by_salesforce_id( $result['Id'], $salesforce_mapping );
1152
							foreach ( $mapping_objects as $mapping_object ) {
1153
								if ( isset( $mapping_object['id'] ) ) {
1154
									$this->mappings->delete_object_map( $mapping_object['id'] );
1155
								}
1156
							}
1157
						}
1158
						continue;
1159
					}
1160
1161
					// setup the Id and the deletedDate for passing to the queue.
1162
					$deleted_item = array(
1163
						'Id'          => $result['Id'],
1164
						'deletedDate' => $result['deletedDate'],
1165
					);
1166
1167
					// Add a queue action to delete data from WordPress after it has been deleted from Salesforce.
1168
					$this->queue->add(
1169
						$this->schedulable_classes[ $this->schedule_name ]['callback'],
1170
						array(
1171
							'object_type'     => $type,
1172
							'object'          => $deleted_item,
1173
							'sf_sync_trigger' => $sf_sync_trigger,
1174
						),
1175
						$this->schedule_name
1176
					);
1177
1178
				} // end foreach on deleted records.
1179
1180
				$this->pull_options->set( 'delete_last', $type, $salesforce_mapping['id'], time() );
1181
1182
			} // End foreach() loop on relevant mappings.
1183
		} // End foreach() loop on active mappings.
1184
	}
1185
1186
	/**
1187
	 * Method for ajax hooks to call for pulling manually
1188
	 *
1189
	 * @param string $object_type the Salesforce object type.
1190
	 * @param string $salesforce_id the Salesforce record ID.
1191
	 * @return array $result
1192
	 */
1193
	public function manual_pull( $object_type, $salesforce_id = '' ) {
1194
1195
		if ( '' === $salesforce_id ) {
1196
			$sf_sync_trigger = $this->mappings->sync_sf_create;
1197
		} else {
1198
			$sf_sync_trigger = $this->mappings->sync_sf_update;
1199
		}
1200
1201
		$results = $this->salesforce_pull_process_records( $object_type, $salesforce_id, $sf_sync_trigger );
1202
1203
		$code = '201';
1204
		foreach ( $results as $result ) {
0 ignored issues
show
Bug introduced by
The expression $results of type boolean is not traversable.
Loading history...
1205
			if ( ! isset( $result['status'] ) || 'success' !== $result['status'] ) {
1206
				$code = '403';
1207
			}
1208
		}
1209
1210
		$result = array(
1211
			'code' => $code,
1212
			'data' => array(
1213
				'success' => $results,
1214
			),
1215
		);
1216
1217
		return $result;
1218
	}
1219
1220
	/**
1221
	 * Sync WordPress objects and Salesforce objects from the queue using the REST API.
1222
	 *
1223
	 * @param string       $object_type Type of Salesforce object.
1224
	 * @param array|string $object The Salesforce data or its Id value.
1225
	 * @param int          $sf_sync_trigger Trigger for this sync.
1226
	 * @return bool true or exit the method
1227
	 * @throws Object_Sync_Sf_Exception Exception $e.
1228
	 */
1229
	public function salesforce_pull_process_records( $object_type, $object, $sf_sync_trigger ) {
1230
1231
		$sfapi = $this->salesforce['sfapi'];
1232
1233
		if ( is_string( $object ) ) {
1234
			$salesforce_id = $object;
1235
			// Load the Salesforce object data to save in WordPress. We need to make sure that this data does not get cached, which is consistent with other pull behavior as well as in other methods in this class.
1236
			// We should only do this if we're not trying to delete data in WordPress - otherwise, we'll get a 404 from Salesforce and the delete will fail.
1237
			if ( $sf_sync_trigger !== $this->mappings->sync_sf_delete ) {
1238
				$object = $sfapi->object_read(
1239
					$object_type,
1240
					$salesforce_id,
1241
					array(
1242
						'cache' => false,
1243
					)
1244
				)['data'];
1245
			} else {
1246
				if ( true === $this->debug ) {
1247
					// create log entry for failed pull.
1248
					$status = 'debug';
1249
					$title  = sprintf(
1250
						// translators: placeholders are: 1) the log status.
1251
						esc_html__( '%1$s: we are missing a deletedDate attribute here, but are expected to delete an item.', 'object-sync-for-salesforce' ),
0 ignored issues
show
Bug introduced by
The function esc_html__ was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1251
						/** @scrutinizer ignore-call */ 
1252
      esc_html__( '%1$s: we are missing a deletedDate attribute here, but are expected to delete an item.', 'object-sync-for-salesforce' ),
Loading history...
1252
						ucfirst( esc_attr( $status ) )
0 ignored issues
show
Bug introduced by
The function esc_attr was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1252
						ucfirst( /** @scrutinizer ignore-call */ esc_attr( $status ) )
Loading history...
1253
					);
1254
					$debug = array(
1255
						'title'   => $title,
1256
						'message' => '',
1257
						'trigger' => $sf_sync_trigger,
1258
						'parent'  => '',
1259
						'status'  => $status,
1260
					);
1261
					$this->logging->setup( $debug );
1262
				}
1263
1264
				$object = array(
1265
					'Id'          => $object,
1266
					'deletedDate' => gmdate( 'Y-m-d\TH:i:s\Z' ), // this should hopefully never happen.
1267
				);
1268
			} // deleted records should always come through with their own deletedDate value.
1269
		} elseif ( is_object( $object ) ) {
0 ignored issues
show
introduced by
The condition is_object($object) is always false.
Loading history...
1270
			$salesforce_id = $object['Id'];
1271
		}
1272
1273
		$mapping_conditions = array(
1274
			'salesforce_object' => $object_type,
1275
		);
1276
1277
		if ( isset( $object['RecordTypeId'] ) && $object['RecordTypeId'] !== $this->mappings->salesforce_default_record_type ) {
1278
			// use this condition to filter the mappings, at that time.
1279
			$mapping_conditions['salesforce_record_type'] = $object['RecordTypeId'];
1280
		}
1281
1282
		$salesforce_mappings = $this->mappings->get_fieldmaps(
1283
			null,
1284
			array_merge(
1285
				$this->mappings->active_fieldmap_conditions,
1286
				$mapping_conditions,
1287
			),
1288
		);
1289
1290
		$frequencies = $this->queue->get_frequencies();
1291
		$seconds     = reset( $frequencies )['frequency'] + 60;
1292
1293
		$transients_to_delete = array();
1294
1295
		$results = array();
1296
1297
		foreach ( $salesforce_mappings as $fieldmap_key => $salesforce_mapping ) {
1298
			$transients_to_delete[ $fieldmap_key ] = array(
1299
				'fieldmap'   => $salesforce_mapping,
1300
				'transients' => array(),
1301
			);
1302
			// this returns the row or rows that map an individual Salesforce row to an individual WordPress row.
1303
			if ( isset( $object['Id'] ) ) {
1304
				$mapping_objects = $this->mappings->load_object_maps_by_salesforce_id( $object['Id'], $salesforce_mapping );
1305
			} else {
1306
				$mapping_objects = $this->mappings->load_object_maps_by_salesforce_id( $salesforce_id, $salesforce_mapping );
1307
				// if we don't have a Salesforce object id, we've got no business doing stuff in WordPress.
1308
				$status = 'error';
1309
				$title  = sprintf(
1310
					// translators: placeholders are: 1) the log status.
1311
					esc_html__( '%1$s: Salesforce Pull: unable to process queue item because it has no Salesforce Id.', 'object-sync-for-salesforce' ),
1312
					ucfirst( esc_attr( $status ) )
1313
				);
1314
				$result = array(
1315
					'title'   => $title,
1316
					'message' => print_r( $object, true ), // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
1317
					'trigger' => $sf_sync_trigger,
1318
					'parent'  => 0, // parent id goes here but we don't have one, so make it 0.
1319
					'status'  => $status,
1320
				);
1321
				$this->logging->setup( $result );
1322
				$results[] = $result;
1323
1324
				// update the mapping objects.
1325
				foreach ( $mapping_objects as $mapping_object ) {
1326
					if ( isset( $mapping_object['id'] ) ) {
1327
						$mapping_object['last_sync_status']  = $this->mappings->status_error;
1328
						$mapping_object['last_sync_message'] = isset( $object['message'] ) ? esc_html( $object['message'] ) : esc_html__( 'unable to process queue item because it has no Salesforce Id.', 'object-sync-for-salesforce' );
0 ignored issues
show
Bug introduced by
The function esc_html was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1328
						$mapping_object['last_sync_message'] = isset( $object['message'] ) ? /** @scrutinizer ignore-call */ esc_html( $object['message'] ) : esc_html__( 'unable to process queue item because it has no Salesforce Id.', 'object-sync-for-salesforce' );
Loading history...
1329
						$mapping_object['last_sync']         = current_time( 'mysql' );
0 ignored issues
show
Bug introduced by
The function current_time was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1329
						$mapping_object['last_sync']         = /** @scrutinizer ignore-call */ current_time( 'mysql' );
Loading history...
1330
						$mapping_object                      = $this->mappings->update_object_map( $mapping_object, $mapping_object['id'] );
1331
					}
1332
				}
1333
1334
				continue;
1335
			}
1336
1337
			// Is this Salesforce object already connected to at least one WordPress object?
1338
			if ( isset( $mapping_objects[0]['id'] ) ) {
1339
				$is_new = false;
1340
			} else {
1341
				// there is not a mapping object for this Salesforce object id yet
1342
				// check to see if there is a pushing transient for that Salesforce Id.
1343
				$is_new = true;
1344
			}
1345
1346
			// by default, we're not doing a merge.
1347
			$is_merge = false;
1348
			$merged   = $this->sync_transients->get( 'salesforce_merged', $object_type, $salesforce_mapping['id'] );
1349
			if ( false !== $merged ) {
1350
				$key = array_search( $object['Id'], array_column( $merged, 'Id' ), true );
1351
				if ( false !== $key ) {
1352
					$is_merge = true;
1353
					$is_new   = false;
1354
				}
1355
			}
1356
1357
			$mapping_object_id_transient = $this->sync_transients->get( 'salesforce_pushing_object_id', '', $salesforce_mapping['id'] );
1358
			if ( false === $mapping_object_id_transient ) {
1359
				$mapping_object_id_transient = $object['Id'];
1360
			}
1361
			// Here's where we check to see whether the current record was updated by a push from this plugin or not. Here's how it works:
1362
			// 1. A record gets pushed to Salesforce by this plugin.
1363
			// 2. We save the LastModifiedDate from the Salesforce result as a timestamp in the transient.
1364
			// 3. Below, in addition to checking the Salesforce Id, we check against $object's LastModifiedDate and if it's not later than the transient value, we skip it because it's still pushing from our activity.
1365
			$salesforce_pushing = (int) $this->sync_transients->get( 'salesforce_pushing_' . $mapping_object_id_transient, '', $salesforce_mapping['id'] );
1366
1367
			if ( 1 !== $salesforce_pushing ) {
1368
				// the format to compare is like this: gmdate( 'Y-m-d\TH:i:s\Z', $salesforce_pushing ).
1369
				if ( $mapping_object_id_transient !== $object['Id'] ) {
1370
					$salesforce_pushing = 0;
1371
				} elseif ( 0 === $salesforce_pushing || ( isset( $object['LastModifiedDate'] ) && strtotime( $object['LastModifiedDate'] ) > $salesforce_pushing ) || ( isset( $object['deletedDate'] ) && strtotime( $object['deletedDate'] ) > $salesforce_pushing ) ) {
1372
					$salesforce_pushing = 0;
1373
				} else {
1374
					$salesforce_pushing = 1;
1375
				}
1376
			} else {
1377
				$salesforce_pushing = 1;
1378
			}
1379
1380
			if ( 1 === $salesforce_pushing ) {
1381
				$transients_to_delete[ $fieldmap_key ]['transients'][] = $mapping_object_id_transient;
1382
				if ( true === $this->debug ) {
1383
					// create log entry for failed pull.
1384
					$status = 'debug';
1385
					$title  = sprintf(
1386
						// translators: placeholders are: 1) the log status, 2) the mapping object ID transient.
1387
						esc_html__( '%1$s: mapping object transient ID %2$s is currently pushing, so we do not pull it.', 'object-sync-for-salesforce' ),
1388
						ucfirst( esc_attr( $status ) ),
1389
						$mapping_object_id_transient
1390
					);
1391
					$debug = array(
1392
						'title'   => $title,
1393
						'message' => '',
1394
						'trigger' => $sf_sync_trigger,
1395
						'parent'  => '',
1396
						'status'  => $status,
1397
					);
1398
					$this->logging->setup( $debug );
1399
				}
1400
1401
				continue;
1402
			}
1403
1404
			$structure               = $this->wordpress->get_wordpress_table_structure( $salesforce_mapping['wordpress_object'] );
1405
			$wordpress_id_field_name = $structure['id_field'];
1406
1407
			// only deal with parameters if we are not deleting.
1408
			if ( ( true === $is_new && $sf_sync_trigger === $this->mappings->sync_sf_create ) || $sf_sync_trigger === $this->mappings->sync_sf_update ) { // trigger is a bit operator
1409
				// map the Salesforce values to WordPress fields.
1410
				$params = $this->mappings->map_params( $salesforce_mapping, $object, $sf_sync_trigger, false, $is_new, $wordpress_id_field_name );
1411
1412
				// hook to allow other plugins to modify the $params array
1413
				// use hook to map fields between the WordPress and Salesforce objects
1414
				// returns $params.
1415
				$params = apply_filters( $this->option_prefix . 'pull_params_modify', $params, $salesforce_mapping, $object, $sf_sync_trigger, false, $is_new );
0 ignored issues
show
Bug introduced by
The function apply_filters was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1415
				$params = /** @scrutinizer ignore-call */ apply_filters( $this->option_prefix . 'pull_params_modify', $params, $salesforce_mapping, $object, $sf_sync_trigger, false, $is_new );
Loading history...
1416
1417
				// setup prematch parameters.
1418
				$prematch = array();
1419
1420
				// if there is a prematch WordPress field - ie email - on the fieldmap object.
1421
				if ( isset( $params['prematch'] ) && is_array( $params['prematch'] ) ) {
1422
					$prematch['field_wordpress']  = $params['prematch']['wordpress_field'];
1423
					$prematch['field_salesforce'] = $params['prematch']['salesforce_field'];
1424
					$prematch['value']            = $params['prematch']['value'];
1425
					$prematch['methods']          = array(
1426
						'method_match'  => isset( $params['prematch']['method_match'] ) ? $params['prematch']['method_match'] : $params['prematch']['method_read'],
1427
						'method_create' => $params['prematch']['method_create'],
1428
						'method_update' => $params['prematch']['method_update'],
1429
						'method_read'   => $params['prematch']['method_read'],
1430
					);
1431
					unset( $params['prematch'] );
1432
				}
1433
1434
				// if there is an external key field in Salesforce - ie a Mailchimp user id - on the fieldmap object, this should not affect how WordPress handles it is not included in the pull parameters.
1435
1436
				// if we don't get any params, there are no fields that should be sent to WordPress.
1437
				if ( empty( $params ) ) {
1438
1439
					// if the parameters array is empty at this point, we should create a log entry to that effect.
1440
					// I think it should be a debug message, unless we learn from users that it should be raised to an error.
1441
					if ( true === $this->debug ) {
1442
						$status = 'debug';
1443
						$title  = sprintf(
1444
							// translators: %1$s is the log status.
1445
							esc_html__( '%1$s Mapping: according to the current plugin settings, there are no parameters in the current dataset that can be pulled from Salesforce.', 'object-sync-for-salesforce' ),
1446
							ucfirst( esc_attr( $status ) )
1447
						);
1448
						$body = sprintf(
1449
							// translators: placeholders are: 1) the fieldmap row ID, 2) the name of the WordPress object, 3) the name of the Salesforce object.
1450
							'<p>' . esc_html__( 'There is a fieldmap with ID of %1$s and it maps the WordPress %2$s object to the Salesforce %3$s object.', 'object-sync-for-salesforce' ) . '</p>',
1451
							absint( $salesforce_mapping['id'] ),
0 ignored issues
show
Bug introduced by
The function absint was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1451
							/** @scrutinizer ignore-call */ 
1452
       absint( $salesforce_mapping['id'] ),
Loading history...
1452
							esc_attr( $salesforce_mapping['wordpress_object'] ),
1453
							esc_attr( $salesforce_mapping['salesforce_object'] )
1454
						);
1455
						// whether it's a new mapping object or not.
1456
						if ( false === $is_new ) {
1457
							// this one is not new.
1458
							foreach ( $mapping_objects as $mapping_object ) {
1459
								$body .= sprintf(
1460
									// translators: placeholders are: 1) the mapping object row ID, 2) the name of the WordPress object, 3) the ID of the WordPress object, 4) the ID of the Salesforce object it was trying to map.
1461
									'<p>' . esc_html__( 'There is an existing object map with ID of %1$s and it is mapped to the WordPress %2$s with ID of %3$s and the Salesforce object with ID of %4$s.', 'object-sync-for-salesforce' ) . '</p>',
1462
									absint( $mapping_object['id'] ),
1463
									esc_attr( $mapping_object['wordpress_object'] ),
1464
									esc_attr( $mapping_object['wordpress_id'] ),
1465
									esc_attr( $mapping_object['salesforce_id'] )
1466
								);
1467
							}
1468
						} else {
1469
							// this one is new.
1470
							$body .= sprintf(
1471
								// translators: placeholders are: 1) the ID of the Salesforce object, 2) the WordPress object type.
1472
								'<p>' . esc_html__( 'The plugin was trying to pull the Salesforce object with ID of %1$s to the WordPress %2$s object type.', 'object-sync-for-salesforce' ) . '</p>',
1473
								esc_attr( $object['Id'] ),
1474
								esc_attr( $salesforce_mapping['wordpress_object'] )
1475
							);
1476
						}
1477
1478
						$body .= sprintf(
1479
							// translators: placeholders are 1) the object's data that was attempted.
1480
							'<p>' . esc_html__( 'The Salesforce object data that was attempted: %1$s', 'object-sync-for-salesforce' ) . '</p>',
1481
							print_r( $object, true ) // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
0 ignored issues
show
Bug introduced by
It seems like print_r($object, true) can also be of type true; however, parameter $values of sprintf() does only seem to accept double|integer|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

1481
							/** @scrutinizer ignore-type */ print_r( $object, true ) // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
Loading history...
1482
						);
1483
1484
						$this->logging->setup(
1485
							$title,
1486
							$body,
1487
							$sf_sync_trigger,
1488
							0,
1489
							$status
1490
						);
1491
					} // end debug mode check.
1492
1493
					continue;
1494
				} // end if params are empty.
1495
			} elseif ( $sf_sync_trigger === $this->mappings->sync_sf_delete ) {
1496
				// this is a deletion. don't deal with parameters.
1497
				$is_new = false;
1498
			} // end checking for create/update/delete.
1499
1500
			// if this Salesforce record is new to WordPress, we can try to create it.
1501
			if ( true === $is_new ) {
1502
				if ( isset( $mapping_objects[0] ) ) {
1503
					$mapping_object = $mapping_objects[0];
1504
				} else {
1505
					$mapping_object = $mapping_objects;
1506
				}
1507
				$synced_object = $this->get_synced_object( $object, $mapping_object, $salesforce_mapping );
1508
				$create        = $this->create_called_from_salesforce( $sf_sync_trigger, $synced_object, $params, $prematch, $wordpress_id_field_name, $seconds );
1509
				$results       = array_merge( $results, $create );
1510
			} elseif ( false === $is_new && false === $is_merge ) {
1511
				// unless we're on a delete, there is already at least one mapping_object['id'] associated with this Salesforce Id
1512
				// right here we should set the pulling transient.
1513
				$this->sync_transients->set( 'salesforce_pulling_' . $object['Id'], '', $salesforce_mapping['id'], 1, $seconds );
1514
				$this->sync_transients->set( 'salesforce_pulling_object_id', '', $salesforce_mapping['id'], $object['Id'] );
1515
1516
				foreach ( $mapping_objects as $mapping_object ) {
1517
					$synced_object = $this->get_synced_object( $object, $mapping_object, $salesforce_mapping );
1518
					// if params is set, this is an update request. if not, it is a delete.
1519
					if ( isset( $params ) ) {
1520
						$update  = $this->update_called_from_salesforce( $sf_sync_trigger, $synced_object, $params, $wordpress_id_field_name, $seconds );
1521
						$results = array_merge( $results, $update );
1522
					} else {
1523
						$delete  = $this->delete_called_from_salesforce( $sf_sync_trigger, $synced_object, $wordpress_id_field_name, $seconds, $mapping_objects );
1524
						$results = array_merge( $results, $delete );
1525
					}
1526
				}
1527
			} elseif ( false === $is_new ) {
1528
				// on merge, we should still update the transient.
1529
				$this->sync_transients->set( 'salesforce_pulling_' . $object['Id'], '', $salesforce_mapping['id'], 1, $seconds );
1530
				$this->sync_transients->set( 'salesforce_pulling_object_id', '', $salesforce_mapping['id'], $object['Id'] );
1531
			}
1532
		} // End foreach() on $salesforce_mappings.
1533
1534
		// delete transients that we've already processed for this Salesforce object.
1535
		foreach ( $transients_to_delete as $key => $value ) {
1536
			$fieldmap_id = $value['fieldmap']['id'];
1537
			$transients  = (array) $value['transients'];
1538
			foreach ( $transients as $transient_end ) {
1539
				$this->sync_transients->delete( 'salesforce_pushing_' . $transient_end, '', $fieldmap_id );
1540
			}
1541
			$pushing_id = $this->sync_transients->get( 'salesforce_pushing_object_id', '', $fieldmap_id );
1542
			if ( in_array( $pushing_id, $transients, true ) ) {
1543
				$this->sync_transients->delete( 'salesforce_pushing_object_id', '', $fieldmap_id );
1544
			}
1545
		}
1546
1547
		return $results;
1548
	}
1549
1550
	/**
1551
	 * Generate the synced_object array
1552
	 *
1553
	 * @param array $object The data for the Salesforce object.
1554
	 * @param array $mapping_object The data for the mapping object between the individual Salesforce and WordPress items.
1555
	 * @param array $salesforce_mapping The data for the fieldmap between the object types.
1556
	 * @return array $synced_object The combined array of these items. It allows for filtering of, at least, the mapping_object.
1557
	 */
1558
	private function get_synced_object( $object, $mapping_object, $salesforce_mapping ) {
1559
		// if there's already a connection between the objects, $mapping_object will be an array at this point
1560
		// if it's not already connected (ie on create), the array will be empty.
1561
1562
		// hook to allow other plugins to define or alter the mapping object.
1563
		$mapping_object = apply_filters( $this->option_prefix . 'pull_mapping_object', $mapping_object, $object, $salesforce_mapping );
0 ignored issues
show
Bug introduced by
The function apply_filters was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1563
		$mapping_object = /** @scrutinizer ignore-call */ apply_filters( $this->option_prefix . 'pull_mapping_object', $mapping_object, $object, $salesforce_mapping );
Loading history...
1564
1565
		// we already have the data from Salesforce at this point; we just need to work with it in WordPress.
1566
		$synced_object = array(
1567
			'salesforce_object' => $object,
1568
			'mapping_object'    => $mapping_object,
1569
			'mapping'           => $salesforce_mapping,
1570
		);
1571
		return $synced_object;
1572
	}
1573
1574
	/**
1575
	 * Create records in WordPress from a Salesforce pull
1576
	 *
1577
	 * @param string $sf_sync_trigger The current operation's trigger.
1578
	 * @param array  $synced_object Combined data for fieldmap, mapping object, and Salesforce object data.
1579
	 * @param array  $params Array of mapped key value pairs between WordPress and Salesforce fields.
1580
	 * @param array  $prematch Array of criteria to determine what to do on upsert operations.
1581
	 * @param string $wordpress_id_field_name The name of the ID field for this particular WordPress object type.
1582
	 * @param int    $seconds Timeout for the transient value to determine the direction for a sync.
1583
	 * @return array $results Currently this contains an array of log entries for each attempt.
1584
	 * @throws Object_Sync_Sf_Exception Exception $e.
1585
	 */
1586
	private function create_called_from_salesforce( $sf_sync_trigger, $synced_object, $params, $prematch, $wordpress_id_field_name, $seconds ) {
1587
1588
		$salesforce_mapping = $synced_object['mapping'];
1589
		$object             = $synced_object['salesforce_object'];
1590
		// methods to run the wp update operations.
1591
		$results = array();
1592
		$op      = '';
1593
1594
		// setup SF record type. CampaignMember objects get their Campaign's type
1595
		// i am still a bit confused about this
1596
		// we should store this as a meta field on each object, if it meets these criteria
1597
		// we need to store the read/modify attributes because the field doesn't exist in the mapping.
1598
		if ( $salesforce_mapping['salesforce_record_type_default'] !== $this->mappings->salesforce_default_record_type && empty( $params['RecordTypeId'] ) && ( 'CampaignMember' !== $salesforce_mapping['salesforce_object'] ) ) {
1599
			$type = $salesforce_mapping['wordpress_object'];
1600
			if ( 'category' === $salesforce_mapping['wordpress_object'] || 'tag' === $salesforce_mapping['wordpress_object'] || 'post_tag' === $salesforce_mapping['wordpress_object'] ) {
1601
				$type = 'term';
1602
			}
1603
			$params['RecordTypeId'] = array(
1604
				'value'         => $salesforce_mapping['salesforce_record_type_default'],
1605
				'method_modify' => 'update_' . $type . '_meta',
1606
				'method_read'   => 'get_' . $type . '_meta',
1607
			);
1608
		}
1609
1610
		// hook to allow other plugins to modify the $wordpress_id string here
1611
		// use hook to change the object that is being matched to developer's own criteria
1612
		// ex: match a WordPress user based on some other criteria than the predefined ones
1613
		// returns a $wordpress_id.
1614
		// it should keep NULL if there is no match
1615
		// the function that calls this hook needs to check the mapping to make sure the WordPress object is the right type.
1616
		$wordpress_id = apply_filters( $this->option_prefix . 'find_wp_object_match', null, $object, $salesforce_mapping, 'pull' );
0 ignored issues
show
Bug introduced by
The function apply_filters was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1616
		$wordpress_id = /** @scrutinizer ignore-call */ apply_filters( $this->option_prefix . 'find_wp_object_match', null, $object, $salesforce_mapping, 'pull' );
Loading history...
1617
1618
		// hook to allow other plugins to do something right before WordPress data is saved
1619
		// ex: run outside methods on an object if it exists, or do something in preparation for it if it doesn't.
1620
		do_action( $this->option_prefix . 'pre_pull', $wordpress_id, $salesforce_mapping, $object, $wordpress_id_field_name, $params );
0 ignored issues
show
Bug introduced by
The function do_action was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1620
		/** @scrutinizer ignore-call */ 
1621
  do_action( $this->option_prefix . 'pre_pull', $wordpress_id, $salesforce_mapping, $object, $wordpress_id_field_name, $params );
Loading history...
1621
1622
		$op = $this->check_for_upsert( $prematch, $wordpress_id );
1623
1624
		if ( 'Upsert' === $op ) {
1625
			// if a prematch criteria exists, make the values queryable.
1626
			if ( isset( $prematch['field_salesforce'] ) ) {
1627
				$upsert_key     = $prematch['field_wordpress'];
1628
				$upsert_value   = $prematch['value'];
1629
				$upsert_methods = $prematch['methods'];
1630
			}
1631
1632
			if ( null !== $wordpress_id ) {
1633
				$upsert_key     = $wordpress_id_field_name;
1634
				$upsert_value   = $wordpress_id;
1635
				$upsert_methods = array();
1636
			}
1637
1638
			// with the flag at the end, upsert returns a $wordpress_id only
1639
			// we can then check to see if it has a mapping object
1640
			// we should only do this if the above hook didn't already set the $wordpress_id.
1641
			if ( null === $wordpress_id ) {
1642
				$wordpress_id = $this->wordpress->object_upsert( $salesforce_mapping['wordpress_object'], $upsert_key, $upsert_value, $upsert_methods, $params, $salesforce_mapping['pull_to_drafts'], true );
1643
			}
1644
1645
			// placeholder mapping object.
1646
			$mapping_object = array();
1647
1648
			// find out if there is a mapping object for this WordPress object already
1649
			// don't do it if the WordPress id is 0.
1650
			if ( 0 !== $wordpress_id ) {
1651
				$mapping_objects = $this->mappings->get_all_object_maps(
1652
					array(
1653
						'wordpress_id'     => $wordpress_id,
1654
						'wordpress_object' => $salesforce_mapping['wordpress_object'],
1655
					)
1656
				);
1657
				if ( isset( $mapping_objects[0] ) && is_array( $mapping_objects[0] ) ) {
1658
					$mapping_object = $mapping_objects[0];
1659
				}
1660
			} else {
1661
				// if the wp object is 0, check to see if there are any object maps that have an id of 0. if there are any, log them.
1662
				$mapping_object_debug = $this->mappings->get_all_object_maps(
1663
					array(
1664
						'wordpress_id' => $wordpress_id,
1665
					)
1666
				);
1667
1668
				if ( ! empty( $mapping_object_debug ) ) {
1669
					// create log entry to warn about at least one id of 0.
1670
					$status = 'error';
1671
					$title  = sprintf(
1672
						// translators: placeholders are: 1) the log status.
1673
						esc_html__( '%1$s: There is at least one object map with a WordPress ID of 0.', 'object-sync-for-salesforce' ),
0 ignored issues
show
Bug introduced by
The function esc_html__ was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1673
						/** @scrutinizer ignore-call */ 
1674
      esc_html__( '%1$s: There is at least one object map with a WordPress ID of 0.', 'object-sync-for-salesforce' ),
Loading history...
1674
						ucfirst( esc_attr( $status ) )
0 ignored issues
show
Bug introduced by
The function esc_attr was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1674
						ucfirst( /** @scrutinizer ignore-call */ esc_attr( $status ) )
Loading history...
1675
					);
1676
					if ( 1 === count( $mapping_object_debug ) ) {
1677
						$body = sprintf(
1678
							// translators: placeholders are: 1) the mapping object row ID, 2) the name of the WordPress object, 3) the ID of the Salesforce object it was trying to map.
1679
							esc_html__( 'There is an object map with ID of %1$s and it is mapped to the WordPress %2$s with ID of 0 and the Salesforce object with ID of %3$s', 'object-sync-for-salesforce' ),
1680
							absint( $mapping_object_debug[0]['id'] ),
0 ignored issues
show
Bug introduced by
The function absint was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1680
							/** @scrutinizer ignore-call */ 
1681
       absint( $mapping_object_debug[0]['id'] ),
Loading history...
1681
							esc_attr( $mapping_object_debug[0]['wordpress_object'] ),
1682
							esc_attr( $mapping_object_debug[0]['salesforce_id'] )
1683
						);
1684
					} else {
1685
						$body = sprintf( esc_html__( 'There are multiple object maps with WordPress ID of 0. Their IDs are: ', 'object-sync-for-salesforce' ) . '<ul>' );
1686
						foreach ( $mapping_object_debug as $mapping_object ) {
1687
							$body .= sprintf(
1688
								// translators: placeholders are: 1) the mapping object row ID, 2) the ID of the Salesforce object, 3) the WordPress object type.
1689
								'<li>' . esc_html__( 'Mapping object id: %1$s. Salesforce Id: %2$s. WordPress object type: %3$s', 'object-sync-for-salesforce' ) . '</li>',
1690
								absint( $mapping_object['id'] ),
1691
								esc_attr( $mapping_object['salesforce_id'] ),
1692
								esc_attr( $salesforce_mapping['wordpress_object'] )
1693
							);
1694
						}
1695
						$body .= sprintf( '</ul>' );
1696
					}
1697
					$parent = 0;
1698
					$result = array(
1699
						'title'   => $title,
1700
						'message' => $body,
1701
						'trigger' => $sf_sync_trigger,
1702
						'parent'  => $parent,
1703
						'status'  => $status,
1704
					);
1705
					$this->logging->setup( $result );
1706
					$results[] = $result;
1707
				} // End if() statement.
1708
			} // End if() statement checking for a mapping object for this WordPress object.
1709
1710
			// there is already a mapping object. don't change the WordPress data to match this new Salesforce record, but log it.
1711
			if ( isset( $mapping_object['id'] ) ) {
1712
				// set the transients so that salesforce_push doesn't start doing stuff, then return out of here.
1713
				$this->sync_transients->set( 'salesforce_pulling_' . $mapping_object['salesforce_id'], '', $salesforce_mapping['id'], 1, $seconds );
1714
				$this->sync_transients->set( 'salesforce_pulling_object_id', '', $salesforce_mapping['id'], $mapping_object['salesforce_id'] );
1715
				// create log entry to indicate that nothing happened.
1716
				$status = 'notice';
1717
				$title  = sprintf(
1718
					// translators: placeholders are: 1) log status, 2) mapping object row id, 3) WordPress object tyoe, 4) individual WordPress item ID, 5) individual Salesforce item ID.
1719
					esc_html__( '%1$s: Because object map %2$s already exists, WordPress %3$s %4$s was not mapped to Salesforce Id %5$s', 'object-sync-for-salesforce' ),
1720
					ucfirst( esc_attr( $status ) ),
1721
					absint( $mapping_object['id'] ),
1722
					esc_attr( $salesforce_mapping['wordpress_object'] ),
1723
					absint( $wordpress_id ),
1724
					esc_attr( $object['Id'] )
1725
				);
1726
				$body = sprintf(
1727
					// translators: placeholders are 1) WordPress object type, 2) field name for the WordPress id, 3) the WordPress id value, 4) the Salesforce object type, 5) the Salesforce object Id that was modified, 6) the mapping object row id.
1728
					esc_html__( 'The WordPress %1$s with %2$s of %3$s is already mapped to the Salesforce %4$s with Id of %5$s in the mapping object with id of %6$s. The Salesforce %4$s with Id of %5$s was created or modified in Salesforce, and would otherwise have been mapped to this WordPress record. No WordPress data has been changed to prevent changing data unintentionally.', 'object-sync-for-salesforce' ),
1729
					esc_attr( $salesforce_mapping['wordpress_object'] ),
1730
					esc_attr( $wordpress_id_field_name ),
1731
					absint( $wordpress_id ),
1732
					esc_attr( $salesforce_mapping['salesforce_object'] ),
1733
					esc_attr( $object['Id'] ),
1734
					absint( $mapping_object['id'] )
1735
				);
1736
				// if we know the WordPress object id we can put it in there.
1737
				if ( null !== $wordpress_id ) {
1738
					$parent = $wordpress_id;
1739
				} else {
1740
					$parent = 0;
1741
				}
1742
				$already_mapped_log = array(
1743
					'title'   => $title,
1744
					'message' => $body,
1745
					'trigger' => $sf_sync_trigger,
1746
					'parent'  => $parent,
1747
					'status'  => $status,
1748
				);
1749
				$this->logging->setup( $already_mapped_log );
1750
				$results[] = $already_mapped_log;
1751
1752
			} // End if() statement.
1753
		} // end if op is upsert.
1754
1755
		// create the mapping object between the rows. we update it with the correct IDs after successful response.
1756
		$mapping_object_id = $this->create_object_map( $object, $this->mappings->generate_temporary_id( 'pull' ), $salesforce_mapping );
1757
		$mapping_objects   = $this->mappings->get_all_object_maps(
1758
			array(
1759
				'id' => $mapping_object_id,
1760
			)
1761
		);
1762
		if ( isset( $mapping_objects[0] ) && is_array( $mapping_objects[0] ) ) {
1763
			$mapping_object = $mapping_objects[0];
1764
		}
1765
1766
		// try to upsert or create a WordPress record.
1767
		try {
1768
			if ( 'Upsert' === $op ) {
1769
				// right here we should update the pulling transient.
1770
				$this->sync_transients->set( 'salesforce_pulling_' . $object['Id'], '', $salesforce_mapping['id'], 1, $seconds );
1771
				$this->sync_transients->set( 'salesforce_pulling_object_id', '', $salesforce_mapping['id'], $object['Id'] );
1772
				// now we can upsert the object in wp if we've gotten to this point
1773
				// this command will either create or update the object.
1774
				$result = $this->wordpress->object_upsert( $salesforce_mapping['wordpress_object'], $upsert_key, $upsert_value, $upsert_methods, $params, $salesforce_mapping['pull_to_drafts'] );
1775
			} else {
1776
				// No key or prematch field exists on this field map object, create a new object in WordPress.
1777
				$this->sync_transients->set( 'salesforce_pulling_' . $mapping_object_id, '', $salesforce_mapping['id'], 1, $seconds );
1778
				$this->sync_transients->set( 'salesforce_pulling_object_id', '', $salesforce_mapping['id'], $mapping_object_id );
1779
				// now we can create the object in wp if we've gotten to this point.
1780
				$result = $this->wordpress->object_create( $salesforce_mapping['wordpress_object'], $params );
1781
			} // End if() statement.
1782
		} catch ( Object_Sync_Sf_Exception $e ) {
1783
			$result['errors'] = $e->getMessage();
1784
		} // End try() method for create or upsert.
1785
1786
		// set $wordpress_data to the WordPress result.
1787
		$wordpress_data = $result['data'];
1788
		if ( isset( $wordpress_data[ "$wordpress_id_field_name" ] ) ) {
1789
			$wordpress_id = $wordpress_data[ "$wordpress_id_field_name" ];
1790
		} else {
1791
			$wordpress_id = 0;
1792
		}
1793
1794
		// WordPress create/upsert call has finished running
1795
		// If it was successful, the object has already been created/updated in WordPress
1796
		// this is where it creates/updates the object mapping rows and log entries in WordPress
1797
		// this is also where it runs pull_success and pull_fail actions hooks.
1798
1799
		if ( empty( $result['errors'] ) ) {
1800
			// set up log entry for successful create or upsert.
1801
			$status = 'success';
1802
			$title  = sprintf(
1803
				// translators: placeholders are: 1) the log status, 2) what operation is happening, 3) the name of the WordPress object type, 4) the WordPress id field name, 5) the WordPress object id value, 6) the name of the Salesforce object, 7) the Salesforce Id value.
1804
				esc_html__( '%1$s: %2$s WordPress %3$s with %4$s of %5$s (Salesforce %6$s Id of %7$s)', 'object-sync-for-salesforce' ),
1805
				ucfirst( esc_attr( $status ) ),
1806
				esc_attr( $op ),
1807
				esc_attr( $salesforce_mapping['wordpress_object'] ),
1808
				esc_attr( $wordpress_id_field_name ),
1809
				esc_attr( $wordpress_id ),
1810
				esc_attr( $salesforce_mapping['salesforce_object'] ),
1811
				esc_attr( $object['Id'] )
1812
			);
1813
			$result = array(
1814
				'title'   => $title,
1815
				'message' => '',
1816
				'trigger' => $sf_sync_trigger,
1817
				'parent'  => $wordpress_id,
1818
				'status'  => $status,
1819
			);
1820
1821
			// set up the mapping object for successful create or upsert.
1822
			$mapping_object['last_sync_status']  = $this->mappings->status_success;
1823
			$mapping_object['last_sync_message'] = esc_html__( 'Mapping object updated via function: ', 'object-sync-for-salesforce' ) . __FUNCTION__;
1824
1825
			// hook for pull success.
1826
			do_action( $this->option_prefix . 'pull_success', $op, $result, $synced_object );
1827
		} else {
1828
			// set up log entry for failed create or upsert.
1829
			$status = 'error';
1830
			$title  = sprintf(
1831
				// translators: placeholders are: 1) the log status, 2) what operation is happening, and 3) the name of the WordPress object.
1832
				esc_html__( '%1$s: %2$s WordPress %3$s', 'object-sync-for-salesforce' ),
1833
				ucfirst( esc_attr( $status ) ),
1834
				esc_attr( $op ),
1835
				esc_attr( $salesforce_mapping['wordpress_object'] )
1836
			);
1837
			if ( isset( $salesforce_id ) && null !== $salesforce_id ) {
1838
				$title .= ' ' . $salesforce_id;
1839
			}
1840
			$title .= sprintf(
1841
				// translators: placeholders are: 1) the name of the Salesforce object, and 2) Id of the Salesforce object.
1842
				esc_html__( ' (Salesforce %1$s with Id of %2$s)', 'object-sync-for-salesforce' ),
1843
				$salesforce_mapping['salesforce_object'],
1844
				$object['Id']
1845
			);
1846
			// if we know the WordPress object id we can put it in there.
1847
			if ( null !== $wordpress_id ) {
1848
				$parent = $wordpress_id;
1849
			} else {
1850
				$parent = 0;
1851
			}
1852
1853
			// set up error message.
1854
			$default_message = esc_html__( 'An error occurred pulling this data from Salesforce. See the plugin logs.', 'object-sync-for-salesforce' );
1855
			$message         = $this->parse_error_message( $result, $default_message );
1856
1857
			$result = array(
1858
				'title'   => $title,
1859
				'message' => $message,
1860
				'trigger' => $sf_sync_trigger,
1861
				'parent'  => $parent,
1862
				'status'  => $status,
1863
			);
1864
1865
			// set up the mapping object for an error.
1866
			$mapping_object['last_sync_status']  = $this->mappings->status_error;
1867
			$mapping_object['last_sync_message'] = substr( $message, 0, 255 );
1868
1869
			// hook for pull fail.
1870
			do_action( $this->option_prefix . 'pull_fail', $op, $result, $synced_object );
1871
		} // End if() statement checking for errors.
1872
1873
		// either way, save the log entry.
1874
		$this->logging->setup( $result );
1875
		$results[] = $result;
1876
1877
		// either way, update the object map.
1878
		if ( 0 !== $wordpress_id ) {
1879
			$mapping_object['wordpress_id'] = $wordpress_id;
1880
		}
1881
		$mapping_object['last_sync'] = current_time( 'mysql' );
0 ignored issues
show
Bug introduced by
The function current_time was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1881
		$mapping_object['last_sync'] = /** @scrutinizer ignore-call */ current_time( 'mysql' );
Loading history...
1882
		$mapping_object              = $this->mappings->update_object_map( $mapping_object, $mapping_object['id'] );
1883
1884
		return $results;
1885
	}
1886
1887
	/**
1888
	 * Determine whether the current operation is an upsert or a create.
1889
	 *
1890
	 * @param array    $prematch Array of criteria to determine what to do on upsert operations.
1891
	 * @param int|null $wordpress_id The value of the WordPress ID.
1892
	 * @return string  $op Whether it's an upsert or create.
1893
	 */
1894
	private function check_for_upsert( $prematch, $wordpress_id ) {
1895
		if ( isset( $prematch['field_salesforce'] ) || null !== $wordpress_id ) {
1896
			$op = 'Upsert';
1897
		} else {
1898
			$op = 'Create';
1899
		}
1900
		return $op;
1901
	}
1902
1903
	/**
1904
	 * Update records in WordPress from a Salesforce pull
1905
	 *
1906
	 * @param string $sf_sync_trigger The current operation's trigger.
1907
	 * @param array  $synced_object Combined data for fieldmap, mapping object, and Salesforce object data.
1908
	 * @param array  $params Array of mapped key value pairs between WordPress and Salesforce fields.
1909
	 * @param string $wordpress_id_field_name The name of the ID field for this particular WordPress object type.
1910
	 * @param int    $seconds Timeout for the transient value to determine the direction for a sync.
1911
	 * @return array $results Currently this contains an array of log entries for each attempt.
1912
	 * @throws Object_Sync_Sf_Exception Exception $e.
1913
	 */
1914
	private function update_called_from_salesforce( $sf_sync_trigger, $synced_object, $params, $wordpress_id_field_name, $seconds ) {
1915
1916
		$salesforce_mapping = $synced_object['mapping'];
1917
		$mapping_object     = $synced_object['mapping_object'];
1918
		$object             = $synced_object['salesforce_object'];
1919
1920
		// methods to run the wp update operations.
1921
		$results = array();
1922
		$op      = '';
1923
1924
		// if the last sync is greater than the last time this object was updated by Salesforce, skip it
1925
		// this keeps us from doing redundant syncs
1926
		// because SF stores all DateTimes in UTC.
1927
		$mapping_object['object_updated'] = current_time( 'mysql' );
0 ignored issues
show
Bug introduced by
The function current_time was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1927
		$mapping_object['object_updated'] = /** @scrutinizer ignore-call */ current_time( 'mysql' );
Loading history...
1928
1929
		$pull_trigger_field = $salesforce_mapping['pull_trigger_field'];
1930
		$pull_trigger_value = $object[ $pull_trigger_field ];
1931
1932
		// hook to allow other plugins to do something right before WordPress data is saved
1933
		// ex: run outside methods on an object if it exists, or do something in preparation for it if it doesn't.
1934
		do_action( $this->option_prefix . 'pre_pull', $mapping_object['wordpress_id'], $salesforce_mapping, $object, $wordpress_id_field_name, $params );
0 ignored issues
show
Bug introduced by
The function do_action was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1934
		/** @scrutinizer ignore-call */ 
1935
  do_action( $this->option_prefix . 'pre_pull', $mapping_object['wordpress_id'], $salesforce_mapping, $object, $wordpress_id_field_name, $params );
Loading history...
1935
1936
		// try to update a WordPress record.
1937
		try {
1938
			$op     = 'Update';
1939
			$result = $this->wordpress->object_update( $salesforce_mapping['wordpress_object'], $mapping_object['wordpress_id'], $params, $mapping_object );
1940
		} catch ( Object_Sync_Sf_Exception $e ) {
1941
			$result['errors'] = $e->getMessage();
1942
		} // End try() method for update.
1943
1944
		// WordPress update call has finished running
1945
		// If it was successful, the object has already been updated in WordPress
1946
		// this is where it updates the object mapping rows and log entries in WordPress
1947
		// this is also where it runs pull_success and pull_fail actions hooks.
1948
1949
		if ( ! isset( $result['errors'] ) || empty( $result['errors'] ) ) {
1950
			// set up the log entry for successful update.
1951
			$status = 'success';
1952
			$title  = sprintf(
1953
				// translators: placeholders are: 1) the log status, 2) what operation is happening, 3) the name of the WordPress object type, 4) the WordPress id field name, 5) the WordPress object id value, 6) the name of the Salesforce object, 7) the Salesforce Id value.
1954
				esc_html__( '%1$s: %2$s WordPress %3$s with %4$s of %5$s (Salesforce %6$s Id of %7$s)', 'object-sync-for-salesforce' ),
0 ignored issues
show
Bug introduced by
The function esc_html__ was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1954
				/** @scrutinizer ignore-call */ 
1955
    esc_html__( '%1$s: %2$s WordPress %3$s with %4$s of %5$s (Salesforce %6$s Id of %7$s)', 'object-sync-for-salesforce' ),
Loading history...
1955
				ucfirst( esc_attr( $status ) ),
0 ignored issues
show
Bug introduced by
The function esc_attr was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1955
				ucfirst( /** @scrutinizer ignore-call */ esc_attr( $status ) ),
Loading history...
1956
				esc_attr( $op ),
1957
				esc_attr( $salesforce_mapping['wordpress_object'] ),
1958
				esc_attr( $wordpress_id_field_name ),
1959
				esc_attr( $mapping_object['wordpress_id'] ),
1960
				esc_attr( $salesforce_mapping['salesforce_object'] ),
1961
				esc_attr( $object['Id'] )
1962
			);
1963
			$result = array(
1964
				'title'   => $title,
1965
				'message' => is_string( $result['errors'] ) ? esc_html( $result['errors'] ) : '',
0 ignored issues
show
Bug introduced by
The function esc_html was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1965
				'message' => is_string( $result['errors'] ) ? /** @scrutinizer ignore-call */ esc_html( $result['errors'] ) : '',
Loading history...
1966
				'trigger' => $sf_sync_trigger,
1967
				'parent'  => $mapping_object['wordpress_id'],
1968
				'status'  => $status,
1969
			);
1970
1971
			// set up the mapping object for successful update.
1972
			$mapping_object['last_sync_status']  = $this->mappings->status_success;
1973
			$mapping_object['last_sync_message'] = esc_html__( 'Mapping object updated via function: ', 'object-sync-for-salesforce' ) . __FUNCTION__;
1974
1975
			// hook for pull success.
1976
			do_action( $this->option_prefix . 'pull_success', $op, $result, $synced_object );
1977
		} else {
1978
1979
			// set up the log entry for a failed update.
1980
			$status = 'error';
1981
			$title  = sprintf(
1982
				// translators: placeholders are: 1) the log status, 2) what operation is happening, 3) the name of the WordPress object, 4) the WordPress id field name, 5) the WordPress object id value, 6) the name of the Salesforce object, 7) the Salesforce Id value.
1983
				esc_html__( '%1$s: %2$s WordPress %3$s with %4$s of %5$s (Salesforce %6$s with Id of %7$s)', 'object-sync-for-salesforce' ),
1984
				ucfirst( esc_attr( $status ) ),
1985
				esc_attr( $op ),
1986
				esc_attr( $salesforce_mapping['wordpress_object'] ),
1987
				esc_attr( $wordpress_id_field_name ),
1988
				esc_attr( $mapping_object['wordpress_id'] ),
1989
				esc_attr( $salesforce_mapping['salesforce_object'] ),
1990
				esc_attr( $object['Id'] )
1991
			);
1992
1993
			// set up error message.
1994
			$default_message = esc_html__( 'An error occurred pulling this data from Salesforce. See the plugin logs.', 'object-sync-for-salesforce' );
1995
			$message         = $this->parse_error_message( $result, $default_message );
1996
1997
			$result = array(
1998
				'title'   => $title,
1999
				'message' => $message,
2000
				'trigger' => $sf_sync_trigger,
2001
				'parent'  => $mapping_object['wordpress_id'],
2002
				'status'  => $status,
2003
			);
2004
2005
			// set up the mapping object for an error.
2006
			$mapping_object['last_sync_status']  = $this->mappings->status_error;
2007
			$mapping_object['last_sync_message'] = substr( $message, 0, 255 );
2008
2009
			// hook for pull fail.
2010
			do_action( $this->option_prefix . 'pull_fail', $op, $result, $synced_object );
2011
		} // end if statement checking for errors.
2012
2013
		// save the log entry.
2014
		$this->logging->setup( $result );
2015
		$results[] = $result;
2016
2017
		// update the mapping object.
2018
		$mapping_object['last_sync_action'] = 'pull';
2019
		$mapping_object['last_sync']        = current_time( 'mysql' );
2020
		$mapping_object                     = $this->mappings->update_object_map( $mapping_object, $mapping_object['id'] );
2021
2022
		return $results;
2023
	}
2024
2025
	/**
2026
	 * Delete records in WordPress from a Salesforce pull
2027
	 *
2028
	 * @param string $sf_sync_trigger The current operation's trigger.
2029
	 * @param array  $synced_object Combined data for fieldmap, mapping object, and Salesforce object data.
2030
	 * @param string $wordpress_id_field_name The name of the ID field for this particular WordPress object type.
2031
	 * @param int    $seconds Timeout for the transient value to determine the direction for a sync.
2032
	 * @param array  $mapping_objects The data for the mapping objects between the individual Salesforce and WordPress items. We only pass this because of the need to count before deleting records.
2033
	 * @return array $results Currently this contains an array of log entries for each attempt.
2034
	 * @throws Object_Sync_Sf_Exception Exception $e.
2035
	 */
2036
	private function delete_called_from_salesforce( $sf_sync_trigger, $synced_object, $wordpress_id_field_name, $seconds, $mapping_objects ) {
2037
2038
		$salesforce_mapping = $synced_object['mapping'];
2039
		$mapping_object     = $synced_object['mapping_object'];
2040
2041
		// methods to run the wp delete operations.
2042
		$results = array();
2043
		$op      = '';
2044
2045
		// deleting mapped objects.
2046
		if ( $sf_sync_trigger === $this->mappings->sync_sf_delete ) { // trigger is a bit operator.
2047
			if ( isset( $mapping_object['id'] ) ) {
2048
2049
				$op = 'Delete';
2050
2051
				// only delete if there are no additional mapping objects for this record.
2052
				if ( 1 === count( $mapping_objects ) ) {
2053
2054
					$this->sync_transients->set( 'salesforce_pulling_' . $mapping_object['salesforce_id'], '', $salesforce_mapping['id'], 1, $seconds );
2055
					$this->sync_transients->set( 'salesforce_pulling_object_id', '', $salesforce_mapping['id'], $mapping_object['salesforce_id'] );
2056
2057
					try {
2058
						$result = $this->wordpress->object_delete( $salesforce_mapping['wordpress_object'], $mapping_object['wordpress_id'] );
2059
					} catch ( Object_Sync_Sf_Exception $e ) {
2060
						$result['errors'] = $e->getMessage();
2061
					} // End try() method.
2062
2063
					if ( ! isset( $result['errors'] ) || empty( $result['errors'] ) ) {
2064
						// set up the log entry for successful delete.
2065
						$status = 'success';
2066
						$title  = sprintf(
2067
							// translators: placeholders are: 1) the log status, 2) what operation is happening, 3) the name of the WordPress object type, 4) the WordPress id field name, 5) the WordPress object id value, 6) the name of the Salesforce object, 7) the Salesforce Id value.
2068
							esc_html__( '%1$s: %2$s WordPress %3$s with %4$s of %5$s (%6$s %7$s)', 'object-sync-for-salesforce' ),
0 ignored issues
show
Bug introduced by
The function esc_html__ was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

2068
							/** @scrutinizer ignore-call */ 
2069
       esc_html__( '%1$s: %2$s WordPress %3$s with %4$s of %5$s (%6$s %7$s)', 'object-sync-for-salesforce' ),
Loading history...
2069
							ucfirst( esc_attr( $status ) ),
0 ignored issues
show
Bug introduced by
The function esc_attr was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

2069
							ucfirst( /** @scrutinizer ignore-call */ esc_attr( $status ) ),
Loading history...
2070
							esc_attr( $op ),
2071
							esc_attr( $salesforce_mapping['wordpress_object'] ),
2072
							esc_attr( $wordpress_id_field_name ),
2073
							esc_attr( $mapping_object['wordpress_id'] ),
2074
							esc_attr( $salesforce_mapping['salesforce_object'] ),
2075
							esc_attr( $mapping_object['salesforce_id'] )
2076
						);
2077
						$result = array(
2078
							'title'   => $title,
2079
							'message' => '',
2080
							'trigger' => $sf_sync_trigger,
2081
							'parent'  => $mapping_object['wordpress_id'],
2082
							'status'  => $status,
2083
						);
2084
2085
						// hook for pull success.
2086
						do_action( $this->option_prefix . 'pull_success', $op, $result, $synced_object );
0 ignored issues
show
Bug introduced by
The function do_action was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

2086
						/** @scrutinizer ignore-call */ 
2087
      do_action( $this->option_prefix . 'pull_success', $op, $result, $synced_object );
Loading history...
2087
					} else {
2088
						$status = 'error';
2089
						// create log entry for failed delete.
2090
						$title = sprintf(
2091
							// translators: placeholders are: 1) the log status, 2) what operation is happening, 3) the name of the WordPress object type, 4) the WordPress id field name, 5) the WordPress object id value, 6) the name of the Salesforce object, 7) the Salesforce Id value.
2092
							esc_html__( '%1$s: %2$s WordPress %3$s with %4$s of %5$s (%6$s %7$s)', 'object-sync-for-salesforce' ),
2093
							ucfirst( esc_attr( $status ) ),
2094
							esc_attr( $op ),
2095
							esc_attr( $salesforce_mapping['wordpress_object'] ),
2096
							esc_attr( $wordpress_id_field_name ),
2097
							esc_attr( $mapping_object['wordpress_id'] ),
2098
							esc_attr( $salesforce_mapping['salesforce_object'] ),
2099
							esc_attr( $mapping_object['salesforce_id'] )
2100
						);
2101
2102
						// set up error message.
2103
						$default_message = esc_html__( 'An error occurred pulling this data from Salesforce. See the plugin logs.', 'object-sync-for-salesforce' );
2104
						$message         = $this->parse_error_message( $result, $default_message );
2105
2106
						$result = array(
2107
							'title'   => $title,
2108
							'message' => $message,
2109
							'trigger' => $sf_sync_trigger,
2110
							'parent'  => $mapping_object['wordpress_id'],
2111
							'status'  => $status,
2112
						);
2113
2114
						// hook for pull fail.
2115
						do_action( $this->option_prefix . 'pull_fail', $op, $result, $synced_object );
2116
					} // end if statement checking for errors.
2117
2118
					// save the log entry.
2119
					$this->logging->setup( $result );
2120
					$results[] = $result;
2121
				} else {
2122
					// create log entry for additional mapped items.
2123
					$more_ids = sprintf(
2124
						// translators: parameter is the name of the WordPress id field name.
2125
						'<p>' . esc_html__( 'The WordPress record was not deleted because there are multiple Salesforce IDs that match this WordPress %1$s.) They are:', 'object-sync-for-salesforce' ) . '</p>',
2126
						esc_attr( $wordpress_id_field_name )
2127
					);
2128
2129
					$more_ids .= '<ul>';
2130
					foreach ( $mapping_objects as $match ) {
2131
						$more_ids .= '<li>' . $match['salesforce_id'] . '</li>';
2132
					}
2133
					$more_ids .= '</ul>';
2134
2135
					$more_ids .= '<p>' . esc_html__( 'The map row between this Salesforce object and the WordPress object, as stored in the WordPress database, will be deleted, and this Salesforce object has been deleted, but WordPress object data will remain untouched.', 'object-sync-for-salesforce' ) . '</p>';
2136
2137
					$status = 'notice';
2138
					$title  = sprintf(
2139
						// translators: placeholders are: 1) the operation that is happening, 2) the name of the WordPress object type, 3) the WordPress id field name, 4) the WordPress object id value, 5) the name of the Salesforce object type, 6) the Salesforce Id.
2140
						esc_html__( '%1$s: %2$s on WordPress %3$s with %4$s of %5$s was stopped because there are other WordPress records mapped to Salesforce %6$s of %7$s', 'object-sync-for-salesforce' ),
2141
						ucfirst( esc_attr( $status ) ),
2142
						esc_attr( $op ),
2143
						esc_attr( $salesforce_mapping['wordpress_object'] ),
2144
						esc_attr( $wordpress_id_field_name ),
2145
						esc_attr( $mapping_object['wordpress_id'] ),
2146
						esc_attr( $salesforce_mapping['salesforce_object'] ),
2147
						esc_attr( $mapping_object['salesforce_id'] )
2148
					);
2149
					$notice = array(
2150
						'title'   => $title,
2151
						'message' => $more_ids,
2152
						'trigger' => $sf_sync_trigger,
2153
						'parent'  => 0,
2154
						'status'  => $status,
2155
					);
2156
					$this->logging->setup( $notice );
2157
				} // End if() on count
2158
				// delete the map row from WordPress after the WordPress row has been deleted
2159
				// we delete the map row even if the WordPress delete failed, because the Salesforce object is gone.
2160
				$this->mappings->delete_object_map( $mapping_object['id'] );
2161
				// there is no map row if we end this if statement.
2162
			} // End if() statement.
2163
		} // End if() statement.
2164
2165
		return $results;
2166
	}
2167
2168
	/**
2169
	 * Format the error message
2170
	 *
2171
	 * @param array  $result from the WordPress class.
2172
	 * @param string $default_message if there is one.
2173
	 * @return string $message what is getting stored.
2174
	 */
2175
	private function parse_error_message( $result, $default_message = '' ) {
2176
		$message = $default_message;
2177
		$errors  = $result['errors'];
2178
		// try to retrieve a usable error message to save.
2179
		if ( is_string( $errors ) ) {
2180
			$message = $errors;
2181
		} else {
2182
			if ( is_wp_error( $errors ) ) {
0 ignored issues
show
Bug introduced by
The function is_wp_error was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

2182
			if ( /** @scrutinizer ignore-call */ is_wp_error( $errors ) ) {
Loading history...
2183
				$message = $errors->get_error_message();
2184
			} else {
2185
				$message = print_r( $errors, true ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
2186
			}
2187
		}
2188
		$message = wp_kses_post( $message );
0 ignored issues
show
Bug introduced by
The function wp_kses_post was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

2188
		$message = /** @scrutinizer ignore-call */ wp_kses_post( $message );
Loading history...
2189
		return $message;
2190
	}
2191
2192
	/**
2193
	 * Clear the currently stored query for the specified content type
2194
	 *
2195
	 * @param string $type e.g. "Contact", "Account", etc.
2196
	 * @param int    $fieldmap_id is the fieldmap ID that goes with this query.
2197
	 */
2198
	public function clear_current_type_query( $type, $fieldmap_id = '' ) {
2199
		// update the last sync timestamp for this content type.
2200
		$this->increment_current_type_datetime( $type, '', $fieldmap_id );
0 ignored issues
show
Bug introduced by
'' of type string is incompatible with the type timestamp expected by parameter $next_query_modified_date of Object_Sync_Sf_Salesforc...current_type_datetime(). ( Ignorable by Annotation )

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

2200
		$this->increment_current_type_datetime( $type, /** @scrutinizer ignore-type */ '', $fieldmap_id );
Loading history...
Bug introduced by
It seems like $fieldmap_id can also be of type string; however, parameter $fieldmap_id of Object_Sync_Sf_Salesforc...current_type_datetime() 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

2200
		$this->increment_current_type_datetime( $type, '', /** @scrutinizer ignore-type */ $fieldmap_id );
Loading history...
2201
		// delete the option value for the currently pulling query for this type.
2202
		$this->pull_options->delete( 'current_query', $type, $fieldmap_id );
2203
		// delete the option value for the last pull record id.
2204
		$this->pull_options->delete( 'last_id', '', $fieldmap_id );
2205
	}
2206
2207
	/**
2208
	 * Increment the currently running query's datetime
2209
	 *
2210
	 * @param string    $type e.g. "Contact", "Account", etc.
2211
	 * @param timestamp $next_query_modified_date the last record's modified datetime, or the current time if there isn't one.
2212
	 * @param int       $fieldmap_id is the fieldmap ID that goes with this query.
2213
	 */
2214
	private function increment_current_type_datetime( $type, $next_query_modified_date = '', $fieldmap_id = '' ) {
2215
		// update the last sync timestamp for this content type.
2216
		if ( '' === $next_query_modified_date ) {
2217
			$next_query_modified_date = time();
2218
		} else {
2219
			$next_query_modified_date = strtotime( $next_query_modified_date );
2220
		}
2221
		$this->pull_options->set( 'last_sync', $type, $fieldmap_id, $next_query_modified_date );
2222
	}
2223
2224
	/**
2225
	 * Create an object map between a Salesforce object and a WordPress object
2226
	 *
2227
	 * @param array  $salesforce_object Array of the salesforce object's data.
2228
	 * @param string $wordpress_id Unique identifier for the WordPress object.
2229
	 * @param array  $field_mapping The row that maps the object types together, including which fields match which other fields.
2230
	 * @return int $wpdb->insert_id This is the database row for the map object
2231
	 */
2232
	private function create_object_map( $salesforce_object, $wordpress_id, $field_mapping ) {
2233
		// Create object map and save it.
2234
		$mapping_object = $this->mappings->create_object_map(
2235
			array(
2236
				'wordpress_id'      => $wordpress_id, // WordPress unique id.
2237
				'salesforce_id'     => $salesforce_object['Id'], // Salesforce unique id. we don't care what kind of object it is at this point.
2238
				'wordpress_object'  => $field_mapping['wordpress_object'], // keep track of what kind of WordPress object this is.
2239
				'last_sync'         => current_time( 'mysql' ),
0 ignored issues
show
Bug introduced by
The function current_time was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

2239
				'last_sync'         => /** @scrutinizer ignore-call */ current_time( 'mysql' ),
Loading history...
2240
				'last_sync_action'  => 'pull',
2241
				'last_sync_status'  => $this->mappings->status_success,
2242
				'last_sync_message' => esc_html__( 'Mapping object created via function: ', 'object-sync-for-salesforce' ) . __FUNCTION__,
0 ignored issues
show
Bug introduced by
The function esc_html__ was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

2242
				'last_sync_message' => /** @scrutinizer ignore-call */ esc_html__( 'Mapping object created via function: ', 'object-sync-for-salesforce' ) . __FUNCTION__,
Loading history...
2243
				'action'            => 'created',
2244
			)
2245
		);
2246
2247
		return $mapping_object;
2248
	}
2249
2250
	/**
2251
	 * Find out if pull is allowed for this record
2252
	 *
2253
	 * @param string $object_type Salesforce object type.
2254
	 * @param array  $object Array of the salesforce object's data.
2255
	 * @param string $sf_sync_trigger The current operation's trigger.
2256
	 * @param array  $salesforce_mapping the fieldmap that maps the two object types.
2257
	 * @param array  $map_sync_triggers the triggers that are enabled.
2258
	 * @return bool $pull_allowed Whether all this stuff allows the $result to be pulled into WordPress
2259
	 */
2260
	private function is_pull_allowed( $object_type, $object, $sf_sync_trigger, $salesforce_mapping, $map_sync_triggers ) {
2261
2262
		// default is pull is allowed.
2263
		$pull_allowed = true;
2264
2265
		// if the current fieldmap does not allow create, we need to check if there is an object map for the Salesforce object Id. if not, set pull_allowed to false.
2266
		if ( ! in_array( $this->mappings->sync_sf_create, (array) $map_sync_triggers, true ) ) {
2267
			$object_map = $this->mappings->load_object_maps_by_salesforce_id( $object['Id'], $salesforce_mapping );
2268
			if ( empty( $object_map ) ) {
2269
				$pull_allowed = false;
2270
			}
2271
		}
2272
2273
		// Hook to allow other plugins to prevent a pull per-mapping.
2274
		// Putting the pull_allowed hook here will keep the queue from storing data when it is not supposed to store it.
2275
		$pull_allowed = apply_filters( $this->option_prefix . 'pull_object_allowed', $pull_allowed, $object_type, $object, $sf_sync_trigger, $salesforce_mapping );
0 ignored issues
show
Bug introduced by
The function apply_filters was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

2275
		$pull_allowed = /** @scrutinizer ignore-call */ apply_filters( $this->option_prefix . 'pull_object_allowed', $pull_allowed, $object_type, $object, $sf_sync_trigger, $salesforce_mapping );
Loading history...
2276
2277
		// example to keep from pulling the Contact with id of abcdef.
2278
		/* // phpcs:ignore Squiz.PHP.CommentedOutCode.Found
2279
		add_filter( 'object_sync_for_salesforce_pull_object_allowed', 'check_user', 10, 5 );
2280
		// can always reduce this number if all the arguments are not necessary
2281
		function check_user( $pull_allowed, $object_type, $object, $sf_sync_trigger, $salesforce_mapping ) {
2282
			if ( 'Contact' === $object_type && 'abcdef' === $object['Id'] ) {
2283
				$pull_allowed = false;
2284
			}
2285
			return $pull_allowed;
2286
		}
2287
		*/
2288
2289
		return $pull_allowed;
2290
	}
2291
}
2292