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

stripe / stripe-php / 6471862601

10 Oct 2023 04:02PM UTC coverage: 69.665% (-0.5%) from 70.141%
6471862601

push

github

web-flow
Merge pull request #1570 from localheinz/feature/coveralls

Enhancement: Use `coverallsapp/github-action` to report code coverage

2393 of 3435 relevant lines covered (69.67%)

3.5 hits per line

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

65.08
/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)
20✔
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)
19✔
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
     *
114
     * @throws Exception\ApiErrorException
115
     *
116
     * @return array tuple containing (ApiReponse, API key)
117
     */
118
    public function request($method, $url, $params = null, $headers = null)
19✔
119
    {
120
        $params = $params ?: [];
19✔
121
        $headers = $headers ?: [];
19✔
122
        list($rbody, $rcode, $rheaders, $myApiKey) =
19✔
123
        $this->_requestRaw($method, $url, $params, $headers);
19✔
124
        $json = $this->_interpretResponse($rbody, $rcode, $rheaders);
18✔
125
        $resp = new ApiResponse($rbody, $rcode, $rheaders, $json);
4✔
126

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

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

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

165
            throw new Exception\UnexpectedValueException($msg);
×
166
        }
167

168
        $errorData = $resp['error'];
14✔
169

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

178
        throw $error;
14✔
179
    }
180

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

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

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

215
            case 401:
4✔
216
                return Exception\AuthenticationException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code);
1✔
217

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

221
            case 403:
2✔
222
                return Exception\PermissionException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code);
1✔
223

224
            case 429:
1✔
225
                return Exception\RateLimitException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code, $param);
1✔
226

227
            default:
228
                return Exception\UnknownApiErrorException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code);
×
229
        }
230
    }
231

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

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

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

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

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

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

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

266
            default:
267
                return Exception\OAuth\UnknownOAuthErrorException::factory($description, $rcode, $rbody, $resp, $rheaders, $errorCode);
×
268
        }
269
    }
270

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

289
            return $string;
19✔
290
        }
291

292
        return null;
×
293
    }
294

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

312
        return false;
20✔
313
    }
314

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

327
        $langVersion = \PHP_VERSION;
19✔
328
        $uname_disabled = self::_isDisabled(\ini_get('disable_functions'), 'php_uname');
19✔
329
        $uname = $uname_disabled ? '(disabled)' : \php_uname();
19✔
330

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

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

355
    private function _prepareRequest($method, $url, $params, $headers)
19✔
356
    {
357
        $myApiKey = $this->_apiKey;
19✔
358
        if (!$myApiKey) {
19✔
359
            $myApiKey = Stripe::$apiKey;
19✔
360
        }
361

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

368
            throw new Exception\AuthenticationException($msg);
1✔
369
        }
370

371
        // Clients can supply arbitrary additional keys to be included in the
372
        // X-Stripe-Client-User-Agent header via the optional getUserAgentInfo()
373
        // method
374
        $clientUAInfo = null;
18✔
375
        if (\method_exists($this->httpClient(), 'getUserAgentInfo')) {
18✔
376
            $clientUAInfo = $this->httpClient()->getUserAgentInfo();
×
377
        }
378

379
        if ($params && \is_array($params)) {
18✔
380
            $optionKeysInParams = \array_filter(
×
381
                self::$OPTIONS_KEYS,
×
382
                function ($key) use ($params) {
×
383
                    return \array_key_exists($key, $params);
×
384
                }
×
385
            );
×
386
            if (\count($optionKeysInParams) > 0) {
×
387
                $message = \sprintf('Options found in $params: %s. Options should '
×
388
                  . 'be passed in their own array after $params. (HINT: pass an '
×
389
                  . 'empty array to $params if you do not have any.)', \implode(', ', $optionKeysInParams));
×
390
                \trigger_error($message, \E_USER_WARNING);
×
391
            }
392
        }
393

394
        $absUrl = $this->_apiBase . $url;
18✔
395
        $params = self::_encodeObjects($params);
