1: | <?php declare(strict_types=1); |
2: | |
3: | namespace Salient\Curler; |
4: | |
5: | use Psr\Http\Client\ClientExceptionInterface as PsrClientExceptionInterface; |
6: | use Psr\Http\Message\RequestInterface as PsrRequestInterface; |
7: | use Psr\Http\Message\ResponseInterface as PsrResponseInterface; |
8: | use Psr\Http\Message\StreamInterface as PsrStreamInterface; |
9: | use Psr\Http\Message\UriInterface as PsrUriInterface; |
10: | use Salient\Contract\Cache\CacheInterface; |
11: | use Salient\Contract\Core\Arrayable; |
12: | use Salient\Contract\Core\Buildable; |
13: | use Salient\Contract\Core\DateFormatterInterface; |
14: | use Salient\Contract\Curler\Exception\CurlErrorException as CurlErrorExceptionInterface; |
15: | use Salient\Contract\Curler\CurlerInterface; |
16: | use Salient\Contract\Curler\CurlerMiddlewareInterface; |
17: | use Salient\Contract\Curler\CurlerPageRequestInterface; |
18: | use Salient\Contract\Curler\CurlerPagerInterface; |
19: | use Salient\Contract\Http\Exception\StreamEncapsulationException; |
20: | use Salient\Contract\Http\Message\MultipartStreamInterface; |
21: | use Salient\Contract\Http\Message\ResponseInterface; |
22: | use Salient\Contract\Http\CredentialInterface; |
23: | use Salient\Contract\Http\HeadersInterface; |
24: | use Salient\Contract\Http\UriInterface; |
25: | use Salient\Core\Concern\BuildableTrait; |
26: | use Salient\Core\Concern\ImmutableTrait; |
27: | use Salient\Core\Facade\Cache; |
28: | use Salient\Core\Facade\Console; |
29: | use Salient\Core\Facade\Event; |
30: | use Salient\Curler\Event\CurlRequestEvent; |
31: | use Salient\Curler\Event\CurlResponseEvent; |
32: | use Salient\Curler\Event\ResponseCacheHitEvent; |
33: | use Salient\Curler\Exception\CurlErrorException; |
34: | use Salient\Curler\Exception\HttpErrorException; |
35: | use Salient\Curler\Exception\NetworkException; |
36: | use Salient\Curler\Exception\RequestException; |
37: | use Salient\Curler\Exception\TooManyRedirectsException; |
38: | use Salient\Http\Exception\InvalidHeaderException; |
39: | use Salient\Http\Message\Request; |
40: | use Salient\Http\Message\Response; |
41: | use Salient\Http\Message\Stream; |
42: | use Salient\Http\HasInnerHeadersTrait; |
43: | use Salient\Http\Headers; |
44: | use Salient\Http\HttpUtil; |
45: | use Salient\Http\Uri; |
46: | use Salient\Utility\Arr; |
47: | use Salient\Utility\File; |
48: | use Salient\Utility\Get; |
49: | use Salient\Utility\Inflect; |
50: | use Salient\Utility\Json; |
51: | use Salient\Utility\Str; |
52: | use Closure; |
53: | use CurlHandle; |
54: | use InvalidArgumentException; |
55: | use LogicException; |
56: | use OutOfRangeException; |
57: | use RuntimeException; |
58: | use Stringable; |
59: | use Throwable; |
60: | |
61: | |
62: | |
63: | |
64: | |
65: | |
66: | |
67: | |
68: | class Curler implements CurlerInterface, Buildable |
69: | { |
70: | |
71: | use BuildableTrait; |
72: | use HasInnerHeadersTrait; |
73: | use ImmutableTrait; |
74: | |
75: | |
76: | |
77: | |
78: | |
79: | |
80: | protected const MAX_INPUT_LENGTH = 2 * 1024 ** 2; |
81: | |
82: | protected const REQUEST_METHOD_HAS_BODY = [ |
83: | Curler::METHOD_POST => true, |
84: | Curler::METHOD_PUT => true, |
85: | Curler::METHOD_PATCH => true, |
86: | Curler::METHOD_DELETE => true, |
87: | ]; |
88: | |
89: | protected Uri $Uri; |
90: | protected ?CredentialInterface $Credential = null; |
91: | protected string $CredentialHeaderName; |
92: | |
93: | protected array $SensitiveHeaders; |
94: | protected ?string $MediaType = null; |
95: | protected ?string $UserAgent = null; |
96: | protected bool $ExpectJson = true; |
97: | protected bool $PostJson = true; |
98: | protected ?DateFormatterInterface $DateFormatter = null; |
99: | |
100: | protected int $FormDataFlags = Curler::DATA_PRESERVE_NUMERIC_KEYS | Curler::DATA_PRESERVE_STRING_KEYS; |
101: | |
102: | protected int $JsonDecodeFlags = \JSON_OBJECT_AS_ARRAY; |
103: | |
104: | protected array $Middleware = []; |
105: | protected ?CurlerPagerInterface $Pager = null; |
106: | protected bool $AlwaysPaginate = false; |
107: | protected ?CacheInterface $Cache = null; |
108: | protected ?string $CookiesCacheKey = null; |
109: | protected bool $CacheResponses = false; |
110: | protected bool $CachePostResponses = false; |
111: | |
112: | protected $CacheKeyCallback = null; |
113: | |
114: | protected int $CacheLifetime = 3600; |
115: | protected bool $RefreshCache = false; |
116: | |
117: | protected ?int $Timeout = null; |
118: | protected bool $FollowRedirects = false; |
119: | |
120: | protected ?int $MaxRedirects = null; |
121: | protected bool $RetryAfterTooManyRequests = false; |
122: | |
123: | protected int $RetryAfterMaxSeconds = 300; |
124: | protected bool $ThrowHttpErrors = true; |
125: | |
126: | |
127: | |
128: | protected ?PsrRequestInterface $LastRequest = null; |
129: | protected ?ResponseInterface $LastResponse = null; |
130: | private ?Curler $WithoutThrowHttpErrors = null; |
131: | private ?Closure $Closure = null; |
132: | |
133: | |
134: | |
135: | private static string $DefaultUserAgent; |
136: | |
137: | private static array $UnstableHeaders; |
138: | |
139: | private static $Handle; |
140: | |
141: | |
142: | |
143: | |
144: | |
145: | |
146: | |
147: | |
148: | final public function __construct( |
149: | $uri = null, |
150: | $headers = null, |
151: | array $sensitiveHeaders = Curler::HEADERS_SENSITIVE |
152: | ) { |
153: | $this->Uri = $this->filterUri($uri); |
154: | $this->Headers = $this->filterHeaders($headers); |
155: | $this->SensitiveHeaders = array_change_key_case( |
156: | array_fill_keys($sensitiveHeaders, true), |
157: | ); |
158: | } |
159: | |
160: | |
161: | |
162: | |
163: | |
164: | |
165: | |
166: | |
167: | |
168: | |
169: | |
170: | |
171: | |
172: | |
173: | |
174: | |
175: | |
176: | |
177: | |
178: | |
179: | |
180: | |
181: | |
182: | |
183: | |
184: | |
185: | |
186: | |
187: | |
188: | |
189: | |
190: | |
191: | |
192: | |
193: | |
194: | public static function create( |
195: | $uri = null, |
196: | $headers = null, |
197: | ?CredentialInterface $credential = null, |
198: | string $credentialHeaderName = Curler::HEADER_AUTHORIZATION, |
199: | array $sensitiveHeaders = Curler::HEADERS_SENSITIVE, |
200: | ?string $mediaType = null, |
201: | ?string $userAgent = null, |
202: | bool $expectJson = true, |
203: | bool $postJson = true, |
204: | ?DateFormatterInterface $dateFormatter = null, |
205: | int $formDataFlags = Curler::DATA_PRESERVE_NUMERIC_KEYS | Curler::DATA_PRESERVE_STRING_KEYS, |
206: | int $jsonDecodeFlags = \JSON_OBJECT_AS_ARRAY, |
207: | array $middleware = [], |
208: | ?CurlerPagerInterface $pager = null, |
209: | bool $alwaysPaginate = false, |
210: | ?CacheInterface $cache = null, |
211: | bool $handleCookies = false, |
212: | ?string $cookiesCacheKey = null, |
213: | bool $cacheResponses = false, |
214: | bool $cachePostResponses = false, |
215: | ?callable $cacheKeyCallback = null, |
216: | int $cacheLifetime = 3600, |
217: | bool $refreshCache = false, |
218: | ?int $timeout = null, |
219: | bool $followRedirects = false, |
220: | ?int $maxRedirects = null, |
221: | bool $retryAfterTooManyRequests = false, |
222: | int $retryAfterMaxSeconds = 300, |
223: | bool $throwHttpErrors = true |
224: | ): self { |
225: | $curler = new static($uri, $headers, $sensitiveHeaders); |
226: | $curler->Credential = $credential; |
227: | if ($credential !== null) { |
228: | $curler->CredentialHeaderName = $credentialHeaderName; |
229: | } |
230: | $curler->MediaType = $mediaType; |
231: | $curler->UserAgent = $userAgent; |
232: | $curler->ExpectJson = $expectJson; |
233: | $curler->PostJson = $postJson; |
234: | $curler->DateFormatter = $dateFormatter; |
235: | $curler->FormDataFlags = $formDataFlags; |
236: | $curler->JsonDecodeFlags = $jsonDecodeFlags; |
237: | foreach ($middleware as $value) { |
238: | $curler->Middleware[] = [$value[0], $value[1] ?? null]; |
239: | } |
240: | $curler->Pager = $pager; |
241: | $curler->AlwaysPaginate = $pager && $alwaysPaginate; |
242: | $curler->Cache = $cache; |
243: | $curler->CookiesCacheKey = $handleCookies || $cookiesCacheKey !== null |
244: | ? self::filterCookiesCacheKey($cookiesCacheKey) |
245: | : null; |
246: | $curler->CacheResponses = $cacheResponses; |
247: | $curler->CachePostResponses = $cachePostResponses; |
248: | $curler->CacheKeyCallback = $cacheKeyCallback; |
249: | $curler->CacheLifetime = $cacheLifetime; |
250: | $curler->RefreshCache = $refreshCache; |
251: | $curler->Timeout = $timeout; |
252: | $curler->FollowRedirects = $followRedirects; |
253: | $curler->MaxRedirects = $maxRedirects; |
254: | $curler->RetryAfterTooManyRequests = $retryAfterTooManyRequests; |
255: | $curler->RetryAfterMaxSeconds = $retryAfterMaxSeconds; |
256: | $curler->ThrowHttpErrors = $throwHttpErrors; |
257: | return $curler; |
258: | } |
259: | |
260: | private function __clone() |
261: | { |
262: | $this->LastRequest = null; |
263: | $this->LastResponse = null; |
264: | $this->WithoutThrowHttpErrors = null; |
265: | $this->Closure = null; |
266: | } |
267: | |
268: | |
269: | |
270: | |
271: | public function getUri(): UriInterface |
272: | { |
273: | return $this->Uri; |
274: | } |
275: | |
276: | |
277: | |
278: | |
279: | public function getLastRequest(): ?PsrRequestInterface |
280: | { |
281: | return $this->LastRequest; |
282: | } |
283: | |
284: | |
285: | |
286: | |
287: | public function getLastResponse(): ?ResponseInterface |
288: | { |
289: | return $this->LastResponse; |
290: | } |
291: | |
292: | |
293: | |
294: | |
295: | public function lastResponseIsJson(): bool |
296: | { |
297: | if ($this->LastResponse === null) { |
298: | throw new OutOfRangeException('No response to check'); |
299: | } |
300: | return $this->responseIsJson($this->LastResponse); |
301: | } |
302: | |
303: | private function responseIsJson(ResponseInterface $response): bool |
304: | { |
305: | $headers = $response->getInnerHeaders(); |
306: | if (!$headers->hasHeader(self::HEADER_CONTENT_TYPE)) { |
307: | return $this->ExpectJson; |
308: | } |
309: | $contentType = $headers->getLastHeaderValue(self::HEADER_CONTENT_TYPE); |
310: | return HttpUtil::mediaTypeIs($contentType, self::TYPE_JSON); |
311: | } |
312: | |
313: | |
314: | |
315: | |
316: | |
317: | |
318: | public function head(?array $query = null): HeadersInterface |
319: | { |
320: | return $this->process(self::METHOD_HEAD, $query); |
321: | } |
322: | |
323: | |
324: | |
325: | |
326: | public function get(?array $query = null) |
327: | { |
328: | return $this->process(self::METHOD_GET, $query); |
329: | } |
330: | |
331: | |
332: | |
333: | |
334: | public function post($data = null, ?array $query = null) |
335: | { |
336: | return $this->process(self::METHOD_POST, $query, $data); |
337: | } |
338: | |
339: | |
340: | |
341: | |
342: | public function put($data = null, ?array $query = null) |
343: | { |
344: | return $this->process(self::METHOD_PUT, $query, $data); |
345: | } |
346: | |
347: | |
348: | |
349: | |
350: | public function patch($data = null, ?array $query = null) |
351: | { |
352: | return $this->process(self::METHOD_PATCH, $query, $data); |
353: | } |
354: | |
355: | |
356: | |
357: | |
358: | public function delete($data = null, ?array $query = null) |
359: | { |
360: | return $this->process(self::METHOD_DELETE, $query, $data); |
361: | } |
362: | |
363: | |
364: | |
365: | |
366: | |
367: | |
368: | private function process(string $method, ?array $query, $data = false) |
369: | { |
370: | $request = $this->createRequest($method, $query, $data); |
371: | $pager = $this->AlwaysPaginate ? $this->Pager : null; |
372: | if ($pager) { |
373: | $request = $pager->getFirstRequest($request, $this, $query); |
374: | if ($request instanceof CurlerPageRequestInterface) { |
375: | $query = $request->getQuery() ?? $query; |
376: | $request = $request->getRequest(); |
377: | } |
378: | } |
379: | $response = $this->doSendRequest($request); |
380: | if ($method === self::METHOD_HEAD) { |
381: | return $response->getInnerHeaders(); |
382: | } |
383: | $result = $this->getResponseBody($response); |
384: | if ($pager) { |
385: | $page = $pager->getPage($result, $request, $response, $this, null, null, $query); |
386: | return Arr::unwrap($page->getEntities(), 1); |
387: | } |
388: | return $result; |
389: | } |
390: | |
391: | |
392: | |
393: | |
394: | |
395: | |
396: | public function getP(?array $query = null): iterable |
397: | { |
398: | return $this->paginate(self::METHOD_GET, $query); |
399: | } |
400: | |
401: | |
402: | |
403: | |
404: | public function postP($data = null, ?array $query = null): iterable |
405: | { |
406: | return $this->paginate(self::METHOD_POST, $query, $data); |
407: | } |
408: | |
409: | |
410: | |
411: | |
412: | public function putP($data = null, ?array $query = null): iterable |
413: | { |
414: | return $this->paginate(self::METHOD_PUT, $query, $data); |
415: | } |
416: | |
417: | |
418: | |
419: | |
420: | public function patchP($data = null, ?array $query = null): iterable |
421: | { |
422: | return $this->paginate(self::METHOD_PATCH, $query, $data); |
423: | } |
424: | |
425: | |
426: | |
427: | |
428: | public function deleteP($data = null, ?array $query = null): iterable |
429: | { |
430: | return $this->paginate(self::METHOD_DELETE, $query, $data); |
431: | } |
432: | |
433: | |
434: | |
435: | |
436: | |
437: | |
438: | private function paginate(string $method, ?array $query, $data = false): iterable |
439: | { |
440: | if ($this->Pager === null) { |
441: | throw new LogicException('No pager'); |
442: | } |
443: | $pager = $this->Pager; |
444: | $request = $this->createRequest($method, $query, $data); |
445: | $request = $pager->getFirstRequest($request, $this, $query); |
446: | $prev = null; |
447: | $yielded = 0; |
448: | do { |
449: | if ($request instanceof CurlerPageRequestInterface) { |
450: | $query = $request->getQuery() ?? $query; |
451: | $request = $request->getRequest(); |
452: | } |
453: | $response = $this->doSendRequest($request); |
454: | $result = $this->getResponseBody($response); |
455: | $page = $pager->getPage($result, $request, $response, $this, $prev, $prev ? $yielded : null, $query); |
456: | |
457: | foreach ($page->getEntities() as $entity) { |
458: | $yielded++; |
459: | yield $entity; |
460: | } |
461: | if (!$page->hasNextRequest()) { |
462: | return; |
463: | } |
464: | $request = $page->getNextRequest(); |
465: | $prev = $page; |
466: | } while (true); |
467: | } |
468: | |
469: | |
470: | |
471: | |
472: | |
473: | |
474: | public function postR(string $data, string $mediaType, ?array $query = null) |
475: | { |
476: | return $this->processRaw(self::METHOD_POST, $data, $mediaType, $query); |
477: | } |
478: | |
479: | |
480: | |
481: | |
482: | public function putR(string $data, string $mediaType, ?array $query = null) |
483: | { |
484: | return $this->processRaw(self::METHOD_PUT, $data, $mediaType, $query); |
485: | } |
486: | |
487: | |
488: | |
489: | |
490: | public function patchR(string $data, string $mediaType, ?array $query = null) |
491: | { |
492: | return $this->processRaw(self::METHOD_PATCH, $data, $mediaType, $query); |
493: | } |
494: | |
495: | |
496: | |
497: | |
498: | public function deleteR(string $data, string $mediaType, ?array $query = null) |
499: | { |
500: | return $this->processRaw(self::METHOD_DELETE, $data, $mediaType, $query); |
501: | } |
502: | |
503: | |
504: | |
505: | |
506: | |
507: | private function processRaw(string $method, string $data, string $mediaType, ?array $query) |
508: | { |
509: | $request = $this->createRequest($method, $query, $data); |
510: | $request = $request->withHeader(self::HEADER_CONTENT_TYPE, $mediaType); |
511: | |
512: | $response = $this->doSendRequest($request); |
513: | return $this->getResponseBody($response); |
514: | } |
515: | |
516: | |
517: | |
518: | |
519: | |
520: | |
521: | public function flushCookies() |
522: | { |
523: | if ($this->CookiesCacheKey !== null) { |
524: | $this->getCacheInstance()->delete($this->CookiesCacheKey); |
525: | } |
526: | return $this; |
527: | } |
528: | |
529: | |
530: | |
531: | |
532: | public function replaceQuery($value, array $query) |
533: | { |
534: | return HttpUtil::replaceQuery( |
535: | $value, |
536: | $query, |
537: | $this->FormDataFlags, |
538: | $this->DateFormatter, |
539: | ); |
540: | } |
541: | |
542: | |
543: | |
544: | |
545: | |
546: | |
547: | public function getInnerHeaders(): HeadersInterface |
548: | { |
549: | $headers = $this->Headers; |
550: | if ($this->Credential !== null) { |
551: | $headers = $headers->authorize($this->Credential, $this->CredentialHeaderName); |
552: | } |
553: | if ($this->MediaType !== null) { |
554: | $headers = $headers->set(self::HEADER_CONTENT_TYPE, $this->MediaType); |
555: | } |
556: | if ($this->ExpectJson) { |
557: | $headers = $headers->set(self::HEADER_ACCEPT, self::TYPE_JSON); |
558: | } |
559: | if ($this->UserAgent !== '' && ( |
560: | $this->UserAgent !== null |
561: | || !$headers->hasHeader(self::HEADER_USER_AGENT) |
562: | )) { |
563: | $headers = $headers->set( |
564: | self::HEADER_USER_AGENT, |
565: | $this->UserAgent ?? $this->getDefaultUserAgent(), |
566: | ); |
567: | } |
568: | return $headers; |
569: | } |
570: | |
571: | |
572: | |
573: | |
574: | public function getPublicHeaders(): HeadersInterface |
575: | { |
576: | $sensitive = $this->SensitiveHeaders; |
577: | if ($this->Credential !== null) { |
578: | $sensitive[Str::lower($this->CredentialHeaderName)] = true; |
579: | } |
580: | return $this->getInnerHeaders()->exceptIn($sensitive); |
581: | } |
582: | |
583: | |
584: | |
585: | |
586: | public function hasCredential(): bool |
587: | { |
588: | return $this->Credential !== null; |
589: | } |
590: | |
591: | |
592: | |
593: | |
594: | public function isSensitiveHeader(string $name): bool |
595: | { |
596: | return isset($this->SensitiveHeaders[Str::lower($name)]); |
597: | } |
598: | |
599: | |
600: | |
601: | |
602: | public function getMediaType(): ?string |
603: | { |
604: | return $this->MediaType; |
605: | } |
606: | |
607: | |
608: | |
609: | |
610: | public function hasUserAgent(): bool |
611: | { |
612: | return $this->UserAgent !== null; |
613: | } |
614: | |
615: | |
616: | |
617: | |
618: | public function getUserAgent(): string |
619: | { |
620: | return $this->UserAgent ?? $this->getDefaultUserAgent(); |
621: | } |
622: | |
623: | |
624: | |
625: | |
626: | public function expectsJson(): bool |
627: | { |
628: | return $this->ExpectJson; |
629: | } |
630: | |
631: | |
632: | |
633: | |
634: | public function postsJson(): bool |
635: | { |
636: | return $this->PostJson; |
637: | } |
638: | |
639: | |
640: | |
641: | |
642: | public function getDateFormatter(): ?DateFormatterInterface |
643: | { |
644: | return $this->DateFormatter; |
645: | } |
646: | |
647: | |
648: | |
649: | |
650: | public function getFormDataFlags(): int |
651: | { |
652: | return $this->FormDataFlags; |
653: | } |
654: | |
655: | |
656: | |
657: | |
658: | public function getPager(): ?CurlerPagerInterface |
659: | { |
660: | return $this->Pager; |
661: | } |
662: | |
663: | |
664: | |
665: | |
666: | public function alwaysPaginates(): bool |
667: | { |
668: | return $this->AlwaysPaginate; |
669: | } |
670: | |
671: | |
672: | |
673: | |
674: | public function getCache(): ?CacheInterface |
675: | { |
676: | return $this->Cache; |
677: | } |
678: | |
679: | |
680: | |
681: | |
682: | public function hasCookies(): bool |
683: | { |
684: | return $this->CookiesCacheKey !== null; |
685: | } |
686: | |
687: | |
688: | |
689: | |
690: | public function hasResponseCache(): bool |
691: | { |
692: | return $this->CacheResponses; |
693: | } |
694: | |
695: | |
696: | |
697: | |
698: | public function hasPostResponseCache(): bool |
699: | { |
700: | return $this->CachePostResponses; |
701: | } |
702: | |
703: | |
704: | |
705: | |
706: | public function getCacheLifetime(): int |
707: | { |
708: | return $this->CacheLifetime; |
709: | } |
710: | |
711: | |
712: | |
713: | |
714: | public function refreshesCache(): bool |
715: | { |
716: | return $this->RefreshCache; |
717: | } |
718: | |
719: | |
720: | |
721: | |
722: | public function getTimeout(): ?int |
723: | { |
724: | return $this->Timeout; |
725: | } |
726: | |
727: | |
728: | |
729: | |
730: | public function followsRedirects(): bool |
731: | { |
732: | return $this->FollowRedirects; |
733: | } |
734: | |
735: | |
736: | |
737: | |
738: | public function getMaxRedirects(): ?int |
739: | { |
740: | return $this->MaxRedirects; |
741: | } |
742: | |
743: | |
744: | |
745: | |
746: | public function getRetryAfterTooManyRequests(): bool |
747: | { |
748: | return $this->RetryAfterTooManyRequests; |
749: | } |
750: | |
751: | |
752: | |
753: | |
754: | public function getRetryAfterMaxSeconds(): int |
755: | { |
756: | return $this->RetryAfterMaxSeconds; |
757: | } |
758: | |
759: | |
760: | |
761: | |
762: | public function throwsHttpErrors(): bool |
763: | { |
764: | return $this->ThrowHttpErrors; |
765: | } |
766: | |
767: | |
768: | |
769: | |
770: | |
771: | |
772: | public function withUri($uri) |
773: | { |
774: | return (string) $uri === (string) $this->Uri |
775: | ? $this |
776: | : $this->with('Uri', $this->filterUri($uri)); |
777: | } |
778: | |
779: | |
780: | |
781: | |
782: | public function withRequest(PsrRequestInterface $request) |
783: | { |
784: | $curler = $this->withUri($request->getUri()); |
785: | |
786: | $headers = Headers::from($request); |
787: | $_headers = $this->getInnerHeaders(); |
788: | if (Arr::same($headers->all(), $_headers->all())) { |
789: | return $curler; |
790: | } |
791: | |
792: | if ($this->Credential !== null) { |
793: | $header = $headers->getHeader($this->CredentialHeaderName); |
794: | $_header = $_headers->getHeader($this->CredentialHeaderName); |
795: | if ($header !== $_header) { |
796: | $curler = $curler->withCredential(null); |
797: | } |
798: | } |
799: | |
800: | $mediaType = Arr::last($headers->getHeaderValues(self::HEADER_CONTENT_TYPE)); |
801: | $userAgent = Arr::last($headers->getHeaderValues(self::HEADER_USER_AGENT)); |
802: | $expectJson = Arr::lower($headers->getHeaderValues(self::HEADER_ACCEPT)) === [self::TYPE_JSON]; |
803: | if ($userAgent !== null && $userAgent === $this->getDefaultUserAgent()) { |
804: | $userAgent = null; |
805: | } |
806: | return $curler |
807: | ->withMediaType($mediaType) |
808: | ->withUserAgent($userAgent) |
809: | ->withExpectJson($expectJson) |
810: | ->with('Headers', $headers); |
811: | } |
812: | |
813: | |
814: | |
815: | |
816: | public function withCredential( |
817: | ?CredentialInterface $credential, |
818: | string $headerName = Curler::HEADER_AUTHORIZATION |
819: | ) { |
820: | return $credential === null |
821: | ? $this |
822: | ->with('Credential', null) |
823: | ->without('CredentialHeaderName') |
824: | : $this |
825: | ->with('Credential', $credential) |
826: | ->with('CredentialHeaderName', $headerName); |
827: | } |
828: | |
829: | |
830: | |
831: | |
832: | public function withSensitiveHeader(string $name) |
833: | { |
834: | return $this->with( |
835: | 'SensitiveHeaders', |
836: | Arr::set($this->SensitiveHeaders, Str::lower($name), true) |
837: | ); |
838: | } |
839: | |
840: | |
841: | |
842: | |
843: | public function withoutSensitiveHeader(string $name) |
844: | { |
845: | return $this->with( |
846: | 'SensitiveHeaders', |
847: | Arr::unset($this->SensitiveHeaders, Str::lower($name)) |
848: | ); |
849: | } |
850: | |
851: | |
852: | |
853: | |
854: | public function withMediaType(?string $type) |
855: | { |
856: | return $this->with('MediaType', $type); |
857: | } |
858: | |
859: | |
860: | |
861: | |
862: | public function withUserAgent(?string $userAgent) |
863: | { |
864: | return $this->with('UserAgent', $userAgent); |
865: | } |
866: | |
867: | |
868: | |
869: | |
870: | public function withExpectJson(bool $expectJson = true) |
871: | { |
872: | return $this->with('ExpectJson', $expectJson); |
873: | } |
874: | |
875: | |
876: | |
877: | |
878: | public function withPostJson(bool $postJson = true) |
879: | { |
880: | return $this->with('PostJson', $postJson); |
881: | } |
882: | |
883: | |
884: | |
885: | |
886: | public function withDateFormatter(?DateFormatterInterface $formatter) |
887: | { |
888: | return $this->with('DateFormatter', $formatter); |
889: | } |
890: | |
891: | |
892: | |
893: | |
894: | public function withFormDataFlags(int $flags) |
895: | { |
896: | return $this->with('FormDataFlags', $flags); |
897: | } |
898: | |
899: | |
900: | |
901: | |
902: | public function withJsonDecodeFlags(int $flags) |
903: | { |
904: | return $this->with('JsonDecodeFlags', $flags); |
905: | } |
906: | |
907: | |
908: | |
909: | |
910: | public function withMiddleware($middleware, ?string $name = null) |
911: | { |
912: | return $this->with( |
913: | 'Middleware', |
914: | Arr::push($this->Middleware, [$middleware, $name]) |
915: | ); |
916: | } |
917: | |
918: | |
919: | |
920: | |
921: | public function withoutMiddleware($middleware) |
922: | { |
923: | $index = is_string($middleware) ? 1 : 0; |
924: | $values = $this->Middleware; |
925: | foreach ($values as $key => $value) { |
926: | if ($middleware === $value[$index]) { |
927: | unset($values[$key]); |
928: | } |
929: | } |
930: | return $this->with('Middleware', $values); |
931: | } |
932: | |
933: | |
934: | |
935: | |
936: | public function withPager(?CurlerPagerInterface $pager, bool $alwaysPaginate = false) |
937: | { |
938: | return $this |
939: | ->with('Pager', $pager) |
940: | ->with('AlwaysPaginate', $pager && $alwaysPaginate); |
941: | } |
942: | |
943: | |
944: | |
945: | |
946: | public function withCache(?CacheInterface $cache = null) |
947: | { |
948: | return $this->with('Cache', $cache); |
949: | } |
950: | |
951: | |
952: | |
953: | |
954: | public function withCookies(?string $cacheKey = null) |
955: | { |
956: | return $this->with('CookiesCacheKey', $this->filterCookiesCacheKey($cacheKey)); |
957: | } |
958: | |
959: | |
960: | |
961: | |
962: | public function withoutCookies() |
963: | { |
964: | return $this->with('CookiesCacheKey', null); |
965: | } |
966: | |
967: | |
968: | |
969: | |
970: | public function withResponseCache(bool $cacheResponses = true) |
971: | { |
972: | return $this->with('CacheResponses', $cacheResponses); |
973: | } |
974: | |
975: | |
976: | |
977: | |
978: | public function withPostResponseCache(bool $cachePostResponses = true) |
979: | { |
980: | return $this->with('CachePostResponses', $cachePostResponses); |
981: | } |
982: | |
983: | |
984: | |
985: | |
986: | public function withCacheKeyCallback(?callable $callback) |
987: | { |
988: | return $this->with('CacheKeyCallback', $callback); |
989: | } |
990: | |
991: | |
992: | |
993: | |
994: | public function withCacheLifetime(int $seconds) |
995: | { |
996: | return $this->with('CacheLifetime', $seconds); |
997: | } |
998: | |
999: | |
1000: | |
1001: | |
1002: | public function withRefreshCache(bool $refresh = true) |
1003: | { |
1004: | return $this->with('RefreshCache', $refresh); |
1005: | } |
1006: | |
1007: | |
1008: | |
1009: | |
1010: | public function withTimeout(?int $seconds) |
1011: | { |
1012: | return $this->with('Timeout', $seconds); |
1013: | } |
1014: | |
1015: | |
1016: | |
1017: | |
1018: | public function withFollowRedirects(bool $follow = true) |
1019: | { |
1020: | return $this->with('FollowRedirects', $follow); |
1021: | } |
1022: | |
1023: | |
1024: | |
1025: | |
1026: | public function withMaxRedirects(?int $redirects) |
1027: | { |
1028: | return $this->with('MaxRedirects', $redirects); |
1029: | } |
1030: | |
1031: | |
1032: | |
1033: | |
1034: | public function withRetryAfterTooManyRequests(bool $retry = true) |
1035: | { |
1036: | return $this->with('RetryAfterTooManyRequests', $retry); |
1037: | } |
1038: | |
1039: | |
1040: | |
1041: | |
1042: | public function withRetryAfterMaxSeconds(int $seconds) |
1043: | { |
1044: | return $this->with('RetryAfterMaxSeconds', $seconds); |
1045: | } |
1046: | |
1047: | |
1048: | |
1049: | |
1050: | public function withThrowHttpErrors(bool $throw = true) |
1051: | { |
1052: | return $this->with('ThrowHttpErrors', $throw); |
1053: | } |
1054: | |
1055: | |
1056: | |
1057: | |
1058: | |
1059: | |
1060: | public function sendRequest(PsrRequestInterface $request): ResponseInterface |
1061: | { |
1062: | |
1063: | |
1064: | |
1065: | |
1066: | $curler = $this->WithoutThrowHttpErrors ??= $this->withThrowHttpErrors(false); |
1067: | try { |
1068: | try { |
1069: | return $curler->doSendRequest($request); |
1070: | } finally { |
1071: | $this->LastRequest = $curler->LastRequest; |
1072: | $this->LastResponse = $curler->LastResponse; |
1073: | } |
1074: | } catch (PsrClientExceptionInterface $ex) { |
1075: | throw $ex; |
1076: | } catch (CurlErrorExceptionInterface $ex) { |
1077: | throw $ex->isNetworkError() |
1078: | ? new NetworkException($ex->getMessage(), $this->LastRequest ?? $request, [], $ex) |
1079: | : new RequestException($ex->getMessage(), $this->LastRequest ?? $request, [], $ex); |
1080: | } catch (Throwable $ex) { |
1081: | throw new RequestException($ex->getMessage(), $this->LastRequest ?? $request, [], $ex); |
1082: | } |
1083: | } |
1084: | |
1085: | |
1086: | |
1087: | |
1088: | |
1089: | private function doSendRequest(PsrRequestInterface $request): ResponseInterface |
1090: | { |
1091: | $this->LastRequest = null; |
1092: | $this->LastResponse = null; |
1093: | return $this->Middleware |
1094: | ? ($this->Closure ??= $this->getClosure())($request) |
1095: | : $this->getResponse($request); |
1096: | } |
1097: | |
1098: | |
1099: | |
1100: | |
1101: | private function getClosure(): Closure |
1102: | { |
1103: | $closure = fn(PsrRequestInterface $request): ResponseInterface => |
1104: | $this->getResponse($request); |
1105: | foreach (array_reverse($this->Middleware) as [$middleware]) { |
1106: | $closure = $middleware instanceof CurlerMiddlewareInterface |
1107: | ? fn(PsrRequestInterface $request): ResponseInterface => |
1108: | $this->handleHttpResponse($middleware($request, $closure, $this), $request) |
1109: | : fn(PsrRequestInterface $request): ResponseInterface => |
1110: | $this->handleResponse($middleware($request, $closure, $this), $request); |
1111: | } |
1112: | return $closure; |
1113: | } |
1114: | |
1115: | private function getResponse(PsrRequestInterface $request): ResponseInterface |
1116: | { |
1117: | $uri = $request->getUri()->withFragment(''); |
1118: | $request = $request->withUri($uri); |
1119: | |
1120: | $version = (int) ((float) $request->getProtocolVersion() * 10); |
1121: | $opt[\CURLOPT_HTTP_VERSION] = [ |
1122: | 10 => \CURL_HTTP_VERSION_1_0, |
1123: | 11 => \CURL_HTTP_VERSION_1_1, |
1124: | 20 => \CURL_HTTP_VERSION_2_0, |
1125: | ][$version] ?? \CURL_HTTP_VERSION_NONE; |
1126: | |
1127: | $method = $request->getMethod(); |
1128: | $body = $request->getBody(); |
1129: | $opt[\CURLOPT_CUSTOMREQUEST] = $method; |
1130: | if ($method === self::METHOD_HEAD) { |
1131: | $opt[\CURLOPT_NOBODY] = true; |
1132: | $size = 0; |
1133: | } else { |
1134: | $size = $body->getSize(); |
1135: | } |
1136: | |
1137: | if ($size === null || $size > 0) { |
1138: | $size ??= HttpUtil::getContentLength($request); |
1139: | if ($size !== null && $size <= static::MAX_INPUT_LENGTH) { |
1140: | $body = (string) $body; |
1141: | $opt[\CURLOPT_POSTFIELDS] = $body; |
1142: | $request = $request |
1143: | ->withoutHeader(self::HEADER_CONTENT_LENGTH) |
1144: | ->withoutHeader(self::HEADER_TRANSFER_ENCODING); |
1145: | } else { |
1146: | $opt[\CURLOPT_UPLOAD] = true; |
1147: | if ($size !== null) { |
1148: | $opt[\CURLOPT_INFILESIZE] = $size; |
1149: | $request = $request |
1150: | ->withoutHeader(self::HEADER_CONTENT_LENGTH); |
1151: | } |
1152: | $opt[\CURLOPT_READFUNCTION] = |
1153: | static function ($handle, $infile, int $length) use ($body): string { |
1154: | return $body->read($length); |
1155: | }; |
1156: | if ($body->isSeekable()) { |
1157: | $body->rewind(); |
1158: | } |
1159: | } |
1160: | } elseif (self::REQUEST_METHOD_HAS_BODY[$method] ?? false) { |
1161: | |
1162: | |
1163: | |
1164: | |
1165: | |
1166: | |
1167: | $request = $request->withHeader(self::HEADER_CONTENT_LENGTH, '0'); |
1168: | } |
1169: | |
1170: | if ($this->Timeout !== null) { |
1171: | $opt[\CURLOPT_CONNECTTIMEOUT] = $this->Timeout; |
1172: | } |
1173: | |
1174: | if (!$request->hasHeader(self::HEADER_ACCEPT_ENCODING)) { |
1175: | |
1176: | |
1177: | $opt[\CURLOPT_ENCODING] = ''; |
1178: | } |
1179: | |
1180: | |
1181: | $statusLine = null; |
1182: | |
1183: | $headersIn = null; |
1184: | $opt[\CURLOPT_HEADERFUNCTION] = |
1185: | static function ($handle, string $header) use (&$statusLine, &$headersIn): int { |
1186: | if (substr($header, 0, 5) === 'HTTP/') { |
1187: | $statusLine = rtrim($header, "\r\n"); |
1188: | $headersIn = new Headers(); |
1189: | return strlen($header); |
1190: | } |
1191: | if ($headersIn === null) { |
1192: | throw new InvalidHeaderException('No status line in HTTP response'); |
1193: | } |
1194: | $headersIn = $headersIn->addLine($header); |
1195: | return strlen($header); |
1196: | }; |
1197: | |
1198: | |
1199: | $bodyIn = null; |
1200: | $opt[\CURLOPT_WRITEFUNCTION] = |
1201: | static function ($handle, string $data) use (&$bodyIn): int { |
1202: | |
1203: | return $bodyIn->write($data); |
1204: | }; |
1205: | |
1206: | if (self::$Handle === null) { |
1207: | $handle = curl_init((string) $uri); |
1208: | if ($handle === false) { |
1209: | throw new RuntimeException('curl_init() failed'); |
1210: | } |
1211: | self::$Handle = $handle; |
1212: | $resetHandle = false; |
1213: | } else { |
1214: | $handle = self::$Handle; |
1215: | $opt[\CURLOPT_URL] = (string) $uri; |
1216: | $resetHandle = true; |
1217: | } |
1218: | |
1219: | $request = $this->normaliseRequest($request, $uri); |
1220: | $headers = Headers::from($request); |
1221: | |
1222: | $cacheKey = null; |
1223: | $transfer = 0; |
1224: | $redirects = $this->FollowRedirects && $this->MaxRedirects !== 0 |
1225: | ? $this->MaxRedirects ?? 30 |
1226: | : false; |
1227: | $retrying = false; |
1228: | do { |
1229: | if ($cacheKey === null) { |
1230: | if ( |
1231: | $this->CacheResponses |
1232: | && ($size === 0 || is_string($body)) |
1233: | && ([ |
1234: | self::METHOD_GET => true, |
1235: | self::METHOD_HEAD => true, |
1236: | self::METHOD_POST => $this->CachePostResponses, |
1237: | ][$method] ?? false) |
1238: | ) { |
1239: | $cacheKey = $this->CacheKeyCallback !== null |
1240: | ? (array) ($this->CacheKeyCallback)($request, $this) |
1241: | : $headers->exceptIn($this->getUnstableHeaders())->getLines('%s:%s'); |
1242: | |
1243: | if ($size !== 0 || $method === self::METHOD_POST) { |
1244: | $cacheKey[] = $size === 0 ? '' : $body; |
1245: | } |
1246: | |
1247: | $cacheUri = $uri->getPath() === '' |
1248: | ? $uri->withPath('/') |
1249: | : $uri; |
1250: | $cacheKey = implode(':', [ |
1251: | self::class, |
1252: | 'response', |
1253: | $method, |
1254: | rawurlencode((string) $cacheUri), |
1255: | Get::hash(implode("\0", $cacheKey)), |
1256: | ]); |
1257: | } else { |
1258: | $cacheKey = false; |
1259: | } |
1260: | } |
1261: | |
1262: | if ( |
1263: | $cacheKey !== false |
1264: | && !$this->RefreshCache |
1265: | && ($last = $this->getCacheInstance()->getArray($cacheKey)) !== null |
1266: | ) { |
1267: | |
1268: | $code = $last['code'] ?? $last[0] ?? 200; |
1269: | $bodyIn = Stream::fromString($last['body'] ?? $last[3] ?? ''); |
1270: | $lastHeaders = $last['headers'] ?? $last[2] ?? null; |
1271: | if (is_array($lastHeaders)) { |
1272: | $lastHeaders = HttpUtil::getNameValuePairs($lastHeaders); |
1273: | } |
1274: | $headersIn = Headers::from($lastHeaders ?? []); |
1275: | $reason = $last['reason'] ?? $last[1] ?? null; |
1276: | $version = $last['version'] ?? '1.1'; |
1277: | $response = new Response($code, $bodyIn, $headersIn, $reason, $version); |
1278: | Event::dispatch(new ResponseCacheHitEvent($this, $request, $response)); |
1279: | } else { |
1280: | if ($transfer) { |
1281: | if ($size !== 0 && $body instanceof PsrStreamInterface) { |
1282: | if (!$body->isSeekable()) { |
1283: | throw new RequestException( |
1284: | 'Request cannot be sent again (body not seekable)', |
1285: | $request, |
1286: | ); |
1287: | } |
1288: | $body->rewind(); |
1289: | } |
1290: | $statusLine = null; |
1291: | $headersIn = null; |
1292: | } |
1293: | $bodyIn = new Stream(File::open('php://temp', 'r+')); |
1294: | |
1295: | if ($resetHandle || !$transfer) { |
1296: | if ($resetHandle) { |
1297: | curl_reset($handle); |
1298: | $resetHandle = false; |
1299: | } |
1300: | $opt[\CURLOPT_HTTPHEADER] = $headers->getLines('%s: %s', '%s;'); |
1301: | curl_setopt_array($handle, $opt); |
1302: | |
1303: | if ($this->CookiesCacheKey !== null) { |
1304: | |
1305: | |
1306: | curl_setopt($handle, \CURLOPT_COOKIEFILE, ''); |
1307: | |
1308: | $cookies = $this->getCacheInstance()->getArray($this->CookiesCacheKey); |
1309: | if ($cookies) { |
1310: | foreach ($cookies as $cookie) { |
1311: | curl_setopt($handle, \CURLOPT_COOKIELIST, $cookie); |
1312: | } |
1313: | } |
1314: | } |
1315: | } |
1316: | |
1317: | $transfer++; |
1318: | |
1319: | Event::dispatch(new CurlRequestEvent($this, $handle, $request)); |
1320: | $result = curl_exec($handle); |
1321: | if ($result === false) { |
1322: | throw new CurlErrorException(curl_errno($handle), $request, $this->getCurlInfo()); |
1323: | } |
1324: | |
1325: | if ( |
1326: | $statusLine === null |
1327: | || count($split = explode(' ', $statusLine, 3)) < 2 |
1328: | || ($version = explode('/', $split[0])[1] ?? null) === null |
1329: | ) { |
1330: | |
1331: | throw new InvalidHeaderException(sprintf( |
1332: | 'HTTP status line invalid or not in response: %s', |
1333: | rtrim((string) $statusLine, "\r\n"), |
1334: | )); |
1335: | |
1336: | } |
1337: | |
1338: | |
1339: | $code = (int) $split[1]; |
1340: | $reason = $split[2] ?? null; |
1341: | $response = new Response($code, $bodyIn, $headersIn, $reason, $version); |
1342: | Event::dispatch(new CurlResponseEvent($this, $handle, $request, $response)); |
1343: | |
1344: | if ($this->CookiesCacheKey !== null) { |
1345: | $this->getCacheInstance()->set( |
1346: | $this->CookiesCacheKey, |
1347: | curl_getinfo($handle, \CURLINFO_COOKIELIST) |
1348: | ); |
1349: | } |
1350: | |
1351: | if ($cacheKey !== false && $this->CacheLifetime >= 0 && $code < 400) { |
1352: | $ttl = $this->CacheLifetime === 0 |
1353: | ? null |
1354: | : $this->CacheLifetime; |
1355: | $this->getCacheInstance()->set($cacheKey, [ |
1356: | 'code' => $code, |
1357: | 'body' => (string) $bodyIn, |
1358: | 'headers' => $headersIn->jsonSerialize(), |
1359: | 'reason' => $reason, |
1360: | 'version' => $version, |
1361: | ], $ttl); |
1362: | } |
1363: | } |
1364: | |
1365: | if ( |
1366: | $redirects !== false |
1367: | && $code >= 300 |
1368: | && $code < 400 |
1369: | && ($location = $headersIn->getOnlyHeaderValue(self::HEADER_LOCATION)) !== '' |
1370: | ) { |
1371: | if (!$redirects) { |
1372: | throw new TooManyRedirectsException(sprintf( |
1373: | 'Redirect limit exceeded: %d', |
1374: | $this->MaxRedirects, |
1375: | ), $request, $response); |
1376: | } |
1377: | $uri = Uri::from($uri)->follow($location)->withFragment(''); |
1378: | $request = $request->withUri($uri); |
1379: | |
1380: | if (($code === 301 || $code === 302 || $code === 303) && ( |
1381: | $size !== 0 |
1382: | || (self::REQUEST_METHOD_HAS_BODY[$method] ?? false) |
1383: | )) { |
1384: | $method = self::METHOD_GET; |
1385: | $body = Stream::fromString(''); |
1386: | $request = $request |
1387: | ->withMethod($method) |
1388: | ->withBody($body) |
1389: | ->withoutHeader(self::HEADER_CONTENT_LENGTH) |
1390: | ->withoutHeader(self::HEADER_TRANSFER_ENCODING); |
1391: | $size = 0; |
1392: | $opt[\CURLOPT_CUSTOMREQUEST] = $method; |
1393: | $opt[\CURLOPT_URL] = (string) $uri; |
1394: | unset( |
1395: | $opt[\CURLOPT_POSTFIELDS], |
1396: | $opt[\CURLOPT_UPLOAD], |
1397: | $opt[\CURLOPT_INFILESIZE], |
1398: | $opt[\CURLOPT_READFUNCTION], |
1399: | ); |
1400: | $resetHandle = true; |
1401: | } else { |
1402: | curl_setopt( |
1403: | $handle, |
1404: | \CURLOPT_URL, |
1405: | |
1406: | $opt[\CURLOPT_URL] = (string) $uri, |
1407: | ); |
1408: | } |
1409: | $request = $this->normaliseRequest($request, $uri); |
1410: | $headers = Headers::from($request); |
1411: | $cacheKey = null; |
1412: | $redirects--; |
1413: | $retrying = false; |
1414: | continue; |
1415: | } |
1416: | |
1417: | if ( |
1418: | !$this->RetryAfterTooManyRequests |
1419: | || $retrying |
1420: | || $code !== 429 |
1421: | || ($after = HttpUtil::getRetryAfter($headersIn)) === null |
1422: | || ($this->RetryAfterMaxSeconds !== 0 && $after > $this->RetryAfterMaxSeconds) |
1423: | ) { |
1424: | break; |
1425: | } |
1426: | |
1427: | $after = max(1, $after); |
1428: | $this->debug(Inflect::format($after, 'Sleeping for {{#}} {{#:second}}')); |
1429: | sleep($after); |
1430: | $retrying = true; |
1431: | } while (true); |
1432: | |
1433: | $this->LastRequest = $request; |
1434: | $this->LastResponse = $response; |
1435: | |
1436: | if ($this->ThrowHttpErrors && $code >= 400) { |
1437: | throw new HttpErrorException($request, $response, $this->getCurlInfo()); |
1438: | } |
1439: | |
1440: | return $response; |
1441: | } |
1442: | |
1443: | private function normaliseRequest( |
1444: | PsrRequestInterface $request, |
1445: | PsrUriInterface $uri |
1446: | ): PsrRequestInterface { |
1447: | |
1448: | if (($host = $request->getHeaderLine(self::HEADER_HOST)) !== '') { |
1449: | try { |
1450: | $host = new Uri("//$host"); |
1451: | } catch (InvalidArgumentException $ex) { |
1452: | throw new InvalidHeaderException(sprintf( |
1453: | 'Invalid value for HTTP request header %s: %s', |
1454: | self::HEADER_HOST, |
1455: | $host, |
1456: | ), $ex); |
1457: | } |
1458: | $host = $host->withScheme($uri->getScheme())->getAuthority(); |
1459: | if ($host === $uri->withUserInfo('')->getAuthority()) { |
1460: | $request = $request->withoutHeader(self::HEADER_HOST); |
1461: | } |
1462: | } |
1463: | return $request; |
1464: | } |
1465: | |
1466: | |
1467: | |
1468: | |
1469: | |
1470: | |
1471: | |
1472: | private function createRequest(string $method, ?array $query, $data): Request |
1473: | { |
1474: | $uri = $this->Uri; |
1475: | if ($query) { |
1476: | $uri = $this->replaceQuery($uri, $query); |
1477: | } |
1478: | $headers = $this->getInnerHeaders(); |
1479: | $request = new Request($method, $uri, null, $headers); |
1480: | return $data !== false |
1481: | ? $this->applyData($request, $data) |
1482: | : $request; |
1483: | } |
1484: | |
1485: | |
1486: | |
1487: | |
1488: | private function applyData(Request $request, $data): Request |
1489: | { |
1490: | if ($data === null) { |
1491: | if ($this->PostJson) { |
1492: | $this->debug(sprintf( |
1493: | 'JSON message bodies cannot be empty; falling back to %s', |
1494: | self::TYPE_FORM, |
1495: | )); |
1496: | } |
1497: | return $request->withHeader(self::HEADER_CONTENT_TYPE, self::TYPE_FORM); |
1498: | } |
1499: | if (is_string($data)) { |
1500: | return $request->withBody(Stream::fromString($data)); |
1501: | } |
1502: | if ($this->PostJson) { |
1503: | try { |
1504: | $body = Stream::fromData($data, $this->FormDataFlags, $this->DateFormatter, true); |
1505: | $mediaType = self::TYPE_JSON; |
1506: | } catch (StreamEncapsulationException $ex) { |
1507: | $this->debug(sprintf( |
1508: | '%s; falling back to %s', |
1509: | $ex->getMessage(), |
1510: | self::TYPE_FORM_MULTIPART, |
1511: | )); |
1512: | } |
1513: | } |
1514: | $body ??= Stream::fromData($data, $this->FormDataFlags, $this->DateFormatter); |
1515: | $mediaType ??= $body instanceof MultipartStreamInterface |
1516: | ? HttpUtil::getMultipartMediaType($body) |
1517: | : self::TYPE_FORM; |
1518: | return $request |
1519: | ->withHeader(self::HEADER_CONTENT_TYPE, $mediaType) |
1520: | ->withBody($body); |
1521: | } |
1522: | |
1523: | |
1524: | |
1525: | |
1526: | private function getResponseBody(ResponseInterface $response) |
1527: | { |
1528: | $body = (string) $response->getBody(); |
1529: | return $this->responseIsJson($response) |
1530: | ? ($body === '' |
1531: | ? null |
1532: | : Json::parse($body, $this->JsonDecodeFlags)) |
1533: | : $body; |
1534: | } |
1535: | |
1536: | private function handleResponse( |
1537: | PsrResponseInterface $response, |
1538: | PsrRequestInterface $request |
1539: | ): ResponseInterface { |
1540: | return $this->handleHttpResponse( |
1541: | $response instanceof ResponseInterface |
1542: | ? $response |
1543: | : Response::fromPsr7($response), |
1544: | $request, |
1545: | ); |
1546: | } |
1547: | |
1548: | private function handleHttpResponse( |
1549: | ResponseInterface $response, |
1550: | PsrRequestInterface $request |
1551: | ): ResponseInterface { |
1552: | $this->LastRequest = $request; |
1553: | $this->LastResponse = $response; |
1554: | return $response; |
1555: | } |
1556: | |
1557: | |
1558: | |
1559: | |
1560: | private function filterUri($uri): Uri |
1561: | { |
1562: | if ($uri === null) { |
1563: | return new Uri(); |
1564: | } |
1565: | $uri = Uri::from($uri); |
1566: | $invalid = array_intersect(['query', 'fragment'], array_keys($uri->getComponents())); |
1567: | if ($invalid) { |
1568: | throw new InvalidArgumentException(Inflect::format( |
1569: | $invalid, |
1570: | 'URI cannot have %s {{#:component}}', |
1571: | implode(' or ', $invalid), |
1572: | )); |
1573: | } |
1574: | return $uri; |
1575: | } |
1576: | |
1577: | |
1578: | |
1579: | |
1580: | private function filterHeaders($headers): HeadersInterface |
1581: | { |
1582: | if ($headers instanceof HeadersInterface) { |
1583: | return $headers; |
1584: | } |
1585: | return new Headers($headers ?? []); |
1586: | } |
1587: | |
1588: | private static function filterCookiesCacheKey(?string $cacheKey): string |
1589: | { |
1590: | return Arr::implode(':', [self::class, 'cookies', $cacheKey], ''); |
1591: | } |
1592: | |
1593: | private function getCacheInstance(): CacheInterface |
1594: | { |
1595: | return $this->Cache ?? Cache::getInstance(); |
1596: | } |
1597: | |
1598: | private function getDefaultUserAgent(): string |
1599: | { |
1600: | return self::$DefaultUserAgent ??= HttpUtil::getProduct(); |
1601: | } |
1602: | |
1603: | |
1604: | |
1605: | |
1606: | private function getUnstableHeaders(): array |
1607: | { |
1608: | return self::$UnstableHeaders ??= array_fill_keys(self::HEADERS_UNSTABLE, true); |
1609: | } |
1610: | |
1611: | |
1612: | |
1613: | |
1614: | private function getCurlInfo(): array |
1615: | { |
1616: | if ( |
1617: | self::$Handle === null |
1618: | || ($info = curl_getinfo(self::$Handle)) === false |
1619: | ) { |
1620: | return []; |
1621: | } |
1622: | foreach ($info as &$value) { |
1623: | if (is_string($value)) { |
1624: | $value = trim($value); |
1625: | } |
1626: | } |
1627: | return $info; |
1628: | } |
1629: | |
1630: | private function debug(string $msg1): void |
1631: | { |
1632: | if (Console::isLoaded()) { |
1633: | Console::debug($msg1); |
1634: | } |
1635: | } |
1636: | } |
1637: | |