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

stripe / stripe-php / 9290644465

29 May 2024 05:37PM UTC coverage: 66.176% (+0.06%) from 66.119%
9290644465

Pull #1700

github

web-flow
Merge 22ad79147 into 7038876e7
Pull Request #1700: Add optional appInfo to StripeClient config

22 of 32 new or added lines in 2 files covered. (68.75%)

1 existing line in 1 file now uncovered.

2383 of 3601 relevant lines covered (66.18%)

3.35 hits per line

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

64.45
/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 null|array
22
     */
23
    private $_appInfo;
24

25
    /**
26
     * @var HttpClient\ClientInterface
27
     */
28
    private static $_httpClient;
29
    /**
30
     * @var HttpClient\StreamingClientInterface
31
     */
32
    private static $_streamingHttpClient;
33

34
    /**
35
     * @var RequestTelemetry
36
     */
37
    private static $requestTelemetry;
38

39
    private static $OPTIONS_KEYS = ['api_key', 'idempotency_key', 'stripe_account', 'stripe_version', 'api_base'];
40

41
    /**
42
     * ApiRequestor constructor.
43
     *
44
     * @param null|string $apiKey
45
     * @param null|string $apiBase
46
     * @param null|array $appInfo
47
     */
48
    public function __construct($apiKey = null, $apiBase = null, $appInfo = null)
20✔
49
    {
50
        $this->_apiKey = $apiKey;
20✔
51
        if (!$apiBase) {
20✔
52
            $apiBase = Stripe::$apiBase;
1✔
53
        }
54
        $this->_apiBase = $apiBase;
20✔
55
        $this->_appInfo = $appInfo;
20✔
56
    }
57

58
    /**
59
     * Creates a telemetry json blob for use in 'X-Stripe-Client-Telemetry' headers.
60
     *
61
     * @static
62
     *
63
     * @param RequestTelemetry $requestTelemetry
64
     *
65
     * @return string
66
     */
67
    private static function _telemetryJson($requestTelemetry)
×
68
    {
69
        $payload = [
×
70
            'last_request_metrics' => [
×
71
                'request_id' => $requestTelemetry->requestId,
×
72
                'request_duration_ms' => $requestTelemetry->requestDuration,
×
73
            ],
×
74
        ];
×
75
        if (\count($requestTelemetry->usage) > 0) {
×
76
            $payload['last_request_metrics']['usage'] = $requestTelemetry->usage;
×
77
        }
78

79
        $result = \json_encode($payload);
×
80
        if (false !== $result) {
×
81
            return $result;
×
82
        }
83
        Stripe::getLogger()->error('Serializing telemetry payload failed!');
×
84

85
        return '{}';
×
86
    }
87

88
    /**
89
     * @static
90
     *
91
     * @param ApiResource|array|bool|mixed $d
92
     *
93
     * @return ApiResource|array|mixed|string
94
     */
95
    private static function _encodeObjects($d)
19✔
96
    {
97
        if ($d instanceof ApiResource) {
19✔
98
            return Util\Util::utf8($d->id);
1✔
99
        }
100
        if (true === $d) {
19✔
101
            return 'true';
1✔
102
        }
103
        if (false === $d) {
19✔
104
            return 'false';
1✔
105
        }
106
        if (\is_array($d)) {
19✔
107
            $res = [];
19✔
108
            foreach ($d as $k => $v) {
19✔
109
                $res[$k] = self::_encodeObjects($v);
1✔
110
            }
111

112
            return $res;
19✔
113
        }
114

115
        return Util\Util::utf8($d);
1✔
116
    }
117

118
    /**
119
     * @param 'delete'|'get'|'post' $method
120
     * @param string     $url
121
     * @param null|array $params
122
     * @param null|array $headers
123
     * @param string[] $usage
124
     *
125
     * @throws Exception\ApiErrorException
126
     *
127
     * @return array tuple containing (ApiReponse, API key)
128
     */
129
    public function request($method, $url, $params = null, $headers = null, $usage = [])
19✔
130
    {
131
        $params = $params ?: [];
19✔
132
        $headers = $headers ?: [];
19✔
133
        list($rbody, $rcode, $rheaders, $myApiKey) =
19✔
134
            $this->_requestRaw($method, $url, $params, $headers, $usage);
19✔
135
        $json = $this->_interpretResponse($rbody, $rcode, $rheaders);
18✔
136
        $resp = new ApiResponse($rbody, $rcode, $rheaders, $json);
4✔
137

138
        return [$resp, $myApiKey];
4✔
139
    }
