• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

stripe / stripe-php / #7087

pending completion
#7087

push

php-coveralls

pakrym-stripe
Bump version to 10.14.0-beta.2

1831 of 2732 relevant lines covered (67.02%)

3.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

64.58
/lib/ApiRequestor.php
1
<?php
2

3
namespace Stripe;
4

5
/**
6
 * Class ApiRequestor.
7
 */
8
class ApiRequestor
9
{
10
    /**
11
     * @var null|string
12
     */
13
    private $_apiKey;
14

15
    /**
16
     * @var string
17
     */
18
    private $_apiBase;
19

20
    /**
21
     * @var HttpClient\ClientInterface
22
     */
23
    private static $_httpClient;
24
    /**
25
     * @var HttpClient\StreamingClientInterface
26
     */
27
    private static $_streamingHttpClient;
28

29
    /**
30
     * @var RequestTelemetry
31
     */
32
    private static $requestTelemetry;
33

34
    private static $OPTIONS_KEYS = ['api_key', 'idempotency_key', 'stripe_account', 'stripe_version', 'api_base'];
35

36
    /**
37
     * ApiRequestor constructor.
38
     *
39
     * @param null|string $apiKey
40
     * @param null|string $apiBase
41
     */
42
    public function __construct($apiKey = null, $apiBase = null)
43
    {
44
        $this->_apiKey = $apiKey;
20✔
45
        if (!$apiBase) {
20✔
46
            $apiBase = Stripe::$apiBase;
1✔
47
        }
48
        $this->_apiBase = $apiBase;
20✔
49
    }
50

51
    /**
52
     * Creates a telemetry json blob for use in 'X-Stripe-Client-Telemetry' headers.
53
     *
54
     * @static
55
     *
56
     * @param RequestTelemetry $requestTelemetry
57
     *
58
     * @return string
59
     */
60
    private static function _telemetryJson($requestTelemetry)
61
    {
62
        $payload = [
×
63
            'last_request_metrics' => [
×
64
                'request_id' => $requestTelemetry->requestId,
×
65
                'request_duration_ms' => $requestTelemetry->requestDuration,
×
66
            ],
×
67
        ];
×
68

69
        $result = \json_encode($payload);
×
70
        if (false !== $result) {
×
71
            return $result;
×
72
        }
73
        Stripe::getLogger()->error('Serializing telemetry payload failed!');
×
74

75
        return '{}';
×
76
    }
77

78
    /**
79
     * @static
80
     *
81
     * @param ApiResource|array|bool|mixed $d
82
     *
83
     * @return ApiResource|array|mixed|string
84
     */
85
    private static function _encodeObjects($d)
86
    {
87
        if ($d instanceof ApiResource) {
19✔
88
            return Util\Util::utf8($d->id);
1✔
89
        }
90
        if (true === $d) {
19✔
91
            return 'true';
1✔
92
        }
93
        if (false === $d) {
19✔
94
            return 'false';
1✔
95
        }
96
        if (\is_array($d)) {
19✔
97
            $res = [];
19✔
98
            foreach ($d as $k => $v) {
19✔
99
                $res[$k] = self::_encodeObjects($v);
1✔
100
            }
101

102
            return $res;
19✔
103
        }
104

105
        return Util\Util::utf8($d);
1✔
106
    }
107

108
    /**
109
     * @param 'delete'|'get'|'post'     $method
110
     * @param string     $url
111
     * @param null|array $params
112
     * @param null|array $headers
113
     * @param 'preview'|'standard' $apiMode
114
     *
115
     * @throws Exception\ApiErrorException
116
     *
117
     * @return array tuple containing (ApiReponse, API key)
118
     */
119
    public function request($method, $url, $params = null, $headers = null, $apiMode = 'standard')
120
    {
121
        $params = $params ?: [];
19✔
122
        $headers = $headers ?: [];
19✔
123
        list($rbody, $rcode, $rheaders, $myApiKey) =
19✔
124
        $this->_requestRaw($method, $url, $params, $headers, $apiMode);
19✔
125
        $json = $this->_interpretResponse($rbody, $rcode, $rheaders);
18✔
126
        $resp = new ApiResponse($rbody, $rcode, $rheaders, $json);
4✔
127

128
        return [$resp, $myApiKey];
4✔
129
    }
130

131
    /**
132
     * @param 'delete'|'get'|'post'     $method
133
     * @param string     $url
134
     * @param callable $readBodyChunkCallable
135
     * @param null|array $params
136
     * @param null|array $headers
137
     * @param 'preview'|'standard' $apiMode
138
     * @param mixed $apiMode
139
     *
140
     * @throws Exception\ApiErrorException
141
     */
142
    public function requestStream($method, $url, $readBodyChunkCallable, $params = null, $headers = null, $apiMode = 'standard')
143
    {
144
        $params = $params ?: [];
×
145
        $headers = $headers ?: [];
×
146
        list($rbody, $rcode, $rheaders, $myApiKey) =
×
147
        $this->_requestRawStreaming($method, $url, $params, $headers, $readBodyChunkCallable, $apiMode);
×
148
        if ($rcode >= 300) {
×
149
            $this->_interpretResponse($rbody, $rcode, $rheaders);
×
150
        }
151
    }
152

153
    /**
154
     * @param string $rbody a JSON string
155
     * @param int $rcode
156
     * @param array $rheaders
157
     * @param array $resp
158
     *
159
     * @throws Exception\UnexpectedValueException
160
     * @throws Exception\ApiErrorException
161
     */
162
    public function handleErrorResponse($rbody, $rcode, $rheaders, $resp)
163
    {
164
        if (!\is_array($resp) || !isset($resp['error'])) {
14✔
165
            $msg = "Invalid response object from API: {$rbody} "
×
166
              . "(HTTP response code was {$rcode})";
×
167

168
            throw new Exception\UnexpectedValueException($msg);
×
169
        }
170

171
        $errorData = $resp['error'];
14✔
172

173
        $error = null;
14✔
174
        if (\is_string($errorData)) {
14✔
175
            $error = self::_specificOAuthError($rbody, $rcode, $rheaders, $resp, $errorData);
6✔
176
        }
177
        if (!$error) {
14✔
178
            $error = self::_specificAPIError($rbody, $rcode, $rheaders, $resp, $errorData);
8✔
179
        }
180

181
        throw $error;
14✔
182
    }
183

184
    /**
185
     * @static
186
     *
187
     * @param string $rbody
188
     * @param int    $rcode
189
     * @param array  $rheaders
190
     * @param array  $resp
191
     * @param array  $errorData
192
     *
193
     * @return Exception\ApiErrorException
194
     */
195
    private static function _specificAPIError($rbody, $rcode, $rheaders, $resp, $errorData)
196
    {
197
        $msg = isset($errorData['message']) ? $errorData['message'] : null;
8✔
198
        $param = isset($errorData['param']) ? $errorData['param'] : null;
8✔
199
        $code = isset($errorData['code']) ? $errorData['code'] : null;
8✔
200
        $type = isset($errorData['type']) ? $errorData['type'] : null;
8✔
201
        $declineCode = isset($errorData['decline_code']) ? $errorData['decline_code'] : null;
8✔
202

203
        switch ($rcode) {
204
            case 400:
8✔
205
                // 'rate_limit' code is deprecated, but left here for backwards compatibility
206
                // for API versions earlier than 2015-09-08
207
                if ('rate_limit' === $code) {
3✔
208
                    return Exception\RateLimitException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code, $param);
1✔
209
                }
210
                if ('idempotency_error' === $type) {
2✔
211
                    return Exception\IdempotencyException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code);
1✔
212
                }
213

214
                // no break
215
            case 404:
5✔
216
                return Exception\InvalidRequestException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code, $param);
