Test Failed
Pull Request — master (#238)
by Jonathan
04:22
created

Object_Sync_Sf_Salesforce   F

Complexity

Total Complexity 145

Size/Duplication

Total Lines 1256
Duplicated Lines 7.09 %

Coupling/Cohesion

Components 1
Dependencies 3

Importance

Changes 0
Metric Value
wmc 145
lcom 1
cbo 3
dl 89
loc 1256
rs 0.8
c 0
b 0
f 0

36 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 27 2
A convert_id() 0 17 6
A get_sobject_type() 0 14 2
A is_authorized() 0 3 3
A get_api_versions() 0 7 1
C api_call() 0 48 12
F api_http_request() 22 81 19
F http_request() 46 164 29
A get_api_endpoint() 0 13 3
A get_instance_url() 0 3 1
A set_instance_url() 0 3 1
A get_access_token() 0 3 1
A set_access_token() 0 3 1
A get_refresh_token() 0 3 1
A set_refresh_token() 0 3 1
B refresh_token() 0 44 5
A set_identity() 0 13 2
A get_identity() 0 3 1
A get_authorization_code() 0 11 1
B request_token() 0 40 5
A objects() 0 27 5
A query() 0 17 3
A object_describe() 0 25 5
A object_create() 7 7 1
A object_upsert() 0 19 3
A object_update() 7 7 1
A object_read() 0 3 1
A analytics_api() 0 3 1
C run_analytics_report() 0 83 12
A object_readby_external_id() 0 3 1
A object_delete() 7 7 1
A get_deleted() 0 6 1
A list_resources() 0 7 2
A get_updated() 0 16 3
B get_record_type_id_by_developer_name() 0 24 7
A cache_expiration() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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;
49
		$this->consumer_secret     = $consumer_secret;
50
		$this->login_url           = $login_url;
51
		$this->callback_url        = $callback_url;
52
		$this->authorize_path      = $authorize_path;
53
		$this->token_path          = $token_path;
54
		$this->rest_api_version    = $rest_api_version;
55
		$this->wordpress           = $wordpress;
56
		$this->slug                = $slug;
57
		$this->option_prefix       = isset( $option_prefix ) ? $option_prefix : 'object_sync_for_salesforce_';
58
		$this->logging             = $logging;
59
		$this->schedulable_classes = $schedulable_classes;
60
		$this->options             = array(
61
			'cache'            => true,
62
			'cache_expiration' => $this->cache_expiration(),
63
			'type'             => 'read',
64
		);
65
66
		$this->success_codes              = array( 200, 201, 204 );
67
		$this->refresh_code               = 401;
68
		$this->success_or_refresh_codes   = $this->success_codes;
69
		$this->success_or_refresh_codes[] = $this->refresh_code;
70
71
		$this->debug = get_option( $this->option_prefix . 'debug_mode', false );
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 );
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 );
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(),
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
Security Bug introduced by
It seems like $headers defined by false on line 259 can also be of type false; however, Object_Sync_Sf_Salesforce::http_request() does only seem to accept array, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

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 );
0 ignored issues
show
Security Bug introduced by
It seems like $headers defined by false on line 259 can also be of type false; however, Object_Sync_Sf_Salesforce::http_request() does only seem to accept array, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
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 View Code Duplication
		if ( 1 === (int) $this->debug ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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 );
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 ) {
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 View Code Duplication
			switch ( $return_format ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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 );
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 View Code Duplication
			} else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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 View Code Duplication
		switch ( $return_format ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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/';
516
		} else {
517
			$identity = $this->get_identity();
518
			$url      = str_replace( '{version}', $this->rest_api_version, $identity['urls'][ $api_type ] );
519
			if ( '' === $identity ) {
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(
0 ignored issues
show
Unused Code introduced by
$headers is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
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(),
607
		);
608
		$headers  = false;
609
		$response = $this->http_request( $url, $data, $headers, 'POST' );
0 ignored issues
show
Documentation introduced by
$headers is of type boolean, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
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(),
644
			//'Content-type'  => 'application/json',
645
			'Accept-Encoding' => 'Accept-Encoding: gzip, deflate',
646
		);
647
		$response = $this->http_request( $id, null, $headers );
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
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 View Code Duplication
	public function object_create( $name, $params ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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 View Code Duplication
	public function object_update( $name, $id, $params ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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.

This check looks from 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 View Code Duplication
	public function object_delete( $name, $id ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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
0 ignored issues
show
Bug introduced by
There is no parameter named $startDate. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
1148
	*   Start date to check for deleted objects (in ISO 8601 format).
1149
	* @param string $endDate
0 ignored issues
show
Bug introduced by
There is no parameter named $endDate. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
1150
	*   End date to check for deleted objects (in ISO 8601 format).
1151
	* @return GetDeletedResult
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'] );
0 ignored issues
show
Unused Code introduced by
$cached is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
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