Test Failed
Push — master ( 3dd85e...34f16b )
by Devin
04:34 queued 10s
created

ApiRequestor::_specificAPIError()   C

Complexity

Conditions 13
Paths 144

Size

Total Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 13
nc 144
nop 5
dl 0
loc 33
rs 6.25
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Stripe;
4
5
/**
6
 * Class ApiRequestor
7
 *
8
 * @package Stripe
9
 */
10
class ApiRequestor
11
{
12
    /**
13
     * @var string|null
14
     */
15
    private $_apiKey;
16
17
    /**
18
     * @var string
19
     */
20
    private $_apiBase;
21
22
    /**
23
     * @var HttpClient\ClientInterface
24
     */
25
    private static $_httpClient;
26
27
    /**
28
     * @var RequestTelemetry
29
     */
30
    private static $requestTelemetry;
31
32
    /**
33
     * ApiRequestor constructor.
34
     *
35
     * @param string|null $apiKey
36
     * @param string|null $apiBase
37
     */
38
    public function __construct($apiKey = null, $apiBase = null)
39
    {
40
        $this->_apiKey = $apiKey;
41
        if (!$apiBase) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $apiBase of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
42
            $apiBase = Stripe::$apiBase;
43
        }
44
        $this->_apiBase = $apiBase;
45
    }
46
47
    /**
48
     * Creates a telemetry json blob for use in 'X-Stripe-Client-Telemetry' headers
49
     * @static
50
     *
51
     * @param RequestTelemetry $requestTelemetry
52
     * @return string
53
     */
54
    private static function _telemetryJson($requestTelemetry)
55
    {
56
        $payload = array(
57
            'last_request_metrics' => array(
58
                'request_id' => $requestTelemetry->requestId,
59
                'request_duration_ms' => $requestTelemetry->requestDuration,
60
        ));
61
62
        $result = json_encode($payload);
63
        if ($result != false) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $result of type string to the boolean false. If you are specifically checking for a non-empty string, consider using the more explicit !== '' instead.
Loading history...
64
            return $result;
65
        } else {
66
            Stripe::getLogger()->error("Serializing telemetry payload failed!");
67
            return "{}";
68
        }
69
    }
70
71
    /**
72
     * @static
73
     *
74
     * @param ApiResource|bool|array|mixed $d
75
     *
76
     * @return ApiResource|array|string|mixed
77
     */
78
    private static function _encodeObjects($d)
79
    {
80
        if ($d instanceof ApiResource) {
81
            return Util\Util::utf8($d->id);
82
        } elseif ($d === true) {
83
            return 'true';
84
        } elseif ($d === false) {
85
            return 'false';
86
        } elseif (is_array($d)) {
87
            $res = [];
88
            foreach ($d as $k => $v) {
89
                $res[$k] = self::_encodeObjects($v);
90
            }
91
            return $res;
92
        } else {
93
            return Util\Util::utf8($d);
94
        }
95
    }
96
97
    /**
98
     * @param string     $method
99
     * @param string     $url
100
     * @param array|null $params
101
     * @param array|null $headers
102
     *
103
     * @return array An array whose first element is an API response and second
104
     *    element is the API key used to make the request.
105
     * @throws Error\Api
106
     * @throws Error\Authentication
107
     * @throws Error\Card
108
     * @throws Error\InvalidRequest
109
     * @throws Error\OAuth\InvalidClient
110
     * @throws Error\OAuth\InvalidGrant
111
     * @throws Error\OAuth\InvalidRequest
112
     * @throws Error\OAuth\InvalidScope
113
     * @throws Error\OAuth\UnsupportedGrantType
114
     * @throws Error\OAuth\UnsupportedResponseType
115
     * @throws Error\Permission
116
     * @throws Error\RateLimit
117
     * @throws Error\Idempotency
118
     * @throws Error\ApiConnection
119
     */
120
    public function request($method, $url, $params = null, $headers = null)
121
    {
122
        $params = $params ?: [];
123
        $headers = $headers ?: [];
124
        list($rbody, $rcode, $rheaders, $myApiKey) =
125
        $this->_requestRaw($method, $url, $params, $headers);
126
        $json = $this->_interpretResponse($rbody, $rcode, $rheaders);
127
        $resp = new ApiResponse($rbody, $rcode, $rheaders, $json);
128
        return [$resp, $myApiKey];
129
    }