2✔
217

218
            case 401:
4✔
219
                return Exception\AuthenticationException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code);
1✔
220

221
            case 402:
3✔
222
                return Exception\CardException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code, $declineCode, $param);
1✔
223

224
            case 403:
2✔
225
                return Exception\PermissionException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code);
1✔
226

227
            case 429:
1✔
228
                return Exception\RateLimitException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code, $param);
1✔
229

230
            default:
231
                return Exception\UnknownApiErrorException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code);
×
232
        }
233
    }
234

235
    /**
236
     * @static
237
     *
238
     * @param bool|string $rbody
239
     * @param int         $rcode
240
     * @param array       $rheaders
241
     * @param array       $resp
242
     * @param string      $errorCode
243
     *
244
     * @return Exception\OAuth\OAuthErrorException
245
     */
246
    private static function _specificOAuthError($rbody, $rcode, $rheaders, $resp, $errorCode)
247
    {
248
        $description = isset($resp['error_description']) ? $resp['error_description'] : $errorCode;
6✔
249

250
        switch ($errorCode) {
251
            case 'invalid_client':
6✔
252
                return Exception\OAuth\InvalidClientException::factory($description, $rcode, $rbody, $resp, $rheaders, $errorCode);
1✔
253

254
            case 'invalid_grant':
5✔
255
                return Exception\OAuth\InvalidGrantException::factory($description, $rcode, $rbody, $resp, $rheaders, $errorCode);
1✔
256

257
            case 'invalid_request':
4✔
258
                return Exception\OAuth\InvalidRequestException::factory($description, $rcode, $rbody, $resp, $rheaders, $errorCode);
1✔
259

260
            case 'invalid_scope':
3✔
261
                return Exception\OAuth\InvalidScopeException::factory($description, $rcode, $rbody, $resp, $rheaders, $errorCode);
1✔
262

263
            case 'unsupported_grant_type':
2✔
264
                return Exception\OAuth\UnsupportedGrantTypeException::factory($description, $rcode, $rbody, $resp, $rheaders, $errorCode);
1✔
265

266
            case 'unsupported_response_type':
1✔
267
                return Exception\OAuth\UnsupportedResponseTypeException::factory($description, $rcode, $rbody, $resp, $rheaders, $errorCode);
1✔
268

269
            default:
270
                return Exception\OAuth\UnknownOAuthErrorException::factory($description, $rcode, $rbody, $resp, $rheaders, $errorCode);
×
271
        }
272
    }