18✔
396
        $defaultHeaders = $this->_defaultHeaders($myApiKey, $clientUAInfo);
18✔
397

398
        if (Stripe::$accountId) {
18✔
399
            $defaultHeaders['Stripe-Account'] = Stripe::$accountId;
1✔
400
        }
401

402
        if (Stripe::$enableTelemetry && null !== self::$requestTelemetry) {
18✔
403
            $defaultHeaders['X-Stripe-Client-Telemetry'] = self::_telemetryJson(self::$requestTelemetry);
×
404
        }
405

406
        $hasFile = false;
18✔
407
        foreach ($params as $k => $v) {
18✔
408
            if (\is_resource($v)) {
×
409
                $hasFile = true;
×
410
                $params[$k] = self::_processResourceParam($v);
×
411
            } elseif ($v instanceof \CURLFile) {
×
412
                $hasFile = true;
×
413
            }
414
        }
415

416
        if ($hasFile) {
18✔
417
            $defaultHeaders['Content-Type'] = 'multipart/form-data';
×
418
        } else {
419
            $defaultHeaders['Content-Type'] = 'application/x-www-form-urlencoded';
18✔
420
        }
421

422
        $combinedHeaders = \array_merge($defaultHeaders, $headers);
18✔
423
        $rawHeaders = [];
18✔
424

425
        foreach ($combinedHeaders as $header => $value) {
18✔
426
            $rawHeaders[] = $header . ': ' . $value;
18✔
427
        }
428

429
        return [$absUrl, $rawHeaders, $params, $hasFile, $myApiKey];
18✔
430
    }
431

432
    /**
433
     * @param 'delete'|'get'|'post' $method
434
     * @param string $url
435
     * @param array $params
436
     * @param array $headers
437
     *
438
     * @throws Exception\AuthenticationException
439
     * @throws Exception\ApiConnectionException
440
     *
441
     * @return array
442
     */
443
    private function _requestRaw($method, $url, $params, $headers)
19✔
444
    {
445
        list($absUrl, $rawHeaders, $params, $hasFile, $myApiKey) = $this->_prepareRequest($method, $url, $params, $headers);
19✔
446

447
        $requestStartMs = Util\Util::currentTimeMillis();
18✔
448

449
        list($rbody, $rcode, $rheaders) = $this->httpClient()->request(
18✔
450
            $method,
18✔
451
            $absUrl,
18✔
452
            $rawHeaders,
18✔
453
            $params,
18✔
454
            $hasFile
18✔
455
        );
18✔
456

457
        if (isset($rheaders['request-id'])
18✔
458
        && \is_string($rheaders['request-id'])
18✔
459
        && '' !== $rheaders['request-id']) {
18✔
460
            self::$requestTelemetry = new RequestTelemetry(
×
461
                $rheaders['request-id'],
×
462
                Util\Util::currentTimeMillis() - $requestStartMs
×
463
            );
×
464
        }
465

466
        return [$rbody, $rcode, $rheaders, $myApiKey];
18✔
467
    }
468

469
    /**
470
     * @param 'delete'|'get'|'post' $method
471
     * @param string $url
472
     * @param array $params
473
     * @param array $headers
474
     * @param callable $readBodyChunkCallable
475
     *
476
     * @throws Exception\AuthenticationException
477
     * @throws Exception\ApiConnectionException
478
     *
479
     * @return array
480
     */
481
    private function _requestRawStreaming($method, $url, $params, $headers, $readBodyChunkCallable)
