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

Object_Sync_Sf_Salesforce   F

Complexity

Total Complexity 145

Size/Duplication

Total Lines 1253
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 1
Metric Value
eloc 431
dl 0
loc 1253
rs 2
c 4
b 0
f 1
wmc 145

36 Methods

Rating   Name   Duplication   Size   Complexity  
A convert_id() 0 16 6
A is_authorized() 0 2 3
A get_sobject_type() 0 13 2
A get_api_versions() 0 6 1
A get_refresh_token() 0 2 1
A refresh_token() 0 43 5
A list_resources() 0 6 2
A object_readby_external_id() 0 2 1
A get_authorization_code() 0 10 1
A cache_expiration() 0 3 1
A set_instance_url() 0 2 1
A set_identity() 0 12 2
A object_update() 0 6 1
C run_analytics_report() 0 81 12
C api_call() 0 47 12
F http_request() 0 162 29
A object_upsert() 0 18 3
A object_delete() 0 6 1
A objects() 0 26 5
F api_http_request() 0 80 19
A set_refresh_token() 0 2 1
A __construct() 0 25 2
A get_access_token() 0 2 1
A query() 0 16 3
A object_describe() 0 24 5
A set_access_token() 0 2 1
A get_api_endpoint() 0 12 3
A get_instance_url() 0 2 1
A get_updated() 0 15 3
A get_deleted() 0 5 1
A request_token() 0 39 5
A analytics_api() 0 2 1
A object_read() 0 2 1
B get_record_type_id_by_developer_name() 0 22 7
A object_create() 0 6 1
A get_identity() 0 2 1

How to fix   Complexity   

Complex Class

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

1
<?php
2
/**
3
 * Class file for the Object_Sync_Sf_Salesforce class.
4
 *
5
 * @file
6
 */
7
8
if ( ! class_exists( 'Object_Sync_Salesforce' ) ) {
9
	die();
10
}
11
12
/**
13
 * Ability to 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.
14
 */