273

274
    /**
275
     * @static
276
     *
277
     * @param null|array $appInfo
278
     *
279
     * @return null|string
280
     */
281
    private static function _formatAppInfo($appInfo)
282
    {
283
        if (null !== $appInfo) {
19✔
284
            $string = $appInfo['name'];
19✔
285
            if (null !== $appInfo['version']) {
19✔
286
                $string .= '/' . $appInfo['version'];
19✔
287
            }
288
            if (null !== $appInfo['url']) {
19✔
289
                $string .= ' (' . $appInfo['url'] . ')';
19✔
290
            }
291

292
            return $string;
19✔
293
        }
294

295
        return null;
×
296
    }
297

298
    /**
299
     * @static
300
     *
301
     * @param string $disableFunctionsOutput - String value of the 'disable_function' setting, as output by \ini_get('disable_functions')
302
     * @param string $functionName - Name of the function we are interesting in seeing whether or not it is disabled
303
     *
304
     * @return bool
305
     */
306
    private static function _isDisabled($disableFunctionsOutput, $functionName)
307
    {
308
        $disabledFunctions = \explode(',', $disableFunctionsOutput);
20✔
309
        foreach ($disabledFunctions as $disabledFunction) {
20✔
310
            if (\trim($disabledFunction) === $functionName) {
20✔
311
                return true;
1✔
312
            }
313
        }
314

315
        return false;
20✔
316
    }
317

318
    /**
319
     * @static
320
     *
321
     * @param string $apiKey
322
     * @param null   $clientInfo
323
     *
324
     * @return array
325
     */
326
    private static function _defaultHeaders($apiKey, $clientInfo = null)
