Passed
Push — master ( 91fe13...12bba4 )
by Shahrad
10:04
created

Client::setCurlOpts()   B

Complexity

Conditions 7
Paths 48

Size

Total Lines 47
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 24
nc 48
nop 3
dl 0
loc 47
rs 8.6026
c 0
b 0
f 0
1
<?php
2
3
namespace EasyHttp;
4
5
use CurlHandle;
6
use EasyHttp\Enums\ErrorCode;
7
use EasyHttp\Model\DownloadResult;
8
use EasyHttp\Model\HttpOptions;
9
use EasyHttp\Model\HttpResponse;
10
use EasyHttp\Traits\ClientTrait;
11
use EasyHttp\Util\Utils;
12
use InvalidArgumentException;
13
use RuntimeException;
14
15
/**
16
 * Client
17
 *
18
 * @link    https://github.com/shahradelahi/easy-http
19
 * @author  Shahrad Elahi (https://github.com/shahradelahi)
20
 * @license https://github.com/shahradelahi/easy-http/blob/master/LICENSE (MIT License)
21
 */
22
class Client
23
{
24
25
    use ClientTrait;
26
27
    /**
28
     * This variable is used to defied is certificate self-signed or not
29
     *
30
     * @var bool
31
     */
32
    private bool $isSelfSigned = true;
33
34
    /**
35
     * The temp directory to download files - default is $_SERVER['TEMP']
36
     *
37
     * @var ?string
38
     */
39
    private ?string $tempDir;
40
41
    /**
42
     * The Max count of chunk to download file
43
     *
44
     * @var int
45
     */
46
    public int $maxChunkCount = 10;
47
48
    /**
49
     * The constructor of the client
50
     */
51
    public function __construct()
52
    {
53
        $this->tempDir = $_SERVER['TEMP'] ?? null;
54
        $this->setHasSelfSignedCertificate(true);
55
    }
56
57
    /**
58
     * Set has self-signed certificate
59
     *
60
     * This is used to set the curl option CURLOPT_SSL_VERIFYPEER
61
     * and CURLOPT_SSL_VERIFYHOST to false. This is useful when you are
62
     * in local environment, or you have self-signed certificate.
63
     *
64
     * @param bool $has
65
     *
66
     * @return void
67
     */
68
    public function setHasSelfSignedCertificate(bool $has): void
69
    {
70
        $this->isSelfSigned = $has;
71
    }
72
73
    /**
74
     * Set the temporary directory path to save the downloaded files
75
     *
76
     * @param string $path
77
     *
78
     * @return void
79
     */
80
    public function setTempPath(string $path): void
81
    {
82
        if (!file_exists($path)) {
83
            throw new InvalidArgumentException(
84
                sprintf('The path "%s" does not exist', $path)
85
            );
86
        }
87
        $this->tempDir = $path;
88
    }
89
90
    /**
91
     * This method is used to send a http request to a given url.
92
     *
93
     * @param string $method
94
     * @param string $uri
95
     * @param array|HttpOptions $options
96
     *
97
     * @return HttpResponse
98
     */
99
    public function request(string $method, string $uri, array|HttpOptions $options = []): HttpResponse
100
    {
101
        $CurlHandle = $this->create_curl_handler($method, $uri, $options);
102
        if (!$CurlHandle) throw new RuntimeException(
103
            'An error occurred while creating the curl handler'
104
        );
105
106
        $result = new HttpResponse();
107
        $result->setCurlHandle($CurlHandle);
108
109
        $response = curl_exec($CurlHandle);
110
        if (curl_errno($CurlHandle) || !$response) {
111
            $result->setErrorCode(curl_errno($CurlHandle));
112
            $result->setErrorMessage(
113
                curl_error($CurlHandle) ??
114
                ErrorCode::getMessage(curl_errno($CurlHandle))
115
            );
116
            return $result;
117
        }
118
119
        $result->setStatusCode(curl_getinfo($CurlHandle, CURLINFO_HTTP_CODE));
120
        $result->setHeaderSize(curl_getinfo($CurlHandle, CURLINFO_HEADER_SIZE));
121
        $result->setHeaders(substr((string)$response, 0, $result->getHeaderSize()));
122
        $result->setBody(substr((string)$response, $result->getHeaderSize()));
123
124
        curl_close($CurlHandle);
125
126
        return $result;
127
    }
128
129
    /**
130
     * Send multiple requests to a given url.
131
     *
132
     * @param array $requests [{method, uri, options}, ...]
133
     *
134
     * @return array<HttpResponse>
135
     */
136
    public function bulk(array $requests): array
137
    {
138
        $result = [];
139
        $handlers = [];
140
        $multi_handler = curl_multi_init();
141
        foreach ($requests as $request) {
142
143
            $CurlHandle = $this->create_curl_handler(
144
                $request['method'] ?? null,
145
                $request['uri'],
146
                $request['options'] ?? []
147
            );
148
            if (!$CurlHandle) throw new RuntimeException(
149
                'An error occurred while creating the curl handler'
150
            );
151
            $handlers[] = $CurlHandle;
152
            curl_multi_add_handle($multi_handler, $CurlHandle);
0 ignored issues
show
Bug introduced by
It seems like $multi_handler can also be of type true; however, parameter $multi_handle of curl_multi_add_handle() does only seem to accept CurlMultiHandle|resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

152
            curl_multi_add_handle(/** @scrutinizer ignore-type */ $multi_handler, $CurlHandle);
Loading history...
153
154
        }
155
156
        $active = null;
157
        do {
158
            $mrc = curl_multi_exec($multi_handler, $active);
0 ignored issues
show
Bug introduced by
It seems like $multi_handler can also be of type true; however, parameter $multi_handle of curl_multi_exec() does only seem to accept CurlMultiHandle|resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

158
            $mrc = curl_multi_exec(/** @scrutinizer ignore-type */ $multi_handler, $active);
Loading history...
159
        } while ($mrc == CURLM_CALL_MULTI_PERFORM);
160
161
        while ($active && $mrc == CURLM_OK) {
162
            if (curl_multi_select($multi_handler) != -1) {
0 ignored issues
show
Bug introduced by
It seems like $multi_handler can also be of type true; however, parameter $multi_handle of curl_multi_select() does only seem to accept CurlMultiHandle|resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

162
            if (curl_multi_select(/** @scrutinizer ignore-type */ $multi_handler) != -1) {
Loading history...
163
                do {
164
                    $mrc = curl_multi_exec($multi_handler, $active);
165
                } while ($mrc == CURLM_CALL_MULTI_PERFORM);
166
            }
167
        }
168
169
        foreach ($handlers as $handler) {
170
            curl_multi_remove_handle($multi_handler, $handler);
0 ignored issues
show
Bug introduced by
It seems like $multi_handler can also be of type true; however, parameter $multi_handle of curl_multi_remove_handle() does only seem to accept CurlMultiHandle|resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

170
            curl_multi_remove_handle(/** @scrutinizer ignore-type */ $multi_handler, $handler);
Loading history...
171
        }
172
        curl_multi_close($multi_handler);
0 ignored issues
show
Bug introduced by
It seems like $multi_handler can also be of type true; however, parameter $multi_handle of curl_multi_close() does only seem to accept CurlMultiHandle|resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

172
        curl_multi_close(/** @scrutinizer ignore-type */ $multi_handler);
Loading history...
173
174
        foreach ($handlers as $handler) {
175
            $content = curl_multi_getcontent($handler);
176
            $response = new HttpResponse();
177
178
            if (curl_errno($handler)) {
179
                $response->setErrorCode(curl_errno($handler));
180
                $response->setErrorMessage(
181
                    curl_error($handler) ??
182
                    ErrorCode::getMessage(curl_errno($handler))
183
                );
184
            }
185
186
            $response->setCurlHandle($handler);
187
            $response->setStatusCode(curl_getinfo($handler, CURLINFO_HTTP_CODE));
188
            $response->setHeaderSize(curl_getinfo($handler, CURLINFO_HEADER_SIZE));
189
            $response->setHeaders(substr($content, 0, $response->getHeaderSize()));
190
            $response->setBody(substr($content, $response->getHeaderSize()));
191
192
            $result[] = $response;
193
        }
194
195
        return $result;
196
    }
197
198
    /**
199
     * Create curl handler.
200
     *
201
     * @param ?string $method
202
     * @param string $uri
203
     * @param array|HttpOptions $options
204
     *
205
     * @return false|CurlHandle
206
     */
207
    private function create_curl_handler(?string $method, string $uri, array|HttpOptions $options = []): false|CurlHandle
208
    {
209
        $handler = curl_init();
210
        if (is_resource($handler) || !$handler) return false;
211
212
        if (gettype($options) === 'array') {
213
            $options = new HttpOptions($options);
214
        }
215
216
        if (count($options->getQuery()) > 0) {
217
            if (!str_contains($uri, '?')) $uri .= '?';
218
            $uri .= $options->getQueryString();
219
        }
220
221
        curl_setopt($handler, CURLOPT_URL, $uri);
222
223
        $this->set_curl_options($handler, $method, $options);
224
225
        return $handler;
226
    }
227
228
    /**
229
     * Download large files.
230
     *
231
     * This method is used to download large files with creating multiple requests.
232
     *
233
     * Change `max_chunk_count` variable to change the number of chunks. (default: 10)
234
     *
235
     * @param string $url The direct url to the file.
236
     * @param array|HttpOptions $options The options to use.
237
     *
238
     * @return DownloadResult
239
     */
240
    public function download(string $url, array|HttpOptions $options = []): DownloadResult
241
    {
242
        if (empty($this->tempDir)) {
243
            throw new RuntimeException(
244
                'The temp directory is not set. Please set the temp directory using the `setTempDir` method.'
245
            );
246
        }
247
248
        if (!file_exists($this->tempDir)) {
249
            if (mkdir($this->tempDir, 0777, true) === false) {
250
                throw new RuntimeException(
251
                    'The temp directory is not writable. Please set the temp directory using the `setTempDir` method.'
252
                );
253
            }
254
        }
255
256
        if (gettype($options) === 'array') {
257
            $options = new HttpOptions($options);
258
        }
259
260
        $fileSize = $this->get_file_size($url);
261
        $chunkSize = $this->get_chunk_size($fileSize);
262
263
        $result = new DownloadResult();
264
265
        $result->id = uniqid();
266
        $result->chunksPath = $this->tempDir . '/' . $result->id . '/';
267
        mkdir($result->chunksPath, 0777, true);
268
269
        $result->fileSize = $fileSize;
270
        $result->chunkSize = $chunkSize;
271
        $result->chunks = ceil($fileSize / $chunkSize);
272
273
        $result->startTime = time();
274
275
        $requests = [];
276
        for ($i = 0; $i < $result->chunks; $i++) {
277
            $range = $i * $chunkSize . '-' . ($i + 1) * $chunkSize;
278
            if ($i + 1 === $result->chunks) {
279
                $range = $i * $chunkSize . '-' . $fileSize;
280
            }
281
            $requests[] = [
282
                'method' => 'GET',
283
                'uri' => $url,
284
                'options' => array_merge($options->toArray(), [
285
                    'CurlOptions' => [
286
                        CURLOPT_RANGE => $range
287
                    ],
288
                ])
289
            ];
290
        }
291
292
        foreach ($this->bulk($requests) as $response) {
293
            $result->addChunk(
294
                Utils::randomString(16),
295
                $response->getBody(),
296
                $response->getInfoFromCurl()->TOTAL_TIME
297
            );
298
        }
299
300
        $result->endTime = time();
301
302
        return $result;
303
    }
304
305
    /**
306
     * Upload single or multiple files
307
     *
308
     * This method is sending file with request method of POST and
309
     * Content-Type of multipart/form-data.
310
     *
311
     * @param string $url The direct url to the file.
312
     * @param array $filePath The path to the file.
313
     * @param array|HttpOptions $options The options to use.
314
     *
315
     * @return HttpResponse
316
     */
317
    public function upload(string $url, array $filePath, array|HttpOptions $options = []): HttpResponse
318
    {
319
        if (gettype($options) === 'array') {
320
            $options = new HttpOptions($options);
321
        }
322
323
        $multipart = [];
324
325
        foreach ($filePath as $key => $file) {
326
            $multipart[$key] = new \CURLFile(
327
                realpath($file),
328
                $this->get_file_type($file)
329
            );
330
        }
331
332
        $options->setMultipart($multipart);
333
        return $this->post($url, $options);
334
    }
335
336
    /**
337
     * Setup curl options based on the given method and our options.
338
     *
339
     * @param CurlHandle $cHandler
340
     * @param ?string $method
341
     * @param HttpOptions $options
342
     *
343
     * @return void
344
     */
345
    private function set_curl_options(CurlHandle $cHandler, ?string $method, HttpOptions $options): void
346
    {
347
        curl_setopt($cHandler, CURLOPT_HEADER, true);
348
        curl_setopt($cHandler, CURLOPT_CUSTOMREQUEST, $method ?? 'GET');
349
350
        # Fetch the header
351
        $fetchedHeaders = [];
352
        foreach ($options->getHeader() as $header => $value) {
353
            $fetchedHeaders[] = $header . ': ' . $value;
354
        }
355
356
        # Set headers
357
        curl_setopt($cHandler, CURLOPT_HTTPHEADER, $fetchedHeaders ?? []);
358
359
360
        # Add body if we have one.
361
        if ($options->getBody()) {
362
            curl_setopt($cHandler, CURLOPT_CUSTOMREQUEST, $method ?? 'POST');
363
            curl_setopt($cHandler, CURLOPT_POSTFIELDS, $options->getBody());
364
            curl_setopt($cHandler, CURLOPT_POST, true);
365
        }
366
367
        # Check for a proxy
368
        if ($options->getProxy() != null) {
369
            curl_setopt($cHandler, CURLOPT_PROXY, $options->getProxy()->getHost());
370
            curl_setopt($cHandler, CURLOPT_PROXYUSERPWD, $options->getProxy()->getAuth());
371
            if ($options->getProxy()->type !== null) {
372
                curl_setopt($cHandler, CURLOPT_PROXYTYPE, $options->getProxy()->type);
373
            }
374
        }
375
376
        curl_setopt($cHandler, CURLOPT_RETURNTRANSFER, true);
377
        curl_setopt($cHandler, CURLOPT_FOLLOWLOCATION, true);
378
379
        # Add and override the custom curl options.
380
        foreach ($options->getCurlOptions() as $option => $value) {
381
            curl_setopt($cHandler, $option, $value);
382
        }
383
384
        # if we have a timeout, set it.
385
        curl_setopt($cHandler, CURLOPT_TIMEOUT, $options->getTimeout());
386
387
        # If self-signed certs are allowed, set it.
388
        if ($this->isSelfSigned === true) {
389
            curl_setopt($cHandler, CURLOPT_SSL_VERIFYPEER, false);
390
            curl_setopt($cHandler, CURLOPT_SSL_VERIFYHOST, false);
391
        }
392
393
        $this->handle_media($cHandler, $options);
394
    }
395
396
    /**
397
     * Handle the media
398
     *
399
     * @param CurlHandle $handler
400
     * @param HttpOptions $options
401
     * @return void
402
     */
403
    private function handle_media(CurlHandle $handler, HttpOptions $options): void
404
    {
405
        if (count($options->getMultipart()) > 0) {
406
            curl_setopt($handler, CURLOPT_POST, true);
407
            curl_setopt($handler, CURLOPT_CUSTOMREQUEST, 'POST');
408
409
            $form_data = new FormData();
410
            foreach ($options->getMultipart() as $key => $value) {
411
                $form_data->addFile($key, $value);
412
            }
413
414
            $headers = [];
415
            foreach ($options->getHeader() as $header => $value) {
416
                if (Utils::insensitiveString($header, 'content-type')) continue;
417
                $headers[] = $header . ': ' . $value;
418
            }
419
            $headers[] = 'Content-Type: multipart/form-data';
420
421
            curl_setopt($handler, CURLOPT_HTTPHEADER, $headers);
422
            curl_setopt($handler, CURLOPT_POSTFIELDS, $form_data->getFiles());
423
        }
424
    }
425
426
    /**
427
     * Get filetype with the extension.
428
     *
429
     * @param string $filename The absolute path to the file.
430
     * @return string eg. image/jpeg
431
     */
432
    public static function get_file_type(string $filename): string
433
    {
434
        return MimeType::$TYPES[pathinfo($filename, PATHINFO_EXTENSION)] ?? 'application/octet-stream';
435
    }
436
437
    /**
438
     * Get file size.
439
     *
440
     * @param string $url The direct url to the file.
441
     * @return int
442
     */
443
    public function get_file_size(string $url): int
444
    {
445
        if (file_exists($url)) {
446
            return filesize($url);
447
        }
448
449
        $response = $this->get($url, [
450
            'headers' => [
451
                'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36',
452
            ],
453
            'CurlOptions' => [
454
                CURLOPT_NOBODY => true,
455
            ]
456
        ]);
457
458
        return (int)$response->getHeaderLine('Content-Length') ?? 0;
459
    }
460
461
    /**
462
     * Get the size of each chunk.
463
     *
464
     * For default, we're dividing filesize to 10 as max size of each chunk.
465
     * If the file size was smaller than 2MB, we'll use the filesize as single chunk.
466
     *
467
     * @param int $fileSize The file size.
468
     * @return int
469
     */
470
    private function get_chunk_size(int $fileSize): int
471
    {
472
        $maxChunkSize = $fileSize / $this->maxChunkCount;
473
474
        if ($fileSize <= 2 * 1024 * 1024) {
475
            return $fileSize;
476
        }
477
478
        return min($maxChunkSize, $fileSize);
479
    }
480
481
}