15
class Object_Sync_Sf_Salesforce {
16
17
	public $response;
18
19
	/**
20
	* Constructor which initializes the Salesforce APIs.
21
	*
22
	* @param string $consumer_key
23
	*   Salesforce key to connect to your Salesforce instance.
24
	* @param string $consumer_secret
25
	*   Salesforce secret to connect to your Salesforce instance.
26
	* @param string $login_url
27
	*   Login URL for Salesforce auth requests - differs for production and sandbox
28
	* @param string $callback_url
29
	*   WordPress URL where Salesforce should send you after authentication
30
	* @param string $authorize_path
31
	*   Oauth path that Salesforce wants
32
	* @param string $token_path
33
	*   Path Salesforce uses to give you a token
34
	* @param string $rest_api_version
35
	*   What version of the Salesforce REST API to use
36
	* @param object $wordpress
37
	*   Object for doing things to WordPress - retrieving data, cache, etc.
38
	* @param string $slug
39
	*   Slug for this plugin. Can be used for file including, especially
40
	* @param object $logging
41
	*   Logging object for this plugin.
42
	* @param array $schedulable_classes
43
	*   array of classes that can have scheduled tasks specific to them
44
	* @param string $option_prefix
45
	*   Option prefix for this plugin. Used for getting and setting options, actions, etc.
46
	*/
47
	public function __construct( $consumer_key, $consumer_secret, $login_url, $callback_url, $authorize_path, $token_path, $rest_api_version, $wordpress, $slug, $logging, $schedulable_classes, $option_prefix = '' ) {
48
		$this->consumer_key        = $consumer_key;
0 ignored issues
show
Bug Best Practice introduced by
The property consumer_key does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
49
		$this->consumer_secret     = $consumer_secret;
0 ignored issues
show
Bug Best Practice introduced by
The property consumer_secret does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
50
		$this->login_url           = $login_url;
0 ignored issues
show
Bug Best Practice introduced by
The property login_url does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
51
		$this->callback_url        = $callback_url;
0 ignored issues
show
Bug Best Practice introduced by
The property callback_url does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
52
		$this->authorize_path      = $authorize_path;
0 ignored issues
show
Bug Best Practice introduced by
The property authorize_path does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
53
		$this->token_path          = $token_path;
0 ignored issues
show
Bug Best Practice introduced by
The property token_path does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
54
		$this->rest_api_version    = $rest_api_version;
0 ignored issues
show
Bug Best Practice introduced by
The property rest_api_version does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
55
		$this->wordpress           = $wordpress;
0 ignored issues
show
Bug Best Practice introduced by
The property wordpress does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
56
		$this->slug                = $slug;
0 ignored issues
show
Bug Best Practice introduced by
The property slug does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
57
		$this->option_prefix       = isset( $option_prefix ) ? $option_prefix : 'object_sync_for_salesforce_';
0 ignored issues
show
Bug Best Practice introduced by
The property option_prefix does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
58
		$this->logging             = $logging;
0 ignored issues
show
Bug Best Practice introduced by
The property logging does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
59
		$this->schedulable_classes = $schedulable_classes;
0 ignored issues
show
Bug Best Practice introduced by
The property schedulable_classes does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
60
		$this->options             = array(
0 ignored issues
show
Bug Best Practice introduced by
The property options does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
61
			'cache'            => true,
62
			'cache_expiration' => $this->cache_expiration(),
63
			'type'             => 'read',
64
		);
65
66
		$this->success_codes              = array( 200, 201, 204 );
0 ignored issues
show
Bug Best Practice introduced by
The property success_codes does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
67
		$this->refresh_code               = 401;
0 ignored issues
show
Bug Best Practice introduced by
The property refresh_code does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
68
		$this->success_or_refresh_codes   = $this->success_codes;
0 ignored issues
show
Bug Best Practice introduced by
The property success_or_refresh_codes does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
69
		$this->success_or_refresh_codes[] = $this->refresh_code;
70
71
		$this->debug = get_option( $this->option_prefix . 'debug_mode', false );
0 ignored issues
show
Bug Best Practice introduced by
The property debug does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
72
73
	}
74
75
	/**
76
	* Converts a 15-character case-sensitive Salesforce ID to 18-character
77
	* case-insensitive ID. If input is not 15-characters, return input unaltered.
78
	*
79
	* @param string $sf_id_15
80
	*   15-character case-sensitive Salesforce ID
81
	* @return string
82
	*   18-character case-insensitive Salesforce ID
83
	*/
84
	public static function convert_id( $sf_id_15 ) {
85
		if ( strlen( $sf_id_15 ) !== 15 ) {
86
			return $sf_id_15;
87
		}
88
		$chunks = str_split( $sf_id_15, 5 );
89
		$extra  = '';
90
		foreach ( $chunks as $chunk ) {
91
			$chars = str_split( $chunk, 1 );
92
			$bits  = '';
93
			foreach ( $chars as $char ) {
94
				$bits .= ( ! is_numeric( $char ) && strtoupper( $char ) === $char ) ? '1' : '0';
95
			}
96
			$map    = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ012345';
97
			$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 $start 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

97
			$extra .= substr( $map, /** @scrutinizer ignore-type */ base_convert( strrev( $bits ), 2, 10 ), 1 );
Loading history...
98
		}
99
		return $sf_id_15 . $extra;
100
	}
101
102
	/**
103
	* Given a Salesforce ID, return the corresponding SObject name. (Based on
104
	*  keyPrefix from object definition, @see
105
	*  https://developer.salesforce.com/forums/?id=906F0000000901ZIAQ )
106
	*
107
	* @param string $sf_id
108
	*   15- or 18-character Salesforce ID
109
	* @return string
110
	*   sObject name, e.g. "Account", "Contact", "my__Custom_Object__c" or FALSE
111
	*   if no match could be found.
112
	* @throws Object_Sync_Sf_Exception
113
	*/
114
	public function get_sobject_type( $sf_id ) {
115
		$objects = $this->objects(
116
			array(
117
				'keyPrefix' => substr( $sf_id, 0, 3 ),
118
			)
119
		);
120
		if ( 1 === count( $objects ) ) {
121
			// keyPrefix is unique across objects. If there is exactly one return value from objects(), then we have a match.
122
			$object = reset( $objects );
123
			return $object['name'];
124
		}
125
		// Otherwise, we did not find a match.
126
		return false;
127
	}
128
129
	/**
130
	* Determine if this SF instance is fully configured.
131
	*
132
	*/
133
	public function is_authorized() {
134
		return ! empty( $this->consumer_key ) && ! empty( $this->consumer_secret ) && $this->get_refresh_token();
135
	}
136
137
	/**
138
	* Get REST API versions available on this Salesforce organization
139
	* This is not an authenticated call, so it would not be a helpful test
140
	*/
141
	public function get_api_versions() {
142
		$options = array(
143
			'authenticated' => false,
144
			'full_url'      => true,
145
		);
146
		return $this->api_call( $this->get_instance_url() . '/services/data', [], 'GET', $options );
0 ignored issues
show
Bug introduced by
Are you sure $this->get_instance_url() of type false|mixed|string can be used in concatenation? ( Ignorable by Annotation )

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

146
		return $this->api_call( /** @scrutinizer ignore-type */ $this->get_instance_url() . '/services/data', [], 'GET', $options );
Loading history...
147
	}
148
149
	/**
150
	* Make a call to the Salesforce REST API.
151
	*
152
	* @param string $path
153
	*   Path to resource.
154
	* @param array $params
155
	*   Parameters to provide.
156
	* @param string $method
157
	*   Method to initiate the call, such as GET or POST. Defaults to GET.
158
	* @param array $options
159
	*   Any method can supply options for the API call, and they'll be preserved as far as the curl request
160
	*   They get merged with the class options
161
	* @param string $type
162
	*   Type of call. Defaults to 'rest' - currently we don't support other types.
163
	*   Other exammple in Drupal is 'apexrest'
164
	*
165
	* @return mixed
166
	*   The requested response.
167
	*
168
	* @throws Object_Sync_Sf_Exception
169
	*/
170
	public function api_call( $path, $params = array(), $method = 'GET', $options = array(), $type = 'rest' ) {
171
		if ( ! $this->get_access_token() ) {
172
			$this->refresh_token();
173
		}
174
		$this->response = $this->api_http_request( $path, $params, $method, $options, $type );
175
176
		// analytic calls that are expired return 404s for some absurd reason
177
		if ( $this->response['code'] && 'run_analytics_report' === debug_backtrace()[1]['function'] ) {
178
			return $this->response;
179
		}
180
181
		switch ( $this->response['code'] ) {
182
			// The session ID or OAuth token used has expired or is invalid.
183
			case $this->response['code'] === $this->refresh_code:
184
				// Refresh token.
185
				$this->refresh_token();
186
				// Rebuild our request and repeat request.
187
				$options['is_redo'] = true;
188
				$this->response     = $this->api_http_request( $path, $params, $method, $options, $type );
189
				// Throw an error if we still have bad response.
190
				if ( ! in_array( $this->response['code'], $this->success_codes, true ) ) {
191
					throw new Object_Sync_Sf_Exception( $this->response['data'][0]['message'], $this->response['code'] );
192
				}
193
				break;
194
			case in_array( $this->response['code'], $this->success_codes, true ):
195
				// All clear.
196
				break;
197
			default:
198
				// We have problem and no specific Salesforce error provided.
199
				if ( empty( $this->response['data'] ) ) {
200
					throw new Object_Sync_Sf_Exception( $this->response['error'], $this->response['code'] );
201
				}
202
		}
203
204
		if ( ! empty( $this->response['data'][0] ) && 1 === count( $this->response['data'] ) ) {
205
			$this->response['data'] = $this->response['data'][0];
206
		}
207
208
		if ( isset( $this->response['data']['error'] ) ) {
209
			throw new Object_Sync_Sf_Exception( $this->response['data']['error_description'], $this->response['data']['error'] );
210
		}
211
212
		if ( ! empty( $this->response['data']['errorCode'] ) ) {
213
			return $this->response;
214
		}
215
216
		return $this->response;
217
	}
218
219
	/**
220
	* Private helper to issue an SF API request.
221
	* This method is the only place where we read to or write from the cache
222
	*
223
	* @param string $path
224
	*   Path to resource.
225
	* @param array $params
226
	*   Parameters to provide.
227
	* @param string $method
228
	*   Method to initiate the call, such as GET or POST.  Defaults to GET.
229
	* @param array $options
230
	*   This is the options array from the api_call method
231
	*   This is where it gets merged with $this->options
232
	* @param string $type
233
	*   Type of call. Defaults to 'rest' - currently we don't support other types
234
	*   Other exammple in Drupal is 'apexrest'
235
	*
236
	* @return array
237
	*   The requested data.
238
	*/
239
	protected function api_http_request( $path, $params, $method, $options = array(), $type = 'rest' ) {
240
		$options = array_merge( $this->options, $options ); // this will override a value in $this->options with the one in $options if there is a matching key
241
		$url     = $this->get_api_endpoint( $type ) . $path;
242
		if ( isset( $options['full_url'] ) && true === $options['full_url'] ) {
243
			$url = $path;
244
		}
245
		$headers = array(
246
			'Authorization'   => 'Authorization: OAuth ' . $this->get_access_token(),
0 ignored issues
show
Bug introduced by
Are you sure $this->get_access_token() of type false|mixed|string can be used in concatenation? ( Ignorable by Annotation )

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

246
			'Authorization'   => 'Authorization: OAuth ' . /** @scrutinizer ignore-type */ $this->get_access_token(),
Loading history...
247
			'Accept-Encoding' => 'Accept-Encoding: gzip, deflate',
248
		);
249
		if ( 'POST' === $method || 'PATCH' === $method ) {
250
			$headers['Content-Type'] = 'Content-Type: application/json';
251
		}
252
253
		// if headers are being passed in the $options, use them.
254
		if ( isset( $options['headers'] ) ) {
255
			$headers = array_merge( $headers, $options['headers'] );
256
		}
257
258
		if ( isset( $options['authenticated'] ) && true === $options['authenticated'] ) {
259
			$headers = false;
260
		}
261
		// if this request should be cached, see if it already exists
262
		// if it is already cached, load it. if not, load it and then cache it if it should be cached
263
		// add parameters to the array so we can tell if it was cached or not
264
		if ( true === $options['cache'] && 'write' !== $options['type'] ) {
265
			$cached = $this->wordpress->cache_get( $url, $params );
266
			// some api calls can send a reset option, in which case we should redo the request anyway
267
			if ( is_array( $cached ) && ( ! isset( $options['reset'] ) || true !== $options['reset'] ) ) {
268
				$result               = $cached;
269
				$result['from_cache'] = true;
270
				$result['cached']     = true;
271
			} else {
272
				$data   = wp_json_encode( $params );
273
				$result = $this->http_request( $url, $data, $headers, $method, $options );
0 ignored issues
show
Bug introduced by
$data of type false|string is incompatible with the type array expected by parameter $data 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

273
				$result = $this->http_request( $url, /** @scrutinizer ignore-type */ $data, $headers, $method, $options );
Loading history...
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

273
				$result = $this->http_request( $url, $data, /** @scrutinizer ignore-type */ $headers, $method, $options );
Loading history...
274
				if ( in_array( $result['code'], $this->success_codes, true ) ) {
275
					$result['cached'] = $this->wordpress->cache_set( $url, $params, $result, $options['cache_expiration'] );
276
				} else {
277
					$result['cached'] = false;
278
				}
279
				$result['from_cache'] = false;
280
			}
281
		} else {
282
			$data                 = wp_json_encode( $params );
283
			$result               = $this->http_request( $url, $data, $headers, $method, $options );
284
			$result['from_cache'] = false;
285
			$result['cached']     = false;
286
		}
287
288
		if ( isset( $options['is_redo'] ) && true === $options['is_redo'] ) {
289
			$result['is_redo'] = true;
290
		} else {
291
			$result['is_redo'] = false;
292
		}
293
294
		// it would be very unfortunate to ever have to do this in a production site
295
		if ( 1 === (int) $this->debug ) {
296
			// create log entry for the api call if debug is true
297
			$status = 'debug';
298
			if ( isset( $this->logging ) ) {
299
				$logging = $this->logging;
300
			} elseif ( class_exists( 'Object_Sync_Sf_Logging' ) ) {
301
				$logging = new Object_Sync_Sf_Logging( $this->wpdb, $this->version );
0 ignored issues
show
Bug Best Practice introduced by
The property wpdb does not exist on Object_Sync_Sf_Salesforce. Did you maybe forget to declare it?
Loading history...
Bug introduced by
The property version does not exist on Object_Sync_Sf_Salesforce. Did you mean rest_api_version?
Loading history...
302
			}
303
304
			// translators: placeholder is the URL of the Salesforce API request
305
			$title = sprintf( esc_html__( 'Debug: on Salesforce API HTTP Request to URL: %1$s.', 'object-sync-for-salesforce' ),
306
				esc_url( $url )
307
			);
308
309
			$logging->setup(
310
				$title,
311
				print_r( $result, true ), // log the result because we are debugging the whole api call
0 ignored issues
show
introduced by
The use of function print_r() is discouraged
Loading history...
312
				0,
313
				0,
314
				$status
315
			);
316
		}
317
318
		return $result;
319
	}
320
321
	/**
322
	* Make the HTTP request. Wrapper around curl().
323
	*
324
	* @param string $url
325
	*   Path to make request from.
326
	* @param array $data
327
	*   The request body.
328
	* @param array $headers
329
	*   Request headers to send as name => value.
330
	* @param string $method
331
	*   Method to initiate the call, such as GET or POST. Defaults to GET.
332
	* @param array $options
333
	*   This is the options array from the api_http_request method
334
	*
335
	* @return array
336
	*   Salesforce response object.
337
	*/
338
	protected function http_request( $url, $data, $headers = array(), $method = 'GET', $options = array() ) {
339
		// Build the request, including path and headers. Internal use.
340
341
		/*
342
		 * Note: curl is used because wp_remote_get, wp_remote_post, wp_remote_request don't work. Salesforce returns various errors.
343
		 * There is a GitHub branch attempting with the goal of addressing this in a future version: https://github.com/MinnPost/object-sync-for-salesforce/issues/94
344
		*/
345
346
		$curl = curl_init();
0 ignored issues
show
introduced by
Using cURL functions is highly discouraged within VIP context. Check (Fetching Remote Data) on VIP Documentation.
Loading history...
347
		curl_setopt( $curl, CURLOPT_URL, $url );
0 ignored issues
show
introduced by
Using cURL functions is highly discouraged within VIP context. Check (Fetching Remote Data) on VIP Documentation.
Loading history...
348
		curl_setopt( $curl, CURLOPT_RETURNTRANSFER, true );
0 ignored issues
show
introduced by
Using cURL functions is highly discouraged within VIP context. Check (Fetching Remote Data) on VIP Documentation.
Loading history...
349
		curl_setopt( $curl, CURLOPT_FOLLOWLOCATION, true );
0 ignored issues
show
introduced by
Using cURL functions is highly discouraged within VIP context. Check (Fetching Remote Data) on VIP Documentation.
Loading history...
350
		if ( false !== $headers ) {
0 ignored issues
show
introduced by
The condition false !== $headers is always true.
Loading history...
351
			curl_setopt( $curl, CURLOPT_HTTPHEADER, $headers );
0 ignored issues
show
introduced by
Using cURL functions is highly discouraged within VIP context. Check (Fetching Remote Data) on VIP Documentation.
Loading history...
352
		} else {
353
			curl_setopt( $curl, CURLOPT_HEADER, false );
0 ignored issues
show
introduced by
Using cURL functions is highly discouraged within VIP context. Check (Fetching Remote Data) on VIP Documentation.
Loading history...
354
		}
355
356
		if ( 'POST' === $method ) {
357
			curl_setopt( $curl, CURLOPT_POST, true );
0 ignored issues
show
introduced by
Using cURL functions is highly discouraged within VIP context. Check (Fetching Remote Data) on VIP Documentation.
Loading history...
358
			curl_setopt( $curl, CURLOPT_POSTFIELDS, $data );
0 ignored issues
show
introduced by
Using cURL functions is highly discouraged within VIP context. Check (Fetching Remote Data) on VIP Documentation.
Loading history...
359
		} elseif ( 'PATCH' === $method || 'DELETE' === $method ) {
360
			curl_setopt( $curl, CURLOPT_CUSTOMREQUEST, $method );
0 ignored issues
show
introduced by
Using cURL functions is highly discouraged within VIP context. Check (Fetching Remote Data) on VIP Documentation.
Loading history...
361
			curl_setopt( $curl, CURLOPT_POSTFIELDS, $data );
0 ignored issues
show
introduced by
Using cURL functions is highly discouraged within VIP context. Check (Fetching Remote Data) on VIP Documentation.
Loading history...
362
		}
363
		$json_response = curl_exec( $curl ); // this is possibly gzipped json data
0 ignored issues
show
introduced by
Using cURL functions is highly discouraged within VIP context. Check (Fetching Remote Data) on VIP Documentation.
Loading history...
364
		$code          = curl_getinfo( $curl, CURLINFO_HTTP_CODE );
0 ignored issues
show
introduced by
Using cURL functions is highly discouraged within VIP context. Check (Fetching Remote Data) on VIP Documentation.
Loading history...
365
366
		if ( ( 'PATCH' === $method || 'DELETE' === $method ) && '' === $json_response && 204 === $code ) {
367
			// delete and patch requests return a 204 with an empty body upon success for whatever reason
368
			$data = array(
369
				'success' => true,
370
				'body'    => '',
371
			);
372
			curl_close( $curl );
0 ignored issues
show
introduced by
Using cURL functions is highly discouraged within VIP context. Check (Fetching Remote Data) on VIP Documentation.
Loading history...
373
374
			$result = array(
375
				'code' => $code,
376
			);
377
378
			$return_format = isset( $options['return_format'] ) ? $options['return_format'] : 'array';
379
380
			switch ( $return_format ) {
381
				case 'array':
382
					$result['data'] = $data;
383
					break;
384
				case 'json':
385
					$result['json'] = wp_json_encode( $data );
386
					break;
387
				case 'both':
388
					$result['json'] = wp_json_encode( $data );
389
					$result['data'] = $data;
390
					break;
391
			}
392
393
			return $result;
394
		}
395
396
		if ( ( ord( $json_response[0] ) == 0x1f ) && ( ord( $json_response[1] ) == 0x8b ) ) {
397
			// skip header and ungzip the data
398
			$json_response = gzinflate( substr( $json_response, 10 ) );
399
		}
400
		$data = json_decode( $json_response, true ); // decode it into an array
401
402
		// 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)
403
		if ( ! in_array( $code, $this->success_or_refresh_codes, true ) ) {
404
			$curl_error = curl_error( $curl );
0 ignored issues
show
introduced by
Using cURL functions is highly discouraged within VIP context. Check (Fetching Remote Data) on VIP Documentation.
Loading history...
405
			if ( '' !== $curl_error ) {
406
				// create log entry for failed curl
407
				$status = 'error';
408
				if ( isset( $this->logging ) ) {
409
					$logging = $this->logging;
410
				} elseif ( class_exists( 'Object_Sync_Sf_Logging' ) ) {
411
					$logging = new Object_Sync_Sf_Logging( $this->wpdb, $this->version );
0 ignored issues
show
Bug introduced by
The property version does not exist on Object_Sync_Sf_Salesforce. Did you mean rest_api_version?
Loading history...
Bug Best Practice introduced by
The property wpdb does not exist on Object_Sync_Sf_Salesforce. Did you maybe forget to declare it?
Loading history...
412
				}
413
414
				// translators: placeholder is the HTTP status code returned by the Salesforce API request
415
				$title = sprintf( esc_html__( 'Error: %1$s: on Salesforce http request', 'object-sync-for-salesforce' ),
416
					esc_attr( $code )
417
				);
418
419
				$logging->setup(
420
					$title,
421
					$curl_error,
422
					0,
423
					0,
424
					$status
425
				);
426
			} elseif ( isset( $data[0]['errorCode'] ) && '' !== $data[0]['errorCode'] ) { // salesforce uses this structure to return errors
427
				// create log entry for failed curl
428
				$status = 'error';
429
				if ( isset( $this->logging ) ) {
430
					$logging = $this->logging;
431
				} elseif ( class_exists( 'Object_Sync_Sf_Logging' ) ) {
432
					$logging = new Object_Sync_Sf_Logging( $this->wpdb, $this->version );
433
				}
434
435
				// translators: placeholder is the server code returned by the api
436
				$title = sprintf( esc_html__( 'Error: %1$s: on Salesforce http request', 'object-sync-for-salesforce' ),
437
					absint( $code )
438
				);
439
440
				// translators: placeholders are: 1) the URL requested, 2) the message returned by the error, 3) the server code returned
441
				$body = sprintf( '<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' ),
442
					esc_attr( $url ),
443
					esc_html( $data[0]['message'] ),
444
					absint( $code )
445
				);
446
447
				$logging->setup(
448
					$title,
449
					$body,
450
					0,
451
					0,
452
					$status
453
				);
454
			} else {
455
				// create log entry for failed curl
456
				$status = 'error';
457
				if ( isset( $this->logging ) ) {
458
					$logging = $this->logging;
459
				} elseif ( class_exists( 'Object_Sync_Sf_Logging' ) ) {
460
					$logging = new Object_Sync_Sf_Logging( $this->wpdb, $this->version );
461
				}
462
463
				// translators: placeholder is the server code returned by Salesforce
464
				$title = sprintf( esc_html__( 'Error: %1$s: on Salesforce http request', 'object-sync-for-salesforce' ),
465
					absint( $code )
466
				);
467
468
				$logging->setup(
469
					$title,
470
					print_r( $data, true ), // log the result because we are debugging the whole api call
0 ignored issues
show
introduced by
The use of function print_r() is discouraged
Loading history...
471
					0,
472
					0,
473
					$status
474
				);
475
			} // End if().
476
		} // End if().
477
478
		curl_close( $curl );
0 ignored issues
show
introduced by
Using cURL functions is highly discouraged within VIP context. Check (Fetching Remote Data) on VIP Documentation.
Loading history...
479
480
		$result = array(
481
			'code' => $code,
482
		);
483
484
		$return_format = isset( $options['return_format'] ) ? $options['return_format'] : 'array';
485
486
		switch ( $return_format ) {
487
			case 'array':
488
				$result['data'] = $data;
489
				break;
490
			case 'json':
491
				$result['json'] = $json_response;
492
				break;
493
			case 'both':
494
				$result['json'] = $json_response;
495
				$result['data'] = $data;
496
				break;
497
		}
498
499
		return $result;
500
501
	}
