Passed
Push — master ( 57ebbd...f70266 )
by Shahrad
01:49
created

Client::setTempPath()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 1
dl 0
loc 6
rs 10
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\Traits\ClientTrait;
10
use EasyHttp\Util\Utils;
11
12
/**
13
 * Client
14
 *
15
 * @link    https://github.com/shahradelahi/easy-http
16
 * @author  Shahrad Elahi (https://github.com/shahradelahi)
17
 * @license https://github.com/shahradelahi/easy-http/blob/master/LICENSE (MIT License)
18
 */
19
class Client
20
{
21
22
    use ClientTrait;
23
24
    /**
25
     * This variable is used to defied is certificate self-signed or not
26
     *
27
     * @var bool
28
     */
29
    private bool $isSelfSigned = true;
30
31
    /**
32
     * The temp directory to download files - default is $_SERVER['TEMP']
33
     *
34
     * @var ?string
35
     */
36
    private ?string $tempDir;
37
38
    /**
39
     * The constructor of the client
40
     */
41
    public function __construct()
42
    {
43
        $this->tempDir = $_SERVER['TEMP'] ?? null;
44
    }
45
46
    /**
47
     * Set has self-signed certificate
48
     *
49
     * This is used to set the curl option CURLOPT_SSL_VERIFYPEER
50
     * and CURLOPT_SSL_VERIFYHOST to false. This is useful when you are
51
     * in local environment, or you have self-signed certificate.
52
     *
53
     * @param bool $has
54
     *
55
     * @return void
56
     */
57
    public function setHasSelfSignedCertificate(bool $has): void
58
    {
59
        $this->isSelfSigned = $has;
60
    }
61
62
    /**
63
     * Set the temporary directory path to save the downloaded files
64
     *
65
     * @param string $path
66
     *
67
     * @return void
68
     */
69
    public function setTempPath(string $path): void
70
    {
71
        if (!file_exists($path)) {
72
            throw new \InvalidArgumentException('The directory path is not exists');
73
        }
74
        $this->tempDir = $path;
75
    }
76
77
    /**
78
     * This method is used to send a http request to a given url.
79
     *
80
     * @param string $method
81
     * @param string $uri
82
     * @param array|HttpOptions $options
83
     *
84
     * @return HttpResponse
85
     */
86
    public function request(string $method, string $uri, array|HttpOptions $options = []): HttpResponse
87
    {
88
        $CurlHandle = $this->createCurlHandler($method, $uri, $options);
89
90
        $result = new HttpResponse();
91
        $result->setCurlHandle($CurlHandle);
92
93
        $response = curl_exec($CurlHandle);
94
        if (curl_errno($CurlHandle)) {
95
            $result->setError(curl_error($CurlHandle));
96
            $result->setErrorCode(curl_errno($CurlHandle));
97
            return $result;
98
        }
99
100
        $result->setStatusCode(curl_getinfo($CurlHandle, CURLINFO_HTTP_CODE));
101
        $result->setHeaderSize(curl_getinfo($CurlHandle, CURLINFO_HEADER_SIZE));
102
        $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

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

130
            curl_multi_add_handle(/** @scrutinizer ignore-type */ $multi_handler, $CurlHandle);
Loading history...
131
132
        }
133
134
        $active = null;
135
        do {
136
            $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

136
            $mrc = curl_multi_exec(/** @scrutinizer ignore-type */ $multi_handler, $active);
Loading history...
137
        } while ($mrc == CURLM_CALL_MULTI_PERFORM);
138
139
        while ($active && $mrc == CURLM_OK) {
140
            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

140
            if (curl_multi_select(/** @scrutinizer ignore-type */ $multi_handler) != -1) {
Loading history...
141
                do {
142
                    $mrc = curl_multi_exec($multi_handler, $active);
143
                } while ($mrc == CURLM_CALL_MULTI_PERFORM);
144
            }
145
        }
146
147
        foreach ($handlers as $handler) {
148
            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

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

150
        curl_multi_close(/** @scrutinizer ignore-type */ $multi_handler);
Loading history...
151
152
        foreach ($handlers as $handler) {
153
            $content = curl_multi_getcontent($handler);
154
            $response = new HttpResponse();
155
156
            if (curl_errno($handler)) {
157
                $response->setError(curl_error($handler));
158
                $response->setErrorCode(curl_errno($handler));
159
            }
160
161
            $response->setCurlHandle($handler);
162
            $response->setStatusCode(curl_getinfo($handler, CURLINFO_HTTP_CODE));
163
            $response->setHeaderSize(curl_getinfo($handler, CURLINFO_HEADER_SIZE));
164
            $response->setHeaders(substr($content, 0, $response->getHeaderSize()));
165
            $response->setBody(substr($content, $response->getHeaderSize()));
166
167
            $result[] = $response;
168
        }
169
170
        return $result;
171
    }
172
173
    /**
174
     * Create curl handler.
175
     *
176
     * @param ?string $method
177
     * @param string $uri
178
     * @param array|HttpOptions $options
179
     *
180
     * @return ?CurlHandle
181
     */
182
    private function createCurlHandler(?string $method, string $uri, array|HttpOptions $options = []): ?CurlHandle
183
    {
184
        $cHandler = curl_init();
185
186
        if (gettype($options) === 'array') {
187
            $options = new HttpOptions(
188
                $this->getOptions($options)
189
            );
190
        }
191
192
        if (count($options->queries) > 0) {
193
            if (!str_contains($uri, '?')) $uri .= '?';
194
            $uri .= $options->getQueryString();
195
        }
196
197
        curl_setopt($cHandler, CURLOPT_URL, $uri);
198
199
        $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

199
        $this->setCurlOpts(/** @scrutinizer ignore-type */ $cHandler, $method, $options);
Loading history...
200
201
        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...
202
    }
203
204
    /**
205
     * Setup curl options based on the given method and our options.
206
     *
207
     * @param CurlHandle $cHandler
208
     * @param ?string $method
209
     * @param HttpOptions $options
210
     *
211
     * @return void
212
     */
213
    private function setCurlOpts(CurlHandle $cHandler, ?string $method, HttpOptions $options): void
214
    {
215
        curl_setopt($cHandler, CURLOPT_HEADER, true);
216
        curl_setopt($cHandler, CURLOPT_CUSTOMREQUEST, $method ?? 'GET');
217
218
        # Fetch the header
219
        $fetchedHeaders = [];
220
        foreach ($options->headers as $header => $value) {
221
            $fetchedHeaders[] = $header . ': ' . $value;
222
        }
223
224
        # Set headers
225
        if ($fetchedHeaders != []) {
226
            curl_setopt($cHandler, CURLOPT_HTTPHEADER, $fetchedHeaders);
227
        }
228
229
        # Add body if we have one.
230
        if ($options->body) {
231
            curl_setopt($cHandler, CURLOPT_CUSTOMREQUEST, $method ?? 'POST');
232
            curl_setopt($cHandler, CURLOPT_POSTFIELDS, $options->body);
233
            curl_setopt($cHandler, CURLOPT_POST, true);
234
        }
235
236
        # Check for a proxy
237
        if ($options->proxy != null) {
238
            curl_setopt($cHandler, CURLOPT_PROXY, $options->proxy->getProxy());
239
            curl_setopt($cHandler, CURLOPT_PROXYUSERPWD, $options->proxy->getAuth());
240
            if ($options->proxy->type !== null) {
241
                curl_setopt($cHandler, CURLOPT_PROXYTYPE, $options->proxy->type);
242
                curl_setopt($cHandler, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5);
243
            }
244
        }
245
246
        curl_setopt($cHandler, CURLOPT_RETURNTRANSFER, true);
247
        curl_setopt($cHandler, CURLOPT_FOLLOWLOCATION, true);
248
249
        # Add and override the custom curl options.
250
        if (count($options->curlOptions) > 0) {
0 ignored issues
show
Bug introduced by
It seems like $options->curlOptions can also be of type null; however, parameter $value of count() does only seem to accept Countable|array, 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

250
        if (count(/** @scrutinizer ignore-type */ $options->curlOptions) > 0) {
Loading history...
251
            foreach ($options->curlOptions as $option => $value) {
252
                curl_setopt($cHandler, $option, $value);
253
            }
254
        }
255
256
        # if we have a timeout, set it.
257
        if ($options->timeout != null) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $options->timeout of type integer|null against null; this is ambiguous if the integer can be zero. Consider using a strict comparison !== instead.
Loading history...
258
            curl_setopt($cHandler, CURLOPT_TIMEOUT, $options->timeout);
259
        }
260
261
        # If self-signed certs are allowed, set it.
262
        if ($this->isSelfSigned === true) {
263
            curl_setopt($cHandler, CURLOPT_SSL_VERIFYPEER, false);
264
            curl_setopt($cHandler, CURLOPT_SSL_VERIFYHOST, false);
265
        }
266
    }
267
268
    /**
269
     * Initialize options from array.
270
     *
271
     * @param array $options
272
     * @return array
273
     */
274
    private function getOptions(array $options): array
275
    {
276
        $defaults = [
277
            'headers' => [],
278
            'body' => null,
279
            'timeout' => null,
280
            'proxy' => null,
281
            'curlOptions' => [],
282
            'queries' => []
283
        ];
284
285
        return array_merge($defaults, $options);
286
    }
287
288
    /**
289
     * Download large files.
290
     *
291
     * This method is used to download large files with
292
     * creating multiple requests.
293
     *
294
     * @param string $url The direct url to the file.
295
     * @param array|HttpOptions $options The options to use.
296
     *
297
     * @return DownloadResult
298
     */
299
    public function download(string $url, array|HttpOptions $options = []): DownloadResult
300
    {
301
        if (empty($this->tempDir)) {
302
            throw new \RuntimeException('No temp directory set.');
303
        }
304
305
        if (!file_exists($this->tempDir)) {
306
            if (mkdir($this->tempDir, 0777, true) === false) {
307
                throw new \RuntimeException('Could not create temp directory.');
308
            }
309
        }
310
311
        if (gettype($options) === 'array') {
312
            $options = new HttpOptions(
313
                $this->getOptions($options)
314
            );
315
        }
316
317
        $fileSize = $this->getFileSize($url);
318
        $chunkSize = $this->getChunkSize($fileSize);
319
320
        $result = new DownloadResult();
321
322
        $result->id = uniqid();
323
        $result->chunksPath = $this->tempDir . '/' . $result->id . '/';
324
        mkdir($result->chunksPath, 0777, true);
325
326
        $result->fileSize = $fileSize;
327
        $result->chunkSize = $chunkSize;
328
        $result->chunks = ceil($fileSize / $chunkSize);
329
330
        $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...
331
332
        $requests = [];
333
        for ($i = 0; $i < $result->chunks; $i++) {
334
            $range = $i * $chunkSize . '-' . ($i + 1) * $chunkSize;
335
            if ($i + 1 === $result->chunks) {
336
                $range = $i * $chunkSize . '-' . $fileSize;
337
            }
338
            $requests[] = [
339
                'method' => 'GET',
340
                'uri' => $url,
341
                'options' => array_merge($options->toArray(), [
342
                    'CurlOptions' => [
343
                        CURLOPT_RANGE => $range
344
                    ],
345
                ])
346
            ];
347
        }
348
349
        foreach ($this->bulk($requests) as $response) {
350
            $result->addChunk(
351
                Utils::randomString(16),
352
                $response->getBody(),
353
                $response->getCurlInfo()->TOTAL_TIME
354
            );
355
        }
356
357
        $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...
358
359
        return $result;
360
    }
361
362
    /**
363
     * Download a chunk of a file.
364
     *
365
     * @param DownloadResult $result The result object.
366
     * @param HttpOptions $options The options to use.
367
     * @param array $input ['id', 'url', 'range'=> "start-end"]
368
     *
369
     * @return void
370
     */
371
    private function downloadChunk(DownloadResult &$result, HttpOptions $options, array $input): void
0 ignored issues
show
Unused Code introduced by
The method downloadChunk() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
372
    {
373
        $options->setCurlOptions([
374
            CURLOPT_RANGE => $input['range']
375
        ]);
376
        $response = self::get($input['url'], $options);
0 ignored issues
show
Bug Best Practice introduced by
The method EasyHttp\Client::get() is not static, but was called statically. ( Ignorable by Annotation )

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

376
        /** @scrutinizer ignore-call */ 
377
        $response = self::get($input['url'], $options);
Loading history...
377
        $result->addChunk(
378
            $input['id'],
379
            $response->getBody(),
380
            $response->getCurlInfo()->TOTAL_TIME
381
        );
382
    }
383
384
    /**
385
     * Get file size.
386
     *
387
     * @param string $url The direct url to the file.
388
     * @return int
389
     */
390
    private function getFileSize(string $url): int
391
    {
392
        $response = $this->get($url, [
393
            'headers' => [
394
                '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',
395
            ],
396
            'CurlOptions' => [
397
                CURLOPT_NOBODY => true,
398
            ]
399
        ]);
400
401
        return (int)$response->getHeaderLine('Content-Length') ?? 0;
402
    }
403
404
    /**
405
     * Get the size of each chunk.
406
     *
407
     * For default, we're using (fileSize/5) as max chunk size. If the file size
408
     * is smaller than 5MB, we'll use the file size as chunk size.
409
     *
410
     * @param int $fileSize The file size.
411
     * @param ?int $maxChunkSize The maximum chunk size. (default: fileSize/5)
412
     *
413
     * @return int
414
     */
415
    private function getChunkSize(int $fileSize, int $maxChunkSize = null): int
416
    {
417
        if ($maxChunkSize === null) {
418
            $maxChunkSize = $fileSize / 5;
419
        }
420
421
        if ($fileSize <= 5 * 1024 * 1024) {
422
            return $fileSize;
423
        }
424
425
        return min($maxChunkSize, $fileSize);
426
    }
427
428
}