1 | <?php |
||||
2 | |||||
3 | namespace EasyHttp; |
||||
4 | |||||
5 | use EasyHttp\Enums\ErrorCode; |
||||
6 | use EasyHttp\Model\DownloadResult; |
||||
7 | use EasyHttp\Model\HttpOptions; |
||||
8 | use EasyHttp\Model\HttpResponse; |
||||
9 | use EasyHttp\Traits\ClientTrait; |
||||
10 | use EasyHttp\Utils\Toolkit; |
||||
11 | use InvalidArgumentException; |
||||
12 | use RuntimeException; |
||||
13 | |||||
14 | /** |
||||
15 | * Client class |
||||
16 | * |
||||
17 | * @link https://github.com/shahradelahi/easy-http |
||||
18 | * @author Shahrad Elahi (https://github.com/shahradelahi) |
||||
19 | * @license https://github.com/shahradelahi/easy-http/blob/master/LICENSE (MIT License) |
||||
20 | */ |
||||
21 | class Client |
||||
22 | { |
||||
23 | |||||
24 | use ClientTrait; |
||||
25 | |||||
26 | /** |
||||
27 | * The temp directory to download files - default is $_SERVER['TEMP'] |
||||
28 | * |
||||
29 | * @var ?string |
||||
30 | */ |
||||
31 | private ?string $tempDir; |
||||
32 | |||||
33 | /** |
||||
34 | * The Max count of chunk to download file |
||||
35 | * |
||||
36 | * @var int |
||||
37 | */ |
||||
38 | public int $maxChunkCount = 10; |
||||
39 | |||||
40 | /** |
||||
41 | * The constructor of the client |
||||
42 | */ |
||||
43 | public function __construct() |
||||
44 | { |
||||
45 | $this->tempDir = $_SERVER['TEMP'] ?? null; |
||||
46 | $this->setHasSelfSignedCertificate(true); |
||||
47 | } |
||||
48 | |||||
49 | /** |
||||
50 | * Set has self-signed certificate |
||||
51 | * |
||||
52 | * This is used to set the curl option CURLOPT_SSL_VERIFYPEER |
||||
53 | * and CURLOPT_SSL_VERIFYHOST to false. This is useful when you are |
||||
54 | * in local environment, or you have self-signed certificate. |
||||
55 | * |
||||
56 | * @param bool $has |
||||
57 | * |
||||
58 | * @return void |
||||
59 | */ |
||||
60 | public function setHasSelfSignedCertificate(bool $has): void |
||||
61 | { |
||||
62 | putenv('HAS_SELF_SIGNED_CERT=' . ($has ? 'true' : 'false')); |
||||
63 | } |
||||
64 | |||||
65 | /** |
||||
66 | * Set the temporary directory path to save the downloaded files |
||||
67 | * |
||||
68 | * @param string $path |
||||
69 | * |
||||
70 | * @return void |
||||
71 | */ |
||||
72 | public function setTempPath(string $path): void |
||||
73 | { |
||||
74 | if (!file_exists($path)) { |
||||
75 | throw new InvalidArgumentException( |
||||
76 | sprintf('The path "%s" does not exist', $path) |
||||
77 | ); |
||||
78 | } |
||||
79 | $this->tempDir = $path; |
||||
80 | } |
||||
81 | |||||
82 | /** |
||||
83 | * This method is used to send a http request to a given url. |
||||
84 | * |
||||
85 | * @param string $method |
||||
86 | * @param string $uri |
||||
87 | * @param array|HttpOptions $options |
||||
88 | * |
||||
89 | * @return HttpResponse |
||||
90 | */ |
||||
91 | public function request(string $method, string $uri, array|HttpOptions $options = []): HttpResponse |
||||
92 | { |
||||
93 | $CurlHandle = Middleware::create_curl_handler($method, $uri, $options); |
||||
94 | if (!$CurlHandle) { |
||||
95 | throw new RuntimeException('An error occurred while creating the curl handler'); |
||||
96 | } |
||||
97 | |||||
98 | $result = new HttpResponse(); |
||||
99 | $result->setCurlHandle($CurlHandle); |
||||
100 | |||||
101 | $response = curl_exec($CurlHandle); |
||||
102 | if (curl_errno($CurlHandle) || !$response) { |
||||
103 | $result->setErrorCode(curl_errno($CurlHandle)); |
||||
104 | $result->setErrorMessage(curl_error($CurlHandle) ?? ErrorCode::getMessage(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((string)$response, 0, $result->getHeaderSize())); |
||||
111 | $result->setBody(substr((string)$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 = Middleware::create_curl_handler( |
||||
133 | $request['method'] ?? null, |
||||
134 | $request['uri'], |
||||
135 | $request['options'] ?? [] |
||||
136 | ); |
||||
137 | if (!$CurlHandle) { |
||||
138 | throw new RuntimeException( |
||||
139 | 'An error occurred while creating the curl handler' |
||||
140 | ); |
||||
141 | } |
||||
142 | $handlers[] = $CurlHandle; |
||||
143 | curl_multi_add_handle($multi_handler, $CurlHandle); |
||||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||
144 | |||||
145 | } |
||||
146 | |||||
147 | $active = null; |
||||
148 | do { |
||||
149 | $mrc = curl_multi_exec($multi_handler, $active); |
||||
0 ignored issues
–
show
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
![]() |
|||||
150 | } while ($mrc == CURLM_CALL_MULTI_PERFORM); |
||||
151 | |||||
152 | while ($active && $mrc == CURLM_OK) { |
||||
153 | if (curl_multi_select($multi_handler) != -1) { |
||||
0 ignored issues
–
show
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
![]() |
|||||
154 | do { |
||||
155 | $mrc = curl_multi_exec($multi_handler, $active); |
||||
156 | } while ($mrc == CURLM_CALL_MULTI_PERFORM); |
||||
157 | } |
||||
158 | } |
||||
159 | |||||
160 | foreach ($handlers as $handler) { |
||||
161 | curl_multi_remove_handle($multi_handler, $handler); |
||||
0 ignored issues
–
show
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
![]() |
|||||
162 | } |
||||
163 | curl_multi_close($multi_handler); |
||||
0 ignored issues
–
show
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
![]() |
|||||
164 | |||||
165 | foreach ($handlers as $handler) { |
||||
166 | $content = curl_multi_getcontent($handler); |
||||
167 | $response = new HttpResponse(); |
||||
168 | |||||
169 | if (curl_errno($handler)) { |
||||
170 | $response->setErrorCode(curl_errno($handler)); |
||||
171 | $response->setErrorMessage( |
||||
172 | curl_error($handler) ?? |
||||
173 | ErrorCode::getMessage(curl_errno($handler)) |
||||
174 | ); |
||||
175 | } |
||||
176 | |||||
177 | $response->setCurlHandle($handler); |
||||
178 | $response->setStatusCode(curl_getinfo($handler, CURLINFO_HTTP_CODE)); |
||||
179 | $response->setHeaderSize(curl_getinfo($handler, CURLINFO_HEADER_SIZE)); |
||||
180 | $response->setHeaders(substr($content, 0, $response->getHeaderSize())); |
||||
181 | $response->setBody(substr($content, $response->getHeaderSize())); |
||||
182 | |||||
183 | $result[] = $response; |
||||
184 | } |
||||
185 | |||||
186 | return $result; |
||||
187 | } |
||||
188 | |||||
189 | /** |
||||
190 | * Download large files. |
||||
191 | * |
||||
192 | * This method is used to download large files with creating multiple requests. |
||||
193 | * |
||||
194 | * Change `max_chunk_count` variable to change the number of chunks. (default: 10) |
||||
195 | * |
||||
196 | * @param string $url The direct url to the file. |
||||
197 | * @param array|HttpOptions $options The options to use. |
||||
198 | * |
||||
199 | * @return DownloadResult |
||||
200 | */ |
||||
201 | public function download(string $url, array|HttpOptions $options = []): DownloadResult |
||||
202 | { |
||||
203 | if (empty($this->tempDir)) { |
||||
204 | throw new RuntimeException( |
||||
205 | 'The temp directory is not set. Please set the temp directory using the `setTempDir` method.' |
||||
206 | ); |
||||
207 | } |
||||
208 | |||||
209 | if (!file_exists($this->tempDir)) { |
||||
210 | if (mkdir($this->tempDir, 0777, true) === false) { |
||||
211 | throw new RuntimeException( |
||||
212 | 'The temp directory is not writable. Please set the temp directory using the `setTempDir` method.' |
||||
213 | ); |
||||
214 | } |
||||
215 | } |
||||
216 | |||||
217 | if (gettype($options) === 'array') { |
||||
218 | $options = new HttpOptions($options); |
||||
219 | } |
||||
220 | |||||
221 | $fileSize = $this->get_file_size($url); |
||||
222 | $chunkSize = $this->get_chunk_size($fileSize); |
||||
223 | |||||
224 | $result = new DownloadResult(); |
||||
225 | |||||
226 | $result->id = uniqid(); |
||||
227 | $result->chunksPath = $this->tempDir . '/' . $result->id . '/'; |
||||
228 | mkdir($result->chunksPath, 0777, true); |
||||
229 | |||||
230 | $result->fileSize = $fileSize; |
||||
231 | $result->chunkSize = $chunkSize; |
||||
232 | $result->chunks = ceil($fileSize / $chunkSize); |
||||
233 | |||||
234 | $result->startTime = time(); |
||||
235 | |||||
236 | $requests = []; |
||||
237 | for ($i = 0; $i < $result->chunks; $i++) { |
||||
238 | $range = $i * $chunkSize . '-' . ($i + 1) * $chunkSize; |
||||
239 | if ($i + 1 === $result->chunks) { |
||||
240 | $range = $i * $chunkSize . '-' . $fileSize; |
||||
241 | } |
||||
242 | $requests[] = [ |
||||
243 | 'method' => 'GET', |
||||
244 | 'uri' => $url, |
||||
245 | 'options' => array_merge($options->toArray(), [ |
||||
246 | 'CurlOptions' => [ |
||||
247 | CURLOPT_RANGE => $range |
||||
248 | ], |
||||
249 | ]) |
||||
250 | ]; |
||||
251 | } |
||||
252 | |||||
253 | foreach ($this->bulk($requests) as $response) { |
||||
254 | $result->addChunk( |
||||
255 | Toolkit::randomString(16), |
||||
256 | $response->getBody(), |
||||
257 | $response->getInfoFromCurl()->TOTAL_TIME |
||||
258 | ); |
||||
259 | } |
||||
260 | |||||
261 | $result->endTime = time(); |
||||
262 | |||||
263 | return $result; |
||||
264 | } |
||||
265 | |||||
266 | /** |
||||
267 | * Upload single or multiple files |
||||
268 | * |
||||
269 | * This method is sending file with request method of POST and |
||||
270 | * Content-Type of multipart/form-data. |
||||
271 | * |
||||
272 | * @param string $url The direct url to the file. |
||||
273 | * @param array $filePath The path to the file. |
||||
274 | * @param array|HttpOptions $options The options to use. |
||||
275 | * |
||||
276 | * @return HttpResponse |
||||
277 | */ |
||||
278 | public function upload(string $url, array $filePath, array|HttpOptions $options = []): HttpResponse |
||||
279 | { |
||||
280 | if (gettype($options) === 'array') { |
||||
281 | $options = new HttpOptions($options); |
||||
282 | } |
||||
283 | |||||
284 | $multipart = []; |
||||
285 | |||||
286 | foreach ($filePath as $key => $file) { |
||||
287 | $multipart[$key] = new \CURLFile( |
||||
288 | realpath($file), |
||||
289 | $this->get_file_type($file) |
||||
290 | ); |
||||
291 | } |
||||
292 | |||||
293 | $options->setMultipart($multipart); |
||||
294 | return $this->post($url, $options); |
||||
295 | } |
||||
296 | |||||
297 | /** |
||||
298 | * Get filetype with the extension. |
||||
299 | * |
||||
300 | * @param string $filename The absolute path to the file. |
||||
301 | * @return string eg. image/jpeg |
||||
302 | */ |
||||
303 | public static function get_file_type(string $filename): string |
||||
304 | { |
||||
305 | return MimeType::TYPES[pathinfo($filename, PATHINFO_EXTENSION)] ?? 'application/octet-stream'; |
||||
306 | } |
||||
307 | |||||
308 | /** |
||||
309 | * Get file size. |
||||
310 | * |
||||
311 | * @param string $url The direct url to the file. |
||||
312 | * @return int |
||||
313 | */ |
||||
314 | public function get_file_size(string $url): int |
||||
315 | { |
||||
316 | if (file_exists($url)) { |
||||
317 | return filesize($url); |
||||
318 | } |
||||
319 | |||||
320 | $response = $this->get($url, [ |
||||
321 | 'headers' => [ |
||||
322 | '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', |
||||
323 | ], |
||||
324 | 'CurlOptions' => [ |
||||
325 | CURLOPT_NOBODY => true, |
||||
326 | ] |
||||
327 | ]); |
||||
328 | |||||
329 | return (int)$response->getHeaderLine('Content-Length') ?? 0; |
||||
330 | } |
||||
331 | |||||
332 | /** |
||||
333 | * Get the size of each chunk. |
||||
334 | * |
||||
335 | * For default, we're dividing filesize to 10 as max size of each chunk. |
||||
336 | * If the file size was smaller than 2MB, we'll use the filesize as single chunk. |
||||
337 | * |
||||
338 | * @param int $fileSize The file size. |
||||
339 | * @return int |
||||
340 | */ |
||||
341 | private function get_chunk_size(int $fileSize): int |
||||
342 | { |
||||
343 | $maxChunkSize = $fileSize / $this->maxChunkCount; |
||||
344 | |||||
345 | if ($fileSize <= 2 * 1024 * 1024) { |
||||
346 | return $fileSize; |
||||
347 | } |
||||
348 | |||||
349 | return min($maxChunkSize, $fileSize); |
||||
350 | } |
||||
351 | |||||
352 | } |