502
503
	/**
504
	* Get the API end point for a given type of the API.
505
	*
506
	* @param string $api_type
507
	*   E.g., rest, partner, enterprise.
508
	*
509
	* @return string
510
	*   Complete URL endpoint for API access.
511
	*/
512
	public function get_api_endpoint( $api_type = 'rest' ) {
513
		// Special handling for apexrest, since it's not in the identity object.
514
		if ( 'apexrest' === $api_type ) {
515
			$url = $this->get_instance_url() . '/services/apexrest/';
0 ignored issues
show
Bug introduced by
Are you sure $this->get_instance_url() of type false|mixed|string can be used in concatenation? ( Ignorable by Annotation )

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

515
			$url = /** @scrutinizer ignore-type */ $this->get_instance_url() . '/services/apexrest/';
Loading history...
516
		} else {
517
			$identity = $this->get_identity();
518
			$url      = str_replace( '{version}', $this->rest_api_version, $identity['urls'][ $api_type ] );
519
			if ( '' === $identity ) {
0 ignored issues
show
introduced by
The condition '' === $identity is always false.
Loading history...
520
				$url = $this->get_instance_url() . '/services/data/v' . $this->rest_api_version . '/';
521
			}
522
		}
523
		return $url;
524
	}
525
526
	/**
527
	* Get the SF instance URL. Useful for linking to objects.
528
	*/