140

141
    /**
142
     * @param 'delete'|'get'|'post' $method
143
     * @param string     $url
144
     * @param callable $readBodyChunkCallable
145
     * @param null|array $params
146
     * @param null|array $headers
147
     * @param string[] $usage
148
     *
149
     * @throws Exception\ApiErrorException
150
     */
151
    public function requestStream($method, $url, $readBodyChunkCallable, $params = null, $headers = null, $usage = [])
×
152
    {
153
        $params = $params ?: [];
×
154
        $headers = $headers ?: [];
×
155
        list($rbody, $rcode, $rheaders, $myApiKey) =
×
NEW
156
            $this->_requestRawStreaming($method, $url, $params, $headers, $usage, $readBodyChunkCallable);
×
157
        if ($rcode >= 300) {
×
158
            $this->_interpretResponse($rbody, $rcode, $rheaders);
×
159
        }
160
    }
161

162
    /**
163
     * @param string $rbody a JSON string
164
     * @param int $rcode
165
     * @param array $rheaders
166
     * @param array $resp
167
     *
168
     * @throws Exception\UnexpectedValueException
169
     * @throws Exception\ApiErrorException
170
     */
171
    public function handleErrorResponse($rbody, $rcode, $rheaders, $resp)
14✔
172
    {
173
        if (!\is_array($resp) || !isset($resp['error'])) {
14✔
174
            $msg = "Invalid response object from API: {$rbody} "
×
NEW
175
                . "(HTTP response code was {$rcode})";
×
176

177
            throw new Exception\UnexpectedValueException($msg);
×
178
        }
179

180
        $errorData = $resp['error'];
14✔
181

182
        $error = null;
14✔
183
        if (\is_string($errorData)) {
14✔
184
            $error = self::_specificOAuthError($rbody, $rcode, $rheaders, $resp, $errorData);
6✔
185
        }
186
        if (!$error) {
14✔
187
            $error = self::_specificAPIError($rbody, $rcode, $rheaders, $resp, $errorData);
8✔
188
        }
189

190
        throw $error;
14✔
191
    }
192

193
    /**
194
     * @static
195
     *
196
     * @param string $rbody
197
     * @param int    $rcode
198
     * @param array  $rheaders
199
     * @param array  $resp
200
     * @param array  $errorData
201
     *
202
     * @return Exception\ApiErrorException
203
     */
204
    private static function _specificAPIError($rbody, $rcode, $rheaders, $resp, $errorData)
8✔
205
    {
206
        $msg = isset($errorData['message']) ? $errorData['message'] : null;
8✔
207
        $param = isset($errorData['param']) ? $errorData['param'] : null;
8✔
208
        $code = isset($errorData['code']) ? $errorData['code'] : null;
8✔
209
        $type = isset($errorData['type']) ? $errorData['type'] : null;
8✔
210
        $declineCode = isset($errorData['decline_code']) ? $errorData['decline_code'] : null;
8✔
211

212
        switch ($rcode) {
213
            case 400:
8✔
214
                // 'rate_limit' code is deprecated, but left here for backwards compatibility
215
                // for API versions earlier than 2015-09-08
216
                if ('rate_limit' === $code) {
3✔
217
                    return Exception\RateLimitException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code, $param);
1✔
218
                }
219
                if ('idempotency_error' === $type) {
2✔
220
                    return Exception\IdempotencyException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code);
1✔
221
                }
222

223
            // no break
224
            case 404:
5✔
225
                return Exception\InvalidRequestException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code, $param);
2✔
226

227
            case 401:
4✔
228
                return Exception\AuthenticationException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code);
1✔
229

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

233
            case 403:
2✔
234
                return Exception\PermissionException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code);
1✔
235

236
            case 429:
1✔
237
                return Exception\RateLimitException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code, $param);
1✔
238

239
            default:
240
                return Exception\UnknownApiErrorException::factory($msg, $rcode, $rbody, $resp, $rheaders, $code);
×
241
        }
242
    }
243

244
    /**
245
     * @static
246
     *
247
     * @param bool|string $rbody
248
     * @param int         $rcode
249
     * @param array       $rheaders
250
     * @param array       $resp
251
     * @param string      $errorCode
252
     *
253
     * @return Exception\OAuth\OAuthErrorException
254
     */
