Completed
Pull Request — master (#275)
by Alexander
01:04
created

MailChimp::hasApiKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
namespace DrewM\MailChimp;
4
5
/**
6
 * Super-simple, minimum abstraction MailChimp API v3 wrapper
7
 * MailChimp API v3: http://developer.mailchimp.com
8
 * This wrapper: https://github.com/drewm/mailchimp-api
9
 *
10
 * @author  Drew McLellan <[email protected]>
11
 * @version 2.5
12
 */
13
class MailChimp
14
{
15
    private $api_key;
16
    private $api_endpoint = 'https://<dc>.api.mailchimp.com/3.0';
17
18
    const TIMEOUT = 10;
19
20
    /*  SSL Verification
21
        Read before disabling:
22
        http://snippets.webaware.com.au/howto/stop-turning-off-curlopt_ssl_verifypeer-and-fix-your-php-config/
23
    */
24
    public $verify_ssl = true;
25
26
    private $request_successful = false;
27
    private $last_error         = '';
28
    private $last_response      = array();
29
    private $last_request       = array();
30
31
    /**
32
     * Create a new instance
33
     *
34
     * @param string $api_key      Your MailChimp API key
35
     * @param string $api_endpoint Optional custom API endpoint
36
     *
37
     * @throws \Exception
38
     */
39
    public function __construct($api_key, $api_endpoint = null)
40
    {
41
        if (!function_exists('curl_init') || !function_exists('curl_setopt')) {
42
            throw new \Exception("cURL support is required, but can't be found.");
43
        }
44
45
        $this->api_key = $api_key;
46
47
        if ($api_endpoint === null) {
48
            if (strpos($this->api_key, '-') === false) {
49
                throw new \Exception("Invalid MailChimp API key supplied.");
50
            }
51
            list(, $data_center) = explode('-', $this->api_key);
52
            $this->api_endpoint = str_replace('<dc>', $data_center, $this->api_endpoint);
53
        } else {
54
            $this->api_endpoint = $api_endpoint;
55
        }
56
57
        $this->last_response = array('headers' => null, 'body' => null);
58
    }
59
60
    /**
61
     * Create a new instance of a Batch request. Optionally with the ID of an existing batch.
62
     *
63
     * @param string $batch_id Optional ID of an existing batch, if you need to check its status for example.
64
     *
65
     * @return Batch            New Batch object.
66
     */
67
    public function new_batch($batch_id = null)
68
    {
69
        return new Batch($this, $batch_id);
70
    }
71
72
    /**
73
     * @return string The url to the API endpoint
74
     */
75
    public function getApiEndpoint()
76
    {
77
        return $this->api_endpoint;
78
    }
79
80
    /**
81
     * @return bool A api key was set
82
     */
83
    public function hasApiKey()
84
    {
85
        return (bool) $this->api_key;
86
    }
87
88
    /**
89
     * Convert an email address into a 'subscriber hash' for identifying the subscriber in a method URL
90
     *
91
     * @param   string $email The subscriber's email address
92
     *
93
     * @return  string          Hashed version of the input
94
     */
95
    public function subscriberHash($email)
96
    {
97
        return md5(strtolower($email));
98
    }
99
100
    /**
101
     * Was the last request successful?
102
     *
103
     * @return bool  True for success, false for failure
104
     */
105
    public function success()
106
    {
107
        return $this->request_successful;
108
    }
109
110
    /**
111
     * Get the last error returned by either the network transport, or by the API.
112
     * If something didn't work, this should contain the string describing the problem.
113
     *
114
     * @return  string|false  describing the error
115
     */
116
    public function getLastError()
117
    {
118
        return $this->last_error ?: false;
119
    }
120
121
    /**
122
     * Get an array containing the HTTP headers and the body of the API response.
123
     *
124
     * @return array  Assoc array with keys 'headers' and 'body'
125
     */
126
    public function getLastResponse()
127
    {
128
        return $this->last_response;
129
    }
130
131
    /**
132
     * Get an array containing the HTTP headers and the body of the API request.
133
     *
134
     * @return array  Assoc array
135
     */
136
    public function getLastRequest()
137
    {
138
        return $this->last_request;
139
    }
140
141
    /**
142
     * Make an HTTP DELETE request - for deleting data
143
     *
144
     * @param   string $method  URL of the API request method
145
     * @param   array  $args    Assoc array of arguments (if any)
146
     * @param   int    $timeout Timeout limit for request in seconds
147
     *
148
     * @return  array|false   Assoc array of API response, decoded from JSON
149
     */
150
    public function delete($method, $args = array(), $timeout = self::TIMEOUT)
151
    {
152
        return $this->makeRequest('delete', $method, $args, $timeout);
153
    }
154
155
    /**
156
     * Make an HTTP GET request - for retrieving data
157
     *
158
     * @param   string $method  URL of the API request method
159
     * @param   array  $args    Assoc array of arguments (usually your data)
160
     * @param   int    $timeout Timeout limit for request in seconds
161
     *
162
     * @return  array|false   Assoc array of API response, decoded from JSON
163
     */
164
    public function get($method, $args = array(), $timeout = self::TIMEOUT)
165
    {
166
        return $this->makeRequest('get', $method, $args, $timeout);
167
    }
168
169
    /**
170
     * Make an HTTP PATCH request - for performing partial updates
171
     *
172
     * @param   string $method  URL of the API request method
173
     * @param   array  $args    Assoc array of arguments (usually your data)
174
     * @param   int    $timeout Timeout limit for request in seconds
175
     *
176
     * @return  array|false   Assoc array of API response, decoded from JSON
177
     */
178
    public function patch($method, $args = array(), $timeout = self::TIMEOUT)
179
    {
180
        return $this->makeRequest('patch', $method, $args, $timeout);
181
    }
182
183
    /**
184
     * Make an HTTP POST request - for creating and updating items
185
     *
186
     * @param   string $method  URL of the API request method
187
     * @param   array  $args    Assoc array of arguments (usually your data)
188
     * @param   int    $timeout Timeout limit for request in seconds
189
     *
190
     * @return  array|false   Assoc array of API response, decoded from JSON
191
     */
192
    public function post($method, $args = array(), $timeout = self::TIMEOUT)
193
    {
194
        return $this->makeRequest('post', $method, $args, $timeout);
195
    }
196
197
    /**
198
     * Make an HTTP PUT request - for creating new items
199
     *
200
     * @param   string $method  URL of the API request method
201
     * @param   array  $args    Assoc array of arguments (usually your data)
202
     * @param   int    $timeout Timeout limit for request in seconds
203
     *
204
     * @return  array|false   Assoc array of API response, decoded from JSON
205
     */
206
    public function put($method, $args = array(), $timeout = self::TIMEOUT)
207
    {
208
        return $this->makeRequest('put', $method, $args, $timeout);
209
    }
210
211
    /**
212
     * Performs the underlying HTTP request. Not very exciting.
213
     *
214
     * @param  string $http_verb The HTTP verb to use: get, post, put, patch, delete
215
     * @param  string $method    The API method to be called
216
     * @param  array  $args      Assoc array of parameters to be passed
217
     * @param int     $timeout
218
     *
219
     * @return array|false Assoc array of decoded result
220
     */
221
    private function makeRequest($http_verb, $method, $args = array(), $timeout = self::TIMEOUT)
222
    {
223
        $url = $this->api_endpoint . '/' . $method;
224
225
        $response = $this->prepareStateForRequest($http_verb, $method, $url, $timeout);
226
227
        $httpHeader = array(
228
            'Accept: application/vnd.api+json',
229
            'Content-Type: application/vnd.api+json',
230
            'Authorization: apikey ' . $this->api_key
231
        );
232
233
        if (isset($args["language"])) {
234
            $httpHeader[] = "Accept-Language: " . $args["language"];
235
        }
236
237
        if ($http_verb === 'put') {
238
            $httpHeader[] = 'Allow: PUT, PATCH, POST';
239
        }
240
241
        $ch = curl_init();
242
        curl_setopt($ch, CURLOPT_URL, $url);
243
        curl_setopt($ch, CURLOPT_HTTPHEADER, $httpHeader);
244
        curl_setopt($ch, CURLOPT_USERAGENT, 'DrewM/MailChimp-API/3.0 (github.com/drewm/mailchimp-api)');
245
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
246
        curl_setopt($ch, CURLOPT_VERBOSE, true);
247
        curl_setopt($ch, CURLOPT_HEADER, true);
248
        curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
249
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $this->verify_ssl);
250
        curl_setopt($ch, CURLOPT_ENCODING, '');
251
        curl_setopt($ch, CURLINFO_HEADER_OUT, true);
252
253
        switch ($http_verb) {
254
            case 'post':
255
                curl_setopt($ch, CURLOPT_POST, true);
256
                $this->attachRequestPayload($ch, $args);
257
                break;
258
259
            case 'get':
260
                $query = http_build_query($args, '', '&');
261
                curl_setopt($ch, CURLOPT_URL, $url . '?' . $query);
262
                break;
263
264
            case 'delete':
265
                curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
266
                break;
267
268
            case 'patch':
269
                curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
270
                $this->attachRequestPayload($ch, $args);
271
                break;
272
273
            case 'put':
274
                curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
275
                $this->attachRequestPayload($ch, $args);
276
                break;
277
        }
278
279
        $responseContent     = curl_exec($ch);
280
        $response['headers'] = curl_getinfo($ch);
281
        $response            = $this->setResponseState($response, $responseContent, $ch);
282
        $formattedResponse   = $this->formatResponse($response);
283
284
        curl_close($ch);
285
286
        $isSuccess = $this->determineSuccess($response, $formattedResponse, $timeout);
287
288
        return is_array($formattedResponse) ? $formattedResponse : $isSuccess;
289
    }
290
291
    /**
292
     * @param string  $http_verb
293
     * @param string  $method
294
     * @param string  $url
295
     * @param integer $timeout
296
     *
297
     * @return array
298
     */
299
    private function prepareStateForRequest($http_verb, $method, $url, $timeout)
300
    {
301
        $this->last_error = '';
302
303
        $this->request_successful = false;
304
305
        $this->last_response = array(
306
            'headers'     => null, // array of details from curl_getinfo()
307
            'httpHeaders' => null, // array of HTTP headers
308
            'body'        => null // content of the response
309
        );
310
311
        $this->last_request = array(
312
            'method'  => $http_verb,
313
            'path'    => $method,
314
            'url'     => $url,
315
            'body'    => '',
316
            'timeout' => $timeout,
317
        );
318
319
        return $this->last_response;
320
    }
321
322
    /**
323
     * Get the HTTP headers as an array of header-name => header-value pairs.
324
     *
325
     * The "Link" header is parsed into an associative array based on the
326
     * rel names it contains. The original value is available under
327
     * the "_raw" key.
328
     *
329
     * @param string $headersAsString
330
     *
331
     * @return array
332
     */
333
    private function getHeadersAsArray($headersAsString)
334
    {
335
        $headers = array();
336
337
        foreach (explode("\r\n", $headersAsString) as $i => $line) {
338
            if (preg_match('/HTTP\/[1-2]/', substr($line, 0, 7)) === 1) { // http code
339
                continue;
340
            }
341
342
            $line = trim($line);
343
            if (empty($line)) {
344
                continue;
345
            }
346
347
            list($key, $value) = explode(': ', $line);
348
349
            if ($key == 'Link') {
350
                $value = array_merge(
351
                    array('_raw' => $value),
352
                    $this->getLinkHeaderAsArray($value)
353
                );
354
            }
355
356
            $headers[$key] = $value;
357
        }
358
359
        return $headers;
360
    }
361
362
    /**
363
     * Extract all rel => URL pairs from the provided Link header value
364
     *
365
     * Mailchimp only implements the URI reference and relation type from
366
     * RFC 5988, so the value of the header is something like this:
367
     *
368
     * 'https://us13.api.mailchimp.com/schema/3.0/Lists/Instance.json; rel="describedBy",
369
     * <https://us13.admin.mailchimp.com/lists/members/?id=XXXX>; rel="dashboard"'
370
     *
371
     * @param string $linkHeaderAsString
372
     *
373
     * @return array
374
     */
375
    private function getLinkHeaderAsArray($linkHeaderAsString)
376
    {
377
        $urls = array();
378
379
        if (preg_match_all('/<(.*?)>\s*;\s*rel="(.*?)"\s*/', $linkHeaderAsString, $matches)) {
380
            foreach ($matches[2] as $i => $relName) {
381
                $urls[$relName] = $matches[1][$i];
382
            }
383
        }
384
385
        return $urls;
386
    }
387
388
    /**
389
     * Encode the data and attach it to the request
390
     *
391
     * @param   resource $ch   cURL session handle, used by reference
392
     * @param   array    $data Assoc array of data to attach
393
     */
394
    private function attachRequestPayload(&$ch, $data)
395
    {
396
        $encoded                    = json_encode($data);
397
        $this->last_request['body'] = $encoded;
398
        curl_setopt($ch, CURLOPT_POSTFIELDS, $encoded);
399
    }
400
401
    /**
402
     * Decode the response and format any error messages for debugging
403
     *
404
     * @param array $response The response from the curl request
405
     *
406
     * @return array|false    The JSON decoded into an array
407
     */
408
    private function formatResponse($response)
409
    {
410
        $this->last_response = $response;
411
412
        if (!empty($response['body'])) {
413
            return json_decode($response['body'], true);
414
        }
415
416
        return false;
417
    }
418
419
    /**
420
     * Do post-request formatting and setting state from the response
421
     *
422
     * @param array    $response        The response from the curl request
423
     * @param string   $responseContent The body of the response from the curl request
424
     * @param resource $ch              The curl resource
425
     *
426
     * @return array    The modified response
427
     */
428
    private function setResponseState($response, $responseContent, $ch)
429
    {
430
        if ($responseContent === false) {
431
            $this->last_error = curl_error($ch);
432
        } else {
433
434
            $headerSize = $response['headers']['header_size'];
435
436
            $response['httpHeaders'] = $this->getHeadersAsArray(substr($responseContent, 0, $headerSize));
437
            $response['body']        = substr($responseContent, $headerSize);
438
439
            if (isset($response['headers']['request_header'])) {
440
                $this->last_request['headers'] = $response['headers']['request_header'];
441
            }
442
        }
443
444
        return $response;
445
    }
446
447
    /**
448
     * Check if the response was successful or a failure. If it failed, store the error.
449
     *
450
     * @param array       $response          The response from the curl request
451
     * @param array|false $formattedResponse The response body payload from the curl request
452
     * @param int         $timeout           The timeout supplied to the curl request.
453
     *
454
     * @return bool     If the request was successful
455
     */
456
    private function determineSuccess($response, $formattedResponse, $timeout)
457
    {
458
        $status = $this->findHTTPStatus($response, $formattedResponse);
459
460
        if ($status >= 200 && $status <= 299) {
461
            $this->request_successful = true;
462
            return true;
463
        }
464
465
        if (isset($formattedResponse['detail'])) {
466
            $this->last_error = sprintf('%d: %s', $formattedResponse['status'], $formattedResponse['detail']);
467
            return false;
468
        }
469
470
        if ($timeout > 0 && $response['headers'] && $response['headers']['total_time'] >= $timeout) {
471
            $this->last_error = sprintf('Request timed out after %f seconds.', $response['headers']['total_time']);
472
            return false;
473
        }
474
475
        $this->last_error = 'Unknown error, call getLastResponse() to find out what happened.';
476
        return false;
477
    }
478
479
    /**
480
     * Find the HTTP status code from the headers or API response body
481
     *
482
     * @param array       $response          The response from the curl request
483
     * @param array|false $formattedResponse The response body payload from the curl request
484
     *
485
     * @return int  HTTP status code
486
     */
487
    private function findHTTPStatus($response, $formattedResponse)
488
    {
489
        if (!empty($response['headers']) && isset($response['headers']['http_code'])) {
490
            return (int)$response['headers']['http_code'];
491
        }
492
493
        if (!empty($response['body']) && isset($formattedResponse['status'])) {
494
            return (int)$formattedResponse['status'];
495
        }
496
497
        return 418;
498
    }
499
}
500