529
	public function get_instance_url() {
530
		return get_option( $this->option_prefix . 'instance_url', '' );
531
	}
532
533
	/**
534
	* Set the SF instanc URL.
535
	*
536
	* @param string $url
537
	*   URL to set.
538
	*/
539
	protected function set_instance_url( $url ) {
540
		update_option( $this->option_prefix . 'instance_url', $url );
541
	}
542
543
	/**
544
	* Get the access token.
545
	*/
546
	public function get_access_token() {
547
		return get_option( $this->option_prefix . 'access_token', '' );
548
	}
549
550
	/**
551
	* Set the access token.
552
	*
553
	* It is stored in session.
554
	*
555
	* @param string $token
556
	*   Access token from Salesforce.
557
	*/
558
	protected function set_access_token( $token ) {
559
		update_option( $this->option_prefix . 'access_token', $token );
560
	}
561
562
	/**
563
	* Get refresh token.
564
	*/
565
	protected function get_refresh_token() {
566
		return get_option( $this->option_prefix . 'refresh_token', '' );
567
	}
568
569
	/**
570
	* Set refresh token.
571
	*
572
	* @param string $token
573
	*   Refresh token from Salesforce.
574
	*/
575
	protected function set_refresh_token( $token ) {
576
		update_option( $this->option_prefix . 'refresh_token', $token );
577
	}
