ci4

Форк
0
/
CURLRequest.php 
701 строка · 20.2 Кб
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\HTTP;
15

16
use CodeIgniter\HTTP\Exceptions\HTTPException;
17
use Config\App;
18
use Config\CURLRequest as ConfigCURLRequest;
19
use InvalidArgumentException;
20

21
/**
22
 * A lightweight HTTP client for sending synchronous HTTP requests via cURL.
23
 *
24
 * @see \CodeIgniter\HTTP\CURLRequestTest
25
 */
26
class CURLRequest extends OutgoingRequest
27
{
28
    /**
29
     * The response object associated with this request
30
     *
31
     * @var ResponseInterface|null
32
     */
33
    protected $response;
34

35
    /**
36
     * The original response object associated with this request
37
     *
38
     * @var ResponseInterface|null
39
     */
40
    protected $responseOrig;
41

42
    /**
43
     * The URI associated with this request
44
     *
45
     * @var URI
46
     */
47
    protected $baseURI;
48

49
    /**
50
     * The setting values
51
     *
52
     * @var array
53
     */
54
    protected $config;
55

56
    /**
57
     * The default setting values
58
     *
59
     * @var array
60
     */
61
    protected $defaultConfig = [
62
        'timeout'         => 0.0,
63
        'connect_timeout' => 150,
64
        'debug'           => false,
65
        'verify'          => true,
66
    ];
67

68
    /**
69
     * Default values for when 'allow_redirects'
70
     * option is true.
71
     *
72
     * @var array
73
     */
74
    protected $redirectDefaults = [
75
        'max'       => 5,
76
        'strict'    => true,
77
        'protocols' => [
78
            'http',
79
            'https',
80
        ],
81
    ];
82

83
    /**
84
     * The number of milliseconds to delay before
85
     * sending the request.
86
     *
87
     * @var float
88
     */
89
    protected $delay = 0.0;
90

91
    /**
92
     * The default options from the constructor. Applied to all requests.
93
     */
94
    private readonly array $defaultOptions;
95

96
    /**
97
     * Whether share options between requests or not.
98
     *
99
     * If true, all the options won't be reset between requests.
100
     * It may cause an error request with unnecessary headers.
101
     */
102
    private readonly bool $shareOptions;
103

104
    /**
105
     * Takes an array of options to set the following possible class properties:
106
     *
107
     *  - baseURI
108
     *  - timeout
109
     *  - any other request options to use as defaults.
110
     *
111
     * @param array<string, mixed> $options
112
     */
113
    public function __construct(App $config, URI $uri, ?ResponseInterface $response = null, array $options = [])
114
    {
115
        if (! function_exists('curl_version')) {
116
            throw HTTPException::forMissingCurl(); // @codeCoverageIgnore
117
        }
118

119
        parent::__construct(Method::GET, $uri);
120

121
        $this->responseOrig = $response ?? new Response($config);
122
        // Remove the default Content-Type header.
123
        $this->responseOrig->removeHeader('Content-Type');
124

125
        $this->baseURI        = $uri->useRawQueryString();
126
        $this->defaultOptions = $options;
127

128
        /** @var ConfigCURLRequest|null $configCURLRequest */
129
        $configCURLRequest  = config(ConfigCURLRequest::class);
130
        $this->shareOptions = $configCURLRequest->shareOptions ?? true;
131

132
        $this->config = $this->defaultConfig;
133
        $this->parseOptions($options);
134
    }
135

136
    /**
137
     * Sends an HTTP request to the specified $url. If this is a relative
138
     * URL, it will be merged with $this->baseURI to form a complete URL.
139
     *
140
     * @param string $method HTTP method
141
     */
142
    public function request($method, string $url, array $options = []): ResponseInterface
143
    {
144
        $this->response = clone $this->responseOrig;
145

146
        $this->parseOptions($options);
147

148
        $url = $this->prepareURL($url);
149

150
        $method = esc(strip_tags($method));
151

152
        $this->send($method, $url);
153

154
        if ($this->shareOptions === false) {
155
            $this->resetOptions();
156
        }
157

158
        return $this->response;
159
    }
160

161
    /**
162
     * Reset all options to default.
163
     *
164
     * @return void
165
     */
166
    protected function resetOptions()
167
    {
168
        // Reset headers
169
        $this->headers   = [];
170
        $this->headerMap = [];
171

172
        // Reset body
173
        $this->body = null;
174

175
        // Reset configs
176
        $this->config = $this->defaultConfig;
177

178
        // Set the default options for next request
179
        $this->parseOptions($this->defaultOptions);
180
    }
181

182
    /**
183
     * Convenience method for sending a GET request.
184
     */
185
    public function get(string $url, array $options = []): ResponseInterface
186
    {
187
        return $this->request(Method::GET, $url, $options);
188
    }
189

190
    /**
191
     * Convenience method for sending a DELETE request.
192
     */
193
    public function delete(string $url, array $options = []): ResponseInterface
194
    {
195
        return $this->request('DELETE', $url, $options);
196
    }
197

198
    /**
199
     * Convenience method for sending a HEAD request.
200
     */
201
    public function head(string $url, array $options = []): ResponseInterface
202
    {
203
        return $this->request('HEAD', $url, $options);
204
    }
205

206
    /**
207
     * Convenience method for sending an OPTIONS request.
208
     */
209
    public function options(string $url, array $options = []): ResponseInterface
210
    {
211
        return $this->request('OPTIONS', $url, $options);
212
    }
213

214
    /**
215
     * Convenience method for sending a PATCH request.
216
     */
217
    public function patch(string $url, array $options = []): ResponseInterface
218
    {
219
        return $this->request('PATCH', $url, $options);
220
    }
221

222
    /**
223
     * Convenience method for sending a POST request.
224
     */
225
    public function post(string $url, array $options = []): ResponseInterface
226
    {
227
        return $this->request(Method::POST, $url, $options);
228
    }
229

230
    /**
231
     * Convenience method for sending a PUT request.
232
     */
233
    public function put(string $url, array $options = []): ResponseInterface
234
    {
235
        return $this->request(Method::PUT, $url, $options);
236
    }
237

238
    /**
239
     * Set the HTTP Authentication.
240
     *
241
     * @param string $type basic or digest
242
     *
243
     * @return $this
244
     */
245
    public function setAuth(string $username, string $password, string $type = 'basic')
246
    {
247
        $this->config['auth'] = [
248
            $username,
249
            $password,
250
            $type,
251
        ];
252

253
        return $this;
254
    }
255

256
    /**
257
     * Set form data to be sent.
258
     *
259
     * @param bool $multipart Set TRUE if you are sending CURLFiles
260
     *
261
     * @return $this
262
     */
263
    public function setForm(array $params, bool $multipart = false)
264
    {
265
        if ($multipart) {
266
            $this->config['multipart'] = $params;
267
        } else {
268
            $this->config['form_params'] = $params;
269
        }
270

271
        return $this;
272
    }
273

274
    /**
275
     * Set JSON data to be sent.
276
     *
277
     * @param array|bool|float|int|object|string|null $data
278
     *
279
     * @return $this
280
     */
281
    public function setJSON($data)
282
    {
283
        $this->config['json'] = $data;
284

285
        return $this;
286
    }
287

288
    /**
289
     * Sets the correct settings based on the options array
290
     * passed in.
291
     *
292
     * @return void
293
     */
294
    protected function parseOptions(array $options)
295
    {
296
        if (array_key_exists('baseURI', $options)) {
297
            $this->baseURI = $this->baseURI->setURI($options['baseURI']);
298
            unset($options['baseURI']);
299
        }
300

301
        if (array_key_exists('headers', $options) && is_array($options['headers'])) {
302
            foreach ($options['headers'] as $name => $value) {
303
                $this->setHeader($name, $value);
304
            }
305

306
            unset($options['headers']);
307
        }
308

309
        if (array_key_exists('delay', $options)) {
310
            // Convert from the milliseconds passed in
311
            // to the seconds that sleep requires.
312
            $this->delay = (float) $options['delay'] / 1000;
313
            unset($options['delay']);
314
        }
315

316
        if (array_key_exists('body', $options)) {
317
            $this->setBody($options['body']);
318
            unset($options['body']);
319
        }
320

321
        foreach ($options as $key => $value) {
322
            $this->config[$key] = $value;
323
        }
324
    }
325

326
    /**
327
     * If the $url is a relative URL, will attempt to create
328
     * a full URL by prepending $this->baseURI to it.
329
     */
330
    protected function prepareURL(string $url): string
331
    {
332
        // If it's a full URI, then we have nothing to do here...
333
        if (str_contains($url, '://')) {
334
            return $url;
335
        }
336

337
        $uri = $this->baseURI->resolveRelativeURI($url);
338

339
        // Create the string instead of casting to prevent baseURL muddling
340
        return URI::createURIString(
341
            $uri->getScheme(),
342
            $uri->getAuthority(),
343
            $uri->getPath(),
344
            $uri->getQuery(),
345
            $uri->getFragment()
346
        );
347
    }
348

349
    /**
350
     * Fires the actual cURL request.
351
     *
352
     * @return ResponseInterface
353
     */
354
    public function send(string $method, string $url)
355
    {
356
        // Reset our curl options so we're on a fresh slate.
357
        $curlOptions = [];
358

359
        if (! empty($this->config['query']) && is_array($this->config['query'])) {
360
            // This is likely too naive a solution.
361
            // Should look into handling when $url already
362
            // has query vars on it.
363
            $url .= '?' . http_build_query($this->config['query']);
364
            unset($this->config['query']);
365
        }
366

367
        $curlOptions[CURLOPT_URL]            = $url;
368
        $curlOptions[CURLOPT_RETURNTRANSFER] = true;
369
        $curlOptions[CURLOPT_HEADER]         = true;
370
        $curlOptions[CURLOPT_FRESH_CONNECT]  = true;
371
        // Disable @file uploads in post data.
372
        $curlOptions[CURLOPT_SAFE_UPLOAD] = true;
373

374
        $curlOptions = $this->setCURLOptions($curlOptions, $this->config);
375
        $curlOptions = $this->applyMethod($method, $curlOptions);
376
        $curlOptions = $this->applyRequestHeaders($curlOptions);
377

378
        // Do we need to delay this request?
379
        if ($this->delay > 0) {
380
            usleep((int) $this->delay * 1_000_000);
381
        }
382

383
        $output = $this->sendRequest($curlOptions);
384

385
        // Set the string we want to break our response from
386
        $breakString = "\r\n\r\n";
387

388
        while (str_starts_with($output, 'HTTP/1.1 100 Continue')) {
389
            $output = substr($output, strpos($output, $breakString) + 4);
390
        }
391

392
        if (str_starts_with($output, 'HTTP/1.1 200 Connection established')) {
393
            $output = substr($output, strpos($output, $breakString) + 4);
394
        }
395

396
        // If request and response have Digest
397
        if (isset($this->config['auth'][2]) && $this->config['auth'][2] === 'digest' && str_contains($output, 'WWW-Authenticate: Digest')) {
398
            $output = substr($output, strpos($output, $breakString) + 4);
399
        }
400

401
        // Split out our headers and body
402
        $break = strpos($output, $breakString);
403

404
        if ($break !== false) {
405
            // Our headers
406
            $headers = explode("\n", substr($output, 0, $break));
407

408
            $this->setResponseHeaders($headers);
409

410
            // Our body
411
            $body = substr($output, $break + 4);
412
            $this->response->setBody($body);
413
        } else {
414
            $this->response->setBody($output);
415
        }
416

417
        return $this->response;
418
    }
419

420
    /**
421
     * Adds $this->headers to the cURL request.
422
     */
423
    protected function applyRequestHeaders(array $curlOptions = []): array
424
    {
425
        if (empty($this->headers)) {
426
            return $curlOptions;
427
        }
428

429
        $set = [];
430

431
        foreach (array_keys($this->headers) as $name) {
432
            $set[] = $name . ': ' . $this->getHeaderLine($name);
433
        }
434

435
        $curlOptions[CURLOPT_HTTPHEADER] = $set;
436

437
        return $curlOptions;
438
    }
439

440
    /**
441
     * Apply method
442
     */
443
    protected function applyMethod(string $method, array $curlOptions): array
444
    {
445
        $this->method                       = $method;
446
        $curlOptions[CURLOPT_CUSTOMREQUEST] = $method;
447

448
        $size = strlen($this->body ?? '');
449

450
        // Have content?
451
        if ($size > 0) {
452
            return $this->applyBody($curlOptions);
453
        }
454

455
        if ($method === Method::PUT || $method === Method::POST) {
456
            // See http://tools.ietf.org/html/rfc7230#section-3.3.2
457
            if ($this->header('content-length') === null && ! isset($this->config['multipart'])) {
458
                $this->setHeader('Content-Length', '0');
459
            }
460
        } elseif ($method === 'HEAD') {
461
            $curlOptions[CURLOPT_NOBODY] = 1;
462
        }
463

464
        return $curlOptions;
465
    }
466

467
    /**
468
     * Apply body
469
     */
470
    protected function applyBody(array $curlOptions = []): array
471
    {
472
        if (! empty($this->body)) {
473
            $curlOptions[CURLOPT_POSTFIELDS] = (string) $this->getBody();
474
        }
475

476
        return $curlOptions;
477
    }
478

479
    /**
480
     * Parses the header retrieved from the cURL response into
481
     * our Response object.
482
     *
483
     * @return void
484
     */
485
    protected function setResponseHeaders(array $headers = [])
486
    {
487
        foreach ($headers as $header) {
488
            if (($pos = strpos($header, ':')) !== false) {
489
                $title = trim(substr($header, 0, $pos));
490
                $value = trim(substr($header, $pos + 1));
491

492
                if ($this->response instanceof Response) {
493
                    $this->response->addHeader($title, $value);
494
                } else {
495
                    $this->response->setHeader($title, $value);
496
                }
497
            } elseif (str_starts_with($header, 'HTTP')) {
498
                preg_match('#^HTTP\/([12](?:\.[01])?) (\d+) (.+)#', $header, $matches);
499

500
                if (isset($matches[1])) {
501
                    $this->response->setProtocolVersion($matches[1]);
502
                }
503

504
                if (isset($matches[2])) {
505
                    $this->response->setStatusCode((int) $matches[2], $matches[3] ?? null);
506
                }
507
            }
508
        }
509
    }
510

511
    /**
512
     * Set CURL options
513
     *
514
     * @return array
515
     *
516
     * @throws InvalidArgumentException
517
     */
518
    protected function setCURLOptions(array $curlOptions = [], array $config = [])
519
    {
520
        // Auth Headers
521
        if (! empty($config['auth'])) {
522
            $curlOptions[CURLOPT_USERPWD] = $config['auth'][0] . ':' . $config['auth'][1];
523

524
            if (! empty($config['auth'][2]) && strtolower($config['auth'][2]) === 'digest') {
525
                $curlOptions[CURLOPT_HTTPAUTH] = CURLAUTH_DIGEST;
526
            } else {
527
                $curlOptions[CURLOPT_HTTPAUTH] = CURLAUTH_BASIC;
528
            }
529
        }
530

531
        // Certificate
532
        if (! empty($config['cert'])) {
533
            $cert = $config['cert'];
534

535
            if (is_array($cert)) {
536
                $curlOptions[CURLOPT_SSLCERTPASSWD] = $cert[1];
537
                $cert                               = $cert[0];
538
            }
539

540
            if (! is_file($cert)) {
541
                throw HTTPException::forSSLCertNotFound($cert);
542
            }
543

544
            $curlOptions[CURLOPT_SSLCERT] = $cert;
545
        }
546

547
        // SSL Verification
548
        if (isset($config['verify'])) {
549
            if (is_string($config['verify'])) {
550
                $file = realpath($config['verify']) ?: $config['verify'];
551

552
                if (! is_file($file)) {
553
                    throw HTTPException::forInvalidSSLKey($config['verify']);
554
                }
555

556
                $curlOptions[CURLOPT_CAINFO]         = $file;
557
                $curlOptions[CURLOPT_SSL_VERIFYPEER] = true;
558
                $curlOptions[CURLOPT_SSL_VERIFYHOST] = 2;
559
            } elseif (is_bool($config['verify'])) {
560
                $curlOptions[CURLOPT_SSL_VERIFYPEER] = $config['verify'];
561
                $curlOptions[CURLOPT_SSL_VERIFYHOST] = $config['verify'] ? 2 : 0;
562
            }
563
        }
564

565
        // Proxy
566
        if (isset($config['proxy'])) {
567
            $curlOptions[CURLOPT_HTTPPROXYTUNNEL] = true;
568
            $curlOptions[CURLOPT_PROXY]           = $config['proxy'];
569
        }
570

571
        // Debug
572
        if ($config['debug']) {
573
            $curlOptions[CURLOPT_VERBOSE] = 1;
574
            $curlOptions[CURLOPT_STDERR]  = is_string($config['debug']) ? fopen($config['debug'], 'a+b') : fopen('php://stderr', 'wb');
575
        }
576

577
        // Decode Content
578
        if (! empty($config['decode_content'])) {
579
            $accept = $this->getHeaderLine('Accept-Encoding');
580

581
            if ($accept !== '') {
582
                $curlOptions[CURLOPT_ENCODING] = $accept;
583
            } else {
584
                $curlOptions[CURLOPT_ENCODING]   = '';
585
                $curlOptions[CURLOPT_HTTPHEADER] = 'Accept-Encoding';
586
            }
587
        }
588

589
        // Allow Redirects
590
        if (array_key_exists('allow_redirects', $config)) {
591
            $settings = $this->redirectDefaults;
592

593
            if (is_array($config['allow_redirects'])) {
594
                $settings = array_merge($settings, $config['allow_redirects']);
595
            }
596

597
            if ($config['allow_redirects'] === false) {
598
                $curlOptions[CURLOPT_FOLLOWLOCATION] = 0;
599
            } else {
600
                $curlOptions[CURLOPT_FOLLOWLOCATION] = 1;
601
                $curlOptions[CURLOPT_MAXREDIRS]      = $settings['max'];
602

603
                if ($settings['strict'] === true) {
604
                    $curlOptions[CURLOPT_POSTREDIR] = 1 | 2 | 4;
605
                }
606

607
                $protocols = 0;
608

609
                foreach ($settings['protocols'] as $proto) {
610
                    $protocols += constant('CURLPROTO_' . strtoupper($proto));
611
                }
612

613
                $curlOptions[CURLOPT_REDIR_PROTOCOLS] = $protocols;
614
            }
615
        }
616

617
        // Timeout
618
        $curlOptions[CURLOPT_TIMEOUT_MS] = (float) $config['timeout'] * 1000;
619

620
        // Connection Timeout
621
        $curlOptions[CURLOPT_CONNECTTIMEOUT_MS] = (float) $config['connect_timeout'] * 1000;
622

623
        // Post Data - application/x-www-form-urlencoded
624
        if (! empty($config['form_params']) && is_array($config['form_params'])) {
625
            $postFields                      = http_build_query($config['form_params']);
626
            $curlOptions[CURLOPT_POSTFIELDS] = $postFields;
627

628
            // Ensure content-length is set, since CURL doesn't seem to
629
            // calculate it when HTTPHEADER is set.
630
            $this->setHeader('Content-Length', (string) strlen($postFields));
631
            $this->setHeader('Content-Type', 'application/x-www-form-urlencoded');
632
        }
633

634
        // Post Data - multipart/form-data
635
        if (! empty($config['multipart']) && is_array($config['multipart'])) {
636
            // setting the POSTFIELDS option automatically sets multipart
637
            $curlOptions[CURLOPT_POSTFIELDS] = $config['multipart'];
638
        }
639

640
        // HTTP Errors
641
        $curlOptions[CURLOPT_FAILONERROR] = array_key_exists('http_errors', $config) ? (bool) $config['http_errors'] : true;
642

643
        // JSON
644
        if (isset($config['json'])) {
645
            // Will be set as the body in `applyBody()`
646
            $json = json_encode($config['json']);
647
            $this->setBody($json);
648
            $this->setHeader('Content-Type', 'application/json');
649
            $this->setHeader('Content-Length', (string) strlen($json));
650
        }
651

652
        // version
653
        if (! empty($config['version'])) {
654
            $version = sprintf('%.1F', $config['version']);
655
            if ($version === '1.0') {
656
                $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0;
657
            } elseif ($version === '1.1') {
658
                $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1;
659
            } elseif ($version === '2.0') {
660
                $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2_0;
661
            }
662
        }
663

664
        // Cookie
665
        if (isset($config['cookie'])) {
666
            $curlOptions[CURLOPT_COOKIEJAR]  = $config['cookie'];
667
            $curlOptions[CURLOPT_COOKIEFILE] = $config['cookie'];
668
        }
669

670
        // User Agent
671
        if (isset($config['user_agent'])) {
672
            $curlOptions[CURLOPT_USERAGENT] = $config['user_agent'];
673
        }
674

675
        return $curlOptions;
676
    }
677

678
    /**
679
     * Does the actual work of initializing cURL, setting the options,
680
     * and grabbing the output.
681
     *
682
     * @codeCoverageIgnore
683
     */
684
    protected function sendRequest(array $curlOptions = []): string
685
    {
686
        $ch = curl_init();
687

688
        curl_setopt_array($ch, $curlOptions);
689

690
        // Send the request and wait for a response.
691
        $output = curl_exec($ch);
692

693
        if ($output === false) {
694
            throw HTTPException::forCurlError((string) curl_errno($ch), curl_error($ch));
695
        }
696

697
        curl_close($ch);
698

699
        return $output;
700
    }
701
}
702

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.