130
131
    /**
132
     * @param string $rbody A JSON string.
133
     * @param int $rcode
134
     * @param array $rheaders
135
     * @param array $resp
136
     *
137
     * @throws Error\InvalidRequest if the error is caused by the user.
138
     * @throws Error\Authentication if the error is caused by a lack of
139
     *    permissions.
140
     * @throws Error\Permission if the error is caused by insufficient
141
     *    permissions.
142
     * @throws Error\Card if the error is the error code is 402 (payment
143
     *    required)
144
     * @throws Error\InvalidRequest if the error is caused by the user.
145
     * @throws Error\Idempotency if the error is caused by an idempotency key.
146
     * @throws Error\OAuth\InvalidClient
147
     * @throws Error\OAuth\InvalidGrant
148
     * @throws Error\OAuth\InvalidRequest
149
     * @throws Error\OAuth\InvalidScope
150
     * @throws Error\OAuth\UnsupportedGrantType
151
     * @throws Error\OAuth\UnsupportedResponseType
152
     * @throws Error\Permission if the error is caused by insufficient
153
     *    permissions.
154
     * @throws Error\RateLimit if the error is caused by too many requests
155
     *    hitting the API.
156
     * @throws Error\Api otherwise.
157
     */
158
    public function handleErrorResponse($rbody, $rcode, $rheaders, $resp)
159
    {
160
        if (!is_array($resp) || !isset($resp['error'])) {
161
            $msg = "Invalid response object from API: $rbody "
162
              . "(HTTP response code was $rcode)";
163
            throw new Error\Api($msg, $rcode, $rbody, $resp, $rheaders);
164
        }
165
166
        $errorData = $resp['error'];
167
168
        $error = null;
169
        if (is_string($errorData)) {
170
            $error = self::_specificOAuthError($rbody, $rcode, $rheaders, $resp, $errorData);
171
        }
172
        if (!$error) {
173
            $error = self::_specificAPIError($rbody, $rcode, $rheaders, $resp, $errorData);
174
        }
175
176
        throw $error;
177
    }
178
179
    /**
180
     * @static
181
     *
182
     * @param string $rbody
183
     * @param int    $rcode
184
     * @param array  $rheaders
185
     * @param array  $resp
186
     * @param array  $errorData
187
     *
188
     * @return Error\RateLimit|Error\Idempotency|Error\InvalidRequest|Error\Authentication|Error\Card|Error\Permission|Error\Api
189
     */
190
    private static function _specificAPIError($rbody, $rcode, $rheaders, $resp, $errorData)
191
    {
192
        $msg = isset($errorData['message']) ? $errorData['message'] : null;
193
        $param = isset($errorData['param']) ? $errorData['param'] : null;
194
        $code = isset($errorData['code']) ? $errorData['code'] : null;
195
        $type = isset($errorData['type']) ? $errorData['type'] : null;
196
197
        switch ($rcode) {
198
            case 400:
199
                // 'rate_limit' code is deprecated, but left here for backwards compatibility
200
                // for API versions earlier than 2015-09-08
201
                if ($code == 'rate_limit') {
202
                    return new Error\RateLimit($msg, $param, $rcode, $rbody, $resp, $rheaders);
203
                }
204
                if ($type == 'idempotency_error') {
205
                    return new Error\Idempotency($msg, $rcode, $rbody, $resp, $rheaders);
206
                }
207
208
                // intentional fall-through
209
            case 404:
210
                return new Error\InvalidRequest($msg, $param, $rcode, $rbody, $resp, $rheaders);
211
            case 401:
212
                return new Error\Authentication($msg, $rcode, $rbody, $resp, $rheaders);
213
            case 402:
214
                return new Error\Card($msg, $param, $code, $rcode, $rbody, $resp, $rheaders);
215
            case 403:
216
                return new Error\Permission($msg, $rcode, $rbody, $resp, $rheaders);
217
            case 429:
218
                return new Error\RateLimit($msg, $param, $rcode, $rbody, $resp, $rheaders);
219
            default:
220
                return new Error\Api($msg, $rcode, $rbody, $resp, $rheaders);
221
        }
222
    }
223
224
    /**
225
     * @static
226
     *
227
     * @param string|bool $rbody
228
     * @param int         $rcode
229
     * @param array       $rheaders
230
     * @param array       $resp
231
     * @param string      $errorCode
232
     *
233
     * @return null|Error\OAuth\InvalidClient|Error\OAuth\InvalidGrant|Error\OAuth\InvalidRequest|Error\OAuth\InvalidScope|Error\OAuth\UnsupportedGrantType|Error\OAuth\UnsupportedResponseType
234
     */
235
    private static function _specificOAuthError($rbody, $rcode, $rheaders, $resp, $errorCode)