327
    {
328
        $uaString = 'Stripe/v1 PhpBindings/' . Stripe::VERSION;
19✔
329

330
        $langVersion = \PHP_VERSION;
19✔
331
        $uname_disabled = self::_isDisabled(\ini_get('disable_functions'), 'php_uname');
19✔
332
        $uname = $uname_disabled ? '(disabled)' : \php_uname();
19✔
333

334
        $appInfo = Stripe::getAppInfo();
19✔
335
        $ua = [
19✔
336
            'bindings_version' => Stripe::VERSION,
19✔
337
            'lang' => 'php',
19✔
338
            'lang_version' => $langVersion,
19✔
339
            'publisher' => 'stripe',
19✔
340
            'uname' => $uname,
19✔
341
        ];
19✔
342
        if ($clientInfo) {
19✔
343
            $ua = \array_merge($clientInfo, $ua);
1✔
344
        }
345
        if (null !== $appInfo) {
19✔
346
            $uaString .= ' ' . self::_formatAppInfo($appInfo);
19✔
347
            $ua['application'] = $appInfo;
19✔
348
        }
349

350
        return [
19✔
351
            'X-Stripe-Client-User-Agent' => \json_encode($ua),
19✔
352
            'User-Agent' => $uaString,
19✔
353
            'Authorization' => 'Bearer ' . $apiKey,
19✔
354
        ];
19✔
355
    }
356

357
    /**
358
     * @param 'delete'|'get'|'post' $method
359
     * @param string $url
360
     * @param array $params
361
     * @param array $headers
362
     * @param 'preview'|'standard' $apiMode
363
     */
364
    private function _prepareRequest($method, $url, $params, $headers, $apiMode)
365
    {
366
        $myApiKey = $this->_apiKey;
19✔
367
        if (!$myApiKey) {
19✔
368
            $myApiKey = Stripe::$apiKey;
19✔
369
        }
370

371
        if (!$myApiKey) {
19✔
372
            $msg = 'No API key provided.  (HINT: set your API key using '
1✔
373
              . '"Stripe::setApiKey(<API-KEY>)".  You can generate API keys from '
1✔
374
              . 'the Stripe web interface.  See https://stripe.com/api for '
1✔
375
              . 'details, or email support@stripe.com if you have any questions.';
1✔
376

377
            throw new Exception\AuthenticationException($msg);
1✔
378
        }
379

380
        // Clients can supply arbitrary additional keys to be included in the
381
        // X-Stripe-Client-User-Agent header via the optional getUserAgentInfo()
382
        // method
383
        $clientUAInfo = null;
18✔
384
        if (\method_exists($this->httpClient(), 'getUserAgentInfo')) {
18✔
385
            $clientUAInfo = $this->httpClient()->getUserAgentInfo();
×
386
        }
387

388
        if ($params && \is_array($params)) {
18✔
389
            $optionKeysInParams = \array_filter(
×
390
                self::$OPTIONS_KEYS,
×
391
                function ($key) use ($params) {
×
392
                    return \array_key_exists($key, $params);
×
393
                }
×
394
            );
×
395
            if (\count($optionKeysInParams) > 0) {
×
396
                $message = \sprintf('Options found in $params: %s. Options should '
×
397
                  . 'be passed in their own array after $params. (HINT: pass an '
×
398
                  . 'empty array to $params if you do not have any.)', \implode(', ', $optionKeysInParams));
×
399
                \trigger_error($message, \E_USER_WARNING);
×
400
            }
401
        }
402

403
        $absUrl = $this->_apiBase . $url;
18✔
404
        if ('standard' === $apiMode) {
18✔
405
            $params = self::_encodeObjects($params);
18✔
406
        }
407
        $defaultHeaders = $this->_defaultHeaders($myApiKey, $clientUAInfo);
18✔
408

409
        if ('preview' === $apiMode && !isset($headers['Stripe-Version'])) {
18✔
410
            $headers['Stripe-Version'] = \Stripe\Util\ApiVersion::PREVIEW;
×
411
        } elseif (Stripe::$apiVersion) {
18✔
412
            $headers['Stripe-Version'] = Stripe::$apiVersion;
1✔
413
        }
414

415
        if (Stripe::$accountId) {
18✔
416
            $defaultHeaders['Stripe-Account'] = Stripe::$accountId;
1✔
417
        }
418

419
        if (Stripe::$enableTelemetry && null !== self::$requestTelemetry) {
18✔
420
            $defaultHeaders['X-Stripe-Client-Telemetry'] = self::_telemetryJson(self::$requestTelemetry);
×
421
        }
422

423
        $hasFile = false;
18✔
424
        foreach ($params as $k => $v) {
18✔
425
            if (\is_resource($v)) {
×
426
                $hasFile = true;
×
427
                $params[$k] = self::_processResourceParam($v);
×
428
            } elseif ($v instanceof \CURLFile) {
×
429
                $hasFile = true;
×
430
            }
431
        }
432

433
        if ($hasFile) {
18✔
434
            $defaultHeaders['Content-Type'] = 'multipart/form-data';
×
435
        } elseif ('preview' === $apiMode) {
18✔
436
            $defaultHeaders['Content-Type'] = 'application/json';
×
437
        } elseif ('standard' === $apiMode) {
18✔
438
            $defaultHeaders['Content-Type'] = 'application/x-www-form-urlencoded';
18✔
439
        } else {
440
            throw new Exception\InvalidArgumentException('Unknown API mode: ' . $apiMode);
×
441
        }
442

443
        $combinedHeaders = \array_merge($defaultHeaders, $headers);
18✔
444
        $rawHeaders = [];
18✔
445

446
        foreach ($combinedHeaders as $header => $value) {
18✔
447
            $rawHeaders[] = $header . ': ' . $value;
18✔
448
        }
449

450
        return [$absUrl, $rawHeaders, $params, $hasFile, $myApiKey];
18✔
451
    }