255
    private static function _specificOAuthError($rbody, $rcode, $rheaders, $resp, $errorCode)
6✔
256
    {
257
        $description = isset($resp['error_description']) ? $resp['error_description'] : $errorCode;
6✔
258

259
        switch ($errorCode) {
260
            case 'invalid_client':
6✔
261
                return Exception\OAuth\InvalidClientException::factory($description, $rcode, $rbody, $resp, $rheaders, $errorCode);
1✔
262

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

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

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

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

275
            case 'unsupported_response_type':
1✔
276
                return Exception\OAuth\UnsupportedResponseTypeException::factory($description, $rcode, $rbody, $resp, $rheaders, $errorCode);
1✔
277

278
            default:
279
                return Exception\OAuth\UnknownOAuthErrorException::factory($description, $rcode, $rbody, $resp, $rheaders, $errorCode);
×
280
        }
281
    }
282

283
    /**
284
     * @static
285
     *
286
     * @param null|array $appInfo
287
     *
288
     * @return null|string
289
     */
290
    private static function _formatAppInfo($appInfo)
19✔
291
    {
292
        if (null !== $appInfo) {
19✔
293
            $string = $appInfo['name'];
19✔
294
            if (\array_key_exists('version', $appInfo) && null !== $appInfo['version']) {
19✔
295
                $string .= '/' . $appInfo['version'];
19✔
296
            }
297
            if (\array_key_exists('url', $appInfo) && null !== $appInfo['url']) {
19✔
298
                $string .= ' (' . $appInfo['url'] . ')';
19✔
299
            }
300

301
            return $string;
19✔
302
        }
303

304
        return null;
×
305
    }
306

307
    /**
308
     * @static
309
     *
310
     * @param string $disableFunctionsOutput - String value of the 'disable_function' setting, as output by \ini_get('disable_functions')
311
     * @param string $functionName - Name of the function we are interesting in seeing whether or not it is disabled
312
     *
313
     * @return bool
314
     */
315
    private static function _isDisabled($disableFunctionsOutput, $functionName)
20✔
316
    {
317
        $disabledFunctions = \explode(',', $disableFunctionsOutput);
20✔
318
        foreach ($disabledFunctions as $disabledFunction) {
20✔
319
            if (\trim($disabledFunction) === $functionName) {
20✔
320
                return true;
1✔
321
            }
322
        }
323

324
        return false;
20✔
325
    }
326

327
    /**
328
     * @static
329
     *
330
     * @param string     $apiKey the Stripe API key, to be used in regular API requests
331
     * @param null       $clientInfo client user agent information
332
     * @param null       $appInfo information to identify a plugin that integrates Stripe using this library
333
     *
334
     * @return array
335
     */
336
    private static function _defaultHeaders($apiKey, $clientInfo = null, $appInfo = null)
19✔
337
    {
338
        $uaString = 'Stripe/v1 PhpBindings/' . Stripe::VERSION;
19✔
339

340
        $langVersion = \PHP_VERSION;
19✔
341
        $uname_disabled = self::_isDisabled(\ini_get('disable_functions'), 'php_uname');
19✔
342
        $uname = $uname_disabled ? '(disabled)' : \php_uname();
19✔
343

344
        // Fallback to global configuration to maintain backwards compatibility.
345
        $appInfo = $appInfo ?: Stripe::getAppInfo();
19✔
346
        $ua = [
19✔
347
            'bindings_version' => Stripe::VERSION,
19✔
348
            'lang' => 'php',
19✔
349
            'lang_version' => $langVersion,
19✔
350
            'publisher' => 'stripe',
19✔
351
            'uname' => $uname,
19✔
352
        ];
19✔
353
        if ($clientInfo) {
19✔
354
            $ua = \array_merge($clientInfo, $ua);
1✔
355
        }
356
        if (null !== $appInfo) {
19✔
357
            $uaString .= ' ' . self::_formatAppInfo($appInfo);
19✔
358
            $ua['application'] = $appInfo;
19✔
359
        }
360

361
        return [
19✔
362
            'X-Stripe-Client-User-Agent' => \json_encode($ua),
19✔
363
            'User-Agent' => $uaString,
19✔
364
            'Authorization' => 'Bearer ' . $apiKey,
19✔
365
            'Stripe-Version' => Stripe::getApiVersion(),
19✔
366
        ];
19✔
367
    }
368

369
    private function _prepareRequest($method, $url, $params, $headers)
