Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
427 / 427 |
|
100.00% |
40 / 40 |
CRAP | |
100.00% |
1 / 1 |
Request | |
100.00% |
427 / 427 |
|
100.00% |
40 / 40 |
78 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
__toString | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
8 | |||
getMultipartBody | |
100.00% |
39 / 39 |
|
100.00% |
1 / 1 |
4 | |||
getFileInfo | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
5 | |||
setUrl | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
getUrl | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getMethod | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isMethod | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setMethod | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setProtocol | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setBody | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
setJson | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
setPost | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
hasFiles | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFiles | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setFiles | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
setContentType | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
setCookie | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
setCookies | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
removeCookie | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
removeCookies | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
setCookieHeader | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
setHeader | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setHeaders | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
removeHeader | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
removeHeaders | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setBasicAuth | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
setBearerAuth | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
setUserAgent | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setDownloadFunction | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
setOption | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
setOptions | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getOptions | |
100.00% |
26 / 26 |
|
100.00% |
1 / 1 |
5 | |||
getOption | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getPostAndFiles | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
8 | |||
setGetResponseInfo | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
isGettingResponseInfo | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setCheckOptions | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
isCheckingOptions | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
checkOption | |
100.00% |
220 / 220 |
|
100.00% |
1 / 1 |
6 |
1 | <?php declare(strict_types=1); |
2 | /* |
3 | * This file is part of Aplus Framework HTTP Client Library. |
4 | * |
5 | * (c) Natan Felles <natanfelles@gmail.com> |
6 | * |
7 | * For the full copyright and license information, please view the LICENSE |
8 | * file that was distributed with this source code. |
9 | */ |
10 | namespace Framework\HTTP\Client; |
11 | |
12 | use CURLFile; |
13 | use CURLStringFile; |
14 | use Framework\Helpers\ArraySimple; |
15 | use Framework\HTTP\Cookie; |
16 | use Framework\HTTP\Header; |
17 | use Framework\HTTP\Message; |
18 | use Framework\HTTP\Method; |
19 | use Framework\HTTP\Protocol; |
20 | use Framework\HTTP\RequestHeader; |
21 | use Framework\HTTP\RequestInterface; |
22 | use Framework\HTTP\URL; |
23 | use InvalidArgumentException; |
24 | use JetBrains\PhpStorm\ArrayShape; |
25 | use JetBrains\PhpStorm\Pure; |
26 | use JsonException; |
27 | use OutOfBoundsException; |
28 | use SensitiveParameter; |
29 | |
30 | /** |
31 | * Class Request. |
32 | * |
33 | * @package http-client |
34 | */ |
35 | class Request extends Message implements RequestInterface |
36 | { |
37 | /** |
38 | * HTTP Request Method. |
39 | */ |
40 | protected string $method = Method::GET; |
41 | /** |
42 | * HTTP Request URL. |
43 | */ |
44 | protected URL $url; |
45 | /** |
46 | * POST files. |
47 | * |
48 | * @var array<string,array<mixed>|string> |
49 | */ |
50 | protected array $files = []; |
51 | /** |
52 | * Client default curl options. |
53 | * |
54 | * @var array<int,mixed> |
55 | */ |
56 | protected array $defaultOptions = [ |
57 | \CURLOPT_CONNECTTIMEOUT => 10, |
58 | \CURLOPT_TIMEOUT => 60, |
59 | \CURLOPT_FOLLOWLOCATION => false, |
60 | \CURLOPT_MAXREDIRS => 1, |
61 | \CURLOPT_AUTOREFERER => true, |
62 | \CURLOPT_RETURNTRANSFER => true, |
63 | \CURLOPT_ENCODING => '', |
64 | ]; |
65 | /** |
66 | * Custom curl options. |
67 | * |
68 | * @var array<int,mixed> |
69 | */ |
70 | protected array $options = []; |
71 | protected bool $checkOptions = false; |
72 | protected bool $getResponseInfo = false; |
73 | |
74 | /** |
75 | * Request constructor. |
76 | * |
77 | * @param string|URL $url |
78 | */ |
79 | public function __construct(URL | string $url) |
80 | { |
81 | $this->setUrl($url); |
82 | } |
83 | |
84 | public function __toString() : string |
85 | { |
86 | if ($this->parseContentType() === 'multipart/form-data') { |
87 | $this->setBody($this->getMultipartBody()); |
88 | } |
89 | if ( ! $this->hasHeader(RequestHeader::ACCEPT)) { |
90 | $accept = '*/*'; |
91 | $this->setHeader(RequestHeader::ACCEPT, $accept); |
92 | } |
93 | $options = $this->getOptions(); |
94 | if (isset($options[\CURLOPT_ENCODING]) |
95 | && ! $this->hasHeader(RequestHeader::ACCEPT_ENCODING) |
96 | ) { |
97 | $encoding = $options[\CURLOPT_ENCODING] === '' |
98 | ? 'deflate, gzip, br, zstd' |
99 | : $options[\CURLOPT_ENCODING]; |
100 | $this->setHeader(RequestHeader::ACCEPT_ENCODING, $encoding); |
101 | } |
102 | $message = parent::__toString(); |
103 | if (isset($accept)) { |
104 | $this->removeHeader(RequestHeader::ACCEPT); |
105 | } |
106 | if (isset($encoding)) { |
107 | $this->removeHeader(RequestHeader::ACCEPT_ENCODING); |
108 | } |
109 | return $message; |
110 | } |
111 | |
112 | protected function getMultipartBody() : string |
113 | { |
114 | $bodyParts = []; |
115 | \parse_str($this->getBody(), $post); |
116 | /** |
117 | * @var array<string,string> $post |
118 | */ |
119 | $post = ArraySimple::convert($post); |
120 | foreach ($post as $field => $value) { |
121 | $field = \htmlspecialchars($field, \ENT_QUOTES | \ENT_HTML5); |
122 | $bodyParts[] = \implode("\r\n", [ |
123 | "Content-Disposition: form-data; name=\"{$field}\"", |
124 | '', |
125 | $value, |
126 | ]); |
127 | } |
128 | /** |
129 | * @var array<string,CURLFile|string> $files |
130 | */ |
131 | $files = ArraySimple::convert($this->getFiles()); |
132 | foreach ($files as $field => $file) { |
133 | $field = (string) $field; |
134 | $field = \htmlspecialchars($field, \ENT_QUOTES | \ENT_HTML5); |
135 | $info = $this->getFileInfo($file); |
136 | $filename = \htmlspecialchars($info['filename'], \ENT_QUOTES | \ENT_HTML5); |
137 | $bodyParts[] = \implode("\r\n", [ |
138 | 'Content-Disposition: form-data; name="' . $field . '"; filename="' . $filename . '"', |
139 | 'Content-Type: ' . $info['mime'], |
140 | '', |
141 | $info['data'], |
142 | ]); |
143 | } |
144 | unset($info); |
145 | $boundary = \str_repeat('-', 24) . \substr(\md5(\implode("\r\n", $bodyParts)), 0, 16); |
146 | $this->setHeader( |
147 | Header::CONTENT_TYPE, |
148 | 'multipart/form-data; charset=UTF-8; boundary=' . $boundary |
149 | ); |
150 | foreach ($bodyParts as &$part) { |
151 | $part = "--{$boundary}\r\n{$part}"; |
152 | } |
153 | unset($part); |
154 | $bodyParts[] = "--{$boundary}--"; |
155 | $bodyParts[] = ''; |
156 | $bodyParts = \implode("\r\n", $bodyParts); |
157 | $this->setHeader( |
158 | Header::CONTENT_LENGTH, |
159 | (string) \strlen($bodyParts) |
160 | ); |
161 | return $bodyParts; |
162 | } |
163 | |
164 | /** |
165 | * @param CURLFile|CURLStringFile|string $file |
166 | * |
167 | * @return array<string,string> |
168 | */ |
169 | #[ArrayShape(['filename' => 'string', 'data' => 'string', 'mime' => 'string'])] |
170 | protected function getFileInfo(CURLFile | CURLStringFile | string $file) : array |
171 | { |
172 | if ($file instanceof CURLFile) { |
173 | return [ |
174 | 'filename' => $file->getPostFilename(), |
175 | 'data' => (string) \file_get_contents($file->getFilename()), |
176 | 'mime' => $file->getMimeType() ?: 'application/octet-stream', |
177 | ]; |
178 | } |
179 | if ($file instanceof CURLStringFile) { |
180 | return [ |
181 | 'filename' => $file->postname, |
182 | 'data' => $file->data, |
183 | 'mime' => $file->mime, |
184 | ]; |
185 | } |
186 | return [ |
187 | 'filename' => \basename($file), |
188 | 'data' => (string) \file_get_contents($file), |
189 | 'mime' => \mime_content_type($file) ?: 'application/octet-stream', |
190 | ]; |
191 | } |
192 | |
193 | /** |
194 | * @param string|URL $url |
195 | * |
196 | * @return static |
197 | */ |
198 | public function setUrl(string | URL $url) : static |
199 | { |
200 | if ( ! $url instanceof URL) { |
201 | $url = new URL($url); |
202 | } |
203 | $this->setHeader(RequestHeader::HOST, $url->getHost()); |
204 | return parent::setUrl($url); |
205 | } |
206 | |
207 | #[Pure] |
208 | public function getUrl() : URL |
209 | { |
210 | return parent::getUrl(); |
211 | } |
212 | |
213 | #[Pure] |
214 | public function getMethod() : string |
215 | { |
216 | return parent::getMethod(); |
217 | } |
218 | |
219 | /** |
220 | * @param string $method |
221 | * |
222 | * @throws InvalidArgumentException for invalid method |
223 | * |
224 | * @return bool |
225 | */ |
226 | public function isMethod(string $method) : bool |
227 | { |
228 | return parent::isMethod($method); |
229 | } |
230 | |
231 | /** |
232 | * @param string $method |
233 | * |
234 | * @return static |
235 | */ |
236 | public function setMethod(string $method) : static |
237 | { |
238 | return parent::setMethod($method); |
239 | } |
240 | |
241 | /** |
242 | * @param string $protocol |
243 | * |
244 | * @return static |
245 | */ |
246 | public function setProtocol(string $protocol) : static |
247 | { |
248 | return parent::setProtocol($protocol); |
249 | } |
250 | |
251 | /** |
252 | * Set the request body. |
253 | * |
254 | * @param array<string,mixed>|string $body |
255 | * |
256 | * @return static |
257 | */ |
258 | public function setBody(array | string $body) : static |
259 | { |
260 | if (\is_array($body)) { |
261 | $body = \http_build_query($body); |
262 | } |
263 | return parent::setBody($body); |
264 | } |
265 | |
266 | /** |
267 | * Set body with JSON data. |
268 | * |
269 | * @param mixed $data |
270 | * @param int $options [optional] <p> |
271 | * Bitmask consisting of <b>JSON_HEX_QUOT</b>, |
272 | * <b>JSON_HEX_TAG</b>, |
273 | * <b>JSON_HEX_AMP</b>, |
274 | * <b>JSON_HEX_APOS</b>, |
275 | * <b>JSON_NUMERIC_CHECK</b>, |
276 | * <b>JSON_PRETTY_PRINT</b>, |
277 | * <b>JSON_UNESCAPED_SLASHES</b>, |
278 | * <b>JSON_FORCE_OBJECT</b>, |
279 | * <b>JSON_UNESCAPED_UNICODE</b>. |
280 | * <b>JSON_THROW_ON_ERROR</b> |
281 | * </p> |
282 | * <p>Default is <b>JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE</b> |
283 | * when null</p> |
284 | * @param int<1,max> $depth [optional] Set the maximum depth. Must be greater than zero. |
285 | * |
286 | * @throws JsonException if json_encode() fails |
287 | * |
288 | * @return static |
289 | */ |
290 | public function setJson(mixed $data, int $options = null, int $depth = 512) : static |
291 | { |
292 | if ($options === null) { |
293 | $options = \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE; |
294 | } |
295 | $data = \json_encode($data, $options | \JSON_THROW_ON_ERROR, $depth); |
296 | $this->setContentType('application/json'); |
297 | $this->setBody($data); |
298 | return $this; |
299 | } |
300 | |
301 | /** |
302 | * Set POST data simulating a browser request. |
303 | * |
304 | * @param array<string,mixed> $data |
305 | * |
306 | * @return static |
307 | */ |
308 | public function setPost(array $data) : static |
309 | { |
310 | $this->setMethod(Method::POST); |
311 | $this->setBody($data); |
312 | return $this; |
313 | } |
314 | |
315 | #[Pure] |
316 | public function hasFiles() : bool |
317 | { |
318 | return ! empty($this->files); |
319 | } |
320 | |
321 | /** |
322 | * Get files for upload. |
323 | * |
324 | * @return array<mixed> |
325 | */ |
326 | #[Pure] |
327 | public function getFiles() : array |
328 | { |
329 | return $this->files; |
330 | } |
331 | |
332 | /** |
333 | * Set files for upload. |
334 | * |
335 | * @param array<mixed> $files Fields as keys, files (CURLFile, |
336 | * CURLStringFile or string filename) as values. |
337 | * Multi-dimensional array is allowed. |
338 | * |
339 | * @throws InvalidArgumentException for invalid file path |
340 | * |
341 | * @return static |
342 | */ |
343 | public function setFiles(array $files) : static |
344 | { |
345 | $this->setMethod(Method::POST); |
346 | $this->setContentType('multipart/form-data'); |
347 | $this->files = $files; |
348 | return $this; |
349 | } |
350 | |
351 | /** |
352 | * Set the Content-Type header. |
353 | * |
354 | * @param string $mime |
355 | * @param string $charset |
356 | * |
357 | * @return static |
358 | */ |
359 | public function setContentType(string $mime, string $charset = 'UTF-8') : static |
360 | { |
361 | $this->setHeader( |
362 | Header::CONTENT_TYPE, |
363 | $mime . ($charset ? '; charset=' . $charset : '') |
364 | ); |
365 | return $this; |
366 | } |
367 | |
368 | /** |
369 | * @param Cookie $cookie |
370 | * |
371 | * @return static |
372 | */ |
373 | public function setCookie(Cookie $cookie) : static |
374 | { |
375 | parent::setCookie($cookie); |
376 | $this->setCookieHeader(); |
377 | return $this; |
378 | } |
379 | |
380 | /** |
381 | * @param array<int,Cookie> $cookies |
382 | * |
383 | * @return static |
384 | */ |
385 | public function setCookies(array $cookies) : static |
386 | { |
387 | return parent::setCookies($cookies); |
388 | } |
389 | |
390 | /** |
391 | * @param string $name |
392 | * |
393 | * @return static |
394 | */ |
395 | public function removeCookie(string $name) : static |
396 | { |
397 | parent::removeCookie($name); |
398 | $this->setCookieHeader(); |
399 | return $this; |
400 | } |
401 | |
402 | /** |
403 | * @param array<int,string> $names |
404 | * |
405 | * @return static |
406 | */ |
407 | public function removeCookies(array $names) : static |
408 | { |
409 | parent::removeCookies($names); |
410 | $this->setCookieHeader(); |
411 | return $this; |
412 | } |
413 | |
414 | /** |
415 | * @return static |
416 | */ |
417 | protected function setCookieHeader() : static |
418 | { |
419 | $line = []; |
420 | foreach ($this->getCookies() as $cookie) { |
421 | $line[] = $cookie->getName() . '=' . $cookie->getValue(); |
422 | } |
423 | if ($line) { |
424 | $line = \implode('; ', $line); |
425 | return $this->setHeader(RequestHeader::COOKIE, $line); |
426 | } |
427 | return $this->removeHeader(RequestHeader::COOKIE); |
428 | } |
429 | |
430 | /** |
431 | * @param string $name |
432 | * @param string $value |
433 | * |
434 | * @return static |
435 | */ |
436 | public function setHeader(string $name, string $value) : static |
437 | { |
438 | return parent::setHeader($name, $value); |
439 | } |
440 | |
441 | /** |
442 | * @param array<string,string> $headers |
443 | * |
444 | * @return static |
445 | */ |
446 | public function setHeaders(array $headers) : static |
447 | { |
448 | return parent::setHeaders($headers); |
449 | } |
450 | |
451 | /** |
452 | * @param string $name |
453 | * |
454 | * @return static |
455 | */ |
456 | public function removeHeader(string $name) : static |
457 | { |
458 | return parent::removeHeader($name); |
459 | } |
460 | |
461 | /** |
462 | * @return static |
463 | */ |
464 | public function removeHeaders() : static |
465 | { |
466 | return parent::removeHeaders(); |
467 | } |
468 | |
469 | /** |
470 | * Set Authorization header with Basic type. |
471 | * |
472 | * @param string $username |
473 | * @param string $password |
474 | * |
475 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization |
476 | * |
477 | * @return static |
478 | */ |
479 | public function setBasicAuth( |
480 | string $username, |
481 | #[SensitiveParameter] string $password |
482 | ) : static { |
483 | return $this->setHeader( |
484 | RequestHeader::AUTHORIZATION, |
485 | 'Basic ' . \base64_encode($username . ':' . $password) |
486 | ); |
487 | } |
488 | |
489 | /** |
490 | * Set Authorization header with Bearer type. |
491 | * |
492 | * @param string $token |
493 | * |
494 | * @return static |
495 | */ |
496 | public function setBearerAuth(#[SensitiveParameter] string $token) : static |
497 | { |
498 | return $this->setHeader( |
499 | RequestHeader::AUTHORIZATION, |
500 | 'Bearer ' . $token |
501 | ); |
502 | } |
503 | |
504 | /** |
505 | * Set the User-Agent header. |
506 | * |
507 | * @param string|null $userAgent |
508 | * |
509 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent |
510 | * |
511 | * @return static |
512 | */ |
513 | public function setUserAgent(string $userAgent = null) : static |
514 | { |
515 | $userAgent ??= 'Aplus HTTP Client'; |
516 | return $this->setHeader(RequestHeader::USER_AGENT, $userAgent); |
517 | } |
518 | |
519 | /** |
520 | * Set a callback to write the response body with chunks. |
521 | * |
522 | * Used to write data to files, databases, etc... |
523 | * |
524 | * NOTE: Using this function makes the Response body, returned in the |
525 | * {@see Client::run()} method, be set with an empty string. |
526 | * |
527 | * @param callable $callback A callback with the response body $data chunk |
528 | * as first argument and the CurlHandle as the second. Return is not |
529 | * necessary. |
530 | * |
531 | * @return static |
532 | */ |
533 | public function setDownloadFunction(callable $callback) : static |
534 | { |
535 | $function = static function (\CurlHandle $handle, string $data) use ($callback) : int { |
536 | $callback($data, $handle); |
537 | return \strlen($data); |
538 | }; |
539 | $this->setOption(\CURLOPT_WRITEFUNCTION, $function); |
540 | return $this; |
541 | } |
542 | |
543 | /** |
544 | * Set curl options. |
545 | * |
546 | * @param int $option A curl constant |
547 | * @param mixed $value |
548 | * |
549 | * @see Client::$defaultOptions |
550 | * |
551 | * @return static |
552 | */ |
553 | public function setOption(int $option, mixed $value) : static |
554 | { |
555 | if ($this->isCheckingOptions()) { |
556 | $this->checkOption($option, $value); |
557 | } |
558 | $this->options[$option] = $value; |
559 | return $this; |
560 | } |
561 | |
562 | /** |
563 | * Set many curl options. |
564 | * |
565 | * @param array<int,mixed> $options Curl constants as keys and their respective values |
566 | * |
567 | * @return static |
568 | */ |
569 | public function setOptions(array $options) : static |
570 | { |
571 | foreach ($options as $option => $value) { |
572 | $this->setOption($option, $value); |
573 | } |
574 | return $this; |
575 | } |
576 | |
577 | /** |
578 | * Get default options replaced by custom. |
579 | * |
580 | * @return array<int,mixed> |
581 | */ |
582 | public function getOptions() : array |
583 | { |
584 | $options = \array_replace($this->defaultOptions, $this->options); |
585 | $options[\CURLOPT_PROTOCOLS] = \CURLPROTO_HTTPS | \CURLPROTO_HTTP; |
586 | $options[\CURLOPT_HTTP_VERSION] = match ($this->getProtocol()) { |
587 | Protocol::HTTP_1_0 => \CURL_HTTP_VERSION_1_0, |
588 | Protocol::HTTP_1_1 => \CURL_HTTP_VERSION_1_1, |
589 | Protocol::HTTP_2_0 => \CURL_HTTP_VERSION_2_0, |
590 | Protocol::HTTP_2 => \CURL_HTTP_VERSION_2, |
591 | default => throw new InvalidArgumentException( |
592 | 'Invalid Request Protocol: ' . $this->getProtocol() |
593 | ) |
594 | }; |
595 | switch ($this->getMethod()) { |
596 | case Method::POST: |
597 | $options[\CURLOPT_POST] = true; |
598 | $options[\CURLOPT_POSTFIELDS] = $this->getPostAndFiles(); |
599 | break; |
600 | case Method::DELETE: |
601 | case Method::PATCH: |
602 | case Method::PUT: |
603 | $options[\CURLOPT_POSTFIELDS] = $this->getBody(); |
604 | break; |
605 | } |
606 | $options[\CURLOPT_CUSTOMREQUEST] = $this->getMethod(); |
607 | $options[\CURLOPT_HEADER] = false; |
608 | $options[\CURLOPT_URL] = $this->getUrl()->toString(); |
609 | $options[\CURLOPT_HTTPHEADER] = $this->getHeaderLines(); |
610 | return $options; |
611 | } |
612 | |
613 | public function getOption(int $option) : mixed |
614 | { |
615 | return $this->getOptions()[$option] ?? null; |
616 | } |
617 | |
618 | /** |
619 | * Returns string if the Request has not files and curl will set the |
620 | * Content-Type header to application/x-www-form-urlencoded. If the Request |
621 | * has files, returns an array and curl will set the Content-Type to |
622 | * multipart/form-data. |
623 | * |
624 | * If the Request has files, the $post and $files arrays are converted to |
625 | * the array_simple format. Because curl does not understand the PHP |
626 | * multi-dimensional arrays. |
627 | * |
628 | * @see https://www.php.net/manual/en/function.curl-setopt.php CURLOPT_POSTFIELDS |
629 | * @see ArraySimple::convert() |
630 | * |
631 | * @return array<string,mixed>|string |
632 | */ |
633 | public function getPostAndFiles() : array | string |
634 | { |
635 | if ( ! $this->hasFiles()) { |
636 | return $this->getBody(); |
637 | } |
638 | \parse_str($this->getBody(), $post); |
639 | $post = ArraySimple::convert($post); |
640 | foreach ($post as &$value) { |
641 | $value = (string) $value; |
642 | } |
643 | unset($value); |
644 | $files = ArraySimple::convert($this->getFiles()); |
645 | foreach ($files as $field => &$file) { |
646 | if ($file instanceof CURLFile |
647 | || $file instanceof CURLStringFile |
648 | ) { |
649 | continue; |
650 | } |
651 | if ( ! \is_file($file)) { |
652 | throw new InvalidArgumentException( |
653 | "Field '{$field}' does not match a file: {$file}" |
654 | ); |
655 | } |
656 | $file = new CURLFile( |
657 | $file, |
658 | \mime_content_type($file) ?: 'application/octet-stream', |
659 | \basename($file) |
660 | ); |
661 | } |
662 | unset($file); |
663 | return \array_replace($post, $files); |
664 | } |
665 | |
666 | public function setGetResponseInfo(bool $get = true) : static |
667 | { |
668 | $this->getResponseInfo = $get; |
669 | return $this; |
670 | } |
671 | |
672 | public function isGettingResponseInfo() : bool |
673 | { |
674 | return $this->getResponseInfo; |
675 | } |
676 | |
677 | /** |
678 | * @param bool $check |
679 | * |
680 | * @return static |
681 | */ |
682 | public function setCheckOptions(bool $check = true) : static |
683 | { |
684 | $this->checkOptions = $check; |
685 | return $this; |
686 | } |
687 | |
688 | public function isCheckingOptions() : bool |
689 | { |
690 | return $this->checkOptions; |
691 | } |
692 | |
693 | /** |
694 | * @param int $option The curl option |
695 | * @param mixed $value The curl option value |
696 | * |
697 | * @throws InvalidArgumentException if the option value does not match the |
698 | * expected type |
699 | * @throws OutOfBoundsException if the option is invalid |
700 | */ |
701 | protected function checkOption(int $option, mixed $value) : void |
702 | { |
703 | $types = [ |
704 | 'bool' => [ |
705 | \CURLOPT_AUTOREFERER, |
706 | \CURLOPT_COOKIESESSION, |
707 | \CURLOPT_CERTINFO, |
708 | \CURLOPT_CONNECT_ONLY, |
709 | \CURLOPT_CRLF, |
710 | \CURLOPT_DISALLOW_USERNAME_IN_URL, |
711 | \CURLOPT_DNS_SHUFFLE_ADDRESSES, |
712 | \CURLOPT_HAPROXYPROTOCOL, |
713 | \CURLOPT_SSH_COMPRESSION, |
714 | \CURLOPT_DNS_USE_GLOBAL_CACHE, |
715 | \CURLOPT_FAILONERROR, |
716 | \CURLOPT_SSL_FALSESTART, |
717 | \CURLOPT_FILETIME, |
718 | \CURLOPT_FOLLOWLOCATION, |
719 | \CURLOPT_FORBID_REUSE, |
720 | \CURLOPT_FRESH_CONNECT, |
721 | \CURLOPT_FTP_USE_EPRT, |
722 | \CURLOPT_FTP_USE_EPSV, |
723 | \CURLOPT_FTP_CREATE_MISSING_DIRS, |
724 | \CURLOPT_FTPAPPEND, |
725 | \CURLOPT_TCP_NODELAY, |
726 | // CURLOPT_FTPASCII, |
727 | \CURLOPT_FTPLISTONLY, |
728 | \CURLOPT_HEADER, |
729 | \CURLINFO_HEADER_OUT, |
730 | \CURLOPT_HTTP09_ALLOWED, |
731 | \CURLOPT_HTTPGET, |
732 | \CURLOPT_HTTPPROXYTUNNEL, |
733 | \CURLOPT_HTTP_CONTENT_DECODING, |
734 | \CURLOPT_KEEP_SENDING_ON_ERROR, |
735 | // CURLOPT_MUTE, |
736 | \CURLOPT_NETRC, |
737 | \CURLOPT_NOBODY, |
738 | \CURLOPT_NOPROGRESS, |
739 | \CURLOPT_NOSIGNAL, |
740 | \CURLOPT_PATH_AS_IS, |
741 | \CURLOPT_PIPEWAIT, |
742 | \CURLOPT_POST, |
743 | \CURLOPT_PUT, |
744 | \CURLOPT_RETURNTRANSFER, |
745 | \CURLOPT_SASL_IR, |
746 | \CURLOPT_SSL_ENABLE_ALPN, |
747 | \CURLOPT_SSL_ENABLE_NPN, |
748 | \CURLOPT_SSL_VERIFYPEER, |
749 | \CURLOPT_SSL_VERIFYSTATUS, |
750 | \CURLOPT_PROXY_SSL_VERIFYPEER, |
751 | \CURLOPT_SUPPRESS_CONNECT_HEADERS, |
752 | \CURLOPT_TCP_FASTOPEN, |
753 | \CURLOPT_TFTP_NO_OPTIONS, |
754 | \CURLOPT_TRANSFERTEXT, |
755 | \CURLOPT_UNRESTRICTED_AUTH, |
756 | \CURLOPT_UPLOAD, |
757 | \CURLOPT_VERBOSE, |
758 | ], |
759 | 'int' => [ |
760 | \CURLOPT_BUFFERSIZE, |
761 | \CURLOPT_CONNECTTIMEOUT, |
762 | \CURLOPT_CONNECTTIMEOUT_MS, |
763 | \CURLOPT_DNS_CACHE_TIMEOUT, |
764 | \CURLOPT_EXPECT_100_TIMEOUT_MS, |
765 | \CURLOPT_HAPPY_EYEBALLS_TIMEOUT_MS, |
766 | \CURLOPT_FTPSSLAUTH, |
767 | \CURLOPT_HEADEROPT, |
768 | \CURLOPT_HTTP_VERSION, |
769 | \CURLOPT_HTTPAUTH, |
770 | \CURLOPT_INFILESIZE, |
771 | \CURLOPT_LOW_SPEED_LIMIT, |
772 | \CURLOPT_LOW_SPEED_TIME, |
773 | \CURLOPT_MAXCONNECTS, |
774 | \CURLOPT_MAXREDIRS, |
775 | \CURLOPT_PORT, |
776 | \CURLOPT_POSTREDIR, |
777 | \CURLOPT_PROTOCOLS, |
778 | \CURLOPT_PROXYAUTH, |
779 | \CURLOPT_PROXYPORT, |
780 | \CURLOPT_PROXYTYPE, |
781 | \CURLOPT_REDIR_PROTOCOLS, |
782 | \CURLOPT_RESUME_FROM, |
783 | \CURLOPT_SOCKS5_AUTH, |
784 | \CURLOPT_SSL_OPTIONS, |
785 | \CURLOPT_SSL_VERIFYHOST, |
786 | \CURLOPT_SSLVERSION, |
787 | \CURLOPT_PROXY_SSL_OPTIONS, |
788 | \CURLOPT_PROXY_SSL_VERIFYHOST, |
789 | \CURLOPT_PROXY_SSLVERSION, |
790 | \CURLOPT_STREAM_WEIGHT, |
791 | \CURLOPT_TCP_KEEPALIVE, |
792 | \CURLOPT_TCP_KEEPIDLE, |
793 | \CURLOPT_TCP_KEEPINTVL, |
794 | \CURLOPT_TIMECONDITION, |
795 | \CURLOPT_TIMEOUT, |
796 | \CURLOPT_TIMEOUT_MS, |
797 | \CURLOPT_TIMEVALUE, |
798 | \CURLOPT_TIMEVALUE_LARGE, |
799 | \CURLOPT_MAX_RECV_SPEED_LARGE, |
800 | \CURLOPT_MAX_SEND_SPEED_LARGE, |
801 | \CURLOPT_SSH_AUTH_TYPES, |
802 | \CURLOPT_IPRESOLVE, |
803 | \CURLOPT_FTP_FILEMETHOD, |
804 | ], |
805 | 'string' => [ |
806 | \CURLOPT_ABSTRACT_UNIX_SOCKET, |
807 | \CURLOPT_CAINFO, |
808 | \CURLOPT_CAPATH, |
809 | \CURLOPT_COOKIE, |
810 | \CURLOPT_COOKIEFILE, |
811 | \CURLOPT_COOKIEJAR, |
812 | \CURLOPT_COOKIELIST, |
813 | \CURLOPT_CUSTOMREQUEST, |
814 | \CURLOPT_DEFAULT_PROTOCOL, |
815 | \CURLOPT_DNS_INTERFACE, |
816 | \CURLOPT_DNS_LOCAL_IP4, |
817 | \CURLOPT_DNS_LOCAL_IP6, |
818 | \CURLOPT_DOH_URL, |
819 | \CURLOPT_EGDSOCKET, |
820 | \CURLOPT_ENCODING, |
821 | \CURLOPT_FTPPORT, |
822 | \CURLOPT_INTERFACE, |
823 | \CURLOPT_KEYPASSWD, |
824 | \CURLOPT_KRB4LEVEL, |
825 | \CURLOPT_LOGIN_OPTIONS, |
826 | \CURLOPT_PINNEDPUBLICKEY, |
827 | \CURLOPT_POSTFIELDS, |
828 | \CURLOPT_PRIVATE, |
829 | \CURLOPT_PRE_PROXY, |
830 | \CURLOPT_PROXY, |
831 | \CURLOPT_PROXY_SERVICE_NAME, |
832 | \CURLOPT_PROXY_CAINFO, |
833 | \CURLOPT_PROXY_CAPATH, |
834 | \CURLOPT_PROXY_CRLFILE, |
835 | \CURLOPT_PROXY_KEYPASSWD, |
836 | \CURLOPT_PROXY_PINNEDPUBLICKEY, |
837 | \CURLOPT_PROXY_SSLCERT, |
838 | \CURLOPT_PROXY_SSLCERTTYPE, |
839 | \CURLOPT_PROXY_SSL_CIPHER_LIST, |
840 | \CURLOPT_PROXY_TLS13_CIPHERS, |
841 | \CURLOPT_PROXY_SSLKEY, |
842 | \CURLOPT_PROXY_SSLKEYTYPE, |
843 | \CURLOPT_PROXY_TLSAUTH_PASSWORD, |
844 | \CURLOPT_PROXY_TLSAUTH_TYPE, |
845 | \CURLOPT_PROXY_TLSAUTH_USERNAME, |
846 | \CURLOPT_PROXYUSERPWD, |
847 | \CURLOPT_RANDOM_FILE, |
848 | \CURLOPT_RANGE, |
849 | \CURLOPT_REFERER, |
850 | \CURLOPT_SERVICE_NAME, |
851 | \CURLOPT_SSH_HOST_PUBLIC_KEY_MD5, |
852 | \CURLOPT_SSH_PUBLIC_KEYFILE, |
853 | \CURLOPT_SSH_PRIVATE_KEYFILE, |
854 | \CURLOPT_SSL_CIPHER_LIST, |
855 | \CURLOPT_SSLCERT, |
856 | \CURLOPT_SSLCERTPASSWD, |
857 | \CURLOPT_SSLCERTTYPE, |
858 | \CURLOPT_SSLENGINE, |
859 | \CURLOPT_SSLENGINE_DEFAULT, |
860 | \CURLOPT_SSLKEY, |
861 | \CURLOPT_SSLKEYPASSWD, |
862 | \CURLOPT_SSLKEYTYPE, |
863 | \CURLOPT_TLS13_CIPHERS, |
864 | \CURLOPT_UNIX_SOCKET_PATH, |
865 | \CURLOPT_URL, |
866 | \CURLOPT_USERAGENT, |
867 | \CURLOPT_USERNAME, |
868 | \CURLOPT_PASSWORD, |
869 | \CURLOPT_USERPWD, |
870 | \CURLOPT_XOAUTH2_BEARER, |
871 | ], |
872 | 'array' => [ |
873 | \CURLOPT_CONNECT_TO, |
874 | \CURLOPT_HTTP200ALIASES, |
875 | \CURLOPT_HTTPHEADER, |
876 | \CURLOPT_POSTQUOTE, |
877 | \CURLOPT_PROXYHEADER, |
878 | \CURLOPT_QUOTE, |
879 | \CURLOPT_RESOLVE, |
880 | ], |
881 | 'fopen' => [ |
882 | \CURLOPT_FILE, |
883 | \CURLOPT_INFILE, |
884 | \CURLOPT_STDERR, |
885 | \CURLOPT_WRITEHEADER, |
886 | ], |
887 | 'function' => [ |
888 | \CURLOPT_HEADERFUNCTION, |
889 | // CURLOPT_PASSWDFUNCTION, |
890 | \CURLOPT_PROGRESSFUNCTION, |
891 | \CURLOPT_READFUNCTION, |
892 | \CURLOPT_WRITEFUNCTION, |
893 | ], |
894 | 'curl_share_init' => [ |
895 | \CURLOPT_SHARE, |
896 | ], |
897 | ]; |
898 | foreach ($types as $type => $constants) { |
899 | foreach ($constants as $constant) { |
900 | if ($option !== $constant) { |
901 | continue; |
902 | } |
903 | if ($value === null) { |
904 | return; |
905 | } |
906 | $valid = match ($type) { |
907 | 'bool' => \is_bool($value), |
908 | 'int' => \is_int($value), |
909 | 'string' => \is_string($value), |
910 | 'array' => \is_array($value), |
911 | 'fopen' => \is_resource($value), |
912 | 'function' => \is_callable($value), |
913 | 'curl_share_init' => $value instanceof \CurlShareHandle |
914 | }; |
915 | if ($valid) { |
916 | return; |
917 | } |
918 | $message = match ($type) { |
919 | 'bool' => 'The value of option %d should be of bool type', |
920 | 'int' => 'The value of option %d should be of int type', |
921 | 'string' => 'The value of option %d should be of string type', |
922 | 'array' => 'The value of option %d should be of array type', |
923 | 'fopen' => 'The value of option %d should be a fopen() resource', |
924 | 'function' => 'The value of option %d should be a callable', |
925 | 'curl_share_init' => 'The value of option %d should be a result of curl_share_init()' |
926 | }; |
927 | throw new InvalidArgumentException(\sprintf($message, $option)); |
928 | } |
929 | } |
930 | throw new OutOfBoundsException('Invalid curl constant option: ' . $option); |
931 | } |
932 | } |