452

453
    /**
454
     * @param 'delete'|'get'|'post' $method
455
     * @param string $url
456
     * @param array $params
457
     * @param array $headers
458
     * @param 'preview'|'standard' $apiMode
459
     *
460
     * @throws Exception\AuthenticationException
461
     * @throws Exception\ApiConnectionException
462
     *
463
     * @return array
464
     */
465
    private function _requestRaw($method, $url, $params, $headers, $apiMode)
466
    {
467
        list($absUrl, $rawHeaders, $params, $hasFile, $myApiKey) = $this->_prepareRequest($method, $url, $params, $headers, $apiMode);
19✔
468

469
        $requestStartMs = Util\Util::currentTimeMillis();
18✔
470

471
        list($rbody, $rcode, $rheaders) = $this->httpClient()->request(
18✔
472
            $method,
18✔
473
            $absUrl,
18✔
474
            $rawHeaders,
18✔
475
            $params,
18✔
476
            $hasFile,
18✔
477
            $apiMode
18✔
478
        );
18✔
479

480
        if (isset($rheaders['request-id'])
18✔
481
        && \is_string($rheaders['request-id'])
18✔
482
        && '' !== $rheaders['request-id']) {
18✔
483
            self::$requestTelemetry = new RequestTelemetry(
×
484
                $rheaders['request-id'],
×
485
                Util\Util::currentTimeMillis() - $requestStartMs
×
486
            );
×
487
        }
488

489
        return [$rbody, $rcode, $rheaders, $myApiKey];
18✔
490
    }
491

492
    /**
493
     * @param 'delete'|'get'|'post' $method
494
     * @param string $url
495
     * @param array $params
496
     * @param array $headers
497
     * @param callable $readBodyChunkCallable
498
     * @param 'preview'|'standard' $apiMode
499
     *
500
     * @throws Exception\AuthenticationException
501
     * @throws Exception\ApiConnectionException
502
     *
503
     * @return array
504
     */
505
    private function _requestRawStreaming($method, $url, $params, $headers, $readBodyChunkCallable, $apiMode)