19✔
370
    {
371
        $myApiKey = $this->_apiKey;
19✔
372
        if (!$myApiKey) {
19✔
373
            $myApiKey = Stripe::$apiKey;
19✔
374
        }
375

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

382
            throw new Exception\AuthenticationException($msg);
1✔
383
        }
384

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

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

408
        $absUrl = $this->_apiBase . $url;
18✔
409
        $params = self::_encodeObjects($params);
18✔
410
        $defaultHeaders = $this->_defaultHeaders($myApiKey, $clientUAInfo, $this->_appInfo);
18✔
411

412
        if (Stripe::$accountId) {
18✔
413
            $defaultHeaders['Stripe-Account'] = Stripe::$accountId;
1✔
414
        }
415

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

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

430
        if ($hasFile) {
18✔
431
            $defaultHeaders['Content-Type'] = 'multipart/form-data';
×
432
        } else {
433
            $defaultHeaders['Content-Type'] = 'application/x-www-form-urlencoded';
18✔
434
        }
435

436
        $combinedHeaders = \array_merge($defaultHeaders, $headers);
18✔
437
        $rawHeaders = [];
18✔
438

439
        foreach ($combinedHeaders as $header => $value) {
18✔
440
            $rawHeaders[] = $header . ': ' . $value;
18✔
441
        }
442

443
        return [$absUrl, $rawHeaders, $params, $hasFile, $myApiKey];
18✔
444
    }
445

446
    /**
447
     * @param 'delete'|'get'|'post' $method
448
     * @param string $url
449
     * @param array $params
450
     * @param array $headers
451
     * @param string[] $usage
452
     *
453
     * @throws Exception\AuthenticationException
454
     * @throws Exception\ApiConnectionException
455
     *
456
     * @return array
457
     */
458
    private function _requestRaw($method, $url, $params, $headers, $usage)
19✔
459
    {
460
        list($absUrl, $rawHeaders, $params, $hasFile, $myApiKey) = $this->_prepareRequest($method, $url, $params, $headers);
19✔
461

462
        $requestStartMs = Util\Util::currentTimeMillis();
18✔
463

464
        list($rbody, $rcode, $rheaders) = $this->httpClient()->request(
18✔
465
            $method,
18✔
466
            $absUrl,
18✔
467
            $rawHeaders,
18✔
468
            $params,
18✔
469
            $hasFile
18✔
470
        );
18✔
471

472
        if (
473
            isset($rheaders['request-id'])
18✔
474
            && \is_string($rheaders['request-id'])
18✔
475
            && '' !== $rheaders['request-id']
18✔
476
        ) {
477
            self::$requestTelemetry = new RequestTelemetry(
×
478
                $rheaders['request-id'],
×
479
                Util\Util::currentTimeMillis() - $requestStartMs,
×
480
                $usage
×
481
            );
×
482
        }
483

484
        return [$rbody, $rcode, $rheaders, $myApiKey];
18✔
485
    }
486

487
    /**
488
     * @param 'delete'|'get'|'post' $method
489
     * @param string $url
490
     * @param array $params
491
     * @param array $headers
492
     * @param string[] $usage
493
     * @param callable $readBodyChunkCallable
494
     *
495
     * @throws Exception\AuthenticationException
496
     * @throws Exception\ApiConnectionException
497
     *
498
     * @return array
499
     */
500
    private function _requestRawStreaming($method, $url, $params, $headers, $usage, $readBodyChunkCallable)
×
501
    {
502
        list($absUrl, $rawHeaders, $params, $hasFile, $myApiKey) = $this->_prepareRequest($method, $url, $params, $headers);
×
503

504
        $requestStartMs = Util\Util::currentTimeMillis();
×
505

506
        list($rbody, $rcode, $rheaders) = $this->streamingHttpClient()->requestStream(
×
507
            $method,
×
508
            $absUrl,
×
509
            $rawHeaders,
×
510
            $params,
×
511
            $hasFile,
×
512
            $readBodyChunkCallable
×
513
        );
×
514

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

526
        return [$rbody, $rcode, $rheaders, $myApiKey];
×
527
    }
528

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

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

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

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

573
            throw new Exception\UnexpectedValueException($msg, $rcode);
×
574
        }
575

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

580
        return $resp;
4✔
581
    }
582

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

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

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

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

622
        return self::$_httpClient;
19✔
623
    }
624

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

634
        return self::$_streamingHttpClient;
×
635
    }
636
}
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

© 2024 Coveralls, Inc