Completed
Pull Request — master (#243)
by
unknown
01:18
created

MailChimp::getResponseHeader()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17

Duplication

Lines 17
Ratio 100 %

Importance

Changes 0
Metric Value
dl 17
loc 17
rs 9.7
c 0
b 0
f 0
cc 2
nc 2
nop 1
1
<?php
2
3
namespace DrewM\MailChimp;
4
5
use GuzzleHttp\ClientInterface;
6
use GuzzleHttp\Psr7\Request;
7
use GuzzleHttp\Exception\RequestException;
8
use Psr\Http\Message\RequestInterface;
9
use Psr\Http\Message\ResponseInterface;
10
11
/**
12
 * Super-simple, minimum abstraction MailChimp API v3 wrapper
13
 * MailChimp API v3: http://developer.mailchimp.com
14
 * This wrapper: https://github.com/drewm/mailchimp-api
15
 *
16
 * @author  Drew McLellan <[email protected]>
17
 * @version 2.5
18
 */
19
class MailChimp
20
{
21
22
    /**
23
     * @var ClientInterface
24
     */
25
    private $client;
26
27
    private $api_key;
28
    private $api_endpoint = 'https://<dc>.api.mailchimp.com/3.0';
29
30
    const TIMEOUT = 10;
31
32
    private $request_successful = false;
33
    private $last_error         = '';
34
    private $last_response      = array();
35
    private $last_request       = array();
36
37
    /**
38
     * Create a new instance
39
     *
40
     * @param string $api_key      Your MailChimp API key
41
     * @param string $api_endpoint Optional custom API endpoint
42
     *
43
     * @throws \Exception
44
     */
45
    public function __construct(ClientInterface $client, $api_key, $api_endpoint = null)
46
    {
47
        $this->client = $client;
48
49
        $this->api_key = $api_key;
50
51
        if ($api_endpoint === null) {
52
            if (strpos($this->api_key, '-') === false) {
53
                throw new \Exception("Invalid MailChimp API key supplied.");
54
            }
55
            list(, $data_center) = explode('-', $this->api_key);
56
            $this->api_endpoint = str_replace('<dc>', $data_center, $this->api_endpoint);
57
        } else {
58
            $this->api_endpoint = $api_endpoint;
59
        }
60
61
        $this->last_response = array('headers' => null, 'body' => null);
62
    }
63
64
    /**
65
     * Create a new instance of a Batch request. Optionally with the ID of an existing batch.
66
     *
67
     * @param string $batch_id Optional ID of an existing batch, if you need to check its status for example.
68
     *
69
     * @return Batch            New Batch object.
70
     */
71
    public function new_batch($batch_id = null)
72
    {
73
        return new Batch($this, $batch_id);
74
    }
75
76
    /**
77
     * @return string The url to the API endpoint
78
     */
79
    public function getApiEndpoint()
80
    {
81
        return $this->api_endpoint;
82
    }
83
84
85
    /**
86
     * Convert an email address into a 'subscriber hash' for identifying the subscriber in a method URL
87
     *
88
     * @param   string $email The subscriber's email address
89
     *
90
     * @return  string          Hashed version of the input
91
     */
92
    public function subscriberHash($email)
93
    {
94
        return md5(strtolower($email));
95
    }
96
97
    /**
98
     * Was the last request successful?
99
     *
100
     * @return bool  True for success, false for failure
101
     */
102
    public function success()
103
    {
104
        return $this->request_successful;
105
    }
106
107
    /**
108
     * Get the last error returned by either the network transport, or by the API.
109
     * If something didn't work, this should contain the string describing the problem.
110
     *
111
     * @return  string|false  describing the error
112
     */
113
    public function getLastError()
114
    {
115
        return $this->last_error ?: false;
116
    }
117
118
    /**
119
     * Get an array containing the HTTP headers and the body of the API response.
120
     *
121
     * @return array  Assoc array with keys 'headers' and 'body'
122
     */
123
    public function getLastResponse()
124
    {
125
        return $this->last_response;
126
    }
127
128
    /**
129
     * Get an array containing the HTTP headers and the body of the API request.
130
     *
131
     * @return array  Assoc array
132
     */
133
    public function getLastRequest()
134
    {
135
        return $this->last_request;
136
    }
137
138
    /**
139
     * Make an HTTP DELETE request - for deleting data
140
     *
141
     * @param   string $method  URL of the API request method
142
     * @param   array  $args    Assoc array of arguments (if any)
143
     * @param   int    $timeout Timeout limit for request in seconds
144
     *
145
     * @return  array|false   Assoc array of API response, decoded from JSON
146
     */
147
    public function delete($method, $args = array(), $timeout = self::TIMEOUT)
148
    {
149
        return $this->makeRequest('delete', $method, $args, $timeout);
150
    }
151
152
    /**
153
     * Make an HTTP GET request - for retrieving data
154
     *
155
     * @param   string $method  URL of the API request method
156
     * @param   array  $args    Assoc array of arguments (usually your data)
157
     * @param   int    $timeout Timeout limit for request in seconds
158
     *
159
     * @return  array|false   Assoc array of API response, decoded from JSON
160
     */
161
    public function get($method, $args = array(), $timeout = self::TIMEOUT)
162
    {
163
        return $this->makeRequest('get', $method, $args, $timeout);
164
    }
165
166
    /**
167
     * Make an HTTP PATCH request - for performing partial updates
168
     *
169
     * @param   string $method  URL of the API request method
170
     * @param   array  $args    Assoc array of arguments (usually your data)
171
     * @param   int    $timeout Timeout limit for request in seconds
172
     *
173
     * @return  array|false   Assoc array of API response, decoded from JSON
174
     */
175
    public function patch($method, $args = array(), $timeout = self::TIMEOUT)
176
    {
177
        return $this->makeRequest('patch', $method, $args, $timeout);
178
    }
179
180
    /**
181
     * Make an HTTP POST request - for creating and updating items
182
     *
183
     * @param   string $method  URL of the API request method
184
     * @param   array  $args    Assoc array of arguments (usually your data)
185
     * @param   int    $timeout Timeout limit for request in seconds
186
     *
187
     * @return  array|false   Assoc array of API response, decoded from JSON
188
     */
189
    public function post($method, $args = array(), $timeout = self::TIMEOUT)
190
    {
191
        return $this->makeRequest('post', $method, $args, $timeout);
192
    }
193
194
    /**
195
     * Make an HTTP PUT request - for creating new items
196
     *
197
     * @param   string $method  URL of the API request method
198
     * @param   array  $args    Assoc array of arguments (usually your data)
199
     * @param   int    $timeout Timeout limit for request in seconds
200
     *
201
     * @return  array|false   Assoc array of API response, decoded from JSON
202
     */
203
    public function put($method, $args = array(), $timeout = self::TIMEOUT)
204
    {
205
        return $this->makeRequest('put', $method, $args, $timeout);
206
    }
207
208
    /**
209
     * Performs the underlying HTTP request. Not very exciting.
210
     *
211
     * @param  string $http_verb The HTTP verb to use: get, post, put, patch, delete
212
     * @param  string $method    The API method to be called
213
     * @param  array  $args      Assoc array of parameters to be passed
214
     * @param int     $timeout
215
     *
216
     * @return array|false Assoc array of decoded result
217
     */
218
    private function makeRequest($http_verb, $method, $args = array(), $timeout = self::TIMEOUT)
219
    {
220
        $resource = $method;
221
        $method = strtoupper($http_verb);
222
223
        $uri = $this->api_endpoint . '/' . $resource;
224
225
        $dummyResponse = $this->prepareStateForRequest($method, $resource, $uri, $timeout);
226
227
        $headers = [
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
            $headers['Accept-Language'] = [$args['language']];
235
        }
236
237
        $body = null;
238
239
        switch ($method) {
240
            case 'POST':
241
            case 'PATCH':
242
            case 'PUT':
243
                $body = json_encode($args);
244
                $this->last_request['body'] = $body;
245
                break;
246
            case 'GET':
247
                $uri .= '?' . http_build_query($args, '', '&');
248
                break;
249
        }
250
251
        $request = new Request($method, $uri, $headers, $body);
252
253
        $start = microtime(true);
254
255
        $errorMessage = '';
256
257
        try {
258
            $response = $this->client->send($request);
259
        } catch (RequestException $e) {
260
            if (null === $response = $e->getResponse()) {
261
                $errorMessage = $e->getMessage();
262
            }
263
        }
264
265
        $end = microtime(true);
266
267
        $requestHeader = $this->getRequestHeader($request);
268
        $responseHeader = null !== $response ? $this->getResponseHeader($response) : false;
269
        $responseBody = null !== $response ? $responseHeader . (string) $response->getBody() : false;
270
271
        $dummyResponse['headers'] = [
272
            'http_code' => null !== $response ? $response->getStatusCode() : 0,
273
            'header_size' => null !== $response ? strlen($responseHeader) : 0,
274
            'total_time' => $end - $start,
275
            'request_header' => $requestHeader
276
        ];
277
278
        $dummyResponse = $this->setResponseState($dummyResponse, $responseBody, $errorMessage);
0 ignored issues
show
Security Bug introduced by
It seems like $responseBody defined by null !== $response ? $re...onse->getBody() : false on line 269 can also be of type false; however, DrewM\MailChimp\MailChimp::setResponseState() does only seem to accept string, 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...
Documentation introduced by
$errorMessage is of type string, but the function expects a resource.

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...
279
        $formattedResponse = $this->formatResponse($dummyResponse);
280
281
        $isSuccess = $this->determineSuccess($dummyResponse, $formattedResponse, $timeout);
282
283
        return is_array($formattedResponse) ? $formattedResponse : $isSuccess;
284
    }
285
286
    /**
287
     * @param RequestInterface $request
288
     * @return string
289
     */
290 View Code Duplication
    private function getRequestHeader(RequestInterface $request): string
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...
291
    {
292
        $requestHeader = sprintf(
293
            "%s %s HTTP/%s\r\n",
294
            $request->getMethod(),
295
            $request->getRequestTarget(),
296
            $request->getProtocolVersion()
297
        );
298
299
        foreach ($request->getHeaders() as $headerName => $headerValues) {
300
            $requestHeader .= sprintf("%s: %s\r\n", $headerName, implode(', ', $headerValues));
301
        }
302
303
        $requestHeader .= "\r\n";
304
305
        return $requestHeader;
306
    }
307
308
    /**
309
     * @param ResponseInterface $request
0 ignored issues
show
Bug introduced by
There is no parameter named $request. 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...
310
     * @return string
311
     */
312 View Code Duplication
    private function getResponseHeader(ResponseInterface $response): string
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...
313
    {
314
        $responseHeader = sprintf(
315
            "HTTP/%s %s %s\r\n",
316
            $response->getProtocolVersion(),
317
            $response->getStatusCode(),
318
            $response->getReasonPhrase()
319
        );
320
321
        foreach ($response->getHeaders() as $headerName => $headerValues) {
322
            $responseHeader .= sprintf("%s: %s\r\n", $headerName, implode(', ', $headerValues));
323
        }
324
325
        $responseHeader .= "\r\n";
326
327
        return $responseHeader;
328
    }
329
330
    /**
331
     * @param string  $http_verb
332
     * @param string  $method
333
     * @param string  $url
334
     * @param integer $timeout
335
     *
336
     * @return array
337
     */
338
    private function prepareStateForRequest($http_verb, $method, $url, $timeout)
339
    {
340
        $this->last_error = '';
341
342
        $this->request_successful = false;
343
344
        $this->last_response = array(
345
            'headers'     => null, // array of details from curl_getinfo()
346
            'httpHeaders' => null, // array of HTTP headers
347
            'body'        => null // content of the response
348
        );
349
350
        $this->last_request = array(
351
            'method'  => $http_verb,
352
            'path'    => $method,
353
            'url'     => $url,
354
            'body'    => '',
355
            'timeout' => $timeout,
356
        );
357
358
        return $this->last_response;
359
    }
360
361
    /**
362
     * Get the HTTP headers as an array of header-name => header-value pairs.
363
     *
364
     * The "Link" header is parsed into an associative array based on the
365
     * rel names it contains. The original value is available under
366
     * the "_raw" key.
367
     *
368
     * @param string $headersAsString
369
     *
370
     * @return array
371
     */
372
    private function getHeadersAsArray($headersAsString)
373
    {
374
        $headers = array();
375
376
        foreach (explode("\r\n", $headersAsString) as $i => $line) {
377
            if ($i === 0) { // HTTP code
378
                continue;
379
            }
380
381
            $line = trim($line);
382
            if (empty($line)) {
383
                continue;
384
            }
385
386
            list($key, $value) = explode(': ', $line);
387
388
            if ($key == 'Link') {
389
                $value = array_merge(
390
                    array('_raw' => $value),
391
                    $this->getLinkHeaderAsArray($value)
392
                );
393
            }
394
395
            $headers[$key] = $value;
396
        }
397
398
        return $headers;
399
    }
400
401
    /**
402
     * Extract all rel => URL pairs from the provided Link header value
403
     *
404
     * Mailchimp only implements the URI reference and relation type from
405
     * RFC 5988, so the value of the header is something like this:
406
     *
407
     * 'https://us13.api.mailchimp.com/schema/3.0/Lists/Instance.json; rel="describedBy",
408
     * <https://us13.admin.mailchimp.com/lists/members/?id=XXXX>; rel="dashboard"'
409
     *
410
     * @param string $linkHeaderAsString
411
     *
412
     * @return array
413
     */
414
    private function getLinkHeaderAsArray($linkHeaderAsString)
415
    {
416
        $urls = array();
417
418
        if (preg_match_all('/<(.*?)>\s*;\s*rel="(.*?)"\s*/', $linkHeaderAsString, $matches)) {
419
            foreach ($matches[2] as $i => $relName) {
420
                $urls[$relName] = $matches[1][$i];
421
            }
422
        }
423
424
        return $urls;
425
    }
426
427
    /**
428
     * Decode the response and format any error messages for debugging
429
     *
430
     * @param array $response The response from the curl request
431
     *
432
     * @return array|false    The JSON decoded into an array
433
     */
434
    private function formatResponse($response)
435
    {
436
        $this->last_response = $response;
437
438
        if (!empty($response['body'])) {
439
            return json_decode($response['body'], true);
440
        }
441
442
        return false;
443
    }
444
445
    /**
446
     * Do post-request formatting and setting state from the response
447
     *
448
     * @param array    $response        The response from the curl request
449
     * @param string   $responseContent The body of the response from the curl request
450
     * @param resource $errorMessage    The error message
451
     * @return array    The modified response
452
     */
453
    private function setResponseState($response, $responseContent, $errorMessage)
454
    {
455
        if ($responseContent === false) {
456
            $this->last_error = $errorMessage;
0 ignored issues
show
Documentation Bug introduced by
It seems like $errorMessage of type resource is incompatible with the declared type string of property $last_error.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
457
        } else {
458
459
            $headerSize = $response['headers']['header_size'];
460
461
            $response['httpHeaders'] = $this->getHeadersAsArray(substr($responseContent, 0, $headerSize));
462
            $response['body']        = substr($responseContent, $headerSize);
463
464
            if (isset($response['headers']['request_header'])) {
465
                $this->last_request['headers'] = $response['headers']['request_header'];
466
            }
467
        }
468
469
        return $response;
470
    }
471
472
    /**
473
     * Check if the response was successful or a failure. If it failed, store the error.
474
     *
475
     * @param array       $response          The response from the curl request
476
     * @param array|false $formattedResponse The response body payload from the curl request
477
     * @param int         $timeout           The timeout supplied to the curl request.
478
     *
479
     * @return bool     If the request was successful
480
     */
481
    private function determineSuccess($response, $formattedResponse, $timeout)
482
    {
483
        $status = $this->findHTTPStatus($response, $formattedResponse);
484
485
        if ($status >= 200 && $status <= 299) {
486
            $this->request_successful = true;
487
            return true;
488
        }
489
490
        if (isset($formattedResponse['detail'])) {
491
            $this->last_error = sprintf('%d: %s', $formattedResponse['status'], $formattedResponse['detail']);
492
            return false;
493
        }
494
495
        if ($timeout > 0 && $response['headers'] && $response['headers']['total_time'] >= $timeout) {
496
            $this->last_error = sprintf('Request timed out after %f seconds.', $response['headers']['total_time']);
497
            return false;
498
        }
499
500
        $this->last_error = 'Unknown error, call getLastResponse() to find out what happened.';
501
        return false;
502
    }
503
504
    /**
505
     * Find the HTTP status code from the headers or API response body
506
     *
507
     * @param array       $response          The response from the curl request
508
     * @param array|false $formattedResponse The response body payload from the curl request
509
     *
510
     * @return int  HTTP status code
511
     */
512
    private function findHTTPStatus($response, $formattedResponse)
513
    {
514
        if (!empty($response['headers']) && isset($response['headers']['http_code'])) {
515
            return (int)$response['headers']['http_code'];
516
        }
517
518
        if (!empty($response['body']) && isset($formattedResponse['status'])) {
519
            return (int)$formattedResponse['status'];
520
        }
521
522
        return 418;
523
    }
524
}
525