Object_Sync_Sf_Salesforce::object_create()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
c 0
b 0
f 0
dl 0
loc 6
rs 10
cc 1
nc 1
nop 2
1
<?php
2
/**
3
 * Authorize and communicate with the Salesforce REST API. This class can make read and write calls to Salesforce, and also cache the responses in WordPress.
4
 *
5
 * @class   Object_Sync_Sf_Salesforce
6
 * @package Object_Sync_Salesforce
7
 */
8
9
defined( 'ABSPATH' ) || exit;
10
11
/**
12
 * Object_Sync_Sf_Salesforce class.
13
 */
14
class Object_Sync_Sf_Salesforce {
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
	 * The plugin's slug so we can include it when necessary
32
	 *
33
	 * @var string
34
	 */
35
	public $slug;
36
37
	/**
38
	 * The plugin's prefix when saving options to the database
39
	 *
40
	 * @var string
41
	 */
42
	public $option_prefix;
43
44
	/**
45
	 * Login credentials for the Salesforce API; comes from wp-config or from the plugin settings
46
	 *
47
	 * @var array
48
	 */
49
	public $login_credentials;
50
51
	/**
52
	 * Array of what classes in the plugin can be scheduled to occur with `wp_cron` events
53
	 *
54
	 * @var array
55
	 */
56
	public $schedulable_classes;
57
58
	/**
59
	 * Object_Sync_Sf_Logging class
60
	 *
61
	 * @var object
62
	 */
63
	public $logging;
64
65
	/**
66
	 * Object_Sync_Sf_WordPress class
67
	 *
68
	 * @var object
69
	 */
70
	public $wordpress;
71
72
	/**
73
	 * Path for the Salesforce authorize URL
74
	 *
75
	 * @var string
76
	 */
77
	public $authorize_path;
78
79
	/**
80
	 * Path for the Salesforce token URL
81
	 *
82
	 * @var string
83
	 */
84
	public $token_path;
85
86
	/**
87
	 * Callback URL for the Salesforce API
88
	 *
89
	 * @var string
90
	 */
91
	public $callback_url;
92
93
	/**
94
	 * Login URL for the Salesforce API
95
	 *
96
	 * @var string
97
	 */
98
	public $login_url;
99
100
	/**
101
	 * REST API version for Salesforce
102
	 *
103
	 * @var string
104
	 */
105
	public $rest_api_version;
106
107
	/**
108
	 * Salesforce consumer key
109
	 *
110
	 * @var string
111
	 */
112
	public $consumer_key;
113
114
	/**
115
	 * Salesforce consumer secret
116
	 *
117
	 * @var string
118
	 */
119
	public $consumer_secret;
120
121
	/**
122
	 * API call options
123
	 *
124
	 * @var array
125
	 */
126
	public $options;
127
128
	/**
129
	 * API success return codes
130
	 *
131
	 * @var array
132
	 */
133
	public $success_codes;
134
135
	/**
136
	 * API refresh return code
137
	 *
138
	 * @var int
139
	 */
140
	public $refresh_code;
141
142
	/**
143
	 * API success or refresh return codes
144
	 *
145
	 * @var array
146
	 */
147
	public $success_or_refresh_codes;
148
149
	/**
150
	 * Whether the plugin is in debug mode
151
	 *
152
	 * @var bool
153
	 */
154
	public $debug;
155
156
	/**
157
	 * API response from Salesforce
158
	 *
159
	 * @var array
160
	 */
161
	public $response;
162
163
	/**
164
	 * Constructor for Salesforce class
165
	 */
166
	public function __construct() {
167
		$this->version       = object_sync_for_salesforce()->version;
168
		$this->file          = object_sync_for_salesforce()->file;
169
		$this->slug          = object_sync_for_salesforce()->slug;
170
		$this->option_prefix = object_sync_for_salesforce()->option_prefix;
171
172
		$this->login_credentials   = object_sync_for_salesforce()->login_credentials;
173
		$this->wordpress           = object_sync_for_salesforce()->wordpress;
174
		$this->logging             = object_sync_for_salesforce()->logging;
175
		$this->schedulable_classes = object_sync_for_salesforce()->schedulable_classes;
176
177
		$this->consumer_key     = $this->login_credentials['consumer_key'];
178
		$this->consumer_secret  = $this->login_credentials['consumer_secret'];
179
		$this->login_url        = $this->login_credentials['login_url'];
180
		$this->callback_url     = $this->login_credentials['callback_url'];
181
		$this->authorize_path   = $this->login_credentials['authorize_path'];
182
		$this->token_path       = $this->login_credentials['token_path'];
183
		$this->rest_api_version = $this->login_credentials['rest_api_version'];
184
185
		$this->options = array(
186
			'cache'            => true,
187
			'cache_expiration' => $this->cache_expiration(),
188
			'type'             => 'read',
189
		);
190
191
		$this->success_codes              = array( 200, 201, 204 );
192
		$this->refresh_code               = 401;
193
		$this->success_or_refresh_codes   = $this->success_codes;
194
		$this->success_or_refresh_codes[] = $this->refresh_code;
195
196
		// use the option value for whether we're in debug mode.
197
		$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

197
		$this->debug = filter_var( /** @scrutinizer ignore-call */ get_option( $this->option_prefix . 'debug_mode', false ), FILTER_VALIDATE_BOOLEAN );
Loading history...
198
199
	}
200
201
	/**
202
	 * Converts a 15-character case-sensitive Salesforce ID to 18-character
203
	 * case-insensitive ID. If input is not 15-characters, return input unaltered.
204
	 *
205
	 * @param string $sf_id_15 15-character case-sensitive Salesforce ID.
206
	 * @return string 18-character case-insensitive Salesforce ID
207
	 */
208
	public static function convert_id( $sf_id_15 ) {
209
		if ( strlen( $sf_id_15 ) !== 15 ) {
210
			return $sf_id_15;
211
		}
212
		$chunks = str_split( $sf_id_15, 5 );
213
		$extra  = '';
214
		foreach ( $chunks as $chunk ) {
215
			$chars = str_split( $chunk, 1 );
216
			$bits  = '';
217
			foreach ( $chars as $char ) {
218
				$bits .= ( ! is_numeric( $char ) && strtoupper( $char ) === $char ) ? '1' : '0';
219
			}
220
			$map    = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ012345';
221
			$extra .= substr( $map, base_convert( strrev( $bits ), 2, 10 ), 1 );
0 ignored issues
show
Bug introduced by
base_convert(strrev($bits), 2, 10) of type string is incompatible with the type integer expected by parameter $offset of substr(). ( Ignorable by Annotation )

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

221
			$extra .= substr( $map, /** @scrutinizer ignore-type */ base_convert( strrev( $bits ), 2, 10 ), 1 );
Loading history...
222
		}
223
		return $sf_id_15 . $extra;
224
	}
225
226
	/**
227
	 * Given a Salesforce ID, return the corresponding SObject name. (Based on keyPrefix from object definition,
228
	 *
229
	 * @see https://developer.salesforce.com/forums/?id=906F0000000901ZIAQ )
230
	 *
231
	 * @param string $sf_id 15- or 18-character Salesforce ID.
232
	 * @return string sObject name, e.g. "Account", "Contact", "my__Custom_Object__c" or false if no match could be found.
233
	 */
234
	public function get_sobject_type( $sf_id ) {
235
		$objects = $this->objects(
236
			array(
237
				'keyPrefix' => substr( $sf_id, 0, 3 ),
238
			)
239
		);
240
		if ( 1 === count( $objects ) ) {
241
			// keyPrefix is unique across objects. If there is exactly one return value from objects(), then we have a match.
242
			$object = reset( $objects );
243
			return $object['name'];
244
		}
245
		// Otherwise, we did not find a match.
246
		return false;
247
	}
248
249
	/**
250
	 * Determine if this SF instance is fully configured.
251
	 */
252
	public function is_authorized() {
253
		return ! empty( $this->consumer_key ) && ! empty( $this->consumer_secret ) && $this->get_refresh_token();
254
	}
255
256
	/**
257
	 * Get REST API versions available on this Salesforce organization
258
	 * This is not an authenticated call, so it would not be a helpful test
259
	 *
260
	 * @deprecated since version 2.2.0; will be removed in 3.0.0.
261
	 */
262
	public function get_api_versions() {
263
		$options = array(
264
			'authenticated' => false,
265
			'full_url'      => true,
266
		);
267
		return $this->api_call( $this->get_instance_url() . '/services/data', array(), 'GET', $options );
268
	}
269
270
	/**
271
	 * Make a call to the Salesforce REST API.
272
	 *
273
	 * @param string $path Path to resource.
274
	 * @param array  $params Parameters to provide.
275
	 * @param string $method Method to initiate the call, such as GET or POST. Defaults to GET.
276
	 * @param array  $options Any method can supply options for the API call, and they'll be preserved as far as the curl request. They get merged with the class options.
277
	 * @param string $type Type of call. Defaults to 'rest' - currently we don't support other types. Other exammple in Drupal is 'apexrest'.
278
	 * @return mixed The requested response.
279
	 * @throws Object_Sync_Sf_Exception The plugin's exception class.
280
	 */
281
	public function api_call( $path, $params = array(), $method = 'GET', $options = array(), $type = 'rest' ) {
282
		if ( ! $this->get_access_token() ) {
283
			$this->refresh_token();
284
		}
285
		$this->response = $this->api_http_request( $path, $params, $method, $options, $type );
286
287
		// analytic calls that are expired return 404s for some absurd reason.
288
		if ( $this->response['code'] && 'run_analytics_report' === debug_backtrace()[1]['function'] ) {
289
			return $this->response;
290
		}
291
292
		switch ( $this->response['code'] ) {
293
			// The session ID or OAuth token used has expired or is invalid.
294
			case $this->response['code'] === $this->refresh_code:
295
				// Refresh token.
296
				$this->refresh_token();
297
				// Rebuild our request and repeat request.
298
				$options['is_redo'] = true;
299
				$this->response     = $this->api_http_request( $path, $params, $method, $options, $type );
300
				// Throw an error if we still have bad response.
301
				if ( ! in_array( $this->response['code'], $this->success_codes, true ) ) {
302
					throw new Object_Sync_Sf_Exception( $this->response['data'][0]['message'], $this->response['code'] );
303
				}
304
				break;
305
			case in_array( $this->response['code'], $this->success_codes, true ):
306
				// All clear.
307
				break;
308
			default:
309
				// We have problem and no specific Salesforce error provided.
310
				if ( empty( $this->response['data'] ) ) {
311
					throw new Object_Sync_Sf_Exception( $this->response['error'], $this->response['code'] );
312
				}
313
		}
314
315
		if ( ! empty( $this->response['data'][0] ) && 1 === count( $this->response['data'] ) ) {
316
			$this->response['data'] = $this->response['data'][0];
317
		}
318
319
		if ( isset( $this->response['data']['error'] ) ) {
320
			throw new Object_Sync_Sf_Exception( $this->response['data']['error_description'], $this->response['data']['error'] );
321
		}
322
323
		if ( ! empty( $this->response['data']['errorCode'] ) ) {
324
			return $this->response;
325
		}
326
327
		return $this->response;
328
	}
329
330
	/**
331
	 * Private helper to issue an SF API request.
332
	 * This method is the only place where we read to or write from the cache
333
	 *
334
	 * @param string $path Path to resource.
335
	 * @param array  $params Parameters to provide.
336
	 * @param string $method Method to initiate the call, such as GET or POST. Defaults to GET.
337
	 * @param array  $options This is the options array from the api_call method. This is where it gets merged with $this->options.
338
	 * @param string $type Type of call. Defaults to 'rest' - currently we don't support other types. Other exammple in Drupal is 'apexrest'.
339
	 * @return array The requested data.
340
	 */
341
	protected function api_http_request( $path, $params, $method, $options = array(), $type = 'rest' ) {
342
		// this merge will override a value in $this->options with the one in $options parameter if there is a matching key.
343
		$options = array_merge( $this->options, $options );
344
		$url     = $this->get_api_endpoint( $type ) . $path;
345
		if ( isset( $options['full_url'] ) && true === $options['full_url'] ) {
346
			$url = $path;
347
		}
348
		$headers = array(
349
			'Authorization'   => 'Authorization: OAuth ' . $this->get_access_token(),
350
			'Accept-Encoding' => 'Accept-Encoding: gzip, deflate',
351
		);
352
		if ( 'POST' === $method || 'PATCH' === $method ) {
353
			$headers['Content-Type'] = 'Content-Type: application/json';
354
		}
355
356
		// if headers are being passed in the $options, use them.
357
		if ( isset( $options['headers'] ) ) {
358
			$headers = array_merge( $headers, $options['headers'] );
359
		}
360
361
		if ( isset( $options['authenticated'] ) && true === $options['authenticated'] ) {
362
			$headers = false;
363
		}
364
		// if this request should be cached, see if it already exists
365
		// if it is already cached, load it. if not, load it and then cache it if it should be cached
366
		// add parameters to the array so we can tell if it was cached or not.
367
		if ( true === $options['cache'] && 'write' !== $options['type'] ) {
368
			$cached = $this->wordpress->cache_get( $url, $params );
369
			// some api calls can send a reset option, in which case we should redo the request anyway.
370
			if ( is_array( $cached ) && ( ! isset( $options['reset'] ) || true !== $options['reset'] ) ) {
371
				$result               = $cached;
372
				$result['from_cache'] = true;
373
				$result['cached']     = true;
374
			} else {
375
				$data   = wp_json_encode( $params );
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

375
				$data   = /** @scrutinizer ignore-call */ wp_json_encode( $params );
Loading history...
376
				$result = $this->http_request( $url, $data, $headers, $method, $options );
0 ignored issues
show
Bug introduced by
It seems like $headers can also be of type false; however, parameter $headers of Object_Sync_Sf_Salesforce::http_request() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

376
				$result = $this->http_request( $url, $data, /** @scrutinizer ignore-type */ $headers, $method, $options );
Loading history...
377
				if ( in_array( $result['code'], $this->success_codes, true ) ) {
378
					$result['cached'] = $this->wordpress->cache_set( $url, $params, $result, $options['cache_expiration'] );
379
				} else {
380
					$result['cached'] = false;
381
				}
382
				$result['from_cache'] = false;
383
			}
384
		} else {
385
			$data                 = wp_json_encode( $params );
386
			$result               = $this->http_request( $url, $data, $headers, $method, $options );
387
			$result['from_cache'] = false;
388
			$result['cached']     = false;
389
		}
390
391
		if ( isset( $options['is_redo'] ) && true === $options['is_redo'] ) {
392
			$result['is_redo'] = true;
393
		} else {
394
			$result['is_redo'] = false;
395
		}
396
397
		// in debug mode, this will log what we know about a Salesforce API call.
398
		if ( true === $this->debug ) {
399
			// create log entry for the api call if debug is true.
400
			$status = 'debug';
401
402
			// try to get the SOQL query if there was one.
403
			parse_str( $url, $salesforce_url_parts );
404
405
			if ( function_exists( 'array_key_first' ) ) {
406
				$query_key = array_key_first( $salesforce_url_parts );
407
			} else {
408
				$query_key = array_keys( $salesforce_url_parts )[0];
409
			}
410
411
			$is_soql_query = false;
412
			$query_end     = 'query?q';
413
414
			// does this API call include a SOQL query?
415
			// in PHP 8, there's a new str_ends_with function.
416
			if ( function_exists( 'str_ends_with' ) ) {
417
				if ( true === str_ends_with( $query_key, $query_end ) ) {
418
					$is_soql_query = true;
419
				}
420
			} else {
421
				$query_end_length = strlen( $query_end );
422
				$is_soql_query    = $query_end_length > 0 ? substr( $query_key, -$query_end_length ) === $query_end : true;
423
			}
424
425
			$title = sprintf(
426
				// translators: placeholders are: 1) the log status, 2) a sentence about whether there is an SOQL query included.
427
				esc_html__( '%1$s Salesforce API call: read the full log entry for request and response details. %2$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

427
				/** @scrutinizer ignore-call */ 
428
    esc_html__( '%1$s Salesforce API call: read the full log entry for request and response details. %2$s', 'object-sync-for-salesforce' ),
Loading history...
428
				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

428
				ucfirst( /** @scrutinizer ignore-call */ esc_attr( $status ) ),
Loading history...
429
				( false === $is_soql_query ) ? esc_html__( 'There is not an SOQL query included in this request.', 'object-sync-for-salesforce' ) : esc_html__( 'There is an SOQL query included in this request.', 'object-sync-for-salesforce' )
430
			);
431
			$body = sprintf(
432
				// translators: placeholder is: 1) the API call's HTTP method.
433
				'<p><strong>' . esc_html__( 'HTTP method:', 'object-sync-for-salesforce' ) . '</strong> %1$s</p>',
434
				esc_attr( $method )
435
			);
436
			$body .= sprintf(
437
				// translators: placeholder is: 1) the API call's URL.
438
				'<p><strong>' . esc_html__( 'URL of API call to Salesforce:', 'object-sync-for-salesforce' ) . '</strong> %1$s</p>',
439
				esc_url( $url )
0 ignored issues
show
Bug introduced by
The function esc_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

439
				/** @scrutinizer ignore-call */ 
440
    esc_url( $url )
Loading history...
440
			);
441
			if ( true === $is_soql_query ) {
442
				$query = $salesforce_url_parts[ $query_key ];
443
				$soql  = urldecode( $query );
444
				$body .= sprintf(
445
					// translators: placeholder is: 1) the SOQL query that was run.
446
					'<h3>' . esc_html__( 'SOQL query that was sent to Salesforce', 'object-sync-for-salesforce' ) . '</h3> <p>%1$s</p>',
447
					'<code>' . esc_html( $soql ) . '</code>'
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

447
					'<code>' . /** @scrutinizer ignore-call */ esc_html( $soql ) . '</code>'
Loading history...
448
				);
449
			}
450
			if ( ! empty( $params ) ) {
451
				$body .= sprintf(
452
					// translators: placeholder is: 1) the params sent to Salesforce.
453
					'<h3>' . esc_html__( 'Parameters sent to the Salesforce API', 'object-sync-for-salesforce' ) . '</h3> <div>%1$s</div>',
454
					print_r( $params, true ) // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
0 ignored issues
show
Bug introduced by
It seems like print_r($params, 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

454
					/** @scrutinizer ignore-type */ print_r( $params, true ) // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
Loading history...
455
				);
456
			}
457
			$body .= sprintf(
458
				// translators: placeholder is: 1) the API call's result.
459
				'<h3>' . esc_html__( 'API result from Salesforce', 'object-sync-for-salesforce' ) . '</h3> <div>%1$s</div>',
460
				print_r( $result, true ) // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
461
			);
462
			$this->logging->setup(
463
				$title,
464
				$body,
465
				0,
466
				0,
467
				$status
468
			);
469
		}
470
471
		return $result;
472
	}
473
474
	/**
475
	 * Make the HTTP request. Wrapper around curl().
476
	 *
477
	 * @param string $url Path to make request from.
478
	 * @param array  $data The request body.
479
	 * @param array  $headers Request headers to send as name => value.
480
	 * @param string $method Method to initiate the call, such as GET or POST. Defaults to GET.
481
	 * @param array  $options This is the options array from the api_http_request method.
482
	 * @return array Salesforce response object.
483
	 */
484
	protected function http_request( $url, $data, $headers = array(), $method = 'GET', $options = array() ) {
485
		// Build the request, including path and headers. Internal use.
486
487
		/**
488
		 * Short-circuits the return value of an HTTP API call.
489
		 *
490
		 * This allows other plugins to communicate with the Salesforce API on behalf of
491
		 * Object Sync for Salesforce, for example by using the WordPress HTTP API.
492
		 *
493
		 * @since 2.2.7
494
		 *
495
		 * @param null|array $check   Whether to short-circuit the HTTP request. Default null.
496
		 * @param string     $url     Path to make request from.
497
		 * @param array      $data    The request body.
498
		 * @param array      $headers Request headers to send as name => value.
499
		 * @param string     $method  Method to initiate the call, such as GET or POST. Defaults to GET.
500
		 * @param array      $options This is the options array from the api_http_request method.
501
		 */
502
		$check = apply_filters( $this->option_prefix . 'http_request', null, $url, $data, $headers, $method, $options );
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

502
		$check = /** @scrutinizer ignore-call */ apply_filters( $this->option_prefix . 'http_request', null, $url, $data, $headers, $method, $options );
Loading history...
503
504
		if ( null !== $check ) {
505
			return $check;
506
		}
507
508
		/*
509
		 * Note: curl is used because wp_remote_get, wp_remote_post, wp_remote_request don't work. Salesforce returns various errors.
510
		 * todo: There is a GitHub branch attempting with the goal of addressing this: https://github.com/MinnPost/object-sync-for-salesforce/issues/94
511
		*/
512
513
		$curl = curl_init();
514
		curl_setopt( $curl, CURLOPT_URL, $url );
515
		curl_setopt( $curl, CURLOPT_RETURNTRANSFER, true );
516
		curl_setopt( $curl, CURLOPT_FOLLOWLOCATION, true );
517
		if ( false !== $headers ) {
0 ignored issues
show
introduced by
The condition false !== $headers is always true.
Loading history...
518
			curl_setopt( $curl, CURLOPT_HTTPHEADER, $headers );
519
		} else {
520
			curl_setopt( $curl, CURLOPT_HEADER, false );
521
		}
522
523
		if ( 'POST' === $method ) {
524
			curl_setopt( $curl, CURLOPT_POST, true );
525
			curl_setopt( $curl, CURLOPT_POSTFIELDS, $data );
526
		} elseif ( 'PATCH' === $method || 'DELETE' === $method ) {
527
			curl_setopt( $curl, CURLOPT_CUSTOMREQUEST, $method );
528
			curl_setopt( $curl, CURLOPT_POSTFIELDS, $data );
529
		}
530
		$json_response = curl_exec( $curl ); // this is possibly gzipped json data.
531
		$code          = curl_getinfo( $curl, CURLINFO_HTTP_CODE );
532
533
		if ( ( 'PATCH' === $method || 'DELETE' === $method ) && '' === $json_response && 204 === $code ) {
534
			// delete and patch requests return a 204 with an empty body upon success for whatever reason.
535
			$data = array(
536
				'success' => true,
537
				'body'    => '',
538
			);
539
			curl_close( $curl );
540
541
			$result = array(
542
				'code' => $code,
543
			);
544
545
			$return_format = isset( $options['return_format'] ) ? $options['return_format'] : 'array';
546
547
			switch ( $return_format ) {
548
				case 'array':
549
					$result['data'] = $data;
550
					break;
551
				case 'json':
552
					$result['json'] = wp_json_encode( $data );
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

552
					$result['json'] = /** @scrutinizer ignore-call */ wp_json_encode( $data );
Loading history...
553
					break;
554
				case 'both':
555
					$result['json'] = wp_json_encode( $data );
556
					$result['data'] = $data;
557
					break;
558
			}
559
560
			return $result;
561
		}
562
563
		if ( ( ord( $json_response[0] ) == 0x1f ) && ( ord( $json_response[1] ) == 0x8b ) ) {
564
			// skip header and ungzip the data.
565
			$json_response = gzinflate( substr( $json_response, 10 ) );
0 ignored issues
show
Bug introduced by
It seems like $json_response can also be of type true; however, parameter $string of substr() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

565
			$json_response = gzinflate( substr( /** @scrutinizer ignore-type */ $json_response, 10 ) );
Loading history...
566
		}
567
		$data = json_decode( $json_response, true ); // decode it into an array.
0 ignored issues
show
Bug introduced by
It seems like $json_response can also be of type true; however, parameter $json of json_decode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

567
		$data = json_decode( /** @scrutinizer ignore-type */ $json_response, true ); // decode it into an array.
Loading history...
568
569
		// don't use the exception if the status is a success one, or if it just needs a refresh token (salesforce uses 401 for this).
570
		if ( ! in_array( $code, $this->success_or_refresh_codes, true ) ) {
571
			$curl_error = curl_error( $curl );
572
			if ( '' !== $curl_error ) {
573
				// create log entry for failed curl.
574
				$status = 'error';
575
				$title  = sprintf(
576
					// translators: placeholders are: 1) the log status, 2) the HTTP status code returned by the Salesforce API request.
577
					esc_html__( '%1$s: %2$s: on Salesforce HTTP request', '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

577
					/** @scrutinizer ignore-call */ 
578
     esc_html__( '%1$s: %2$s: on Salesforce HTTP request', 'object-sync-for-salesforce' ),
Loading history...
578
					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

578
					ucfirst( /** @scrutinizer ignore-call */ esc_attr( $status ) ),
Loading history...
579
					absint( $code )
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

579
					/** @scrutinizer ignore-call */ 
580
     absint( $code )
Loading history...
580
				);
581
				$this->logging->setup(
582
					$title,
583
					$curl_error,
584
					0,
585
					0,
586
					$status
587
				);
588
			} elseif ( isset( $data[0]['errorCode'] ) && '' !== $data[0]['errorCode'] ) { // salesforce uses this structure to return errors
589
				// create log entry for failed curl.
590
				$status = 'error';
591
				$title  = sprintf(
592
					// translators: placeholders are: 1) the log status, 2) the HTTP status code returned by the Salesforce API request.
593
					esc_html__( '%1$s: %2$s: on Salesforce HTTP request', 'object-sync-for-salesforce' ),
594
					ucfirst( esc_attr( $status ) ),
595
					absint( $code )
596
				);
597
				$body = sprintf(
598
					// translators: placeholders are: 1) the URL requested, 2) the message returned by the error, 3) the server code returned.
599
					'<p>' . esc_html__( 'URL: %1$s', 'object-sync-for-salesforce' ) . '</p><p>' . esc_html__( 'Message: %2$s', 'object-sync-for-salesforce' ) . '</p><p>' . esc_html__( 'Code: %3$s', 'object-sync-for-salesforce' ),
600
					esc_attr( $url ),
601
					esc_html( $data[0]['message'] ),
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

601
					/** @scrutinizer ignore-call */ 
602
     esc_html( $data[0]['message'] ),
Loading history...
602
					absint( $code )
603
				);
604
				$this->logging->setup(
605
					$title,
606
					$body,
607
					0,
608
					0,
609
					$status
610
				);
611
			} else {
612
				// create log entry for failed curl.
613
				$status = 'error';
614
				$title  = sprintf(
615
					// translators: placeholders are: 1) the log status, 2) the HTTP status code returned by the Salesforce API request.
616
					esc_html__( '%1$s: %2$s: on Salesforce HTTP request', 'object-sync-for-salesforce' ),
617
					ucfirst( esc_attr( $status ) ),
618
					absint( $code )
619
				);
620
				$this->logging->setup(
621
					$title,
622
					print_r( $data, true ), // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
623
					0,
624
					0,
625
					$status
626
				);
627
			} // End if() statement.
628
		} // End if() statement.
629
630
		curl_close( $curl );
631
632
		$result = array(
633
			'code' => $code,
634
		);
635
636
		$return_format = isset( $options['return_format'] ) ? $options['return_format'] : 'array';
637
638
		switch ( $return_format ) {
639
			case 'array':
640
				$result['data'] = $data;
641
				break;
642
			case 'json':
643
				$result['json'] = $json_response;
644
				break;
645
			case 'both':
646
				$result['json'] = $json_response;
647
				$result['data'] = $data;
648
				break;
649
		}
650
651
		return $result;
652
653
	}
654
655
	/**
656
	 * Get the API end point for a given type of the API.
657
	 *
658
	 * @param string $api_type E.g., rest, partner, enterprise.
659
	 * @return string Complete URL endpoint for API access.
660
	 */
661
	public function get_api_endpoint( $api_type = 'rest' ) {
662
		// Special handling for apexrest, since it's not in the identity object.
663
		if ( 'apexrest' === $api_type ) {
664
			$url = $this->get_instance_url() . '/services/apexrest/';
665
		} else {
666
			$identity = $this->get_identity();
667
			$url      = str_replace( '{version}', $this->rest_api_version, $identity['urls'][ $api_type ] );
668
			if ( '' === $identity ) {
0 ignored issues
show
introduced by
The condition '' === $identity is always false.
Loading history...
669
				$url = $this->get_instance_url() . '/services/data/v' . $this->rest_api_version . '/';
670
			}
671
		}
672
		return $url;
673
	}
674
675
	/**
676
	 * Get the SF instance URL. Useful for linking to objects.
677
	 */
678
	public function get_instance_url() {
679
		return get_option( $this->option_prefix . 'instance_url', '' );
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

679
		return /** @scrutinizer ignore-call */ get_option( $this->option_prefix . 'instance_url', '' );
Loading history...
680
	}
681
682
	/**
683
	 * Set the SF instance URL.
684
	 *
685
	 * @param string $url URL to set.
686
	 */
687
	protected function set_instance_url( $url ) {
688
		update_option( $this->option_prefix . 'instance_url', $url );
0 ignored issues
show
Bug introduced by
The function update_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

688
		/** @scrutinizer ignore-call */ 
689
  update_option( $this->option_prefix . 'instance_url', $url );
Loading history...
689
	}
690
691
	/**
692
	 * Get the access token.
693
	 */
694
	public function get_access_token() {
695
		return get_option( $this->option_prefix . 'access_token', '' );
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

695
		return /** @scrutinizer ignore-call */ get_option( $this->option_prefix . 'access_token', '' );
Loading history...
696
	}
697
698
	/**
699
	 * Set the access token.
700
	 * It is stored in session.
701
	 *
702
	 * @param string $token Access token from Salesforce.
703
	 */
704
	protected function set_access_token( $token ) {
705
		update_option( $this->option_prefix . 'access_token', $token );
0 ignored issues
show
Bug introduced by
The function update_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

705
		/** @scrutinizer ignore-call */ 
706
  update_option( $this->option_prefix . 'access_token', $token );
Loading history...
706
	}
707
708
	/**
709
	 * Get refresh token.
710
	 */
711
	protected function get_refresh_token() {
712
		return get_option( $this->option_prefix . 'refresh_token', '' );
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

712
		return /** @scrutinizer ignore-call */ get_option( $this->option_prefix . 'refresh_token', '' );
Loading history...
713
	}
714
715
	/**
716
	 * Set refresh token.
717
	 *
718
	 * @param string $token Refresh token from Salesforce.
719
	 */
720
	protected function set_refresh_token( $token ) {
721
		update_option( $this->option_prefix . 'refresh_token', $token );
0 ignored issues
show
Bug introduced by
The function update_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

721
		/** @scrutinizer ignore-call */ 
722
  update_option( $this->option_prefix . 'refresh_token', $token );
Loading history...
722
	}
723
724
	/**
725
	 * Refresh access token based on the refresh token. Updates session variable.
726
	 *
727
	 * Todo: figure out how to do this as part of the schedule class
728
	 * This is a scheduleable class and so we could add a method from this class to run every 24 hours, but it's unclear to me that we need it. salesforce seems to refresh itself as it needs to.
729
	 * but it could be a performance boost to do it at scheduleable intervals instead.
730
	 *
731
	 * @throws Object_Sync_Sf_Exception The plugin's exception class.
732
	 */
733
	protected function refresh_token() {
734
		$refresh_token = $this->get_refresh_token();
735
		if ( empty( $refresh_token ) ) {
736
			throw new Object_Sync_Sf_Exception( esc_html__( 'There is no refresh token.', '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

736
			throw new Object_Sync_Sf_Exception( /** @scrutinizer ignore-call */ esc_html__( 'There is no refresh token.', 'object-sync-for-salesforce' ) );
Loading history...
737
		}
738
739
		$data = array(
740
			'grant_type'    => 'refresh_token',
741
			'refresh_token' => $refresh_token,
742
			'client_id'     => $this->consumer_key,
743
			'client_secret' => $this->consumer_secret,
744
		);
745
746
		$url      = $this->login_url . $this->token_path;
747
		$headers  = array(
748
			// This is an undocumented requirement on Salesforce's end.
749
			'Content-Type'    => 'Content-Type: application/x-www-form-urlencoded',
750
			'Accept-Encoding' => 'Accept-Encoding: gzip, deflate',
751
			'Authorization'   => 'Authorization: OAuth ' . $this->get_access_token(),
752
		);
753
		$headers  = false;
754
		$response = $this->http_request( $url, $data, $headers, 'POST' );
0 ignored issues
show
Bug introduced by
$headers of type false is incompatible with the type array expected by parameter $headers of Object_Sync_Sf_Salesforce::http_request(). ( Ignorable by Annotation )

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

754
		$response = $this->http_request( $url, $data, /** @scrutinizer ignore-type */ $headers, 'POST' );
Loading history...
755
756
		if ( 200 !== $response['code'] ) {
757
			throw new Object_Sync_Sf_Exception(
758
				esc_html(
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

758
				/** @scrutinizer ignore-call */ 
759
    esc_html(
Loading history...
759
					sprintf(
760
						__( 'Unable to get a Salesforce access token. Salesforce returned the following errorCode: ', 'object-sync-for-salesforce' ) . $response['code']
0 ignored issues
show
Bug introduced by
The function __ 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

760
						/** @scrutinizer ignore-call */ 
761
      __( 'Unable to get a Salesforce access token. Salesforce returned the following errorCode: ', 'object-sync-for-salesforce' ) . $response['code']
Loading history...
761
					)
762
				),
763
				$response['code']
764
			);
765
		}
766
767
		$data = $response['data'];
768
769
		if ( is_array( $data ) && isset( $data['error'] ) ) {
770
			throw new Object_Sync_Sf_Exception( $data['error_description'], $data['error'] );
771
		}
772
773
		$this->set_access_token( $data['access_token'] );
774
		$this->set_identity( $data['id'] );
775
		$this->set_instance_url( $data['instance_url'] );
776
	}
777
778
	/**
779
	 * Retrieve and store the Salesforce identity given an ID url.
780
	 *
781
	 * @param string $id Identity URL.
782
	 *
783
	 * @throws Object_Sync_Sf_Exception The plugin's exception class.
784
	 */
785
	protected function set_identity( $id ) {
786
		$headers  = array(
787
			'Authorization'   => 'Authorization: OAuth ' . $this->get_access_token(),
788
			// 'Content-type'  => 'application/json', todo: remove this if it's not necessary
789
			'Accept-Encoding' => 'Accept-Encoding: gzip, deflate',
790
		);
791
		$response = $this->http_request( $id, null, $headers );
792
		if ( 200 !== $response['code'] ) {
793
			throw new Object_Sync_Sf_Exception( esc_html__( 'Unable to access identity service.', 'object-sync-for-salesforce' ), $response['code'] );
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

793
			throw new Object_Sync_Sf_Exception( /** @scrutinizer ignore-call */ esc_html__( 'Unable to access identity service.', 'object-sync-for-salesforce' ), $response['code'] );
Loading history...
794
		}
795
		$data = $response['data'];
796
		update_option( $this->option_prefix . 'identity', $data );
0 ignored issues
show
Bug introduced by
The function update_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

796
		/** @scrutinizer ignore-call */ 
797
  update_option( $this->option_prefix . 'identity', $data );
Loading history...
797
	}
798
799
	/**
800
	 * Return the Salesforce identity, which is stored in a variable.
801
	 *
802
	 * @return array Returns false if no identity has been stored.
803
	 */
804
	public function get_identity() {
805
		return get_option( $this->option_prefix . 'identity', false );
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

805
		return /** @scrutinizer ignore-call */ get_option( $this->option_prefix . 'identity', false );
Loading history...
806
	}
807
808
	/**
809
	 * OAuth step 1: Redirect to Salesforce and request and authorization code.
810
	 */
811
	public function get_authorization_code() {
812
		$url = add_query_arg(
0 ignored issues
show
Bug introduced by
The function add_query_arg 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

812
		$url = /** @scrutinizer ignore-call */ add_query_arg(
Loading history...
813
			array(
814
				'response_type' => 'code',
815
				'client_id'     => $this->consumer_key,
816
				'redirect_uri'  => $this->callback_url,
817
			),
818
			$this->login_url . $this->authorize_path
819
		);
820
		return $url;
821
	}
822
823
	/**
824
	 * OAuth step 2: Exchange an authorization code for an access token.
825
	 *
826
	 * @param string $code Code from Salesforce.
827
	 * @throws Object_Sync_Sf_Exception The plugin's exception class.
828
	 */
829
	public function request_token( $code ) {
830
		$data = array(
831
			'code'          => $code,
832
			'grant_type'    => 'authorization_code',
833
			'client_id'     => $this->consumer_key,
834
			'client_secret' => $this->consumer_secret,
835
			'redirect_uri'  => $this->callback_url,
836
		);
837
838
		$url      = $this->login_url . $this->token_path;
839
		$headers  = array(
840
			// This is an undocumented requirement on SF's end.
841
			// 'Content-Type'  => 'application/x-www-form-urlencoded', todo: remove this if it's not needed.
842
			'Accept-Encoding' => 'Accept-Encoding: gzip, deflate',
843
		);
844
		$response = $this->http_request( $url, $data, $headers, 'POST' );
845
846
		$data = $response['data'];
847
848
		if ( 200 !== $response['code'] ) {
849
			$error = isset( $data['error_description'] ) ? $data['error_description'] : $response['error'];
850
			throw new Object_Sync_Sf_Exception( $error, $response['code'] );
851
		}
852
853
		// Ensure all required attributes are returned. They can be omitted if the
854
		// OAUTH scope is inadequate.
855
		$required = array( 'refresh_token', 'access_token', 'id', 'instance_url' );
856
		foreach ( $required as $key ) {
857
			if ( ! isset( $data[ $key ] ) ) {
858
				return false;
859
			}
860
		}
861
862
		$this->set_refresh_token( $data['refresh_token'] );
863
		$this->set_access_token( $data['access_token'] );
864
		$this->set_identity( $data['id'] );
865
		$this->set_instance_url( $data['instance_url'] );
866
867
		return true;
868
	}
869
870
	/* Core API calls */
871
872
	/**
873
	 * Available objects and their metadata for your organization's data.
874
	 * part of core API calls. this call does require authentication, and the basic url it becomes is like this:
875
	 * https://instance.salesforce.com/services/data/v#.0/sobjects
876
	 * note: updateable is really how the api spells it
877
	 *
878
	 * @param array $conditions Associative array of filters to apply to the returned objects. Filters are applied after the list is returned from Salesforce.
879
	 * @param bool  $reset Whether to reset the cache and retrieve a fresh version from Salesforce.
880
	 * @return array Available objects and metadata.
881
	 */
882
	public function objects(
883
		$conditions = array(
884
			'updateable'  => true,
885
			'triggerable' => true,
886
		),
887
		$reset = false
888
	) {
889
890
		$options = array(
891
			'reset' => $reset,
892
		);
893
		$result  = $this->api_call( 'sobjects', array(), 'GET', $options );
894
895
		if ( ! empty( $conditions ) ) {
896
			foreach ( $result['data']['sobjects'] as $key => $object ) {
897
				foreach ( $conditions as $condition => $value ) {
898
					if ( $object[ $condition ] !== $value ) {
899
						unset( $result['data']['sobjects'][ $key ] );
900
					}
901
				}
902
			}
903
		}
904
905
		ksort( $result['data']['sobjects'] );
906
907
		return $result['data']['sobjects'];
908
	}
909
910
	/**
911
	 * Use SOQL to get objects based on query string. Part of core API calls.
912
	 *
913
	 * @param string $query The SOQL query.
914
	 * @param array  $options Allow for the query to have options based on what the user needs from it, ie caching, read/write, etc.
915
	 * @param bool   $all Whether this should get all results for the query.
916
	 * @param bool   $explain If set, Salesforce will return feedback on the query performance.
917
	 * @return array Array of Salesforce objects that match the query.
918
	 */
919
	public function query( $query, $options = array(), $all = false, $explain = false ) {
920
		$search_data = array(
921
			'q' => (string) $query,
922
		);
923
		if ( true === $explain ) {
924
			$search_data['explain'] = $search_data['q'];
925
			unset( $search_data['q'] );
926
		}
927
		// all is a search through deleted and merged data as well.
928
		if ( true === $all ) {
929
			$path = 'queryAll';
930
		} else {
931
			$path = 'query';
932
		}
933
		$result = $this->api_call( $path . '?' . http_build_query( $search_data ), array(), 'GET', $options );
934
		return $result;
935
	}
936
937
	/**
938
	 * Retrieve all the metadata for an object. Part of core API calls.
939
	 *
940
	 * @param string $name Object type name, E.g., Contact, Account, etc.
941
	 * @param bool   $reset Whether to reset the cache and retrieve a fresh version from Salesforce.
942
	 * @return array All the metadata for an object, including information about each field, URLs, and child relationships.
943
	 */
944
	public function object_describe( $name, $reset = false ) {
945
		if ( empty( $name ) ) {
946
			return array();
947
		}
948
		$options = array(
949
			'reset' => $reset,
950
		);
951
		$object  = $this->api_call( "sobjects/{$name}/describe", array(), 'GET', $options );
952
		// Sort field properties, because salesforce API always provides them in a
953
		// random order. We sort them so that stored and exported data are
954
		// standardized and predictable.
955
		$fields = array();
956
		foreach ( $object['data']['fields'] as &$field ) {
957
			ksort( $field );
958
			if ( ! empty( $field['picklistValues'] ) ) {
959
				foreach ( $field['picklistValues'] as &$picklist_value ) {
960
					ksort( $picklist_value );
961
				}
962
			}
963
			$fields[ $field['name'] ] = $field;
964
		}
965
		ksort( $fields );
966
		$object['fields'] = $fields;
967
		return $object;
968
	}
969
970
	/**
971
	 * Create a new object of the given type. Part of core API calls.
972
	 *
973
	 * @param string $name Object type name, E.g., Contact, Account, etc.
974
	 * @param array  $params Values of the fields to set for the object.
975
	 * @return array
976
	 *   json: {"id":"00190000001pPvHAAU","success":true,"errors":[]}
977
	 *   code: 201
978
	 *   data:
979
	 *     "id" : "00190000001pPvHAAU",
980
	 *     "success" : true
981
	 *     "errors" : [ ],
982
	 *   from_cache:
983
	 *   cached:
984
	 *   is_redo:
985
	 */
986
	public function object_create( $name, $params ) {
987
		$options = array(
988
			'type' => 'write',
989
		);
990
		$result  = $this->api_call( "sobjects/{$name}", $params, 'POST', $options );
991
		return $result;
992
	}
993
994
	/**
995
	 * Create new records or update existing records.
996
	 * The new records or updated records are based on the value of the specified
997
	 * field. If the value is not unique, REST API returns a 300 response with
998
	 * the list of matching records. Part of core API calls.
999
	 *
1000
	 * @param string $name Object type name, E.g., Contact, Account.
1001
	 * @param string $key The field to check if this record should be created or updated.
1002
	 * @param string $value The value for this record of the field specified for $key.
1003
	 * @param array  $params Values of the fields to set for the object.
1004
	 * @return array
1005
	 *   json: {"id":"00190000001pPvHAAU","success":true,"errors":[]}
1006
	 *   code: 201
1007
	 *   data:
1008
	 *     "id" : "00190000001pPvHAAU",
1009
	 *     "success" : true
1010
	 *     "errors" : [ ],
1011
	 *   from_cache:
1012
	 *   cached:
1013
	 *   is_redo:
1014
	 */
1015
	public function object_upsert( $name, $key, $value, $params ) {
1016
		$options = array(
1017
			'type' => 'write',
1018
		);
1019
		// If key is set, remove from $params to avoid UPSERT errors.
1020
		if ( isset( $params[ $key ] ) ) {
1021
			unset( $params[ $key ] );
1022
		}
1023
1024
		// allow developers to change both the key and value by which objects should be matched.
1025
		$key   = apply_filters( $this->option_prefix . 'modify_upsert_key', $key );
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

1025
		$key   = /** @scrutinizer ignore-call */ apply_filters( $this->option_prefix . 'modify_upsert_key', $key );
Loading history...
1026
		$value = apply_filters( $this->option_prefix . 'modify_upsert_value', $value );
1027
1028
		$data = $this->api_call( "sobjects/{$name}/{$key}/{$value}", $params, 'PATCH', $options );
1029
		if ( 300 === $this->response['code'] ) {
1030
			$data['message'] = esc_html( 'The value provided is not unique.' );
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

1030
			$data['message'] = /** @scrutinizer ignore-call */ esc_html( 'The value provided is not unique.' );
Loading history...
1031
		}
1032
		return $data;
1033
	}
1034
1035
	/**
1036
	 * Update an existing object. Part of core API calls.
1037
	 *
1038
	 * @param string $name Object type name, E.g., Contact, Account.
1039
	 * @param string $id Salesforce id of the object.
1040
	 * @param array  $params Values of the fields to set for the object.
1041
	 * @return array
1042
	 * json: {"success":true,"body":""}
1043
	 * code: 204
1044
	 * data:
1045
	 * success: 1
1046
	 * body:
1047
	 *   from_cache:
1048
	 *   cached:
1049
	 *   is_redo:
1050
	 */
1051
	public function object_update( $name, $id, $params ) {
1052
		$options = array(
1053
			'type' => 'write',
1054
		);
1055
		$result  = $this->api_call( "sobjects/{$name}/{$id}", $params, 'PATCH', $options );
1056
		return $result;
1057
	}
1058
1059
	/**
1060
	 * Return a full loaded Salesforce object. Part of core API calls.
1061
	 *
1062
	 * @param string $name Object type name, E.g., Contact, Account.
1063
	 * @param string $id Salesforce id of the object.
1064
	 * @param array  $options Optional options to pass to the API call.
1065
	 * @return object Object of the requested Salesforce object.
1066
	 */
1067
	public function object_read( $name, $id, $options = array() ) {
1068
		return $this->api_call( "sobjects/{$name}/{$id}", array(), 'GET', $options );
1069
	}
1070
1071
	/**
1072
	 * Make a call to the Analytics API. Part of core API calls.
1073
	 *
1074
	 * @param string $name Object type name, E.g., Report.
1075
	 * @param string $id Salesforce id of the object.
1076
	 * @param string $route What comes after the ID? E.g. instances, ?includeDetails=True.
1077
	 * @param array  $params Params to put with the request.
1078
	 * @param string $method GET or POST.
1079
	 * @return object Object of the requested Salesforce object.
1080
	 * */
1081
	public function analytics_api( $name, $id, $route = '', $params = array(), $method = 'GET' ) {
1082
		return $this->api_call( "analytics/{$name}/{$id}/{$route}", $params, $method );
1083
	}
1084
1085
	/**
1086
	 * Run a specific Analytics report. Part of core API calls.
1087
	 *
1088
	 * @param string $id Salesforce id of the object.
1089
	 * @param bool   $async Whether the report is asynchronous.
1090
	 * @param bool   $clear_cache Whether the cache is being cleared.
1091
	 * @param array  $params Params to put with the request.
1092
	 * @param string $method GET or POST.
1093
	 * @param string $report_cache_expiration How long to keep the report's cache result around for.
1094
	 * @param bool   $cache_instance Whether to cache the instance results.
1095
	 * @param string $instance_cache_expiration How long to keep the instance's cache result around for.
1096
	 * @return object Object of the requested Salesforce object.
1097
	 */
1098
	public function run_analytics_report( $id, $async = true, $clear_cache = false, $params = array(), $method = 'GET', $report_cache_expiration = '', $cache_instance = true, $instance_cache_expiration = '' ) {
0 ignored issues
show
Unused Code introduced by
The parameter $params is not used and could be removed. ( Ignorable by Annotation )

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

1098
	public function run_analytics_report( $id, $async = true, $clear_cache = false, /** @scrutinizer ignore-unused */ $params = array(), $method = 'GET', $report_cache_expiration = '', $cache_instance = true, $instance_cache_expiration = '' ) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1099
1100
		$id         = $this->convert_id( $id );
1101
		$report_url = 'analytics/reports/' . $id . '/instances';
1102
1103
		if ( true === $clear_cache ) {
1104
			delete_transient( $report_url );
0 ignored issues
show
Bug introduced by
The function delete_transient 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

1104
			/** @scrutinizer ignore-call */ 
1105
   delete_transient( $report_url );
Loading history...
1105
		}
1106
1107
		$instance_id = $this->wordpress->cache_get( $report_url, '' );
1108
1109
		// there is no stored instance id or this is synchronous; retrieve the results for that instance.
1110
		if ( false === $async || false === $instance_id ) {
1111
1112
			$result = $this->analytics_api(
1113
				'reports',
1114
				$id,
1115
				'?includeDetails=true',
1116
				array(),
1117
				'GET'
1118
			);
1119
			// if we get a reportmetadata array out of this, continue.
1120
			if ( is_array( $result['data']['reportMetadata'] ) ) {
1121
				$params = array(
1122
					'reportMetadata' => $result['data']['reportMetadata'],
1123
				);
1124
				$report = $this->analytics_api(
1125
					'reports',
1126
					$id,
1127
					'instances',
1128
					$params,
1129
					'POST'
1130
				);
1131
				// if we get an id from the post, that is the instance id.
1132
				if ( isset( $report['data']['id'] ) ) {
1133
					$instance_id = $report['data']['id'];
1134
				} else {
1135
					// run the call again if we don't have an instance id.
1136
					$this->run_analytics_report( $id, true );
1137
				}
1138
1139
				// cache the instance id so we can get the report results if they are applicable.
1140
				if ( '' === $report_cache_expiration ) {
1141
					$report_cache_expiration = $this->cache_expiration();
1142
				}
1143
				$this->wordpress->cache_set( $report_url, '', $instance_id, $report_cache_expiration );
1144
			} else {
1145
				// run the call again if we don't have a reportMetadata array.
1146
				$this->run_analytics_report( $id, true );
1147
			}
1148
		} // End if() statement.
1149
1150
		$result = $this->api_call( $report_url . "/{$instance_id}", array(), $method );
1151
1152
		// the report instance is expired. rerun it.
1153
		if ( 404 === $result['code'] ) {
1154
			$this->run_analytics_report( $id, true, true );
1155
		}
1156
1157
		// cache the instance results as a long fallback if the setting says so
1158
		// do this because salesforce will have errors if the instance has expired or is currently running
1159
		// remember: the result of the above api_call is already cached (or not) according to the plugin's generic settings
1160
		// this is fine I think, although it is a bit of redundancy in this case.
1161
		if ( true === $cache_instance ) {
1162
			$cached = $this->wordpress->cache_get( $report_url . '_instance_cached', '' );
1163
			if ( is_array( $cached ) ) {
1164
				$result = $cached;
1165
			} else {
1166
				if ( 'Success' === $result['data']['attributes']['status'] ) {
1167
					if ( '' === $instance_cache_expiration ) {
1168
						$instance_cache_expiration = $this->cache_expiration();
1169
					}
1170
					$this->wordpress->cache_set( $report_url . '_instance_cached', '', $result, $instance_cache_expiration );
1171
				}
1172
			}
1173
		}
1174
		return $result;
1175
	}
1176
1177
	/**
1178
	 * Return a full loaded Salesforce object from External ID. Part of core API calls.
1179
	 *
1180
	 * @param string $name Object type name, E.g., Contact, Account.
1181
	 * @param string $field Salesforce external id field name.
1182
	 * @param string $value Value of external id.
1183
	 * @param array  $options Optional options to pass to the API call.
1184
	 * @return object Object of the requested Salesforce object.
1185
	 */
1186
	public function object_readby_external_id( $name, $field, $value, $options = array() ) {
1187
		return $this->api_call( "sobjects/{$name}/{$field}/{$value}", array(), 'GET', $options );
1188
	}
1189
1190
	/**
1191
	 * Delete a Salesforce object. Part of core API calls
1192
	 *
1193
	 * @param string $name Object type name, E.g., Contact, Account.
1194
	 * @param string $id Salesforce id of the object.
1195
	 * @return array
1196
	 */
1197
	public function object_delete( $name, $id ) {
1198
		$options = array(
1199
			'type' => 'write',
1200
		);
1201
		$result  = $this->api_call( "sobjects/{$name}/{$id}", array(), 'DELETE', $options );
1202
		return $result;
1203
	}
1204
1205
	/**
1206
	 * Retrieves the list of individual objects that have been deleted within the
1207
	 * given timespan for a specified object type.
1208
	 *
1209
	 * @param string $type Object type name, E.g., Contact, Account.
1210
	 * @param string $start_date Start date to check for deleted objects (in ISO 8601 format).
1211
	 * @param string $end_date End date to check for deleted objects (in ISO 8601 format).
1212
	 * @return mixed $result
1213
	 */
1214
	public function get_deleted( $type, $start_date, $end_date ) {
1215
		$options = array(
1216
			'cache' => false,
1217
		); // this is timestamp level specific; we don't cache it.
1218
		return $this->api_call( "sobjects/{$type}/deleted/?start={$start_date}&end={$end_date}", array(), 'GET', $options );
1219
	}
1220
1221
1222
	/**
1223
	 * Return a list of available resources for the configured API version. Part of core API calls.
1224
	 *
1225
	 * @return array Associative array keyed by name with a URI value.
1226
	 */
1227
	public function list_resources() {
1228
		$resources = $this->api_call( '' );
1229
		foreach ( $resources as $key => $path ) {
1230
			$items[ $key ] = $path;
1231
		}
1232
		return $items;
1233
	}
1234
1235
	/**
1236
	 * Return a list of SFIDs for the given object, which have been created or
1237
	 * updated in the given timeframe. Part of core API calls.
1238
	 *
1239
	 * @param string $type Object type name, E.g., Contact, Account.
1240
	 * @param int    $start unix timestamp for older timeframe for updates. Defaults to "-29 days" if empty.
1241
	 * @param int    $end unix timestamp for end of timeframe for updates. Defaults to now if empty.
1242
	 * @return array
1243
	 *   return array has 2 indexes:
1244
	 *     "ids": a list of SFIDs of those records which have been created or
1245
	 *       updated in the given timeframe.
1246
	 *     "latestDateCovered": ISO 8601 format timestamp (UTC) of the last date
1247
	 *       covered in the request.
1248
	 *
1249
	 * @see https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_getupdated.htm
1250
	 */
1251
	public function get_updated( $type, $start = null, $end = null ) {
1252
		if ( empty( $start ) ) {
1253
			$start = strtotime( '-29 days' );
1254
		}
1255
		$start = rawurlencode( gmdate( DATE_ATOM, $start ) );
1256
1257
		if ( empty( $end ) ) {
1258
			$end = time();
1259
		}
1260
		$end = rawurlencode( gmdate( DATE_ATOM, $end ) );
1261
1262
		$options = array(
1263
			'cache' => false,
1264
		); // this is timestamp level specific; we don't cache it.
1265
		return $this->api_call( "sobjects/{$type}/updated/?start=$start&end=$end", array(), 'GET', $options );
1266
	}
1267
1268
	/**
1269
	 * Given a DeveloperName and SObject Name, return the SFID of the
1270
	 * corresponding RecordType. DeveloperName doesn't change between Salesforce
1271
	 * environments, so it's safer to rely on compared to SFID.
1272
	 *
1273
	 * @param string $name Object type name, E.g., Contact, Account.
1274
	 * @param string $devname RecordType DeveloperName, e.g. Donation, Membership, etc.
1275
	 * @param bool   $reset whether this is resetting the cache.
1276
	 * @return string SFID The Salesforce ID of the given Record Type, or null.
1277
	 */
1278
	public function get_record_type_id_by_developer_name( $name, $devname, $reset = false ) {
1279
1280
		// example of how this runs: $this->get_record_type_id_by_developer_name( 'Account', 'HH_Account' );.
1281
1282
		$cached = $this->wordpress->cache_get( 'salesforce_record_types', '' );
1283
		if ( is_array( $cached ) && ( ! isset( $reset ) || true !== $reset ) ) {
1284
			return ! empty( $cached[ $name ][ $devname ] ) ? $cached[ $name ][ $devname ]['Id'] : null;
1285
		}
1286
1287
		$query         = new Object_Sync_Sf_Salesforce_Select_Query( 'RecordType' );
1288
		$query->fields = array( 'Id', 'Name', 'DeveloperName', 'SobjectType' );
1289
1290
		$result       = $this->query( $query );
1291
		$record_types = array();
1292
1293
		foreach ( $result['data']['records'] as $record_type ) {
1294
			$record_types[ $record_type['SobjectType'] ][ $record_type['DeveloperName'] ] = $record_type;
1295
		}
1296
1297
		$cached = $this->wordpress->cache_set( 'salesforce_record_types', '', $record_types, $this->options['cache_expiration'] );
1298
1299
		return ! empty( $record_types[ $name ][ $devname ] ) ? $record_types[ $name ][ $devname ]['Id'] : null;
1300
	}
1301
1302
	/**
1303
	 * If there is a WordPress setting for how long to keep the cache, return it and set the object property
1304
	 * Otherwise, return seconds in 24 hours.
1305
	 */
1306
	private function cache_expiration() {
1307
		$cache_expiration = $this->wordpress->cache_expiration( $this->option_prefix . 'cache_expiration', 86400 );
1308
		return $cache_expiration;
1309
	}
1310
1311
}
1312