578
579
	/**
580
	* Refresh access token based on the refresh token. Updates session variable.
581
	*
582
	* todo: figure out how to do this as part of the schedule class
583
	* 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.
584
	* but it could be a performance boost to do it at scheduleable intervals instead.
585
	*
586
	* @throws Object_Sync_Sf_Exception
587
	*/
588
	protected function refresh_token() {
589
		$refresh_token = $this->get_refresh_token();
590
		if ( empty( $refresh_token ) ) {
591
			throw new Object_Sync_Sf_Exception( esc_html__( 'There is no refresh token.', 'object-sync-for-salesforce' ) );
592
		}
593
594
		$data = array(
595
			'grant_type'    => 'refresh_token',
596
			'refresh_token' => $refresh_token,
597
			'client_id'     => $this->consumer_key,
598
			'client_secret' => $this->consumer_secret,
599
		);
600
601
		$url      = $this->login_url . $this->token_path;
602
		$headers  = array(
603
			// This is an undocumented requirement on Salesforce's end.
604
			'Content-Type'    => 'Content-Type: application/x-www-form-urlencoded',
605
			'Accept-Encoding' => 'Accept-Encoding: gzip, deflate',
606
			'Authorization'   => 'Authorization: OAuth ' . $this->get_access_token(),
0 ignored issues
show
Bug introduced by
Are you sure $this->get_access_token() of type false|mixed|string can be used in concatenation? ( Ignorable by Annotation )

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

606
			'Authorization'   => 'Authorization: OAuth ' . /** @scrutinizer ignore-type */ $this->get_access_token(),
Loading history...
607
		);
608
		$headers  = false;
609
		$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

609
		$response = $this->http_request( $url, $data, /** @scrutinizer ignore-type */ $headers, 'POST' );
Loading history...
610
611
		if ( 200 !== $response['code'] ) {
612
			throw new Object_Sync_Sf_Exception(
613
				esc_html(
614
					sprintf(
615
						__( 'Unable to get a Salesforce access token. Salesforce returned the following errorCode: ', 'object-sync-for-salesforce' ) . $response['code']
0 ignored issues
show
introduced by
Expected next thing to be a escaping function, not '$response'
Loading history...
616
					)
617
				),
618
				$response['code']
619
			);
620
		}
621
622
		$data = $response['data'];
623
624
		if ( is_array( $data ) && isset( $data['error'] ) ) {
625
			throw new Object_Sync_Sf_Exception( $data['error_description'], $data['error'] );
626
		}
627
628
		$this->set_access_token( $data['access_token'] );
629
		$this->set_identity( $data['id'] );
630
		$this->set_instance_url( $data['instance_url'] );
631
	}
632
633
	/**
634
	* Retrieve and store the Salesforce identity given an ID url.
635
	*
636
	* @param string $id
637
	*   Identity URL.
638
	*
639
	* @throws Object_Sync_Sf_Exception
640
	*/
641
	protected function set_identity( $id ) {
642
		$headers  = array(
643
			'Authorization'   => 'Authorization: OAuth ' . $this->get_access_token(),
0 ignored issues
show
Bug introduced by
Are you sure $this->get_access_token() of type false|mixed|string can be used in concatenation? ( Ignorable by Annotation )

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

643
			'Authorization'   => 'Authorization: OAuth ' . /** @scrutinizer ignore-type */ $this->get_access_token(),
Loading history...
644
			//'Content-type'  => 'application/json',
645
			'Accept-Encoding' => 'Accept-Encoding: gzip, deflate',
646
		);
647
		$response = $this->http_request( $id, null, $headers );
648
		if ( 200 !== $response['code'] ) {
649
			throw new Object_Sync_Sf_Exception( esc_html__( 'Unable to access identity service.', 'object-sync-for-salesforce' ), $response['code'] );
650
		}
651
		$data = $response['data'];
652
		update_option( $this->option_prefix . 'identity', $data );
653
	}
654
655
	/**
656
	* Return the Salesforce identity, which is stored in a variable.
657
	*
658
	* @return array
659
	*   Returns FALSE if no identity has been stored.
660
	*/
661
	public function get_identity() {
662
		return get_option( $this->option_prefix . 'identity', false );
663
	}
664
665
	/**
666
	* OAuth step 1: Redirect to Salesforce and request and authorization code.
667
	*/
668
	public function get_authorization_code() {
669
		$url = add_query_arg(
670
			array(
671
				'response_type' => 'code',
672
				'client_id'     => $this->consumer_key,
673
				'redirect_uri'  => $this->callback_url,
674
			),
675
			$this->login_url . $this->authorize_path
676
		);
677
		return $url;
678
	}
679
680
	/**
681
	* OAuth step 2: Exchange an authorization code for an access token.
682
	*
683
	* @param string $code
684
	*   Code from Salesforce.
685
	*/
