1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace CliveWalkden\CampaignMonitor; |
4
|
|
|
|
5
|
|
|
class CampaignMonitor |
6
|
|
|
{ |
7
|
|
|
private $api_key; |
8
|
|
|
private $api_endpoint = 'https://api.createsend.com/api/v3.1'; |
9
|
|
|
private $format = 'json'; |
10
|
|
|
|
11
|
|
|
private $client_id; |
12
|
|
|
private $list_id; |
13
|
|
|
|
14
|
|
|
const TIMEOUT = 10; |
15
|
|
|
|
16
|
|
|
public $verify_ssl = true; |
17
|
|
|
|
18
|
|
|
private $request_successful = false; |
19
|
|
|
private $last_error = ''; |
20
|
|
|
private $last_response = []; |
21
|
|
|
private $last_request = []; |
22
|
|
|
|
23
|
|
|
public function __construct($api_key, $client_id = null) |
24
|
|
|
{ |
25
|
|
|
if (!function_exists('curl_init') || !function_exists('curl_setopt')) { |
26
|
|
|
throw new \Exception('cURL not found and is required for this to work.'); |
27
|
|
|
} |
28
|
|
|
|
29
|
|
|
if (!preg_match('/^[a-f0-9]{30,100}$/', $api_key)) { |
30
|
|
|
throw new \Exception("Invalid Campaign Monitor API Key `{$api_key}` supplied"); |
31
|
|
|
throw new \Exception("Invalid Campain Monitor API Key `{$api_key}` supplied"); |
|
|
|
|
32
|
|
|
} else { |
33
|
|
|
$this->api_key = $api_key; |
34
|
|
|
} |
35
|
|
|
|
36
|
|
|
if ($client_id) { |
37
|
|
|
$this->client_id = trim($client_id); |
38
|
|
|
} |
39
|
|
|
|
40
|
|
|
$this->last_response = ['headers' => null, 'body' => null]; |
41
|
|
|
} |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* @param string $client_id |
45
|
|
|
*/ |
46
|
|
|
public function setClientId($client_id) |
47
|
|
|
{ |
48
|
|
|
$this->client_id = $client_id; |
49
|
|
|
|
50
|
|
|
return $this; |
51
|
|
|
} |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* @param mixed $list_id |
55
|
|
|
*/ |
56
|
|
|
public function setListId($list_id) |
57
|
|
|
{ |
58
|
|
|
$this->list_id = $list_id; |
59
|
|
|
|
60
|
|
|
return $this; |
61
|
|
|
} |
62
|
|
|
|
63
|
|
|
public function getApiEndpoint() |
64
|
|
|
{ |
65
|
|
|
return $this->api_endpoint; |
66
|
|
|
} |
67
|
|
|
|
68
|
|
|
public function success() |
69
|
|
|
{ |
70
|
|
|
return $this->request_successful; |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* @return string|boolean The error message |
75
|
|
|
*/ |
76
|
|
|
public function getLastError() |
77
|
|
|
{ |
78
|
|
|
return $this->last_error ?: false; |
79
|
|
|
} |
80
|
|
|
|
81
|
|
|
/** |
82
|
|
|
* @return array |
83
|
|
|
*/ |
84
|
|
|
public function getLastRequest() |
85
|
|
|
{ |
86
|
|
|
return $this->last_request; |
87
|
|
|
} |
88
|
|
|
|
89
|
|
|
/** |
90
|
|
|
* Make an HTTP DELETE request - for deleting data |
91
|
|
|
* |
92
|
|
|
* @param string $method URL of the API request method |
93
|
|
|
* @param array $args Assoc array of arguments (if any) |
94
|
|
|
* @param int $timeout Timeout limit for request in seconds |
95
|
|
|
* |
96
|
|
|
* @return array|boolean Assoc array of API response, decoded from JSON |
97
|
|
|
*/ |
98
|
|
|
public function delete($method, $args = array(), $timeout = self::TIMEOUT) |
99
|
|
|
{ |
100
|
|
|
return $this->makeRequest('delete', $method, $args, $timeout); |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
/** |
104
|
|
|
* Make an HTTP GET request - for retrieving data |
105
|
|
|
* |
106
|
|
|
* @param string $method URL of the API request method |
107
|
|
|
* @param array $args Assoc array of arguments (usually your data) |
108
|
|
|
* @param int $timeout Timeout limit for request in seconds |
109
|
|
|
* |
110
|
|
|
* @return array|boolean Assoc array of API response, decoded from JSON |
111
|
|
|
*/ |
112
|
|
|
public function get($method, $args = array(), $timeout = self::TIMEOUT) |
113
|
|
|
{ |
114
|
|
|
return $this->makeRequest('get', $method, $args, $timeout); |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
/** |
118
|
|
|
* Make an HTTP PATCH request - for performing partial updates |
119
|
|
|
* |
120
|
|
|
* @param string $method URL of the API request method |
121
|
|
|
* @param array $args Assoc array of arguments (usually your data) |
122
|
|
|
* @param int $timeout Timeout limit for request in seconds |
123
|
|
|
* |
124
|
|
|
* @return array|boolean Assoc array of API response, decoded from JSON |
125
|
|
|
*/ |
126
|
|
|
public function patch($method, $args = array(), $timeout = self::TIMEOUT) |
127
|
|
|
{ |
128
|
|
|
return $this->makeRequest('patch', $method, $args, $timeout); |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
/** |
132
|
|
|
* Make an HTTP POST request - for creating and updating items |
133
|
|
|
* |
134
|
|
|
* @param string $method URL of the API request method |
135
|
|
|
* @param array $args Assoc array of arguments (usually your data) |
136
|
|
|
* @param int $timeout Timeout limit for request in seconds |
137
|
|
|
* |
138
|
|
|
* @return array|boolean Assoc array of API response, decoded from JSON |
139
|
|
|
*/ |
140
|
|
|
public function post($method, $args = array(), $timeout = self::TIMEOUT) |
141
|
|
|
{ |
142
|
|
|
return $this->makeRequest('post', $method, $args, $timeout); |
143
|
|
|
} |
144
|
|
|
|
145
|
|
|
/** |
146
|
|
|
* Make an HTTP PUT request - for creating new items |
147
|
|
|
* |
148
|
|
|
* @param string $method URL of the API request method |
149
|
|
|
* @param array $args Assoc array of arguments (usually your data) |
150
|
|
|
* @param int $timeout Timeout limit for request in seconds |
151
|
|
|
* |
152
|
|
|
* @return array|boolean Assoc array of API response, decoded from JSON |
153
|
|
|
*/ |
154
|
|
|
public function put($method, $args = array(), $timeout = self::TIMEOUT) |
155
|
|
|
{ |
156
|
|
|
return $this->makeRequest('put', $method, $args, $timeout); |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
private function formatMethod($method) |
160
|
|
|
{ |
161
|
|
|
$method = str_replace( |
162
|
|
|
['{client_id}', '{list_id}'], |
163
|
|
|
[$this->client_id, $this->list_id], |
164
|
|
|
$method |
165
|
|
|
); |
166
|
|
|
|
167
|
|
|
return $method; |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
private function makeRequest($http_verb, $method, $args = [], $timeout = self::TIMEOUT) |
171
|
|
|
{ |
172
|
|
|
$method = $this->formatMethod($method); |
173
|
|
|
|
174
|
|
|
$url = $this->api_endpoint . $method . '.' . $this->format; |
175
|
|
|
|
176
|
|
|
$response = $this->prepareStateForRequest($http_verb, $method, $url, $timeout); |
177
|
|
|
|
178
|
|
|
$httpHeader = [ |
179
|
|
|
'Accept: application/vnd.api+json', |
180
|
|
|
'Content-Type: application/vnd.api+json' |
181
|
|
|
]; |
182
|
|
|
|
183
|
|
|
if (isset($args["language"])) { |
184
|
|
|
$httpHeader[] = "Accept-Language: " . $args["language"]; |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
$ch = curl_init(); |
188
|
|
|
curl_setopt($ch, CURLOPT_URL, $url); |
189
|
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, $httpHeader); |
190
|
|
|
curl_setopt($ch, CURLOPT_USERAGENT, |
191
|
|
|
'CliveWalkden/CampaignMonitor-API/3.1 (github.com/clivewalkden/campaign-monitor-api)'); |
192
|
|
|
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); |
193
|
|
|
curl_setopt($ch, CURLOPT_USERPWD, $this->api_key . ':nopass'); |
194
|
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); |
195
|
|
|
curl_setopt($ch, CURLOPT_VERBOSE, true); |
196
|
|
|
curl_setopt($ch, CURLOPT_HEADER, true); |
197
|
|
|
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); |
198
|
|
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $this->verify_ssl); |
199
|
|
|
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); |
200
|
|
|
curl_setopt($ch, CURLOPT_ENCODING, ''); |
201
|
|
|
curl_setopt($ch, CURLINFO_HEADER_OUT, true); |
202
|
|
|
|
203
|
|
|
// Debug |
204
|
|
|
// $fp = fopen(SOZO_LOG_DIR . 'curl.txt', 'w'); |
205
|
|
|
// curl_setopt($ch, CURLOPT_VERBOSE, 1); |
206
|
|
|
// curl_setopt($ch, CURLOPT_STDERR, $fp); |
207
|
|
|
|
208
|
|
|
switch ($http_verb) { |
209
|
|
|
case 'post': |
210
|
|
|
curl_setopt($ch, CURLOPT_POST, true); |
211
|
|
|
$this->attachRequestPayload($ch, $args); |
212
|
|
|
break; |
213
|
|
|
|
214
|
|
|
case 'get': |
215
|
|
|
$query = http_build_query($args, '', '&'); |
216
|
|
|
curl_setopt($ch, CURLOPT_URL, $url . '?' . $query); |
217
|
|
|
break; |
218
|
|
|
|
219
|
|
|
case 'delete': |
220
|
|
|
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); |
221
|
|
|
break; |
222
|
|
|
|
223
|
|
|
case 'patch': |
224
|
|
|
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH'); |
225
|
|
|
$this->attachRequestPayload($ch, $args); |
226
|
|
|
break; |
227
|
|
|
|
228
|
|
|
case 'put': |
229
|
|
|
if ($args['email']) { |
230
|
|
|
$query = http_build_query(['email' => $args['email']], '', '&'); |
231
|
|
|
curl_setopt($ch, CURLOPT_URL, $url . '?' . $query); |
232
|
|
|
} |
233
|
|
|
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); |
234
|
|
|
$this->attachRequestPayload($ch, $args); |
235
|
|
|
break; |
236
|
|
|
} |
237
|
|
|
|
238
|
|
|
$responseContent = curl_exec($ch); |
239
|
|
|
$response['headers'] = curl_getinfo($ch); |
240
|
|
|
$response = $this->setResponseState($response, $responseContent, $ch); |
241
|
|
|
$formattedResponse = $this->formatResponse($response); |
242
|
|
|
|
243
|
|
|
curl_close($ch); |
244
|
|
|
|
245
|
|
|
$isSuccess = $this->determineSuccess($response, $formattedResponse, $timeout); |
246
|
|
|
|
247
|
|
|
return is_array($formattedResponse) ? $formattedResponse : $isSuccess; |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
private function prepareStateForRequest($http_verb, $method, $url, $timeout) |
251
|
|
|
{ |
252
|
|
|
$this->last_error = ''; |
253
|
|
|
|
254
|
|
|
$this->request_successful = false; |
255
|
|
|
|
256
|
|
|
$this->last_response = array( |
257
|
|
|
'headers' => null, // array of details from curl_getinfo() |
258
|
|
|
'httpHeaders' => null, // array of HTTP headers |
259
|
|
|
'body' => null // content of the response |
260
|
|
|
); |
261
|
|
|
|
262
|
|
|
$this->last_request = array( |
263
|
|
|
'method' => $http_verb, |
264
|
|
|
'path' => $method, |
265
|
|
|
'url' => $url, |
266
|
|
|
'body' => '', |
267
|
|
|
'timeout' => $timeout, |
268
|
|
|
); |
269
|
|
|
|
270
|
|
|
return $this->last_response; |
271
|
|
|
} |
272
|
|
|
|
273
|
|
|
/** |
274
|
|
|
* Get the HTTP headers as an array of header-name => header-value pairs. |
275
|
|
|
* |
276
|
|
|
* The "Link" header is parsed into an associative array based on the |
277
|
|
|
* rel names it contains. The original value is available under |
278
|
|
|
* the "_raw" key. |
279
|
|
|
* |
280
|
|
|
* @param string $headersAsString |
281
|
|
|
* |
282
|
|
|
* @return array |
283
|
|
|
*/ |
284
|
|
|
private function getHeadersAsArray($headersAsString) |
285
|
|
|
{ |
286
|
|
|
$headers = array(); |
287
|
|
|
|
288
|
|
|
foreach (explode("\r\n", $headersAsString) as $i => $line) { |
289
|
|
|
if ($i === 0) { // HTTP code |
290
|
|
|
continue; |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
$line = trim($line); |
294
|
|
|
if (empty($line)) { |
295
|
|
|
continue; |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
list($key, $value) = explode(': ', $line); |
299
|
|
|
|
300
|
|
|
// if ($key == 'Link') { |
301
|
|
|
// $value = array_merge( |
302
|
|
|
// array('_raw' => $value), |
303
|
|
|
// $this->getLinkHeaderAsArray($value) |
304
|
|
|
// ); |
305
|
|
|
// } |
306
|
|
|
|
307
|
|
|
$headers[$key] = $value; |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
return $headers; |
311
|
|
|
} |
312
|
|
|
|
313
|
|
|
/** |
314
|
|
|
* Encode the data and attach it to the request |
315
|
|
|
* |
316
|
|
|
* @param resource $ch cURL session handle, used by reference |
317
|
|
|
* @param array $data Assoc array of data to attach |
318
|
|
|
*/ |
319
|
|
|
private function attachRequestPayload(&$ch, $data) |
320
|
|
|
{ |
321
|
|
|
$encoded = json_encode($data); |
322
|
|
|
$this->last_request['body'] = $encoded; |
323
|
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $encoded); |
324
|
|
|
} |
325
|
|
|
|
326
|
|
|
/** |
327
|
|
|
* Decode the response and format any error messages for debugging |
328
|
|
|
* |
329
|
|
|
* @param array $response The response from the curl request |
330
|
|
|
* |
331
|
|
|
* @return array|boolean The JSON decoded into an array |
332
|
|
|
*/ |
333
|
|
|
private function formatResponse($response) |
334
|
|
|
{ |
335
|
|
|
$this->last_response = $response; |
336
|
|
|
|
337
|
|
|
if (!empty($response['body'])) { |
338
|
|
|
return json_decode($response['body'], true); |
339
|
|
|
} |
340
|
|
|
|
341
|
|
|
return false; |
342
|
|
|
} |
343
|
|
|
|
344
|
|
|
/** |
345
|
|
|
* Do post-request formatting and setting state from the response |
346
|
|
|
* |
347
|
|
|
* @param array $response The response from the curl request |
348
|
|
|
* @param string $responseContent The body of the response from the curl request |
349
|
|
|
* @param resource $ch The curl resource |
350
|
|
|
* |
351
|
|
|
* @return array The modified response |
352
|
|
|
*/ |
353
|
|
|
private function setResponseState($response, $responseContent, $ch) |
354
|
|
|
{ |
355
|
|
|
if ($responseContent === false) { |
|
|
|
|
356
|
|
|
$this->last_error = curl_error($ch); |
357
|
|
|
} else { |
358
|
|
|
|
359
|
|
|
$headerSize = $response['headers']['header_size']; |
360
|
|
|
|
361
|
|
|
$response['httpHeaders'] = $this->getHeadersAsArray(substr($responseContent, 0, $headerSize)); |
362
|
|
|
$response['body'] = substr($responseContent, $headerSize); |
363
|
|
|
|
364
|
|
|
if (isset($response['headers']['request_header'])) { |
365
|
|
|
$this->last_request['headers'] = $response['headers']['request_header']; |
366
|
|
|
} |
367
|
|
|
} |
368
|
|
|
|
369
|
|
|
return $response; |
370
|
|
|
} |
371
|
|
|
|
372
|
|
|
/** |
373
|
|
|
* Check if the response was successful or a failure. If it failed, store the error. |
374
|
|
|
* |
375
|
|
|
* @param array $response The response from the curl request |
376
|
|
|
* @param array|false $formattedResponse The response body payload from the curl request |
377
|
|
|
* @param int $timeout The timeout supplied to the curl request. |
378
|
|
|
* |
379
|
|
|
* @return boolean If the request was successful |
380
|
|
|
*/ |
381
|
|
|
private function determineSuccess($response, $formattedResponse, $timeout) |
382
|
|
|
{ |
383
|
|
|
$status = $this->findHTTPStatus($response, $formattedResponse); |
384
|
|
|
|
385
|
|
|
if ($status >= 200 && $status <= 299) { |
386
|
|
|
$this->request_successful = true; |
387
|
|
|
return true; |
388
|
|
|
} |
389
|
|
|
|
390
|
|
|
if (isset($formattedResponse['detail'])) { |
391
|
|
|
$this->last_error = sprintf('%d: %s', $formattedResponse['status'], $formattedResponse['detail']); |
392
|
|
|
return false; |
393
|
|
|
} |
394
|
|
|
|
395
|
|
|
if ($timeout > 0 && $response['headers'] && $response['headers']['total_time'] >= $timeout) { |
396
|
|
|
$this->last_error = sprintf('Request timed out after %f seconds.', $response['headers']['total_time']); |
397
|
|
|
return false; |
398
|
|
|
} |
399
|
|
|
|
400
|
|
|
$this->last_error = 'Unknown error, call getLastResponse() to find out what happened.'; |
401
|
|
|
return false; |
402
|
|
|
} |
403
|
|
|
|
404
|
|
|
/** |
405
|
|
|
* Find the HTTP status code from the headers or API response body |
406
|
|
|
* |
407
|
|
|
* @param array $response The response from the curl request |
408
|
|
|
* @param array|false $formattedResponse The response body payload from the curl request |
409
|
|
|
* |
410
|
|
|
* @return int HTTP status code |
411
|
|
|
*/ |
412
|
|
|
private function findHTTPStatus($response, $formattedResponse) |
413
|
|
|
{ |
414
|
|
|
if (!empty($response['headers']) && isset($response['headers']['http_code'])) { |
415
|
|
|
return (int)$response['headers']['http_code']; |
416
|
|
|
} |
417
|
|
|
|
418
|
|
|
if (!empty($response['body']) && isset($formattedResponse['status'])) { |
419
|
|
|
return (int)$formattedResponse['status']; |
420
|
|
|
} |
421
|
|
|
|
422
|
|
|
return 418; |
423
|
|
|
} |
424
|
|
|
} |
This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.
Unreachable code is most often the result of
return
,die
orexit
statements that have been added for debug purposes.In the above example, the last
return false
will never be executed, because a return statement has already been met in every possible execution path.