×
482
    {
483
        list($absUrl, $rawHeaders, $params, $hasFile, $myApiKey) = $this->_prepareRequest($method, $url, $params, $headers);
×
484

485
        $requestStartMs = Util\Util::currentTimeMillis();
×
486

487
        list($rbody, $rcode, $rheaders) = $this->streamingHttpClient()->requestStream(
×
488
            $method,
×
489
            $absUrl,
×
490
            $rawHeaders,
×
491
            $params,
×
492
            $hasFile,
×
493
            $readBodyChunkCallable
×
494
        );
×
495

496
        if (isset($rheaders['request-id'])
×
497
        && \is_string($rheaders['request-id'])
×
498
        && '' !== $rheaders['request-id']) {
×
499
            self::$requestTelemetry = new RequestTelemetry(
×
500
                $rheaders['request-id'],
×
501
                Util\Util::currentTimeMillis() - $requestStartMs
×
502
            );
×
503
        }
504

505
        return [$rbody, $rcode, $rheaders, $myApiKey];
×
506
    }
507

508
    /**
509
     * @param resource $resource
510
     *
511
     * @throws Exception\InvalidArgumentException
512
     *
513
     * @return \CURLFile|string
514
     */
515
    private function _processResourceParam($resource)
×
516
    {
517
        if ('stream' !== \get_resource_type($resource)) {
×
518
            throw new Exception\InvalidArgumentException(
×
519
                'Attempted to upload a resource that is not a stream'
×
520
            );
×
521
        }
522

523
        $metaData = \stream_get_meta_data($resource);
×
524
        if ('plainfile' !== $metaData['wrapper_type']) {
×
525
            throw new Exception\InvalidArgumentException(
×
526
                'Only plainfile resource streams are supported'
×
527
            );
×
528
        }
529

530
        // We don't have the filename or mimetype, but the API doesn't care
531
        return new \CURLFile($metaData['uri']);
×
532
    }
533

534
    /**
535
     * @param string $rbody
536
     * @param int    $rcode
537
     * @param array  $rheaders
538
     *
539
     * @throws Exception\UnexpectedValueException
540
     * @throws Exception\ApiErrorException
541
     *
542
     * @return array
543
     */
544
    private function _interpretResponse($rbody, $rcode, $rheaders)
18✔
545
    {
546
        $resp = \json_decode($rbody, true);
18✔
547
        $jsonError = \json_last_error();
18✔
548
        if (null === $resp && \JSON_ERROR_NONE !== $jsonError) {
18✔
549
            $msg = "Invalid response body from API: {$rbody} "
×
550
              . "(HTTP response code was {$rcode}, json_last_error() was {$jsonError})";
×
551

552
            throw new Exception\UnexpectedValueException($msg, $rcode);
×
553
        }
554

555
        if ($rcode < 200 || $rcode >= 300) {
18✔
556
            $this->handleErrorResponse($rbody, $rcode, $rheaders, $resp);
14✔
557
        }
558

559
        return $resp;
4✔
560
    }
561

562
    /**
563
     * @static
564
     *
565
     * @param HttpClient\ClientInterface $client
566
     */
567
    public static function setHttpClient($client)
23✔
568
    {
569
        self::$_httpClient = $client;
23✔
570
    }
571

572
    /**
573
     * @static
574
     *
575
     * @param HttpClient\StreamingClientInterface $client
576
     */
577
    public static function setStreamingHttpClient($client)
23✔
578
    {
579
        self::$_streamingHttpClient = $client;
23✔
580
    }
581

582
    /**
583
     * @static
584
     *
585
     * Resets any stateful telemetry data
586
     */
587
    public static function resetTelemetry()
×
588
    {
589
        self::$requestTelemetry = null;
×
590
    }
591

592
    /**
593
     * @return HttpClient\ClientInterface
594
     */
595
    private function httpClient()
19✔
596
    {
597
        if (!self::$_httpClient) {
19✔
598
            self::$_httpClient = HttpClient\CurlClient::instance();
×
599
        }
600

601
        return self::$_httpClient;
19✔
602
    }
603

604
    /**
605
     * @return HttpClient\StreamingClientInterface
606
     */
607
    private function streamingHttpClient()
×
608
    {
609
        if (!self::$_streamingHttpClient) {
×
610
            self::$_streamingHttpClient = HttpClient\CurlClient::instance();
×
611
        }
612

613
        return self::$_streamingHttpClient;
×
614
    }
615
}
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