686
	public function request_token( $code ) {
687
		$data = array(
688
			'code'          => $code,
689
			'grant_type'    => 'authorization_code',
690
			'client_id'     => $this->consumer_key,
691
			'client_secret' => $this->consumer_secret,
692
			'redirect_uri'  => $this->callback_url,
693
		);
694
695
		$url      = $this->login_url . $this->token_path;
696
		$headers  = array(
697
			// This is an undocumented requirement on SF's end.
698
			//'Content-Type'  => 'application/x-www-form-urlencoded',
699
			'Accept-Encoding' => 'Accept-Encoding: gzip, deflate',
700
		);
701
		$response = $this->http_request( $url, $data, $headers, 'POST' );
702
703
		$data = $response['data'];
704
705
		if ( 200 !== $response['code'] ) {
706
			$error = isset( $data['error_description'] ) ? $data['error_description'] : $response['error'];
707
			throw new Object_Sync_Sf_Exception( $error, $response['code'] );
708
		}
709
710
		// Ensure all required attributes are returned. They can be omitted if the
711
		// OAUTH scope is inadequate.
712
		$required = array( 'refresh_token', 'access_token', 'id', 'instance_url' );
713
		foreach ( $required as $key ) {
714
			if ( ! isset( $data[ $key ] ) ) {
715
				return false;
716
			}
717
		}
718
719
		$this->set_refresh_token( $data['refresh_token'] );
720
		$this->set_access_token( $data['access_token'] );
721
		$this->set_identity( $data['id'] );
722
		$this->set_instance_url( $data['instance_url'] );
723
724
		return true;
725
	}
726
727
	/* Core API calls */
728
729
	/**
730
	* Available objects and their metadata for your organization's data.
731
	*
732
	* @param array $conditions
733
	*   Associative array of filters to apply to the returned objects. Filters
734
	*   are applied after the list is returned from Salesforce.
735
	* @param bool $reset
736
	*   Whether to reset the cache and retrieve a fresh version from Salesforce.
737
	*
738
	* @return array
739
	*   Available objects and metadata.
740
	*
741
	* part of core API calls. this call does require authentication, and the basic url it becomes is like this:
742
	* https://instance.salesforce.com/services/data/v#.0/sobjects
743
	*
744
	* updateable is really how the api spells it
745
	*/
746
	public function objects(
747
		$conditions = array(
748
			'updateable'  => true,
749
			'triggerable' => true,
750
		),
751
		$reset = false
752
	) {
753
754
		$options = array(
755
			'reset' => $reset,
756
		);
757
		$result  = $this->api_call( 'sobjects', array(), 'GET', $options );
758
759
		if ( ! empty( $conditions ) ) {
760
			foreach ( $result['data']['sobjects'] as $key => $object ) {
761
				foreach ( $conditions as $condition => $value ) {
762
					if ( $object[ $condition ] !== $value ) {
763
						unset( $result['data']['sobjects'][ $key ] );
764
					}
765
				}
766
			}
767
		}
768
769
		ksort( $result['data']['sobjects'] );
770
771
		return $result['data']['sobjects'];
772
	}
773
774
	/**
775
	* Use SOQL to get objects based on query string.
776
	*
777
	* @param string $query
778
	*   The SOQL query.
779
	* @param array $options
780
	*   Allow for the query to have options based on what the user needs from it, ie caching, read/write, etc.
781
	* @param boolean $all
782
	*   Whether this should get all results for the query
783
	* @param boolean $explain
784
	*   If set, Salesforce will return feedback on the query performance
785
	*
786
	* @return array
787
	*   Array of Salesforce objects that match the query.
788
	*
789
	* part of core API calls
790
	*/
791
	public function query( $query, $options = array(), $all = false, $explain = false ) {
792
		$search_data = [
793
			'q' => (string) $query,
794
		];
795
		if ( true === $explain ) {
796
			$search_data['explain'] = $search_data['q'];
797
			unset( $search_data['q'] );
798
		}
799
		// all is a search through deleted and merged data as well
800
		if ( true === $all ) {
801
			$path = 'queryAll';
802
		} else {
803
			$path = 'query';
804
		}
805
		$result = $this->api_call( $path . '?' . http_build_query( $search_data ), array(), 'GET', $options );
806
		return $result;
807
	}
808
809
	/**
810
	* Retrieve all the metadata for an object.
811
	*
812
	* @param string $name
813
	*   Object type name, E.g., Contact, Account, etc.
814
	* @param bool $reset
815
	*   Whether to reset the cache and retrieve a fresh version from Salesforce.
816
	*
817
	* @return array
818
	*   All the metadata for an object, including information about each field,
819
	*   URLs, and child relationships.
820
	*
821
	* part of core API calls
822
	*/
823
	public function object_describe( $name, $reset = false ) {
824
		if ( empty( $name ) ) {
825
			return array();
826
		}
827
		$options = array(
828
			'reset' => $reset,
829
		);
830
		$object  = $this->api_call( "sobjects/{$name}/describe", array(), 'GET', $options );
831
		// Sort field properties, because salesforce API always provides them in a
832
		// random order. We sort them so that stored and exported data are
833
		// standardized and predictable.
834
		$fields = array();
835
		foreach ( $object['data']['fields'] as &$field ) {
836
			ksort( $field );
837
			if ( ! empty( $field['picklistValues'] ) ) {
838
				foreach ( $field['picklistValues'] as &$picklist_value ) {
839
					ksort( $picklist_value );
840
				}
841
			}
842
			$fields[ $field['name'] ] = $field;
843
		}
844
		ksort( $fields );
845
		$object['fields'] = $fields;
846
		return $object;
847
	}
848
849
	/**
850
	* Create a new object of the given type.
851
	*
852
	* @param string $name
853
	*   Object type name, E.g., Contact, Account, etc.
854
	* @param array $params
855
	*   Values of the fields to set for the object.
856
	*
857
	* @return array
858
	*   json: {"id":"00190000001pPvHAAU","success":true,"errors":[]}
859
	*   code: 201
860
	*   data:
861
	*     "id" : "00190000001pPvHAAU",
862
	*     "success" : true
863
	*     "errors" : [ ],
864
	*   from_cache:
865
	*   cached:
866
	*   is_redo:
867
	*
868
	* part of core API calls
869
	*/
870
	public function object_create( $name, $params ) {
871
		$options = array(
872
			'type' => 'write',
873
		);
874
		$result  = $this->api_call( "sobjects/{$name}", $params, 'POST', $options );
875
		return $result;
876
	}
877
878
	/**
879
	* Create new records or update existing records.
880
	*
881
	* The new records or updated records are based on the value of the specified
882
	* field.  If the value is not unique, REST API returns a 300 response with
883
	* the list of matching records.
884
	*
885
	* @param string $name
886
	*   Object type name, E.g., Contact, Account.
887
	* @param string $key
888
	*   The field to check if this record should be created or updated.
889
	* @param string $value
890
	*   The value for this record of the field specified for $key.
891
	* @param array $params
892
	*   Values of the fields to set for the object.
893
	*
894
	* @return array
895
	*   json: {"id":"00190000001pPvHAAU","success":true,"errors":[]}
896
	*   code: 201
897
	*   data:
898
	*     "id" : "00190000001pPvHAAU",
899
	*     "success" : true
900
	*     "errors" : [ ],
901
	*   from_cache:
902
	*   cached:
903
	*   is_redo:
904
	*
905
	* part of core API calls
906
	*/