506
    {
507
        list($absUrl, $rawHeaders, $params, $hasFile, $myApiKey) = $this->_prepareRequest($method, $url, $params, $headers, $apiMode);
×
508

509
        $requestStartMs = Util\Util::currentTimeMillis();
×
510

511
        list($rbody, $rcode, $rheaders) = $this->streamingHttpClient()->requestStream(
×
512
            $method,
×
513
            $absUrl,
×
514
            $rawHeaders,
×
515
            $params,
×
516
            $hasFile,
×
517
            $readBodyChunkCallable
×
518
        );
×
519

520
        if (isset($rheaders['request-id'])
×
521
        && \is_string($rheaders['request-id'])
×
522
        && '' !== $rheaders['request-id']) {
×
523
            self::$requestTelemetry = new RequestTelemetry(
×
524
                $rheaders['request-id'],
×
525
                Util\Util::currentTimeMillis() - $requestStartMs
×
526
            );
×
527
        }
528

529
        return [$rbody, $rcode, $rheaders, $myApiKey];
×
530
    }
531

532
    /**
533
     * @param resource $resource
534
     *
535
     * @throws Exception\InvalidArgumentException
536
     *
537
     * @return \CURLFile|string
538
     */
539
    private function _processResourceParam($resource)
540
    {
541
        if ('stream' !== \get_resource_type($resource)) {
×
542
            throw new Exception\InvalidArgumentException(
×
543
                'Attempted to upload a resource that is not a stream'
×
544
            );
×
545
        }
546

547
        $metaData = \stream_get_meta_data($resource);
×
548
        if ('plainfile' !== $metaData['wrapper_type']) {
×
549
            throw new Exception\InvalidArgumentException(
×
550
                'Only plainfile resource streams are supported'
×
551
            );
×
552
        }
553

554
        // We don't have the filename or mimetype, but the API doesn't care
555
        return new \CURLFile($metaData['uri']);
×
556
    }
557

558
    /**
559
     * @param string $rbody
560
     * @param int    $rcode
561
     * @param array  $rheaders
562
     *
563
     * @throws Exception\UnexpectedValueException
564
     * @throws Exception\ApiErrorException
565
     *
566
     * @return array
567
     */
568
    private function _interpretResponse($rbody, $rcode, $rheaders)
569
    {
570
        $resp = \json_decode($rbody, true);
18✔
571
        $jsonError = \json_last_error();
18✔
572
        if (null === $resp && \JSON_ERROR_NONE !== $jsonError) {
18✔
573
            $msg = "Invalid response body from API: {$rbody} "
×
574
              . "(HTTP response code was {$rcode}, json_last_error() was {$jsonError})";
×
575

576
            throw new Exception\UnexpectedValueException($msg, $rcode);
×
577
        }
578

579
        if ($rcode < 200 || $rcode >= 300) {
18✔
580
            $this->handleErrorResponse($rbody, $rcode, $rheaders, $resp);
14✔
581
        }
582

583
        return $resp;
4✔
584
    }
585

586
    /**
587
     * @static
588
     *
589
     * @param HttpClient\ClientInterface $client
590
     */
591
    public static function setHttpClient($client)
592
    {
593
        self::$_httpClient = $client;
23✔
594
    }
595

596
    /**
597
     * @static
598
     *
599
     * @param HttpClient\StreamingClientInterface $client
600
     */
601
    public static function setStreamingHttpClient($client)
602
    {
603
        self::$_streamingHttpClient = $client;
23✔
604
    }
605

606
    /**
607
     * @static
608
     *
609
     * Resets any stateful telemetry data
610
     */
611
    public static function resetTelemetry()
612
    {
613
        self::$requestTelemetry = null;
×
614
    }
615

616
    /**
617
     * @return HttpClient\ClientInterface
618
     */
619
    private function httpClient()
620
    {
621
        if (!self::$_httpClient) {
19✔
622
            self::$_httpClient = HttpClient\CurlClient::instance();
×
623
        }
624

625
        return self::$_httpClient;
19✔
626
    }
627

628
    /**
629
     * @return HttpClient\StreamingClientInterface
630
     */
631
    private function streamingHttpClient()
632
    {
633
        if (!self::$_streamingHttpClient) {
×
634
            self::$_streamingHttpClient = HttpClient\CurlClient::instance();
×
635
        }
636

637
        return self::$_streamingHttpClient;
×
638
    }
639
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc