Passed
Push — master ( f70266...e410c8 )
by Shahrad
01:49
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\Model\DownloadResult;
7
use EasyHttp\Model\HttpOptions;
8
use EasyHttp\Model\HttpResponse;
9
use EasyHttp\Model\UploadResult;
10
use EasyHttp\Traits\ClientTrait;
11
use EasyHttp\Util\Utils;
12
13
/**
14
 * Client
15
 *
16
 * @link    https://github.com/shahradelahi/easy-http
17
 * @author  Shahrad Elahi (https://github.com/shahradelahi)
18
 * @license https://github.com/shahradelahi/easy-http/blob/master/LICENSE (MIT License)
19
 */
20
class Client
21
{
22
23
    use ClientTrait;
24
25
    /**
26
     * This variable is used to defied is certificate self-signed or not
27
     *
28
     * @var bool
29
     */
30
    private bool $isSelfSigned = true;
31
32
    /**
33
     * The temp directory to download files - default is $_SERVER['TEMP']
34
     *
35
     * @var ?string
36
     */
37
    private ?string $tempDir;
38
39
    /**
40
     * The Max count of chunk to download file
41
     *
42
     * @var int
43
     */
44
    public int $maxChunkCount = 10;
45
46
    /**
47
     * The constructor of the client
48
     */
49
    public function __construct()
50
    {
51
        $this->tempDir = $_SERVER['TEMP'] ?? null;
52
    }
53
54
    /**
55
     * Set has self-signed certificate
56
     *
57
     * This is used to set the curl option CURLOPT_SSL_VERIFYPEER
58
     * and CURLOPT_SSL_VERIFYHOST to false. This is useful when you are
59
     * in local environment, or you have self-signed certificate.
60
     *
61
     * @param bool $has
62
     *
63
     * @return void
64
     */
65
    public function setHasSelfSignedCertificate(bool $has): void
66
    {
67
        $this->isSelfSigned = $has;
68
    }
69
70
    /**
71
     * Set the temporary directory path to save the downloaded files
72
     *
73
     * @param string $path
74
     *
75
     * @return void
76
     */
77
    public function setTempPath(string $path): void
78
    {
79
        if (!file_exists($path)) {
80
            throw new \InvalidArgumentException('The directory path is not exists');
81
        }
82
        $this->tempDir = $path;
83
    }
84
85
    /**
86
     * This method is used to send a http request to a given url.
87
     *
88
     * @param string $method
89
     * @param string $uri
90
     * @param array|HttpOptions $options
91
     *
92
     * @return HttpResponse
93
     */
94
    public function request(string $method, string $uri, array|HttpOptions $options = []): HttpResponse
95
    {
96
        $CurlHandle = $this->createCurlHandler($method, $uri, $options);
97
98
        $result = new HttpResponse();
99
        $result->setCurlHandle($CurlHandle);
100
101
        $response = curl_exec($CurlHandle);
102
        if (curl_errno($CurlHandle)) {
103
            $result->setError(curl_error($CurlHandle));
104
            $result->setErrorCode(curl_errno($CurlHandle));
105
            return $result;
106
        }
107
108
        $result->setStatusCode(curl_getinfo($CurlHandle, CURLINFO_HTTP_CODE));
109
        $result->setHeaderSize(curl_getinfo($CurlHandle, CURLINFO_HEADER_SIZE));
110
        $result->setHeaders(substr($response, 0, $result->getHeaderSize()));
0 ignored issues
show
Bug introduced by
It seems like $response can also be of type true; however, parameter $string of substr() does only seem to accept string, 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

110
        $result->setHeaders(substr(/** @scrutinizer ignore-type */ $response, 0, $result->getHeaderSize()));
Loading history...
111
        $result->setBody(substr($response, $result->getHeaderSize()));
112
113
        curl_close($CurlHandle);
114
115
        return $result;
116
    }
117
118
    /**
119
     * Send multiple requests to a given url.
120
     *
121
     * @param array $requests [{method, uri, options}, ...]
122
     *
123
     * @return array<HttpResponse>
124
     */
125
    public function bulk(array $requests): array
126
    {
127
        $result = [];
128
        $handlers = [];
129
        $multi_handler = curl_multi_init();
130
        foreach ($requests as $request) {
131
132
            $CurlHandle = $this->createCurlHandler(
133
                $request['method'] ?? null,
134
                $request['uri'],
135
                $request['options'] ?? []
136
            );
137
            $handlers[] = $CurlHandle;
138
            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

138
            curl_multi_add_handle(/** @scrutinizer ignore-type */ $multi_handler, $CurlHandle);
Loading history...
139
140
        }
141
142
        $active = null;
143
        do {
144
            $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

144
            $mrc = curl_multi_exec(/** @scrutinizer ignore-type */ $multi_handler, $active);
Loading history...
145
        } while ($mrc == CURLM_CALL_MULTI_PERFORM);
146
147
        while ($active && $mrc == CURLM_OK) {
148
            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

148
            if (curl_multi_select(/** @scrutinizer ignore-type */ $multi_handler) != -1) {
Loading history...
149
                do {
150
                    $mrc = curl_multi_exec($multi_handler, $active);
151
                } while ($mrc == CURLM_CALL_MULTI_PERFORM);
152
            }
153
        }
154
155
        foreach ($handlers as $handler) {
156
            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

156
            curl_multi_remove_handle(/** @scrutinizer ignore-type */ $multi_handler, $handler);
Loading history...
157
        }
158
        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

158
        curl_multi_close(/** @scrutinizer ignore-type */ $multi_handler);
Loading history...
159
160
        foreach ($handlers as $handler) {
161
            $content = curl_multi_getcontent($handler);
162
            $response = new HttpResponse();
163
164
            if (curl_errno($handler)) {
165
                $response->setError(curl_error($handler));
166
                $response->setErrorCode(curl_errno($handler));
167
            }
168
169
            $response->setCurlHandle($handler);
170
            $response->setStatusCode(curl_getinfo($handler, CURLINFO_HTTP_CODE));
171
            $response->setHeaderSize(curl_getinfo($handler, CURLINFO_HEADER_SIZE));
172
            $response->setHeaders(substr($content, 0, $response->getHeaderSize()));
173
            $response->setBody(substr($content, $response->getHeaderSize()));
174
175
            $result[] = $response;
176
        }
177
178
        return $result;
179
    }
180
181
    /**
182
     * Create curl handler.
183
     *
184
     * @param ?string $method
185
     * @param string $uri
186
     * @param array|HttpOptions $options
187
     *
188
     * @return ?CurlHandle
189
     */
190
    private function createCurlHandler(?string $method, string $uri, array|HttpOptions $options = []): ?CurlHandle
191
    {
192
        $cHandler = curl_init();
193
194
        if (gettype($options) === 'array') {
195
            $options = new HttpOptions(
196
                $this->getOptions($options)
197
            );
198
        }
199
200
        if (count($options->queries) > 0) {
201
            if (!str_contains($uri, '?')) $uri .= '?';
202
            $uri .= $options->getQueryString();
203
        }
204
205
        curl_setopt($cHandler, CURLOPT_URL, $uri);
206
207
        $this->setCurlOpts($cHandler, $method, $options);
0 ignored issues
show
Bug introduced by
It seems like $cHandler can also be of type resource; however, parameter $cHandler of EasyHttp\Client::setCurlOpts() does only seem to accept CurlHandle, 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

207
        $this->setCurlOpts(/** @scrutinizer ignore-type */ $cHandler, $method, $options);
Loading history...
208
209
        return $cHandler;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $cHandler could return the type resource which is incompatible with the type-hinted return CurlHandle|null. Consider adding an additional type-check to rule them out.
Loading history...
210
    }
211
212
    /**
213
     * Setup curl options based on the given method and our options.
214
     *
215
     * @param CurlHandle $cHandler
216
     * @param ?string $method
217
     * @param HttpOptions $options
218
     *
219
     * @return void
220
     */
221
    private function setCurlOpts(CurlHandle $cHandler, ?string $method, HttpOptions $options): void
222
    {
223
        curl_setopt($cHandler, CURLOPT_HEADER, true);
224
        curl_setopt($cHandler, CURLOPT_CUSTOMREQUEST, $method ?? 'GET');
225
226
        # Fetch the header
227
        $fetchedHeaders = [];
228
        foreach ($options->headers as $header => $value) {
229
            $fetchedHeaders[] = $header . ': ' . $value;
230
        }
231
232
        # Set headers
233
        curl_setopt($cHandler, CURLOPT_HTTPHEADER, $fetchedHeaders ?? []);
234
235
236
        # Add body if we have one.
237
        if ($options->body) {
238
            curl_setopt($cHandler, CURLOPT_CUSTOMREQUEST, $method ?? 'POST');
239
            curl_setopt($cHandler, CURLOPT_POSTFIELDS, $options->body);
240
            curl_setopt($cHandler, CURLOPT_POST, true);
241
        }
242
243
        # Check for a proxy
244
        if ($options->proxy != null) {
245
            curl_setopt($cHandler, CURLOPT_PROXY, $options->proxy->getProxy());
246
            curl_setopt($cHandler, CURLOPT_PROXYUSERPWD, $options->proxy->getAuth());
247
            if ($options->proxy->type !== null) {
248
                curl_setopt($cHandler, CURLOPT_PROXYTYPE, $options->proxy->type);
249
                curl_setopt($cHandler, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5);
250
            }
251
        }
252
253
        curl_setopt($cHandler, CURLOPT_RETURNTRANSFER, true);
254
        curl_setopt($cHandler, CURLOPT_FOLLOWLOCATION, true);
255
256
        # Add and override the custom curl options.
257
        foreach ($options->curlOptions as $option => $value) {
258
            curl_setopt($cHandler, $option, $value);
259
        }
260
261
        # if we have a timeout, set it.
262
        curl_setopt($cHandler, CURLOPT_TIMEOUT, $options->timeout ?? 10);
263
264
        # If self-signed certs are allowed, set it.
265
        if ($this->isSelfSigned === true) {
266
            curl_setopt($cHandler, CURLOPT_SSL_VERIFYPEER, false);
267
            curl_setopt($cHandler, CURLOPT_SSL_VERIFYHOST, false);
268
        }
269
    }
270
271
    /**
272
     * Initialize options from array.
273
     *
274
     * @param array $options
275
     * @return array
276
     */
277
    private function getOptions(array $options): array
278
    {
279
        $defaults = [
280
            'headers' => [],
281
            'body' => null,
282
            'timeout' => null,
283
            'proxy' => null,
284
            'curlOptions' => [],
285
            'queries' => []
286
        ];
287
288
        return array_merge($defaults, $options);
289
    }
290
291
    /**
292
     * Download large files.
293
     *
294
     * This method is used to download large files with creating multiple requests.
295
     *
296
     * Change `max_chunk_count` variable to change the number of chunks. (default: 10)
297
     *
298
     * @param string $url The direct url to the file.
299
     * @param array|HttpOptions $options The options to use.
300
     *
301
     * @return DownloadResult
302
     */
303
    public function download(string $url, array|HttpOptions $options = []): DownloadResult
304
    {
305
        if (empty($this->tempDir)) {
306
            throw new \RuntimeException('No temp directory set.');
307
        }
308
309
        if (!file_exists($this->tempDir)) {
310
            if (mkdir($this->tempDir, 0777, true) === false) {
311
                throw new \RuntimeException('Could not create temp directory.');
312
            }
313
        }
314
315
        if (gettype($options) === 'array') {
316
            $options = new HttpOptions(
317
                $this->getOptions($options)
318
            );
319
        }
320
321
        $fileSize = $this->getFileSize($url);
322
        $chunkSize = $this->getChunkSize($fileSize, $this->maxChunkCount);
0 ignored issues
show
Unused Code introduced by
The call to EasyHttp\Client::getChunkSize() has too many arguments starting with $this->maxChunkCount. ( Ignorable by Annotation )

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

322
        /** @scrutinizer ignore-call */ 
323
        $chunkSize = $this->getChunkSize($fileSize, $this->maxChunkCount);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
323
324
        $result = new DownloadResult();
325
326
        $result->id = uniqid();
327
        $result->chunksPath = $this->tempDir . '/' . $result->id . '/';
328
        mkdir($result->chunksPath, 0777, true);
329
330
        $result->fileSize = $fileSize;
331
        $result->chunkSize = $chunkSize;
332
        $result->chunks = ceil($fileSize / $chunkSize);
333
334
        $result->startTime = microtime(true);
0 ignored issues
show
Documentation Bug introduced by
It seems like microtime(true) can also be of type string. However, the property $startTime is declared as type double. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
335
336
        $requests = [];
337
        for ($i = 0; $i < $result->chunks; $i++) {
338
            $range = $i * $chunkSize . '-' . ($i + 1) * $chunkSize;
339
            if ($i + 1 === $result->chunks) {
340
                $range = $i * $chunkSize . '-' . $fileSize;
341
            }
342
            $requests[] = [
343
                'method' => 'GET',
344
                'uri' => $url,
345
                'options' => array_merge($options->toArray(), [
346
                    'CurlOptions' => [
347
                        CURLOPT_RANGE => $range
348
                    ],
349
                ])
350
            ];
351
        }
352
353
        foreach ($this->bulk($requests) as $response) {
354
            $result->addChunk(
355
                Utils::randomString(16),
356
                $response->getBody(),
357
                $response->getCurlInfo()->TOTAL_TIME
358
            );
359
        }
360
361
        $result->endTime = microtime(true);
0 ignored issues
show
Documentation Bug introduced by
It seems like microtime(true) can also be of type string. However, the property $endTime is declared as type double. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
362
363
        return $result;
364
    }
365
366
    /**
367
     * Upload single or multiple files with request method of POST.
368
     *
369
     * @param string $url The direct url to the file.
370
     * @param string|array $filePath The path to the file.
371
     * @param array|HttpOptions $options The options to use.
372
     *
373
     * @return UploadResult
374
     */
375
    public function upload(string $url, string|array $filePath, array|HttpOptions $options = []): UploadResult
376
    {
377
        if (gettype($options) === 'array') {
378
            $options = new HttpOptions(
379
                $this->getOptions($options)
380
            );
381
        }
382
383
        if (gettype($filePath) === 'string') {
384
            $filePath = [$filePath];
385
        }
386
387
        $result = new UploadResult();
388
        $result->startTime = microtime(true);
0 ignored issues
show
Documentation Bug introduced by
It seems like microtime(true) can also be of type string. However, the property $startTime is declared as type double. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
389
390
        foreach ($filePath as $file) {
391
            $options->addMultiPart('file', [
392
                'name' => basename($file),
393
                'contents' => fopen($file, 'r')
394
            ]);
395
        }
396
397
        $response = $this->request('POST', $url, array_merge($options->toArray(), [
398
            'header' => [
399
                'Content-Type' => 'multipart/form-data'
400
            ]
401
        ]));
402
403
        $result->response = $response;
404
        $result->endTime = microtime(true);
0 ignored issues
show
Documentation Bug introduced by
It seems like microtime(true) can also be of type string. However, the property $endTime is declared as type double. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
405
        if ($response->getStatusCode() === 200) {
406
            $result->success = true;
407
        }
408
409
        return $result;
410
    }
411
412
    /**
413
     * Get file size.
414
     *
415
     * @param string $url The direct url to the file.
416
     * @return int
417
     */
418
    private function getFileSize(string $url): int
419
    {
420
        $response = $this->get($url, [
421
            'headers' => [
422
                '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',
423
            ],
424
            'CurlOptions' => [
425
                CURLOPT_NOBODY => true,
426
            ]
427
        ]);
428
429
        return (int)$response->getHeaderLine('Content-Length') ?? 0;
430
    }
431
432
    /**
433
     * Get the size of each chunk.
434
     *
435
     * For default, we're dividing filesize to 10 as max size of each chunk.
436
     * If the file size was smaller than 2MB, we'll use the filesize as single chunk.
437
     *
438
     * @param int $fileSize The file size.
439
     * @return int
440
     */
441
    private function getChunkSize(int $fileSize): int
442
    {
443
        $maxChunkSize = $fileSize / $this->maxChunkCount;
444
445
        if ($fileSize <= 2 * 1024 * 1024) {
446
            return $fileSize;
447
        }
448
449
        return min($maxChunkSize, $fileSize);
450
    }
451
452
}