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