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 | } |