236
    {
237
        $description = isset($resp['error_description']) ? $resp['error_description'] : $errorCode;
238
239
        switch ($errorCode) {
240
            case 'invalid_client':
241
                return new Error\OAuth\InvalidClient($errorCode, $description, $rcode, $rbody, $resp, $rheaders);
242
            case 'invalid_grant':
243
                return new Error\OAuth\InvalidGrant($errorCode, $description, $rcode, $rbody, $resp, $rheaders);
244
            case 'invalid_request':
245
                return new Error\OAuth\InvalidRequest($errorCode, $description, $rcode, $rbody, $resp, $rheaders);
246
            case 'invalid_scope':
247
                return new Error\OAuth\InvalidScope($errorCode, $description, $rcode, $rbody, $resp, $rheaders);
248
            case 'unsupported_grant_type':
249
                return new Error\OAuth\UnsupportedGrantType($errorCode, $description, $rcode, $rbody, $resp, $rheaders);
250
            case 'unsupported_response_type':
251
                return new Error\OAuth\UnsupportedResponseType($errorCode, $description, $rcode, $rbody, $resp, $rheaders);
252
        }
253
254
        return null;
255
    }
256
257
    /**
258
     * @static
259
     *
260
     * @param null|array $appInfo
261
     *
262
     * @return null|string
263
     */
264
    private static function _formatAppInfo($appInfo)
265
    {
266
        if ($appInfo !== null) {
267
            $string = $appInfo['name'];
268
            if ($appInfo['version'] !== null) {
269
                $string .= '/' . $appInfo['version'];
270
            }
271
            if ($appInfo['url'] !== null) {
272
                $string .= ' (' . $appInfo['url'] . ')';
273
            }
274
            return $string;
275
        } else {
276
            return null;
277
        }
278
    }
279
280
    /**
281
     * @static
282
     *
283
     * @param string $apiKey
284
     * @param null   $clientInfo
285
     *
286
     * @return array
287
     */
288
    private static function _defaultHeaders($apiKey, $clientInfo = null)
289
    {
290
        $uaString = 'Stripe/v1 PhpBindings/' . Stripe::VERSION;
291
292
        $langVersion = phpversion();
293
        $uname = php_uname();
294
295
        $appInfo = Stripe::getAppInfo();
296
        $ua = [
297
            'bindings_version' => Stripe::VERSION,
298
            'lang' => 'php',
299
            'lang_version' => $langVersion,
300
            'publisher' => 'stripe',
301
            'uname' => $uname,
302
        ];
303
        if ($clientInfo) {
304
            $ua = array_merge($clientInfo, $ua);
305
        }
306
        if ($appInfo !== null) {
307
            $uaString .= ' ' . self::_formatAppInfo($appInfo);
308
            $ua['application'] = $appInfo;
309
        }
310
311
        $defaultHeaders = [
312
            'X-Stripe-Client-User-Agent' => json_encode($ua),
313
            'User-Agent' => $uaString,
314
            'Authorization' => 'Bearer ' . $apiKey,
315
        ];
316
        return $defaultHeaders;
317
    }
318
319
    /**
320
     * @param string $method
321
     * @param string $url
322
     * @param array  $params
323
     * @param array  $headers
324
     *
325
     * @return array
326
     * @throws Error\Api
327
     * @throws Error\ApiConnection
328
     * @throws Error\Authentication
329
     */
330
    private function _requestRaw($method, $url, $params, $headers)
331
    {
332
        $myApiKey = $this->_apiKey;
333
        if (!$myApiKey) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $myApiKey of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
334
            $myApiKey = Stripe::$apiKey;
335
        }
336
337
        if (!$myApiKey) {
338
            $msg = 'No API key provided.  (HINT: set your API key using '
339
              . '"Stripe::setApiKey(<API-KEY>)".  You can generate API keys from '
340
              . 'the Stripe web interface.  See https://stripe.com/api for '
341
              . 'details, or email [email protected] if you have any questions.';
342
            throw new Error\Authentication($msg);
343
        }
344
345
        // Clients can supply arbitrary additional keys to be included in the
346
        // X-Stripe-Client-User-Agent header via the optional getUserAgentInfo()
347
        // method
348
        $clientUAInfo = null;
349
        if (method_exists($this->httpClient(), 'getUserAgentInfo')) {
350
            $clientUAInfo = $this->httpClient()->getUserAgentInfo();
351
        }
352
353
        $absUrl = $this->_apiBase.$url;
354
        $params = self::_encodeObjects($params);
355
        $defaultHeaders = $this->_defaultHeaders($myApiKey, $clientUAInfo);
356
        if (Stripe::$apiVersion) {
357
            $defaultHeaders['Stripe-Version'] = Stripe::$apiVersion;
358
        }
359
360
        if (Stripe::$accountId) {
361
            $defaultHeaders['Stripe-Account'] = Stripe::$accountId;
362
        }
363
364
        if (Stripe::$enableTelemetry && self::$requestTelemetry != null) {
365
            $defaultHeaders["X-Stripe-Client-Telemetry"] = self::_telemetryJson(self::$requestTelemetry);
366
        }