907
	public function object_upsert( $name, $key, $value, $params ) {
908
		$options = array(
909
			'type' => 'write',
910
		);
911
		// If key is set, remove from $params to avoid UPSERT errors.
912
		if ( isset( $params[ $key ] ) ) {
913
			unset( $params[ $key ] );
914
		}
915
916
		// allow developers to change both the key and value by which objects should be matched
917
		$key   = apply_filters( $this->option_prefix . 'modify_upsert_key', $key );
918
		$value = apply_filters( $this->option_prefix . 'modify_upsert_value', $value );
919
920
		$data = $this->api_call( "sobjects/{$name}/{$key}/{$value}", $params, 'PATCH', $options );
921
		if ( 300 === $this->response['code'] ) {
922
			$data['message'] = esc_html( 'The value provided is not unique.' );
923
		}
924
		return $data;
925
	}
926
927
	/**
928
	* Update an existing object.
929
	*
930
	* @param string $name
931
	*   Object type name, E.g., Contact, Account.
932
	* @param string $id
933
	*   Salesforce id of the object.
934
	* @param array $params
935
	*   Values of the fields to set for the object.
936
	*
937
	* part of core API calls
938
	*
939
	* @return array
940
	*   json: {"success":true,"body":""}
941
	*   code: 204
942
	*   data:
943
		success: 1
944
		body:
945
	*   from_cache:
946
	*   cached:
947
	*   is_redo:
948
	*/
949
	public function object_update( $name, $id, $params ) {
950
		$options = array(
951
			'type' => 'write',
952
		);
953
		$result  = $this->api_call( "sobjects/{$name}/{$id}", $params, 'PATCH', $options );
954
		return $result;
955
	}
956
957
	/**
958
	* Return a full loaded Salesforce object.
959
	*
960
	* @param string $name
961
	*   Object type name, E.g., Contact, Account.
962
	* @param string $id
963
	*   Salesforce id of the object.
964
	* @param array $options
965
	*   Optional options to pass to the API call
966
	*
967
	* @return object
968
	*   Object of the requested Salesforce object.
969
	*
970
	* part of core API calls
971
	*/
972
	public function object_read( $name, $id, $options = array() ) {
973
		return $this->api_call( "sobjects/{$name}/{$id}", array(), 'GET', $options );
974
	}
975
976
	/**
977
	* Make a call to the Analytics API
978
	*
979
	* @param string $name
980
	*   Object type name, E.g., Report
981
	* @param string $id
982
	*   Salesforce id of the object.
983
	* @param string $route
984
	*   What comes after the ID? E.g. instances, ?includeDetails=True
985
	* @param array $params
986
	*   Params to put with the request
987
	* @param string $method
988
	*   GET or POST
989
	*
990
	* @return object
991
	*   Object of the requested Salesforce object.
992
	*
993
	* part of core API calls
994
	*/
995
	public function analytics_api( $name, $id, $route = '', $params = array(), $method = 'GET' ) {
996
		return $this->api_call( "analytics/{$name}/{$id}/{$route}", $params, $method );
997
	}
998
999
	/**
1000
	* Run a specific Analytics report
1001
	*
1002
	* @param string $id
1003
	*   Salesforce id of the object.
1004
	* @param bool $async
1005
	*   Whether the report is asynchronous
1006
	* @param array $params
1007
	*   Params to put with the request
1008
	* @param string $method
1009
	*   GET or POST
1010
	*
1011
	* @return object
1012
	*   Object of the requested Salesforce object.
1013
	*
1014
	* part of core API calls
1015
	*/
1016
	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

1016
	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...
1017
1018
		$id         = $this->convert_id( $id );
1019
		$report_url = 'analytics/reports/' . $id . '/' . 'instances';
1020
1021
		if ( true === $clear_cache ) {
1022
			delete_transient( $report_url );
1023
		}
1024
1025
		$instance_id = $this->wordpress->cache_get( $report_url, '' );
1026
1027
		// there is no stored instance id or this is synchronous; retrieve the results for that instance
1028
		if ( false === $async || false === $instance_id ) {
1029
1030
			$result = $this->analytics_api(
1031
				'reports',
1032
				$id,
1033
				'?includeDetails=true',
1034
				array(),
1035
				'GET'
1036
			);
1037
			// if we get a reportmetadata array out of this, continue
1038
			if ( is_array( $result['data']['reportMetadata'] ) ) {
1039
				$params = array(
1040
					'reportMetadata' => $result['data']['reportMetadata'],
1041
				);
1042
				$report = $this->analytics_api(
1043
					'reports',
1044
					$id,
1045
					'instances',
1046
					$params,
1047
					'POST'
1048
				);
1049
				// if we get an id from the post, that is the instance id
1050
				if ( isset( $report['data']['id'] ) ) {
1051
					$instance_id = $report['data']['id'];
1052
				} else {
1053
					// run the call again if we don't have an instance id
1054
					//error_log('run report again. we have no instance id.');
1055
					$this->run_analytics_report( $id, true );
1056
				}
1057
1058
				// cache the instance id so we can get the report results if they are applicable
1059
				if ( '' === $report_cache_expiration ) {
1060
					$report_cache_expiration = $this->cache_expiration();
1061
				}
1062
				$this->wordpress->cache_set( $report_url, '', $instance_id, $report_cache_expiration );
1063
			} else {
1064
				// run the call again if we don't have a reportMetadata array
1065
				//error_log('run report again. we have no reportmetadata.');
1066
				$this->run_analytics_report( $id, true );
1067
			}
1068
		} // End if().
1069
1070
		$result = $this->api_call( $report_url . "/{$instance_id}", array(), $method );
1071
1072
		// the report instance is expired. rerun it.
1073
		if ( 404 === $result['code'] ) {
1074
			//error_log('run report again. it expired.');
1075
			$this->run_analytics_report( $id, true, true );
1076
		}
1077
1078
		// cache the instance results as a long fallback if the setting says so
1079
		// do this because salesforce will have errors if the instance has expired or is currently running
1080
		// remember: the result of the above api_call is already cached (or not) according to the plugin's generic settings
1081
		// this is fine I think, although it is a bit of redundancy in this case
1082
		if ( true === $cache_instance ) {
1083
			$cached = $this->wordpress->cache_get( $report_url . '_instance_cached', '' );
1084
			if ( is_array( $cached ) ) {
1085
				$result = $cached;
1086
			} else {
1087
				if ( 'Success' === $result['data']['attributes']['status'] ) {
1088
					if ( '' === $instance_cache_expiration ) {
1089
						$instance_cache_expiration = $this->cache_expiration();
1090
					}
1091
					$this->wordpress->cache_set( $report_url . '_instance_cached', '', $result, $instance_cache_expiration );
1092
				}
1093
			}
1094
		}
