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, $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: | do { |
448: | if ($request instanceof CurlerPageRequestInterface) { |
449: | $query = $request->getQuery() ?? $query; |
450: | $request = $request->getRequest(); |
451: | } |
452: | $response = $this->doSendRequest($request); |
453: | $result = $this->getResponseBody($response); |
454: | $page = $pager->getPage($result, $request, $response, $this, $prev, $query); |
455: | |
456: | foreach ($page->getEntities() as $entity) { |
457: | yield $entity; |
458: | } |
459: | if (!$page->hasNextRequest()) { |
460: | return; |
461: | } |
462: | $request = $page->getNextRequest(); |
463: | $prev = $page; |
464: | } while (true); |
465: | } |
466: | |
467: | |
468: | |
469: | |
470: | |
471: | |
472: | public function postR(string $data, string $mediaType, ?array $query = null) |
473: | { |
474: | return $this->processRaw(self::METHOD_POST, $data, $mediaType, $query); |
475: | } |
476: | |
477: | |
478: | |
479: | |
480: | public function putR(string $data, string $mediaType, ?array $query = null) |
481: | { |
482: | return $this->processRaw(self::METHOD_PUT, $data, $mediaType, $query); |
483: | } |
484: | |
485: | |
486: | |
487: | |
488: | public function patchR(string $data, string $mediaType, ?array $query = null) |
489: | { |
490: | return $this->processRaw(self::METHOD_PATCH, $data, $mediaType, $query); |
491: | } |
492: | |
493: | |
494: | |
495: | |
496: | public function deleteR(string $data, string $mediaType, ?array $query = null) |
497: | { |
498: | return $this->processRaw(self::METHOD_DELETE, $data, $mediaType, $query); |
499: | } |
500: | |
501: | |
502: | |
503: | |
504: | |
505: | private function processRaw(string $method, string $data, string $mediaType, ?array $query) |
506: | { |
507: | $request = $this->createRequest($method, $query, $data); |
508: | $request = $request->withHeader(self::HEADER_CONTENT_TYPE, $mediaType); |
509: | |
510: | $response = $this->doSendRequest($request); |
511: | return $this->getResponseBody($response); |
512: | } |
513: | |
514: | |
515: | |
516: | |
517: | |
518: | |
519: | public function flushCookies() |
520: | { |
521: | if ($this->CookiesCacheKey !== null) { |
522: | $this->getCacheInstance()->delete($this->CookiesCacheKey); |
523: | } |
524: | return $this; |
525: | } |
526: | |
527: | |
528: | |
529: | |
530: | public function replaceQuery($value, array $query) |
531: | { |
532: | return HttpUtil::replaceQuery( |
533: | $value, |
534: | $query, |
535: | $this->FormDataFlags, |
536: | $this->DateFormatter, |
537: | ); |
538: | } |
539: | |
540: | |
541: | |
542: | |
543: | |
544: | |
545: | public function getInnerHeaders(): HeadersInterface |
546: | { |
547: | $headers = $this->Headers; |
548: | if ($this->Credential !== null) { |
549: | $headers = $headers->authorize($this->Credential, $this->CredentialHeaderName); |
550: | } |
551: | if ($this->MediaType !== null) { |
552: | $headers = $headers->set(self::HEADER_CONTENT_TYPE, $this->MediaType); |
553: | } |
554: | if ($this->ExpectJson) { |
555: | $headers = $headers->set(self::HEADER_ACCEPT, self::TYPE_JSON); |
556: | } |
557: | if ($this->UserAgent !== '' && ( |
558: | $this->UserAgent !== null |
559: | || !$headers->hasHeader(self::HEADER_USER_AGENT) |
560: | )) { |
561: | $headers = $headers->set( |
562: | self::HEADER_USER_AGENT, |
563: | $this->UserAgent ?? $this->getDefaultUserAgent(), |
564: | ); |
565: | } |
566: | return $headers; |
567: | } |
568: | |
569: | |
570: | |
571: | |
572: | public function getPublicHeaders(): HeadersInterface |
573: | { |
574: | $sensitive = $this->SensitiveHeaders; |
575: | if ($this->Credential !== null) { |
576: | $sensitive[Str::lower($this->CredentialHeaderName)] = true; |
577: | } |
578: | return $this->getInnerHeaders()->exceptIn($sensitive); |
579: | } |
580: | |
581: | |
582: | |
583: | |
584: | public function hasCredential(): bool |
585: | { |
586: | return $this->Credential !== null; |
587: | } |
588: | |
589: | |
590: | |
591: | |
592: | public function isSensitiveHeader(string $name): bool |
593: | { |
594: | return isset($this->SensitiveHeaders[Str::lower($name)]); |
595: | } |
596: | |
597: | |
598: | |
599: | |
600: | public function getMediaType(): ?string |
601: | { |
602: | return $this->MediaType; |
603: | } |
604: | |
605: | |
606: | |
607: | |
608: | public function hasUserAgent(): bool |
609: | { |
610: | return $this->UserAgent !== null; |
611: | } |
612: | |
613: | |
614: | |
615: | |
616: | public function getUserAgent(): string |
617: | { |
618: | return $this->UserAgent ?? $this->getDefaultUserAgent(); |
619: | } |
620: | |
621: | |
622: | |
623: | |
624: | public function expectsJson(): bool |
625: | { |
626: | return $this->ExpectJson; |
627: | } |
628: | |
629: | |
630: | |
631: | |
632: | public function postsJson(): bool |
633: | { |
634: | return $this->PostJson; |
635: | } |
636: | |
637: | |
638: | |
639: | |
640: | public function getDateFormatter(): ?DateFormatterInterface |
641: | { |
642: | return $this->DateFormatter; |
643: | } |
644: | |
645: | |
646: | |
647: | |
648: | public function getFormDataFlags(): int |
649: | { |
650: | return $this->FormDataFlags; |
651: | } |
652: | |
653: | |
654: | |
655: | |
656: | public function getPager(): ?CurlerPagerInterface |
657: | { |
658: | return $this->Pager; |
659: | } |
660: | |
661: | |
662: | |
663: | |
664: | public function alwaysPaginates(): bool |
665: | { |
666: | return $this->AlwaysPaginate; |
667: | } |
668: | |
669: | |
670: | |
671: | |
672: | public function getCache(): ?CacheInterface |
673: | { |
674: | return $this->Cache; |
675: | } |
676: | |
677: | |
678: | |
679: | |
680: | public function hasCookies(): bool |
681: | { |
682: | return $this->CookiesCacheKey !== null; |
683: | } |
684: | |
685: | |
686: | |
687: | |
688: | public function hasResponseCache(): bool |
689: | { |
690: | return $this->CacheResponses; |
691: | } |
692: | |
693: | |
694: | |
695: | |
696: | public function hasPostResponseCache(): bool |
697: | { |
698: | return $this->CachePostResponses; |
699: | } |
700: | |
701: | |
702: | |
703: | |
704: | public function getCacheLifetime(): int |
705: | { |
706: | return $this->CacheLifetime; |
707: | } |
708: | |
709: | |
710: | |
711: | |
712: | public function refreshesCache(): bool |
713: | { |
714: | return $this->RefreshCache; |
715: | } |
716: | |
717: | |
718: | |
719: | |
720: | public function getTimeout(): ?int |
721: | { |
722: | return $this->Timeout; |
723: | } |
724: | |
725: | |
726: | |
727: | |
728: | public function followsRedirects(): bool |
729: | { |
730: | return $this->FollowRedirects; |
731: | } |
732: | |
733: | |
734: | |
735: | |
736: | public function getMaxRedirects(): ?int |
737: | { |
738: | return $this->MaxRedirects; |
739: | } |
740: | |
741: | |
742: | |
743: | |
744: | public function getRetryAfterTooManyRequests(): bool |
745: | { |
746: | return $this->RetryAfterTooManyRequests; |
747: | } |
748: | |
749: | |
750: | |
751: | |
752: | public function getRetryAfterMaxSeconds(): int |
753: | { |
754: | return $this->RetryAfterMaxSeconds; |
755: | } |
756: | |
757: | |
758: | |
759: | |
760: | public function throwsHttpErrors(): bool |
761: | { |
762: | return $this->ThrowHttpErrors; |
763: | } |
764: | |
765: | |
766: | |
767: | |
768: | |
769: | |
770: | public function withUri($uri) |
771: | { |
772: | return (string) $uri === (string) $this->Uri |
773: | ? $this |
774: | : $this->with('Uri', $this->filterUri($uri)); |
775: | } |
776: | |
777: | |
778: | |
779: | |
780: | public function withRequest(PsrRequestInterface $request) |
781: | { |
782: | $curler = $this->withUri($request->getUri()); |
783: | |
784: | $headers = Headers::from($request); |
785: | $_headers = $this->getInnerHeaders(); |
786: | if (Arr::same($headers->all(), $_headers->all())) { |
787: | return $curler; |
788: | } |
789: | |
790: | if ($this->Credential !== null) { |
791: | $header = $headers->getHeader($this->CredentialHeaderName); |
792: | $_header = $_headers->getHeader($this->CredentialHeaderName); |
793: | if ($header !== $_header) { |
794: | $curler = $curler->withCredential(null); |
795: | } |
796: | } |
797: | |
798: | $mediaType = Arr::last($headers->getHeaderValues(self::HEADER_CONTENT_TYPE)); |
799: | $userAgent = Arr::last($headers->getHeaderValues(self::HEADER_USER_AGENT)); |
800: | $expectJson = Arr::lower($headers->getHeaderValues(self::HEADER_ACCEPT)) === [self::TYPE_JSON]; |
801: | if ($userAgent !== null && $userAgent === $this->getDefaultUserAgent()) { |
802: | $userAgent = null; |
803: | } |
804: | return $curler |
805: | ->withMediaType($mediaType) |
806: | ->withUserAgent($userAgent) |
807: | ->withExpectJson($expectJson) |
808: | ->with('Headers', $headers); |
809: | } |
810: | |
811: | |
812: | |
813: | |
814: | public function withCredential( |
815: | ?CredentialInterface $credential, |
816: | string $headerName = Curler::HEADER_AUTHORIZATION |
817: | ) { |
818: | return $credential === null |
819: | ? $this |
820: | ->with('Credential', null) |
821: | ->without('CredentialHeaderName') |
822: | : $this |
823: | ->with('Credential', $credential) |
824: | ->with('CredentialHeaderName', $headerName); |
825: | } |
826: | |
827: | |
828: | |
829: | |
830: | public function withSensitiveHeader(string $name) |
831: | { |
832: | return $this->with( |
833: | 'SensitiveHeaders', |
834: | Arr::set($this->SensitiveHeaders, Str::lower($name), true) |
835: | ); |
836: | } |
837: | |
838: | |
839: | |
840: | |
841: | public function withoutSensitiveHeader(string $name) |
842: | { |
843: | return $this->with( |
844: | 'SensitiveHeaders', |
845: | Arr::unset($this->SensitiveHeaders, Str::lower($name)) |
846: | ); |
847: | } |
848: | |
849: | |
850: | |
851: | |
852: | public function withMediaType(?string $type) |
853: | { |
854: | return $this->with('MediaType', $type); |
855: | } |
856: | |
857: | |
858: | |
859: | |
860: | public function withUserAgent(?string $userAgent) |
861: | { |
862: | return $this->with('UserAgent', $userAgent); |
863: | } |
864: | |
865: | |
866: | |
867: | |
868: | public function withExpectJson(bool $expectJson = true) |
869: | { |
870: | return $this->with('ExpectJson', $expectJson); |
871: | } |
872: | |
873: | |
874: | |
875: | |
876: | public function withPostJson(bool $postJson = true) |
877: | { |
878: | return $this->with('PostJson', $postJson); |
879: | } |
880: | |
881: | |
882: | |
883: | |
884: | public function withDateFormatter(?DateFormatterInterface $formatter) |
885: | { |
886: | return $this->with('DateFormatter', $formatter); |
887: | } |
888: | |
889: | |
890: | |
891: | |
892: | public function withFormDataFlags(int $flags) |
893: | { |
894: | return $this->with('FormDataFlags', $flags); |
895: | } |
896: | |
897: | |
898: | |
899: | |
900: | public function withJsonDecodeFlags(int $flags) |
901: | { |
902: | return $this->with('JsonDecodeFlags', $flags); |
903: | } |
904: | |
905: | |
906: | |
907: | |
908: | public function withMiddleware($middleware, ?string $name = null) |
909: | { |
910: | return $this->with( |
911: | 'Middleware', |
912: | Arr::push($this->Middleware, [$middleware, $name]) |
913: | ); |
914: | } |
915: | |
916: | |
917: | |
918: | |
919: | public function withoutMiddleware($middleware) |
920: | { |
921: | $index = is_string($middleware) ? 1 : 0; |
922: | $values = $this->Middleware; |
923: | foreach ($values as $key => $value) { |
924: | if ($middleware === $value[$index]) { |
925: | unset($values[$key]); |
926: | } |
927: | } |
928: | return $this->with('Middleware', $values); |
929: | } |
930: | |
931: | |
932: | |
933: | |
934: | public function withPager(?CurlerPagerInterface $pager, bool $alwaysPaginate = false) |
935: | { |
936: | return $this |
937: | ->with('Pager', $pager) |
938: | ->with('AlwaysPaginate', $pager && $alwaysPaginate); |
939: | } |
940: | |
941: | |
942: | |
943: | |
944: | public function withCache(?CacheInterface $cache = null) |
945: | { |
946: | return $this->with('Cache', $cache); |
947: | } |
948: | |
949: | |
950: | |
951: | |
952: | public function withCookies(?string $cacheKey = null) |
953: | { |
954: | return $this->with('CookiesCacheKey', $this->filterCookiesCacheKey($cacheKey)); |
955: | } |
956: | |
957: | |
958: | |
959: | |
960: | public function withoutCookies() |
961: | { |
962: | return $this->with('CookiesCacheKey', null); |
963: | } |
964: | |
965: | |
966: | |
967: | |
968: | public function withResponseCache(bool $cacheResponses = true) |
969: | { |
970: | return $this->with('CacheResponses', $cacheResponses); |
971: | } |
972: | |
973: | |
974: | |
975: | |
976: | public function withPostResponseCache(bool $cachePostResponses = true) |
977: | { |
978: | return $this->with('CachePostResponses', $cachePostResponses); |
979: | } |
980: | |
981: | |
982: | |
983: | |
984: | public function withCacheKeyCallback(?callable $callback) |
985: | { |
986: | return $this->with('CacheKeyCallback', $callback); |
987: | } |
988: | |
989: | |
990: | |
991: | |
992: | public function withCacheLifetime(int $seconds) |
993: | { |
994: | return $this->with('CacheLifetime', $seconds); |
995: | } |
996: | |
997: | |
998: | |
999: | |
1000: | public function withRefreshCache(bool $refresh = true) |
1001: | { |
1002: | return $this->with('RefreshCache', $refresh); |
1003: | } |
1004: | |
1005: | |
1006: | |
1007: | |
1008: | public function withTimeout(?int $seconds) |
1009: | { |
1010: | return $this->with('Timeout', $seconds); |
1011: | } |
1012: | |
1013: | |
1014: | |
1015: | |
1016: | public function withFollowRedirects(bool $follow = true) |
1017: | { |
1018: | return $this->with('FollowRedirects', $follow); |
1019: | } |
1020: | |
1021: | |
1022: | |
1023: | |
1024: | public function withMaxRedirects(?int $redirects) |
1025: | { |
1026: | return $this->with('MaxRedirects', $redirects); |
1027: | } |
1028: | |
1029: | |
1030: | |
1031: | |
1032: | public function withRetryAfterTooManyRequests(bool $retry = true) |
1033: | { |
1034: | return $this->with('RetryAfterTooManyRequests', $retry); |
1035: | } |
1036: | |
1037: | |
1038: | |
1039: | |
1040: | public function withRetryAfterMaxSeconds(int $seconds) |
1041: | { |
1042: | return $this->with('RetryAfterMaxSeconds', $seconds); |
1043: | } |
1044: | |
1045: | |
1046: | |
1047: | |
1048: | public function withThrowHttpErrors(bool $throw = true) |
1049: | { |
1050: | return $this->with('ThrowHttpErrors', $throw); |
1051: | } |
1052: | |
1053: | |
1054: | |
1055: | |
1056: | |
1057: | |
1058: | public function sendRequest(PsrRequestInterface $request): ResponseInterface |
1059: | { |
1060: | |
1061: | |
1062: | |
1063: | |
1064: | $curler = $this->WithoutThrowHttpErrors ??= $this->withThrowHttpErrors(false); |
1065: | try { |
1066: | try { |
1067: | return $curler->doSendRequest($request); |
1068: | } finally { |
1069: | $this->LastRequest = $curler->LastRequest; |
1070: | $this->LastResponse = $curler->LastResponse; |
1071: | } |
1072: | } catch (PsrClientExceptionInterface $ex) { |
1073: | throw $ex; |
1074: | } catch (CurlErrorExceptionInterface $ex) { |
1075: | throw $ex->isNetworkError() |
1076: | ? new NetworkException($ex->getMessage(), $this->LastRequest ?? $request, [], $ex) |
1077: | : new RequestException($ex->getMessage(), $this->LastRequest ?? $request, [], $ex); |
1078: | } catch (Throwable $ex) { |
1079: | throw new RequestException($ex->getMessage(), $this->LastRequest ?? $request, [], $ex); |
1080: | } |
1081: | } |
1082: | |
1083: | |
1084: | |
1085: | |
1086: | |
1087: | private function doSendRequest(PsrRequestInterface $request): ResponseInterface |
1088: | { |
1089: | $this->LastRequest = null; |
1090: | $this->LastResponse = null; |
1091: | return $this->Middleware |
1092: | ? ($this->Closure ??= $this->getClosure())($request) |
1093: | : $this->getResponse($request); |
1094: | } |
1095: | |
1096: | |
1097: | |
1098: | |
1099: | private function getClosure(): Closure |
1100: | { |
1101: | $closure = fn(PsrRequestInterface $request): ResponseInterface => |
1102: | $this->getResponse($request); |
1103: | foreach (array_reverse($this->Middleware) as [$middleware]) { |
1104: | $closure = $middleware instanceof CurlerMiddlewareInterface |
1105: | ? fn(PsrRequestInterface $request): ResponseInterface => |
1106: | $this->handleHttpResponse($middleware($request, $closure, $this), $request) |
1107: | : fn(PsrRequestInterface $request): ResponseInterface => |
1108: | $this->handleResponse($middleware($request, $closure, $this), $request); |
1109: | } |
1110: | return $closure; |
1111: | } |
1112: | |
1113: | private function getResponse(PsrRequestInterface $request): ResponseInterface |
1114: | { |
1115: | $uri = $request->getUri()->withFragment(''); |
1116: | $request = $request->withUri($uri); |
1117: | |
1118: | $version = (int) ((float) $request->getProtocolVersion() * 10); |
1119: | $opt[\CURLOPT_HTTP_VERSION] = [ |
1120: | 10 => \CURL_HTTP_VERSION_1_0, |
1121: | 11 => \CURL_HTTP_VERSION_1_1, |
1122: | 20 => \CURL_HTTP_VERSION_2_0, |
1123: | ][$version] ?? \CURL_HTTP_VERSION_NONE; |
1124: | |
1125: | $method = $request->getMethod(); |
1126: | $body = $request->getBody(); |
1127: | $opt[\CURLOPT_CUSTOMREQUEST] = $method; |
1128: | if ($method === self::METHOD_HEAD) { |
1129: | $opt[\CURLOPT_NOBODY] = true; |
1130: | $size = 0; |
1131: | } else { |
1132: | $size = $body->getSize(); |
1133: | } |
1134: | |
1135: | if ($size === null || $size > 0) { |
1136: | $size ??= HttpUtil::getContentLength($request); |
1137: | if ($size !== null && $size <= static::MAX_INPUT_LENGTH) { |
1138: | $body = (string) $body; |
1139: | $opt[\CURLOPT_POSTFIELDS] = $body; |
1140: | $request = $request |
1141: | ->withoutHeader(self::HEADER_CONTENT_LENGTH) |
1142: | ->withoutHeader(self::HEADER_TRANSFER_ENCODING); |
1143: | } else { |
1144: | $opt[\CURLOPT_UPLOAD] = true; |
1145: | if ($size !== null) { |
1146: | $opt[\CURLOPT_INFILESIZE] = $size; |
1147: | $request = $request |
1148: | ->withoutHeader(self::HEADER_CONTENT_LENGTH); |
1149: | } |
1150: | $opt[\CURLOPT_READFUNCTION] = |
1151: | static function ($handle, $infile, int $length) use ($body): string { |
1152: | return $body->read($length); |
1153: | }; |
1154: | if ($body->isSeekable()) { |
1155: | $body->rewind(); |
1156: | } |
1157: | } |
1158: | } elseif (self::REQUEST_METHOD_HAS_BODY[$method] ?? false) { |
1159: | |
1160: | |
1161: | |
1162: | |
1163: | |
1164: | |
1165: | $request = $request->withHeader(self::HEADER_CONTENT_LENGTH, '0'); |
1166: | } |
1167: | |
1168: | if ($this->Timeout !== null) { |
1169: | $opt[\CURLOPT_CONNECTTIMEOUT] = $this->Timeout; |
1170: | } |
1171: | |
1172: | if (!$request->hasHeader(self::HEADER_ACCEPT_ENCODING)) { |
1173: | |
1174: | |
1175: | $opt[\CURLOPT_ENCODING] = ''; |
1176: | } |
1177: | |
1178: | |
1179: | $statusLine = null; |
1180: | |
1181: | $headersIn = null; |
1182: | $opt[\CURLOPT_HEADERFUNCTION] = |
1183: | static function ($handle, string $header) use (&$statusLine, &$headersIn): int { |
1184: | if (substr($header, 0, 5) === 'HTTP/') { |
1185: | $statusLine = rtrim($header, "\r\n"); |
1186: | $headersIn = new Headers(); |
1187: | return strlen($header); |
1188: | } |
1189: | if ($headersIn === null) { |
1190: | throw new InvalidHeaderException('No status line in HTTP response'); |
1191: | } |
1192: | $headersIn = $headersIn->addLine($header); |
1193: | return strlen($header); |
1194: | }; |
1195: | |
1196: | |
1197: | $bodyIn = null; |
1198: | $opt[\CURLOPT_WRITEFUNCTION] = |
1199: | static function ($handle, string $data) use (&$bodyIn): int { |
1200: | |
1201: | return $bodyIn->write($data); |
1202: | }; |
1203: | |
1204: | if (self::$Handle === null) { |
1205: | $handle = curl_init((string) $uri); |
1206: | if ($handle === false) { |
1207: | throw new RuntimeException('curl_init() failed'); |
1208: | } |
1209: | self::$Handle = $handle; |
1210: | $resetHandle = false; |
1211: | } else { |
1212: | $opt[\CURLOPT_URL] = (string) $uri; |
1213: | $resetHandle = true; |
1214: | } |
1215: | |
1216: | $request = $this->normaliseRequest($request, $uri); |
1217: | $headers = Headers::from($request); |
1218: | |
1219: | $cacheKey = null; |
1220: | $transfer = 0; |
1221: | $redirects = $this->FollowRedirects && $this->MaxRedirects !== 0 |
1222: | ? $this->MaxRedirects ?? 30 |
1223: | : false; |
1224: | $retrying = false; |
1225: | do { |
1226: | if ($cacheKey === null) { |
1227: | if ( |
1228: | $this->CacheResponses |
1229: | && ($size === 0 || is_string($body)) |
1230: | && ([ |
1231: | self::METHOD_GET => true, |
1232: | self::METHOD_HEAD => true, |
1233: | self::METHOD_POST => $this->CachePostResponses, |
1234: | ][$method] ?? false) |
1235: | ) { |
1236: | $cacheKey = $this->CacheKeyCallback !== null |
1237: | ? (array) ($this->CacheKeyCallback)($request, $this) |
1238: | : $headers->exceptIn($this->getUnstableHeaders())->getLines('%s:%s'); |
1239: | |
1240: | if ($size !== 0 || $method === self::METHOD_POST) { |
1241: | $cacheKey[] = $size === 0 ? '' : $body; |
1242: | } |
1243: | |
1244: | $cacheUri = $uri->getPath() === '' |
1245: | ? $uri->withPath('/') |
1246: | : $uri; |
1247: | $cacheKey = implode(':', [ |
1248: | self::class, |
1249: | 'response', |
1250: | $method, |
1251: | rawurlencode((string) $cacheUri), |
1252: | Get::hash(implode("\0", $cacheKey)), |
1253: | ]); |
1254: | } else { |
1255: | $cacheKey = false; |
1256: | } |
1257: | } |
1258: | |
1259: | if ( |
1260: | $cacheKey !== false |
1261: | && !$this->RefreshCache |
1262: | && ($last = $this->getCacheInstance()->getArray($cacheKey)) !== null |
1263: | ) { |
1264: | |
1265: | $code = $last['code'] ?? $last[0] ?? 200; |
1266: | $bodyIn = Stream::fromString($last['body'] ?? $last[3] ?? ''); |
1267: | $lastHeaders = $last['headers'] ?? $last[2] ?? null; |
1268: | if (is_array($lastHeaders)) { |
1269: | $lastHeaders = HttpUtil::getNameValuePairs($lastHeaders); |
1270: | } |
1271: | $headersIn = Headers::from($lastHeaders ?? []); |
1272: | $reason = $last['reason'] ?? $last[1] ?? null; |
1273: | $version = $last['version'] ?? '1.1'; |
1274: | $response = new Response($code, $bodyIn, $headersIn, $reason, $version); |
1275: | Event::dispatch(new ResponseCacheHitEvent($this, $request, $response)); |
1276: | } else { |
1277: | if ($transfer) { |
1278: | if ($size !== 0 && $body instanceof PsrStreamInterface) { |
1279: | if (!$body->isSeekable()) { |
1280: | throw new RequestException( |
1281: | 'Request cannot be sent again (body not seekable)', |
1282: | $request, |
1283: | ); |
1284: | } |
1285: | $body->rewind(); |
1286: | } |
1287: | $statusLine = null; |
1288: | $headersIn = null; |
1289: | } |
1290: | $bodyIn = new Stream(File::open('php://temp', 'r+')); |
1291: | |
1292: | if ($resetHandle || !$transfer) { |
1293: | if ($resetHandle) { |
1294: | curl_reset(self::$Handle); |
1295: | $resetHandle = false; |
1296: | } |
1297: | $opt[\CURLOPT_HTTPHEADER] = $headers->getLines('%s: %s', '%s;'); |
1298: | curl_setopt_array(self::$Handle, $opt); |
1299: | |
1300: | if ($this->CookiesCacheKey !== null) { |
1301: | |
1302: | |
1303: | curl_setopt(self::$Handle, \CURLOPT_COOKIEFILE, ''); |
1304: | |
1305: | $cookies = $this->getCacheInstance()->getArray($this->CookiesCacheKey); |
1306: | if ($cookies) { |
1307: | foreach ($cookies as $cookie) { |
1308: | curl_setopt(self::$Handle, \CURLOPT_COOKIELIST, $cookie); |
1309: | } |
1310: | } |
1311: | } |
1312: | } |
1313: | |
1314: | $transfer++; |
1315: | |
1316: | Event::dispatch(new CurlRequestEvent($this, self::$Handle, $request)); |
1317: | $result = curl_exec(self::$Handle); |
1318: | if ($result === false) { |
1319: | throw new CurlErrorException(curl_errno(self::$Handle), $request, $this->getCurlInfo()); |
1320: | } |
1321: | |
1322: | if ( |
1323: | $statusLine === null |
1324: | || count($split = explode(' ', $statusLine, 3)) < 2 |
1325: | || ($version = explode('/', $split[0])[1] ?? null) === null |
1326: | ) { |
1327: | |
1328: | throw new InvalidHeaderException(sprintf( |
1329: | 'HTTP status line invalid or not in response: %s', |
1330: | rtrim((string) $statusLine, "\r\n"), |
1331: | )); |
1332: | |
1333: | } |
1334: | |
1335: | |
1336: | $code = (int) $split[1]; |
1337: | $reason = $split[2] ?? null; |
1338: | $response = new Response($code, $bodyIn, $headersIn, $reason, $version); |
1339: | Event::dispatch(new CurlResponseEvent($this, self::$Handle, $request, $response)); |
1340: | |
1341: | if ($this->CookiesCacheKey !== null) { |
1342: | $this->getCacheInstance()->set( |
1343: | $this->CookiesCacheKey, |
1344: | curl_getinfo(self::$Handle, \CURLINFO_COOKIELIST) |
1345: | ); |
1346: | } |
1347: | |
1348: | if ($cacheKey !== false && $this->CacheLifetime >= 0 && $code < 400) { |
1349: | $ttl = $this->CacheLifetime === 0 |
1350: | ? null |
1351: | : $this->CacheLifetime; |
1352: | $this->getCacheInstance()->set($cacheKey, [ |
1353: | 'code' => $code, |
1354: | 'body' => (string) $bodyIn, |
1355: | 'headers' => $headersIn->jsonSerialize(), |
1356: | 'reason' => $reason, |
1357: | 'version' => $version, |
1358: | ], $ttl); |
1359: | } |
1360: | } |
1361: | |
1362: | if ( |
1363: | $redirects !== false |
1364: | && $code >= 300 |
1365: | && $code < 400 |
1366: | && ($location = $headersIn->getOnlyHeaderValue(self::HEADER_LOCATION)) !== '' |
1367: | ) { |
1368: | if (!$redirects) { |
1369: | throw new TooManyRedirectsException(sprintf( |
1370: | 'Redirect limit exceeded: %d', |
1371: | $this->MaxRedirects, |
1372: | ), $request, $response); |
1373: | } |
1374: | $uri = Uri::from($uri)->follow($location)->withFragment(''); |
1375: | $request = $request->withUri($uri); |
1376: | |
1377: | if (($code === 301 || $code === 302 || $code === 303) && ( |
1378: | $size !== 0 |
1379: | || (self::REQUEST_METHOD_HAS_BODY[$method] ?? false) |
1380: | )) { |
1381: | $method = self::METHOD_GET; |
1382: | $body = Stream::fromString(''); |
1383: | $request = $request |
1384: | ->withMethod($method) |
1385: | ->withBody($body) |
1386: | ->withoutHeader(self::HEADER_CONTENT_LENGTH) |
1387: | ->withoutHeader(self::HEADER_TRANSFER_ENCODING); |
1388: | $size = 0; |
1389: | $opt[\CURLOPT_CUSTOMREQUEST] = $method; |
1390: | $opt[\CURLOPT_URL] = (string) $uri; |
1391: | unset( |
1392: | $opt[\CURLOPT_POSTFIELDS], |
1393: | $opt[\CURLOPT_UPLOAD], |
1394: | $opt[\CURLOPT_INFILESIZE], |
1395: | $opt[\CURLOPT_READFUNCTION], |
1396: | ); |
1397: | $resetHandle = true; |
1398: | } else { |
1399: | curl_setopt( |
1400: | self::$Handle, |
1401: | \CURLOPT_URL, |
1402: | |
1403: | $opt[\CURLOPT_URL] = (string) $uri, |
1404: | ); |
1405: | } |
1406: | $request = $this->normaliseRequest($request, $uri); |
1407: | $headers = Headers::from($request); |
1408: | $cacheKey = null; |
1409: | $redirects--; |
1410: | $retrying = false; |
1411: | continue; |
1412: | } |
1413: | |
1414: | if ( |
1415: | !$this->RetryAfterTooManyRequests |
1416: | || $retrying |
1417: | || $code !== 429 |
1418: | || ($after = HttpUtil::getRetryAfter($headersIn)) === null |
1419: | || ($this->RetryAfterMaxSeconds !== 0 && $after > $this->RetryAfterMaxSeconds) |
1420: | ) { |
1421: | break; |
1422: | } |
1423: | |
1424: | $after = max(1, $after); |
1425: | $this->debug(Inflect::format($after, 'Sleeping for {{#}} {{#:second}}')); |
1426: | sleep($after); |
1427: | $retrying = true; |
1428: | } while (true); |
1429: | |
1430: | $this->LastRequest = $request; |
1431: | $this->LastResponse = $response; |
1432: | |
1433: | if ($this->ThrowHttpErrors && $code >= 400) { |
1434: | throw new HttpErrorException($request, $response, $this->getCurlInfo()); |
1435: | } |
1436: | |
1437: | return $response; |
1438: | } |
1439: | |
1440: | private function normaliseRequest( |
1441: | PsrRequestInterface $request, |
1442: | PsrUriInterface $uri |
1443: | ): PsrRequestInterface { |
1444: | |
1445: | if (($host = $request->getHeaderLine(self::HEADER_HOST)) !== '') { |
1446: | try { |
1447: | $host = new Uri("//$host"); |
1448: | } catch (InvalidArgumentException $ex) { |
1449: | throw new InvalidHeaderException(sprintf( |
1450: | 'Invalid value for HTTP request header %s: %s', |
1451: | self::HEADER_HOST, |
1452: | $host, |
1453: | ), $ex); |
1454: | } |
1455: | $host = $host->withScheme($uri->getScheme())->getAuthority(); |
1456: | if ($host === $uri->withUserInfo('')->getAuthority()) { |
1457: | $request = $request->withoutHeader(self::HEADER_HOST); |
1458: | } |
1459: | } |
1460: | return $request; |
1461: | } |
1462: | |
1463: | |
1464: | |
1465: | |
1466: | |
1467: | |
1468: | |
1469: | private function createRequest(string $method, ?array $query, $data): Request |
1470: | { |
1471: | $uri = $this->Uri; |
1472: | if ($query) { |
1473: | $uri = $this->replaceQuery($uri, $query); |
1474: | } |
1475: | $headers = $this->getInnerHeaders(); |
1476: | $request = new Request($method, $uri, null, $headers); |
1477: | return $data !== false |
1478: | ? $this->applyData($request, $data) |
1479: | : $request; |
1480: | } |
1481: | |
1482: | |
1483: | |
1484: | |
1485: | private function applyData(Request $request, $data): Request |
1486: | { |
1487: | if ($data === null) { |
1488: | if ($this->PostJson) { |
1489: | $this->debug(sprintf( |
1490: | 'JSON message bodies cannot be empty; falling back to %s', |
1491: | self::TYPE_FORM, |
1492: | )); |
1493: | } |
1494: | return $request->withHeader(self::HEADER_CONTENT_TYPE, self::TYPE_FORM); |
1495: | } |
1496: | if (is_string($data)) { |
1497: | return $request->withBody(Stream::fromString($data)); |
1498: | } |
1499: | if ($this->PostJson) { |
1500: | try { |
1501: | $body = Stream::fromData($data, $this->FormDataFlags, $this->DateFormatter, true); |
1502: | $mediaType = self::TYPE_JSON; |
1503: | } catch (StreamEncapsulationException $ex) { |
1504: | $this->debug(sprintf( |
1505: | '%s; falling back to %s', |
1506: | $ex->getMessage(), |
1507: | self::TYPE_FORM_MULTIPART, |
1508: | )); |
1509: | } |
1510: | } |
1511: | $body ??= Stream::fromData($data, $this->FormDataFlags, $this->DateFormatter); |
1512: | $mediaType ??= $body instanceof MultipartStreamInterface |
1513: | ? HttpUtil::getMultipartMediaType($body) |
1514: | : self::TYPE_FORM; |
1515: | return $request |
1516: | ->withHeader(self::HEADER_CONTENT_TYPE, $mediaType) |
1517: | ->withBody($body); |
1518: | } |
1519: | |
1520: | |
1521: | |
1522: | |
1523: | private function getResponseBody(ResponseInterface $response) |
1524: | { |
1525: | $body = (string) $response->getBody(); |
1526: | return $this->responseIsJson($response) |
1527: | ? ($body === '' |
1528: | ? null |
1529: | : Json::parse($body, $this->JsonDecodeFlags)) |
1530: | : $body; |
1531: | } |
1532: | |
1533: | private function handleResponse( |
1534: | PsrResponseInterface $response, |
1535: | PsrRequestInterface $request |
1536: | ): ResponseInterface { |
1537: | return $this->handleHttpResponse( |
1538: | $response instanceof ResponseInterface |
1539: | ? $response |
1540: | : Response::fromPsr7($response), |
1541: | $request, |
1542: | ); |
1543: | } |
1544: | |
1545: | private function handleHttpResponse( |
1546: | ResponseInterface $response, |
1547: | PsrRequestInterface $request |
1548: | ): ResponseInterface { |
1549: | $this->LastRequest = $request; |
1550: | $this->LastResponse = $response; |
1551: | return $response; |
1552: | } |
1553: | |
1554: | |
1555: | |
1556: | |
1557: | private function filterUri($uri): Uri |
1558: | { |
1559: | if ($uri === null) { |
1560: | return new Uri(); |
1561: | } |
1562: | $uri = Uri::from($uri); |
1563: | $invalid = array_intersect(['query', 'fragment'], array_keys($uri->getComponents())); |
1564: | if ($invalid) { |
1565: | throw new InvalidArgumentException(Inflect::format( |
1566: | $invalid, |
1567: | 'URI cannot have %s {{#:component}}', |
1568: | implode(' or ', $invalid), |
1569: | )); |
1570: | } |
1571: | return $uri; |
1572: | } |
1573: | |
1574: | |
1575: | |
1576: | |
1577: | private function filterHeaders($headers): HeadersInterface |
1578: | { |
1579: | if ($headers instanceof HeadersInterface) { |
1580: | return $headers; |
1581: | } |
1582: | return new Headers($headers ?? []); |
1583: | } |
1584: | |
1585: | private static function filterCookiesCacheKey(?string $cacheKey): string |
1586: | { |
1587: | return Arr::implode(':', [self::class, 'cookies', $cacheKey], ''); |
1588: | } |
1589: | |
1590: | private function getCacheInstance(): CacheInterface |
1591: | { |
1592: | return $this->Cache ?? Cache::getInstance(); |
1593: | } |
1594: | |
1595: | private function getDefaultUserAgent(): string |
1596: | { |
1597: | return self::$DefaultUserAgent ??= HttpUtil::getProduct(); |
1598: | } |
1599: | |
1600: | |
1601: | |
1602: | |
1603: | private function getUnstableHeaders(): array |
1604: | { |
1605: | return self::$UnstableHeaders ??= array_fill_keys(self::HEADERS_UNSTABLE, true); |
1606: | } |
1607: | |
1608: | |
1609: | |
1610: | |
1611: | private function getCurlInfo(): array |
1612: | { |
1613: | if ( |
1614: | self::$Handle === null |
1615: | || ($info = curl_getinfo(self::$Handle)) === false |
1616: | ) { |
1617: | return []; |
1618: | } |
1619: | foreach ($info as &$value) { |
1620: | if (is_string($value)) { |
1621: | $value = trim($value); |
1622: | } |
1623: | } |
1624: | return $info; |
1625: | } |
1626: | |
1627: | private function debug(string $msg1): void |
1628: | { |
1629: | if (Console::isLoaded()) { |
1630: | Console::debug($msg1); |
1631: | } |
1632: | } |
1633: | } |
1634: | |