367
368
        $hasFile = false;
369
        $hasCurlFile = class_exists('\CURLFile', false);
370
        foreach ($params as $k => $v) {
371
            if (is_resource($v)) {
372
                $hasFile = true;
373
                $params[$k] = self::_processResourceParam($v, $hasCurlFile);
374
            } elseif ($hasCurlFile && $v instanceof \CURLFile) {
375
                $hasFile = true;
376
            }
377
        }
378
379
        if ($hasFile) {
380
            $defaultHeaders['Content-Type'] = 'multipart/form-data';
381
        } else {
382
            $defaultHeaders['Content-Type'] = 'application/x-www-form-urlencoded';
383
        }
384
385
        $combinedHeaders = array_merge($defaultHeaders, $headers);
386
        $rawHeaders = [];
387
388
        foreach ($combinedHeaders as $header => $value) {
389
            $rawHeaders[] = $header . ': ' . $value;
390
        }
391
392
        $requestStartMs = Util\Util::currentTimeMillis();
393
394
        list($rbody, $rcode, $rheaders) = $this->httpClient()->request(
395
            $method,
396
            $absUrl,
397
            $rawHeaders,
398
            $params,
399
            $hasFile
400
        );
401
402
        if (array_key_exists('request-id', $rheaders)) {
403
            self::$requestTelemetry = new RequestTelemetry(
404
                $rheaders['request-id'],
405
                Util\Util::currentTimeMillis() - $requestStartMs
406
            );
407
        }
408
409
        return [$rbody, $rcode, $rheaders, $myApiKey];
410
    }
411
412
    /**
413
     * @param resource $resource
414
     * @param bool     $hasCurlFile
415
     *
416
     * @return \CURLFile|string
417
     * @throws Error\Api
418
     */
419
    private function _processResourceParam($resource, $hasCurlFile)
420
    {
421
        if (get_resource_type($resource) !== 'stream') {
422
            throw new Error\Api(
423
                'Attempted to upload a resource that is not a stream'
424
            );
425
        }
426
427
        $metaData = stream_get_meta_data($resource);
428
        if ($metaData['wrapper_type'] !== 'plainfile') {
429
            throw new Error\Api(
430
                'Only plainfile resource streams are supported'
431
            );
432
        }
433
434
        if ($hasCurlFile) {
435
            // We don't have the filename or mimetype, but the API doesn't care
436
            return new \CURLFile($metaData['uri']);
437
        } else {
438
            return '@'.$metaData['uri'];
439
        }
440
    }
441
442
    /**
443
     * @param string $rbody
444
     * @param int    $rcode
445
     * @param array  $rheaders
446
     *
447
     * @return mixed
448
     * @throws Error\Api
449
     * @throws Error\Authentication
450
     * @throws Error\Card
451
     * @throws Error\InvalidRequest
452
     * @throws Error\OAuth\InvalidClient
453
     * @throws Error\OAuth\InvalidGrant
454
     * @throws Error\OAuth\InvalidRequest
455
     * @throws Error\OAuth\InvalidScope
456
     * @throws Error\OAuth\UnsupportedGrantType
457
     * @throws Error\OAuth\UnsupportedResponseType
458
     * @throws Error\Permission
459
     * @throws Error\RateLimit
460
     * @throws Error\Idempotency
461
     */
462
    private function _interpretResponse($rbody, $rcode, $rheaders)
463
    {
464
        $resp = json_decode($rbody, true);
465
        $jsonError = json_last_error();
466
        if ($resp === null && $jsonError !== JSON_ERROR_NONE) {
467
            $msg = "Invalid response body from API: $rbody "
468
              . "(HTTP response code was $rcode, json_last_error() was $jsonError)";
469
            throw new Error\Api($msg, $rcode, $rbody);
470
        }
471
472
        if ($rcode < 200 || $rcode >= 300) {
473
            $this->handleErrorResponse($rbody, $rcode, $rheaders, $resp);
474
        }
475
        return $resp;
476
    }
477
478
    /**
479
     * @static
480
     *
481
     * @param HttpClient\ClientInterface $client
482
     */
483
    public static function setHttpClient($client)
484
    {
485
        self::$_httpClient = $client;
486
    }
487
488
    /**
489
     * @static
490
     *
491
     * Resets any stateful telemetry data
492
     */
493
    public static function resetTelemetry()
494
    {
495
        self::$requestTelemetry = null;
496
    }
497
498
    /**
499
     * @return HttpClient\ClientInterface
500
     */
501
    private function httpClient()
502
    {
503
        if (!self::$_httpClient) {
504
            self::$_httpClient = HttpClient\CurlClient::instance();
505
        }
506
        return self::$_httpClient;
507
    }
508
}
509