1095
1096
		return $result;
1097
1098
	}
1099
1100
	/**
1101
	* Return a full loaded Salesforce object from External ID.
1102
	*
1103
	* @param string $name
1104
	*   Object type name, E.g., Contact, Account.
1105
	* @param string $field
1106
	*   Salesforce external id field name.
1107
	* @param string $value
1108
	*   Value of external id.
1109
	* @param array $options
1110
	*   Optional options to pass to the API call
1111
	*
1112
	* @return object
1113
	*   Object of the requested Salesforce object.
1114
	*
1115
	* part of core API calls
1116
	*/
1117
	public function object_readby_external_id( $name, $field, $value, $options = array() ) {
1118
		return $this->api_call( "sobjects/{$name}/{$field}/{$value}", array(), 'GET', $options );
1119
	}
1120
1121
	/**
1122
	* Delete a Salesforce object.
1123
	*
1124
	* @param string $name
1125
	*   Object type name, E.g., Contact, Account.
1126
	* @param string $id
1127
	*   Salesforce id of the object.
1128
	*
1129
	* @return array
1130
	*
1131
	* part of core API calls
1132
	*/
1133
	public function object_delete( $name, $id ) {
1134
		$options = array(
1135
			'type' => 'write',
1136
		);
1137
		$result  = $this->api_call( "sobjects/{$name}/{$id}", array(), 'DELETE', $options );
1138
		return $result;
1139
	}
1140
1141
	/**
1142
	* Retrieves the list of individual objects that have been deleted within the
1143
	* given timespan for a specified object type.
1144
	*
1145
	* @param string $type
1146
	*   Object type name, E.g., Contact, Account.
1147
	* @param string $startDate
1148
	*   Start date to check for deleted objects (in ISO 8601 format).
1149
	* @param string $endDate
1150
	*   End date to check for deleted objects (in ISO 8601 format).
1151
	* @return GetDeletedResult
0 ignored issues
show
Bug introduced by
The type GetDeletedResult 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...
1152
	*/
1153
	public function get_deleted( $type, $start_date, $end_date ) {
1154
		$options = array(
1155
			'cache' => false,
1156
		); // this is timestamp level specific; probably should not cache it
1157
		return $this->api_call( "sobjects/{$type}/deleted/?start={$start_date}&end={$end_date}", array(), 'GET', $options );
1158
	}
1159
1160
1161
	/**
1162
	* Return a list of available resources for the configured API version.
1163
	*
1164
	* @return array
1165
	*   Associative array keyed by name with a URI value.
1166
	*
1167
	* part of core API calls
1168
	*/
1169
	public function list_resources() {
1170
		$resources = $this->api_call( '' );
1171
		foreach ( $resources as $key => $path ) {
1172
			$items[ $key ] = $path;
1173
		}
1174
		return $items;
1175
	}
1176
1177
	/**
1178
	* Return a list of SFIDs for the given object, which have been created or
1179
	* updated in the given timeframe.
1180
	*
1181
	* @param string $type
1182
	*   Object type name, E.g., Contact, Account.
1183
	*
1184
	* @param int $start
1185
	*   unix timestamp for older timeframe for updates.
1186
	*   Defaults to "-29 days" if empty.
1187
	*
1188
	* @param int $end
1189
	*   unix timestamp for end of timeframe for updates.
1190
	*   Defaults to now if empty
1191
	*
1192
	* @return array
1193
	*   return array has 2 indexes:
1194
	*     "ids": a list of SFIDs of those records which have been created or
1195
	*       updated in the given timeframe.
1196
	*     "latestDateCovered": ISO 8601 format timestamp (UTC) of the last date
1197
	*       covered in the request.
1198
	*
1199
	* @see https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_getupdated.htm
1200
	*
1201
	* part of core API calls
1202
	*/
1203
	public function get_updated( $type, $start = null, $end = null ) {
1204
		if ( empty( $start ) ) {
1205
			$start = strtotime( '-29 days' );
1206
		}
1207
		$start = rawurlencode( gmdate( DATE_ATOM, $start ) );
1208
1209
		if ( empty( $end ) ) {
1210
			$end = time();
1211
		}
1212
		$end = rawurlencode( gmdate( DATE_ATOM, $end ) );
1213
1214
		$options = array(
1215
			'cache' => false,
1216
		); // this is timestamp level specific; probably should not cache it
1217
		return $this->api_call( "sobjects/{$type}/updated/?start=$start&end=$end", array(), 'GET', $options );
1218
	}
1219
1220
	/**
1221
	* Given a DeveloperName and SObject Name, return the SFID of the
1222
	* corresponding RecordType. DeveloperName doesn't change between Salesforce
1223
	* environments, so it's safer to rely on compared to SFID.
1224
	*
1225
	* @param string $name
1226
	*   Object type name, E.g., Contact, Account.
1227
	*
1228
	* @param string $devname
1229
	*   RecordType DeveloperName, e.g. Donation, Membership, etc.
1230
	*
1231
	* @return string SFID
1232
	*   The Salesforce ID of the given Record Type, or null.
1233
	*/
1234
1235
	public function get_record_type_id_by_developer_name( $name, $devname, $reset = false ) {
1236
1237
		// example of how this runs: $this->get_record_type_id_by_developer_name( 'Account', 'HH_Account' );
1238
1239
		$cached = $this->wordpress->cache_get( 'salesforce_record_types', '' );
1240
		if ( is_array( $cached ) && ( ! isset( $reset ) || true !== $reset ) ) {
1241
			return ! empty( $cached[ $name ][ $devname ] ) ? $cached[ $name ][ $devname ]['Id'] : null;
1242
		}
1243
1244
		$query         = new Object_Sync_Sf_Salesforce_Select_Query( 'RecordType' );
1245
		$query->fields = array( 'Id', 'Name', 'DeveloperName', 'SobjectType' );
1246
1247
		$result       = $this->query( $query );
1248
		$record_types = array();
1249
1250
		foreach ( $result['data']['records'] as $record_type ) {
1251
			$record_types[ $record_type['SobjectType'] ][ $record_type['DeveloperName'] ] = $record_type;
1252
		}
1253
1254
		$cached = $this->wordpress->cache_set( 'salesforce_record_types', '', $record_types, $this->options['cache_expiration'] );
1255
1256
		return ! empty( $record_types[ $name ][ $devname ] ) ? $record_types[ $name ][ $devname ]['Id'] : null;
1257
1258
	}
1259
1260
	/**
1261
	* If there is a WordPress setting for how long to keep the cache, return it and set the object property
1262
	* Otherwise, return seconds in 24 hours
1263
	*
1264
	*/
1265
	private function cache_expiration() {
1266
		$cache_expiration = $this->wordpress->cache_expiration( $this->option_prefix . 'cache_expiration', 86400 );
1267
		return $cache_expiration;
1268
	}
1269
1270
}
1271
1272
class Object_Sync_Sf_Exception extends Exception {
1273
}
1274