Browse code

Added README.md appinfo/info.xml appinfo/signature.json lib/Controller/AuthorApiController.php and the providers directory

DoubleBastionAdmin authored on 20/08/2022 16:33:00
Showing 1 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,554 @@
1
+<?php
2
+
3
+namespace Telnyx\HttpClient;
4
+
5
+use Telnyx\Exception;
6
+use Telnyx\Telnyx;
7
+use Telnyx\Util;
8
+
9
+// @codingStandardsIgnoreStart
10
+// PSR2 requires all constants be upper case. Sadly, the CURL_SSLVERSION
11
+// constants do not abide by those rules.
12
+
13
+// Note the values come from their position in the enums that
14
+// defines them in cURL's source code.
15
+
16
+// Available since PHP 5.5.19 and 5.6.3
17
+if (!\defined('CURL_SSLVERSION_TLSv1_2')) {
18
+    \define('CURL_SSLVERSION_TLSv1_2', 6);
19
+}
20
+// @codingStandardsIgnoreEnd
21
+
22
+// Available since PHP 7.0.7 and cURL 7.47.0
23
+if (!\defined('CURL_HTTP_VERSION_2TLS')) {
24
+    \define('CURL_HTTP_VERSION_2TLS', 4);
25
+}
26
+
27
+class CurlClient implements ClientInterface
28
+{
29
+    private static $instance;
30
+
31
+    public static function instance()
32
+    {
33
+        if (!self::$instance) {
34
+            self::$instance = new self();
35
+        }
36
+
37
+        return self::$instance;
38
+    }
39
+
40
+    protected $defaultOptions;
41
+
42
+    /** @var \Telnyx\Util\RandomGenerator */
43
+    protected $randomGenerator;
44
+
45
+    protected $userAgentInfo;
46
+
47
+    protected $enablePersistentConnections = true;
48
+
49
+    protected $enableHttp2;
50
+
51
+    protected $curlHandle;
52
+
53
+    protected $requestStatusCallback;
54
+
55
+    /**
56
+     * CurlClient constructor.
57
+     *
58
+     * Pass in a callable to $defaultOptions that returns an array of CURLOPT_* values to start
59
+     * off a request with, or an flat array with the same format used by curl_setopt_array() to
60
+     * provide a static set of options. Note that many options are overridden later in the request
61
+     * call, including timeouts, which can be set via setTimeout() and setConnectTimeout().
62
+     *
63
+     * Note that request() will silently ignore a non-callable, non-array $defaultOptions, and will
64
+     * throw an exception if $defaultOptions returns a non-array value.
65
+     *
66
+     * @param null|array|callable $defaultOptions
67
+     * @param null|\Telnyx\Util\RandomGenerator $randomGenerator
68
+     */
69
+    public function __construct($defaultOptions = null, $randomGenerator = null)
70
+    {
71
+        $this->defaultOptions = $defaultOptions;
72
+        $this->randomGenerator = $randomGenerator ?: new Util\RandomGenerator();
73
+        $this->initUserAgentInfo();
74
+
75
+        $this->enableHttp2 = $this->canSafelyUseHttp2();
76
+    }
77
+
78
+    public function __destruct()
79
+    {
80
+        $this->closeCurlHandle();
81
+    }
82
+
83
+    public function initUserAgentInfo()
84
+    {
85
+        $curlVersion = \curl_version();
86
+        $this->userAgentInfo = [
87
+            'httplib' => 'curl ' . $curlVersion['version'],
88
+            'ssllib' => $curlVersion['ssl_version'],
89
+        ];
90
+    }
91
+
92
+    public function getDefaultOptions()
93
+    {
94
+        return $this->defaultOptions;
95
+    }
96
+
97
+    public function getUserAgentInfo()
98
+    {
99
+        return $this->userAgentInfo;
100
+    }
101
+
102
+    /**
103
+     * @return bool
104
+     */
105
+    public function getEnablePersistentConnections()
106
+    {
107
+        return $this->enablePersistentConnections;
108
+    }
109
+
110
+    /**
111
+     * @param bool $enable
112
+     */
113
+    public function setEnablePersistentConnections($enable)
114
+    {
115
+        $this->enablePersistentConnections = $enable;
116
+    }
117
+
118
+    /**
119
+     * @return bool
120
+     */
121
+    public function getEnableHttp2()
122
+    {
123
+        return $this->enableHttp2;
124
+    }
125
+
126
+    /**
127
+     * @param bool $enable
128
+     */
129
+    public function setEnableHttp2($enable)
130
+    {
131
+        $this->enableHttp2 = $enable;
132
+    }
133
+
134
+    /**
135
+     * @return null|callable
136
+     */
137
+    public function getRequestStatusCallback()
138
+    {
139
+        return $this->requestStatusCallback;
140
+    }
141
+
142
+    /**
143
+     * Sets a callback that is called after each request. The callback will
144
+     * receive the following parameters:
145
+     * <ol>
146
+     *   <li>string $rbody The response body</li>
147
+     *   <li>integer $rcode The response status code</li>
148
+     *   <li>\Telnyx\Util\CaseInsensitiveArray $rheaders The response headers</li>
149
+     *   <li>integer $errno The curl error number</li>
150
+     *   <li>string|null $message The curl error message</li>
151
+     *   <li>boolean $shouldRetry Whether the request will be retried</li>
152
+     *   <li>integer $numRetries The number of the retry attempt</li>
153
+     * </ol>.
154
+     *
155
+     * @param null|callable $requestStatusCallback
156
+     */
157
+    public function setRequestStatusCallback($requestStatusCallback)
158
+    {
159
+        $this->requestStatusCallback = $requestStatusCallback;
160
+    }
161
+
162
+    // USER DEFINED TIMEOUTS
163
+
164
+    const DEFAULT_TIMEOUT = 80;
165
+    const DEFAULT_CONNECT_TIMEOUT = 30;
166
+
167
+    private $timeout = self::DEFAULT_TIMEOUT;
168
+    private $connectTimeout = self::DEFAULT_CONNECT_TIMEOUT;
169
+
170
+    public function setTimeout($seconds)
171
+    {
172
+        $this->timeout = (int) \max($seconds, 0);
173
+
174
+        return $this;
175
+    }
176
+
177
+    public function setConnectTimeout($seconds)
178
+    {
179
+        $this->connectTimeout = (int) \max($seconds, 0);
180
+
181
+        return $this;
182
+    }
183
+
184
+    public function getTimeout()
185
+    {
186
+        return $this->timeout;
187
+    }
188
+
189
+    public function getConnectTimeout()
190
+    {
191
+        return $this->connectTimeout;
192
+    }
193
+
194
+    // END OF USER DEFINED TIMEOUTS
195
+
196
+    // Simple workaround for mock quirks. Forces json_encode to return {} instead of [] for empty objects.
197
+    private function json_encode_params($params) {
198
+        if (is_array($params) && count($params) == 0) {
199
+            return '{}';
200
+        }
201
+        else {
202
+            return json_encode($params);
203
+        }
204
+    }
205
+
206
+    public function request($method, $absUrl, $headers, $params, $hasFile)
207
+    {
208
+        $method = \strtolower($method);
209
+
210
+        $opts = [];
211
+        if (\is_callable($this->defaultOptions)) { // call defaultOptions callback, set options to return value
212
+            $opts = \call_user_func_array($this->defaultOptions, \func_get_args());
213
+            if (!\is_array($opts)) {
214
+                throw new Exception\UnexpectedValueException('Non-array value returned by defaultOptions CurlClient callback');
215
+            }
216
+        } elseif (\is_array($this->defaultOptions)) { // set default curlopts from array
217
+            $opts = $this->defaultOptions;
218
+        }
219
+
220
+        $params = Util\Util::objectsToIds($params);
221
+
222
+        if ('get' === $method) {
223
+            if ($hasFile) {
224
+                throw new Exception\UnexpectedValueException(
225
+                    'Issuing a GET request with a file parameter'
226
+                );
227
+            }
228
+            $opts[\CURLOPT_HTTPGET] = 1;
229
+            if (\count($params) > 0) {
230
+                $encoded = Util\Util::encodeParameters($params);
231
+                $absUrl = "{$absUrl}?{$encoded}";
232
+            }
233
+        } elseif ($method == 'post') {
234
+            $opts[CURLOPT_POST] = 1;
235
+            $opts[CURLOPT_POSTFIELDS] = $hasFile ? $params : $this->json_encode_params($params);
236
+        } elseif ($method == 'patch') {
237
+            $opts[CURLOPT_CUSTOMREQUEST] = 'PATCH';
238
+            $opts[CURLOPT_POSTFIELDS] = $hasFile ? $params : $this->json_encode_params($params);
239
+        } elseif ($method == 'put') {
240
+            $opts[CURLOPT_CUSTOMREQUEST] = 'PUT';
241
+            $opts[CURLOPT_POSTFIELDS] = $hasFile ? $params : $this->json_encode_params($params);
242
+        } elseif ($method == 'delete') {
243
+            $opts[\CURLOPT_CUSTOMREQUEST] = 'DELETE';
244
+            if (\count($params) > 0) {
245
+                $encoded = Util\Util::encodeParameters($params);
246
+                $absUrl = "{$absUrl}?{$encoded}";
247
+            }
248
+        } else {
249
+            throw new Exception\UnexpectedValueException("Unrecognized method {$method}");
250
+        }
251
+
252
+        // By default for large request body sizes (> 1024 bytes), cURL will
253
+        // send a request without a body and with a `Expect: 100-continue`
254
+        // header, which gives the server a chance to respond with an error
255
+        // status code in cases where one can be determined right away (say
256
+        // on an authentication problem for example), and saves the "large"
257
+        // request body from being ever sent.
258
+        //
259
+        // Unfortunately, the bindings don't currently correctly handle the
260
+        // success case (in which the server sends back a 100 CONTINUE), so
261
+        // we'll error under that condition. To compensate for that problem
262
+        // for the time being, override cURL's behavior by simply always
263
+        // sending an empty `Expect:` header.
264
+        \array_push($headers, 'Expect: ');
265
+
266
+        $absUrl = Util\Util::utf8($absUrl);
267
+        $opts[\CURLOPT_URL] = $absUrl;
268
+        $opts[\CURLOPT_RETURNTRANSFER] = true;
269
+        $opts[\CURLOPT_CONNECTTIMEOUT] = $this->connectTimeout;
270
+        $opts[\CURLOPT_TIMEOUT] = $this->timeout;
271
+        $opts[\CURLOPT_HTTPHEADER] = $headers;
272
+        $opts[\CURLOPT_CAINFO] = Telnyx::getCABundlePath();
273
+        if (!Telnyx::getVerifySslCerts()) {
274
+            $opts[\CURLOPT_SSL_VERIFYPEER] = false;
275
+        }
276
+
277
+        if (!isset($opts[\CURLOPT_HTTP_VERSION]) && $this->getEnableHttp2()) {
278
+            // For HTTPS requests, enable HTTP/2, if supported
279
+            $opts[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_2TLS;
280
+        }
281
+
282
+        list($rbody, $rcode, $rheaders) = $this->executeRequestWithRetries($opts, $absUrl);
283
+
284
+        return [$rbody, $rcode, $rheaders];
285
+    }
286
+
287
+    /**
288
+     * @param array $opts cURL options
289
+     * @param string $absUrl
290
+     */
291
+    private function executeRequestWithRetries($opts, $absUrl)
292
+    {
293
+        $numRetries = 0;
294
+        $isPost = \array_key_exists(\CURLOPT_POST, $opts) && 1 === $opts[\CURLOPT_POST];
295
+
296
+        while (true) {
297
+            $rcode = 0;
298
+            $errno = 0;
299
+            $message = null;
300
+
301
+            // Create a callback to capture HTTP headers for the response
302
+            $rheaders = new Util\CaseInsensitiveArray();
303
+            $headerCallback = function ($curl, $header_line) use (&$rheaders) {
304
+                // Ignore the HTTP request line (HTTP/1.1 200 OK)
305
+                if (false === \strpos($header_line, ':')) {
306
+                    return \strlen($header_line);
307
+                }
308
+                list($key, $value) = \explode(':', \trim($header_line), 2);
309
+                $rheaders[\trim($key)] = \trim($value);
310
+
311
+                return \strlen($header_line);
312
+            };
313
+            $opts[\CURLOPT_HEADERFUNCTION] = $headerCallback;
314
+
315
+            $this->resetCurlHandle();
316
+            \curl_setopt_array($this->curlHandle, $opts);
317
+            $rbody = \curl_exec($this->curlHandle);
318
+
319
+            if (false === $rbody) {
320
+                $errno = \curl_errno($this->curlHandle);
321
+                $message = \curl_error($this->curlHandle);
322
+            } else {
323
+                $rcode = \curl_getinfo($this->curlHandle, \CURLINFO_HTTP_CODE);
324
+            }
325
+            if (!$this->getEnablePersistentConnections()) {
326
+                $this->closeCurlHandle();
327
+            }
328
+
329
+            $shouldRetry = $this->shouldRetry($errno, $rcode, $rheaders, $numRetries);
330
+
331
+            if (\is_callable($this->getRequestStatusCallback())) {
332
+                \call_user_func_array(
333
+                    $this->getRequestStatusCallback(),
334
+                    [$rbody, $rcode, $rheaders, $errno, $message, $shouldRetry, $numRetries]
335
+                );
336
+            }
337
+
338
+            if ($shouldRetry) {
339
+                ++$numRetries;
340
+                $sleepSeconds = $this->sleepTime($numRetries, $rheaders);
341
+                \usleep((int) ($sleepSeconds * 1000000));
342
+            } else {
343
+                break;
344
+            }
345
+        }
346
+
347
+        if (false === $rbody) {
348
+            $this->handleCurlError($absUrl, $errno, $message, $numRetries);
349
+        }
350
+
351
+        return [$rbody, $rcode, $rheaders];
352
+    }
353
+
354
+    /**
355
+     * @param string $url
356
+     * @param int $errno
357
+     * @param string $message
358
+     * @param int $numRetries
359
+     *
360
+     * @throws Exception\ApiConnectionException
361
+     */
362
+    private function handleCurlError($url, $errno, $message, $numRetries)
363
+    {
364
+        switch ($errno) {
365
+            case \CURLE_COULDNT_CONNECT:
366
+            case \CURLE_COULDNT_RESOLVE_HOST:
367
+            case \CURLE_OPERATION_TIMEOUTED:
368
+                $msg = "Could not connect to Telnyx ($url).  Please check your "
369
+                 . "internet connection and try again.  If this problem persists, "
370
+                 . "you should check Telnyx's service status at "
371
+                 . "http://status.telnyx.com/, or";
372
+
373
+                break;
374
+            case \CURLE_SSL_CACERT:
375
+            case \CURLE_SSL_PEER_CERTIFICATE:
376
+                $msg = "Could not verify Telnyx's SSL certificate.  Please make sure "
377
+                 . "that your network is not intercepting certificates.  "
378
+                 . "(Try going to $url in your browser.)  "
379
+                 . "If this problem persists,";
380
+
381
+                break;
382
+            default:
383
+                $msg = 'Unexpected error communicating with Telnyx.  '
384
+                 . 'If this problem persists,';
385
+        }
386
+        $msg .= ' let us know at support@telnyx.com.';
387
+
388
+        $msg .= "\n\n(Network error [errno {$errno}]: {$message})";
389
+
390
+        if ($numRetries > 0) {
391
+            $msg .= "\n\nRequest was retried {$numRetries} times.";
392
+        }
393
+
394
+        throw new Exception\ApiConnectionException($msg);
395
+    }
396
+
397
+    /**
398
+     * Checks if an error is a problem that we should retry on. This includes both
399
+     * socket errors that may represent an intermittent problem and some special
400
+     * HTTP statuses.
401
+     *
402
+     * @param int $errno
403
+     * @param int $rcode
404
+     * @param array|\Telnyx\Util\CaseInsensitiveArray $rheaders
405
+     * @param int $numRetries
406
+     *
407
+     * @return bool
408
+     */
409
+    private function shouldRetry($errno, $rcode, $rheaders, $numRetries)
410
+    {
411
+        if ($numRetries >= Telnyx::getMaxNetworkRetries()) {
412
+            return false;
413
+        }
414
+
415
+        // Retry on timeout-related problems (either on open or read).
416
+        if (\CURLE_OPERATION_TIMEOUTED === $errno) {
417
+            return true;
418
+        }
419
+
420
+        // Destination refused the connection, the connection was reset, or a
421
+        // variety of other connection failures. This could occur from a single
422
+        // saturated server, so retry in case it's intermittent.
423
+        if (\CURLE_COULDNT_CONNECT === $errno) {
424
+            return true;
425
+        }
426
+
427
+        // The API may ask us not to retry (eg; if doing so would be a no-op)
428
+        // or advise us to retry (eg; in cases of lock timeouts); we defer to that.
429
+        if (isset($rheaders['telnyx-should-retry'])) {
430
+            if ('false' === $rheaders['telnyx-should-retry']) {
431
+                return false;
432
+            }
433
+            if ('true' === $rheaders['telnyx-should-retry']) {
434
+                return true;
435
+            }
436
+        }
437
+
438
+        // 409 Conflict
439
+        if (409 === $rcode) {
440
+            return true;
441
+        }
442
+
443
+        // Retry on 500, 503, and other internal errors.
444
+        //
445
+        // Note that we expect the telnyx-should-retry header to be false
446
+        // in most cases when a 500 is returned, since our idempotency framework
447
+        // would typically replay it anyway.
448
+        if ($rcode >= 500) {
449
+            return true;
450
+        }
451
+
452
+        return false;
453
+    }
454
+
455
+    /**
456
+     * Provides the number of seconds to wait before retrying a request.
457
+     *
458
+     * @param int $numRetries
459
+     * @param array|\Telnyx\Util\CaseInsensitiveArray $rheaders
460
+     *
461
+     * @return int
462
+     */
463
+    private function sleepTime($numRetries, $rheaders)
464
+    {
465
+        // Apply exponential backoff with $initialNetworkRetryDelay on the
466
+        // number of $numRetries so far as inputs. Do not allow the number to exceed
467
+        // $maxNetworkRetryDelay.
468
+        $sleepSeconds = \min(
469
+            Telnyx::getInitialNetworkRetryDelay() * 1.0 * 2 ** ($numRetries - 1),
470
+            Telnyx::getMaxNetworkRetryDelay()
471
+        );
472
+
473
+        // Apply some jitter by randomizing the value in the range of
474
+        // ($sleepSeconds / 2) to ($sleepSeconds).
475
+        $sleepSeconds *= 0.5 * (1 + $this->randomGenerator->randFloat());
476
+
477
+        // But never sleep less than the base sleep seconds.
478
+        $sleepSeconds = \max(Telnyx::getInitialNetworkRetryDelay(), $sleepSeconds);
479
+
480
+        // And never sleep less than the time the API asks us to wait, assuming it's a reasonable ask.
481
+        $retryAfter = isset($rheaders['retry-after']) ? (float) ($rheaders['retry-after']) : 0.0;
482
+        if (\floor($retryAfter) === $retryAfter && $retryAfter <= Telnyx::getMaxRetryAfter()) {
483
+            $sleepSeconds = \max($sleepSeconds, $retryAfter);
484
+        }
485
+
486
+        return $sleepSeconds;
487
+    }
488
+
489
+    /**
490
+     * Initializes the curl handle. If already initialized, the handle is closed first.
491
+     */
492
+    private function initCurlHandle()
493
+    {
494
+        $this->closeCurlHandle();
495
+        $this->curlHandle = \curl_init();
496
+    }
497
+
498
+    /**
499
+     * Closes the curl handle if initialized. Do nothing if already closed.
500
+     */
501
+    private function closeCurlHandle()
502
+    {
503
+        if (null !== $this->curlHandle) {
504
+            \curl_close($this->curlHandle);
505
+            $this->curlHandle = null;
506
+        }
507
+    }
508
+
509
+    /**
510
+     * Resets the curl handle. If the handle is not already initialized, or if persistent
511
+     * connections are disabled, the handle is reinitialized instead.
512
+     */
513
+    private function resetCurlHandle()
514
+    {
515
+        if (null !== $this->curlHandle && $this->getEnablePersistentConnections()) {
516
+            \curl_reset($this->curlHandle);
517
+        } else {
518
+            $this->initCurlHandle();
519
+        }
520
+    }
521
+
522
+    /**
523
+     * Indicates whether it is safe to use HTTP/2 or not.
524
+     *
525
+     * @return bool
526
+     */
527
+    private function canSafelyUseHttp2()
528
+    {
529
+        // Versions of curl older than 7.60.0 don't respect GOAWAY frames
530
+        // (cf. https://github.com/curl/curl/issues/2416), which Telnyx use.
531
+        $curlVersion = \curl_version()['version'];
532
+
533
+        return \version_compare($curlVersion, '7.60.0') >= 0;
534
+    }
535
+
536
+    /**
537
+     * Checks if a list of headers contains a specific header name.
538
+     *
539
+     * @param string[] $headers
540
+     * @param string $name
541
+     *
542
+     * @return bool
543
+     */
544
+    private function hasHeader($headers, $name)
545
+    {
546
+        foreach ($headers as $header) {
547
+            if (0 === \strncasecmp($header, "{$name}: ", \strlen($name) + 2)) {
548
+                return true;
549
+            }
550
+        }
551
+
552
+        return false;
553
+    }
554
+}