Issues (1)

src/CampaignMonitor.php (1 issue)

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