1: | <?php declare(strict_types=1); |
2: | |
3: | namespace Salient\Http; |
4: | |
5: | use Psr\Http\Message\MessageInterface; |
6: | use Salient\Collection\ReadableCollectionTrait; |
7: | use Salient\Contract\Collection\CollectionInterface; |
8: | use Salient\Contract\Core\Arrayable; |
9: | use Salient\Contract\Http\AccessTokenInterface; |
10: | use Salient\Contract\Http\HttpHeader; |
11: | use Salient\Contract\Http\HttpHeadersInterface; |
12: | use Salient\Contract\Http\HttpMessageInterface; |
13: | use Salient\Core\Concern\HasMutator; |
14: | use Salient\Core\Concern\ImmutableArrayAccessTrait; |
15: | use Salient\Core\Exception\MethodNotImplementedException; |
16: | use Salient\Http\Exception\InvalidHeaderException; |
17: | use Salient\Utility\Arr; |
18: | use Salient\Utility\Regex; |
19: | use Salient\Utility\Str; |
20: | use Salient\Utility\Test; |
21: | use Generator; |
22: | use InvalidArgumentException; |
23: | use LogicException; |
24: | |
25: | |
26: | |
27: | |
28: | |
29: | |
30: | |
31: | |
32: | |
33: | class HttpHeaders implements HttpHeadersInterface |
34: | { |
35: | |
36: | use ReadableCollectionTrait; |
37: | |
38: | use ImmutableArrayAccessTrait; |
39: | use HasMutator; |
40: | |
41: | private const HTTP_HEADER_FIELD_NAME = '/^[-0-9a-z!#$%&\'*+.^_`|~]++$/iD'; |
42: | private const HTTP_HEADER_FIELD_VALUE = '/^([\x21-\x7e\x80-\xff]++(?:\h++[\x21-\x7e\x80-\xff]++)*+)?$/D'; |
43: | |
44: | private const HTTP_HEADER_FIELD = <<<'REGEX' |
45: | / ^ |
46: | (?(DEFINE) |
47: | (?<token> [-0-9a-z!#$%&'*+.^_`|~]++ ) |
48: | (?<field_vchar> [\x21-\x7e\x80-\xff]++ ) |
49: | (?<field_content> (?&field_vchar) (?: \h++ (?&field_vchar) )*+ ) |
50: | ) |
51: | (?: |
52: | (?<name> (?&token) ) (?<bad_whitespace> \h++ )?+ : \h*+ (?<value> (?&field_content)? ) | |
53: | \h++ (?<extended> (?&field_content)? ) |
54: | ) |
55: | (?<carry> \h++ )? |
56: | $ /xiD |
57: | REGEX; |
58: | |
59: | |
60: | |
61: | |
62: | |
63: | |
64: | protected array $Headers = []; |
65: | |
66: | |
67: | |
68: | |
69: | |
70: | |
71: | protected array $Index = []; |
72: | |
73: | |
74: | |
75: | |
76: | |
77: | |
78: | protected array $Trailers = []; |
79: | |
80: | protected bool $Closed = false; |
81: | |
82: | |
83: | |
84: | |
85: | |
86: | |
87: | protected ?string $Carry = null; |
88: | |
89: | |
90: | |
91: | |
92: | |
93: | |
94: | public function __construct($items = []) |
95: | { |
96: | $headers = []; |
97: | $index = []; |
98: | $i = -1; |
99: | foreach ($this->generateItems($items) as $key => $value) { |
100: | $values = (array) $value; |
101: | $lower = Str::lower($key); |
102: | foreach ($values as $value) { |
103: | $headers[++$i] = [$key => $value]; |
104: | $index[$lower][] = $i; |
105: | } |
106: | } |
107: | $this->Headers = $headers; |
108: | $this->Index = $this->filterIndex($index); |
109: | $this->Items = $this->doGetHeaders(); |
110: | } |
111: | |
112: | |
113: | |
114: | |
115: | |
116: | |
117: | |
118: | |
119: | |
120: | public static function from($value): self |
121: | { |
122: | if ($value instanceof static) { |
123: | return $value; |
124: | } |
125: | if ($value instanceof HttpMessageInterface) { |
126: | return self::from($value->getHttpHeaders()); |
127: | } |
128: | if ($value instanceof MessageInterface) { |
129: | return new static($value->getHeaders()); |
130: | } |
131: | if (is_string($value)) { |
132: | $lines = |
133: | |
134: | Arr::shift( |
135: | |
136: | explode( |
137: | "\r\n", |
138: | |
139: | explode("\r\n\r\n", Str::setEol($value, "\r\n"), 2)[0] . "\r\n" |
140: | ) |
141: | ); |
142: | $instance = new static(); |
143: | foreach ($lines as $line) { |
144: | $instance = $instance->addLine("$line\r\n"); |
145: | } |
146: | return $instance; |
147: | } |
148: | return new static($value); |
149: | } |
150: | |
151: | |
152: | |
153: | |
154: | |
155: | |
156: | |
157: | |
158: | public function getContentLength(): ?int |
159: | { |
160: | if (!$this->hasHeader(HttpHeader::CONTENT_LENGTH)) { |
161: | return null; |
162: | } |
163: | |
164: | $length = $this->getOneHeaderLine(HttpHeader::CONTENT_LENGTH); |
165: | if (!Test::isInteger($length) || (int) $length < 0) { |
166: | throw new InvalidHeaderException(sprintf( |
167: | 'Invalid value for HTTP header %s: %s', |
168: | HttpHeader::CONTENT_LENGTH, |
169: | $length, |
170: | )); |
171: | } |
172: | |
173: | return (int) $length; |
174: | } |
175: | |
176: | |
177: | |
178: | |
179: | |
180: | |
181: | |
182: | |
183: | public function getMultipartBoundary(): ?string |
184: | { |
185: | if (!$this->hasHeader(HttpHeader::CONTENT_TYPE)) { |
186: | return null; |
187: | } |
188: | |
189: | try { |
190: | return HttpUtil::getParameters( |
191: | $this->getOneHeaderLine(HttpHeader::CONTENT_TYPE), |
192: | false, |
193: | false, |
194: | )['boundary'] ?? null; |
195: | } catch (InvalidArgumentException $ex) { |
196: | throw new InvalidHeaderException($ex->getMessage()); |
197: | } |
198: | } |
199: | |
200: | |
201: | |
202: | |
203: | |
204: | |
205: | public function getPreferences(): array |
206: | { |
207: | if (!$this->hasHeader(HttpHeader::PREFER)) { |
208: | return []; |
209: | } |
210: | |
211: | foreach ($this->getHeaderValues(HttpHeader::PREFER) as $pref) { |
212: | |
213: | $params = HttpUtil::getParameters($pref, true); |
214: | if (!$params) { |
215: | continue; |
216: | } |
217: | $value = reset($params); |
218: | $name = key($params); |
219: | unset($params[$name]); |
220: | $prefs[$name] ??= ['value' => $value, 'parameters' => $params]; |
221: | } |
222: | |
223: | return $prefs ?? []; |
224: | } |
225: | |
226: | |
227: | |
228: | |
229: | |
230: | |
231: | public static function mergePreferences(array $preferences): string |
232: | { |
233: | foreach ($preferences as $name => $pref) { |
234: | $lower = Str::lower($name); |
235: | if (isset($prefs[$lower])) { |
236: | continue; |
237: | } |
238: | $prefs[$lower] = HttpUtil::mergeParameters( |
239: | is_string($pref) |
240: | ? [$name => $pref] |
241: | : [$name => $pref['value']] + ($pref['parameters'] ?? []) |
242: | ); |
243: | } |
244: | |
245: | return implode(', ', $prefs ?? []); |
246: | } |
247: | |
248: | |
249: | |
250: | |
251: | |
252: | |
253: | |
254: | public function getRetryAfter(): ?int |
255: | { |
256: | $after = $this->getHeaderLine(HttpHeader::RETRY_AFTER); |
257: | if (Test::isInteger($after) && (int) $after >= 0) { |
258: | return (int) $after; |
259: | } |
260: | |
261: | $after = strtotime($after); |
262: | if ($after === false) { |
263: | return null; |
264: | } |
265: | |
266: | return max(0, $after - time()); |
267: | } |
268: | |
269: | |
270: | |
271: | |
272: | public function __toString(): string |
273: | { |
274: | return implode("\r\n", $this->getLines()); |
275: | } |
276: | |
277: | |
278: | |
279: | |
280: | public function addLine(string $line, bool $strict = false) |
281: | { |
282: | if ($strict && substr($line, -2) !== "\r\n") { |
283: | throw new InvalidArgumentException('HTTP header field must end with CRLF'); |
284: | } |
285: | |
286: | if ($line === "\r\n" || (!$strict && trim($line) === '')) { |
287: | if ($strict && $this->Closed) { |
288: | throw new InvalidArgumentException('HTTP message cannot have empty header after body'); |
289: | } |
290: | return $this->with('Closed', true)->with('Carry', null); |
291: | } |
292: | |
293: | $extend = false; |
294: | $name = null; |
295: | $value = null; |
296: | if ($strict) { |
297: | $line = substr($line, 0, -2); |
298: | if (!Regex::match(self::HTTP_HEADER_FIELD, $line, $matches, \PREG_UNMATCHED_AS_NULL) |
299: | || $matches['bad_whitespace'] !== null) { |
300: | throw new InvalidArgumentException(sprintf('Invalid HTTP header field: %s', $line)); |
301: | } |
302: | |
303: | |
304: | |
305: | |
306: | $carry = $matches['carry']; |
307: | if ($matches['extended'] !== null) { |
308: | if (!$this->Headers) { |
309: | throw new InvalidArgumentException(sprintf('Invalid line folding: %s', $line)); |
310: | } |
311: | $extend = true; |
312: | $line = $this->Carry . ' ' . $matches['extended']; |
313: | } else { |
314: | $name = $matches['name']; |
315: | $value = $matches['value']; |
316: | } |
317: | } else { |
318: | $line = rtrim($line, "\r\n"); |
319: | $carry = Regex::match('/\h+$/', $line, $matches) ? $matches[0] : null; |
320: | if (strpos(" \t", $line[0]) !== false) { |
321: | $extend = true; |
322: | $line = $this->Carry . ' ' . trim($line); |
323: | } else { |
324: | $split = explode(':', $line, 2); |
325: | if (count($split) !== 2) { |
326: | return $this->with('Carry', null); |
327: | } |
328: | |
329: | |
330: | $name = rtrim($split[0]); |
331: | $value = trim($split[1]); |
332: | } |
333: | } |
334: | |
335: | if ($extend) { |
336: | return $this->extendLast($line)->with('Carry', $carry); |
337: | } |
338: | |
339: | |
340: | |
341: | return $this->add($name, $value)->maybeIndexTrailer()->with('Carry', $carry); |
342: | } |
343: | |
344: | |
345: | |
346: | |
347: | public function hasLastLine(): bool |
348: | { |
349: | return $this->Closed; |
350: | } |
351: | |
352: | |
353: | |
354: | |
355: | public function add($key, $value) |
356: | { |
357: | $values = (array) $value; |
358: | if (!$values) { |
359: | throw new InvalidArgumentException( |
360: | sprintf('At least one value must be given for HTTP header: %s', $key) |
361: | ); |
362: | } |
363: | $lower = Str::lower($key); |
364: | $headers = $this->Headers; |
365: | $index = $this->Index; |
366: | $key = $this->filterName($key); |
367: | foreach ($values as $value) { |
368: | $headers[] = [$key => $this->filterValue($value)]; |
369: | $index[$lower][] = array_key_last($headers); |
370: | } |
371: | return $this->replaceHeaders($headers, $index); |
372: | } |
373: | |
374: | |
375: | |
376: | |
377: | public function set($key, $value) |
378: | { |
379: | $values = (array) $value; |
380: | if (!$values) { |
381: | throw new InvalidArgumentException( |
382: | sprintf('At least one value must be given for HTTP header: %s', $key) |
383: | ); |
384: | } |
385: | $lower = Str::lower($key); |
386: | $headers = $this->Headers; |
387: | $index = $this->Index; |
388: | if (isset($index[$lower])) { |
389: | |
390: | if (count($index[$lower]) === count($values)) { |
391: | $headerIndex = $index[$lower]; |
392: | $changed = false; |
393: | foreach ($values as $value) { |
394: | $i = array_shift($headerIndex); |
395: | if ($headers[$i] !== [$key => $value]) { |
396: | $changed = true; |
397: | break; |
398: | } |
399: | } |
400: | if (!$changed) { |
401: | return $this; |
402: | } |
403: | } |
404: | foreach ($index[$lower] as $i) { |
405: | unset($headers[$i]); |
406: | } |
407: | unset($index[$lower]); |
408: | } |
409: | $key = $this->filterName($key); |
410: | foreach ($values as $value) { |
411: | $headers[] = [$key => $this->filterValue($value)]; |
412: | $index[$lower][] = array_key_last($headers); |
413: | } |
414: | return $this->replaceHeaders($headers, $index); |
415: | } |
416: | |
417: | |
418: | |
419: | |
420: | public function unset($key) |
421: | { |
422: | $lower = Str::lower($key); |
423: | if (!isset($this->Index[$lower])) { |
424: | return $this; |
425: | } |
426: | $headers = $this->Headers; |
427: | $index = $this->Index; |
428: | foreach ($index[$lower] as $i) { |
429: | unset($headers[$i]); |
430: | } |
431: | unset($index[$lower]); |
432: | return $this->replaceHeaders($headers, $index); |
433: | } |
434: | |
435: | |
436: | |
437: | |
438: | public function merge($items, bool $addToExisting = false) |
439: | { |
440: | $headers = $this->Headers; |
441: | $index = $this->Index; |
442: | $applied = false; |
443: | foreach ($this->generateItems($items) as $key => $value) { |
444: | $values = (array) $value; |
445: | $lower = Str::lower($key); |
446: | if ( |
447: | !$addToExisting |
448: | |
449: | |
450: | && isset($this->Index[$lower]) |
451: | && !($unset[$lower] ?? false) |
452: | ) { |
453: | $unset[$lower] = true; |
454: | foreach ($index[$lower] as $i) { |
455: | unset($headers[$i]); |
456: | } |
457: | |
458: | $index[$lower] = []; |
459: | } |
460: | foreach ($values as $value) { |
461: | $applied = true; |
462: | $headers[] = [$key => $value]; |
463: | $index[$lower][] = array_key_last($headers); |
464: | } |
465: | } |
466: | |
467: | if ( |
468: | ($addToExisting && !$applied) |
469: | || $this->getIndexValues($headers, $index) === $this->getIndexValues($this->Headers, $this->Index) |
470: | ) { |
471: | return $this; |
472: | } |
473: | |
474: | return $this->replaceHeaders($headers, $index); |
475: | } |
476: | |
477: | |
478: | |
479: | |
480: | public function authorize( |
481: | AccessTokenInterface $token, |
482: | string $headerName = HttpHeader::AUTHORIZATION |
483: | ) { |
484: | return $this->set( |
485: | $headerName, |
486: | sprintf('%s %s', $token->getTokenType(), $token->getToken()) |
487: | ); |
488: | } |
489: | |
490: | |
491: | |
492: | |
493: | public function canonicalize() |
494: | { |
495: | return $this->maybeReplaceHeaders($this->Headers, $this->Index, true); |
496: | } |
497: | |
498: | |
499: | |
500: | |
501: | public function sort() |
502: | { |
503: | return $this->maybeReplaceHeaders( |
504: | Arr::sort($this->Headers, true, fn($a, $b) => array_key_first($a) <=> array_key_first($b)), |
505: | Arr::sortByKey($this->Index), |
506: | true, |
507: | ); |
508: | } |
509: | |
510: | |
511: | |
512: | |
513: | public function reverse() |
514: | { |
515: | return $this->maybeReplaceHeaders( |
516: | array_reverse($this->Headers, true), |
517: | array_reverse($this->Index, true), |
518: | true, |
519: | ); |
520: | } |
521: | |
522: | |
523: | |
524: | |
525: | |
526: | |
527: | public function map(callable $callback, int $mode = CollectionInterface::CALLBACK_USE_VALUE) |
528: | { |
529: | throw new MethodNotImplementedException( |
530: | static::class, |
531: | __FUNCTION__, |
532: | CollectionInterface::class, |
533: | ); |
534: | } |
535: | |
536: | |
537: | |
538: | |
539: | |
540: | |
541: | public function filter(callable $callback, int $mode = CollectionInterface::CALLBACK_USE_VALUE) |
542: | { |
543: | $index = $this->Index; |
544: | $prev = null; |
545: | $item = null; |
546: | $lower = null; |
547: | $count = 0; |
548: | $changed = false; |
549: | |
550: | foreach ($index as $nextLower => $headerIndex) { |
551: | $nextValue = null; |
552: | if ($mode & CollectionInterface::CALLBACK_USE_BOTH !== CollectionInterface::CALLBACK_USE_KEY) { |
553: | foreach ($headerIndex as $i) { |
554: | $nextValue[] = Arr::first($this->Headers[$i]); |
555: | } |
556: | } |
557: | $next = $this->getCallbackValue($mode, $nextLower, $nextValue); |
558: | if ($count++) { |
559: | |
560: | |
561: | if (!$callback($item, $next, $prev)) { |
562: | unset($index[$lower]); |
563: | $changed = true; |
564: | } |
565: | $prev = $item; |
566: | } |
567: | $item = $next; |
568: | $lower = $nextLower; |
569: | } |
570: | |
571: | if ($count && !$callback($item, null, $prev)) { |
572: | unset($index[$lower]); |
573: | $changed = true; |
574: | } |
575: | |
576: | return $changed ? $this->replaceHeaders(null, $index) : $this; |
577: | } |
578: | |
579: | |
580: | |
581: | |
582: | public function only(array $keys) |
583: | { |
584: | return $this->onlyIn(Arr::toIndex($keys)); |
585: | } |
586: | |
587: | |
588: | |
589: | |
590: | public function onlyIn(array $index) |
591: | { |
592: | return $this->maybeReplaceHeaders( |
593: | null, |
594: | array_intersect_key( |
595: | $this->Index, |
596: | array_change_key_case($index) |
597: | ) |
598: | ); |
599: | } |
600: | |
601: | |
602: | |
603: | |
604: | public function except(array $keys) |
605: | { |
606: | return $this->exceptIn(Arr::toIndex($keys)); |
607: | } |
608: | |
609: | |
610: | |
611: | |
612: | public function exceptIn(array $index) |
613: | { |
614: | return $this->maybeReplaceHeaders( |
615: | null, |
616: | array_diff_key( |
617: | $this->Index, |
618: | array_change_key_case($index) |
619: | ) |
620: | ); |
621: | } |
622: | |
623: | |
624: | |
625: | |
626: | public function slice(int $offset, ?int $length = null) |
627: | { |
628: | return $this->maybeReplaceHeaders( |
629: | null, |
630: | array_slice($this->Index, $offset, $length, true) |
631: | ); |
632: | } |
633: | |
634: | |
635: | |
636: | |
637: | public function toArray(): array |
638: | { |
639: | return $this->Items; |
640: | } |
641: | |
642: | |
643: | |
644: | |
645: | public function jsonSerialize(): array |
646: | { |
647: | foreach ($this->headers() as $name => $value) { |
648: | $headers[] = [ |
649: | 'name' => $name, |
650: | 'value' => $value, |
651: | ]; |
652: | } |
653: | return $headers ?? []; |
654: | } |
655: | |
656: | |
657: | |
658: | |
659: | public function pop(&$last = null) |
660: | { |
661: | if (!$this->Index) { |
662: | $last = null; |
663: | return $this; |
664: | } |
665: | $index = $this->Index; |
666: | array_pop($index); |
667: | $last = Arr::last($this->Items); |
668: | return $this->replaceHeaders(null, $index); |
669: | } |
670: | |
671: | |
672: | |
673: | |
674: | public function shift(&$first = null) |
675: | { |
676: | if (!$this->Index) { |
677: | $first = null; |
678: | return $this; |
679: | } |
680: | $index = $this->Index; |
681: | array_shift($index); |
682: | $first = Arr::first($this->Items); |
683: | return $this->replaceHeaders(null, $index); |
684: | } |
685: | |
686: | |
687: | |
688: | |
689: | public function trailers() |
690: | { |
691: | return $this->whereIsTrailer(); |
692: | } |
693: | |
694: | |
695: | |
696: | |
697: | public function withoutTrailers() |
698: | { |
699: | return $this->whereIsTrailer(false); |
700: | } |
701: | |
702: | |
703: | |
704: | |
705: | public function getLines( |
706: | string $format = '%s: %s', |
707: | ?string $emptyFormat = null |
708: | ): array { |
709: | foreach ($this->headers() as $key => $value) { |
710: | if ($emptyFormat !== null && trim($value) === '') { |
711: | $lines[] = sprintf($emptyFormat, $key, ''); |
712: | continue; |
713: | } |
714: | $lines[] = sprintf($format, $key, $value); |
715: | } |
716: | return $lines ?? []; |
717: | } |
718: | |
719: | |
720: | |
721: | |
722: | public function getHeaders(): array |
723: | { |
724: | return $this->doGetHeaders(true); |
725: | } |
726: | |
727: | |
728: | |
729: | |
730: | public function getHeaderLines(): array |
731: | { |
732: | foreach ($this->Items as $lower => $values) { |
733: | $lines[$lower] = implode(',', $values); |
734: | } |
735: | return $lines ?? []; |
736: | } |
737: | |
738: | |
739: | |
740: | |
741: | public function hasHeader(string $name): bool |
742: | { |
743: | return isset($this->Items[Str::lower($name)]); |
744: | } |
745: | |
746: | |
747: | |
748: | |
749: | public function getHeader(string $name): array |
750: | { |
751: | return $this->Items[Str::lower($name)] ?? []; |
752: | } |
753: | |
754: | |
755: | |
756: | |
757: | public function getHeaderValues(string $name): array |
758: | { |
759: | $values = $this->Items[Str::lower($name)] ?? []; |
760: | if (!$values) { |
761: | return []; |
762: | } |
763: | |
764: | |
765: | return Str::splitDelimited(',', implode(',', $values)); |
766: | } |
767: | |
768: | |
769: | |
770: | |
771: | public function getHeaderLine(string $name): string |
772: | { |
773: | return $this->doGetHeaderLine($name); |
774: | } |
775: | |
776: | |
777: | |
778: | |
779: | public function getFirstHeaderLine(string $name): string |
780: | { |
781: | return $this->doGetHeaderLine($name, true); |
782: | } |
783: | |
784: | |
785: | |
786: | |
787: | public function getLastHeaderLine(string $name): string |
788: | { |
789: | return $this->doGetHeaderLine($name, false, true); |
790: | } |
791: | |
792: | |
793: | |
794: | |
795: | public function getOneHeaderLine(string $name, bool $orSame = false): string |
796: | { |
797: | return $this->doGetHeaderLine($name, false, false, true, $orSame); |
798: | } |
799: | |
800: | private function doGetHeaderLine( |
801: | string $name, |
802: | bool $first = false, |
803: | bool $last = false, |
804: | bool $one = false, |
805: | bool $orSame = false |
806: | ): string { |
807: | $values = $this->getHeaderValues($name); |
808: | if (!$values) { |
809: | return ''; |
810: | } |
811: | if ($first) { |
812: | return reset($values); |
813: | } |
814: | if ($last) { |
815: | return end($values); |
816: | } |
817: | if ($one) { |
818: | if ($orSame) { |
819: | $values = array_unique($values); |
820: | } |
821: | if (count($values) > 1) { |
822: | throw new InvalidHeaderException(sprintf( |
823: | 'HTTP header has more than one value: %s', |
824: | $name, |
825: | )); |
826: | } |
827: | return reset($values); |
828: | } |
829: | return implode(', ', $values); |
830: | } |
831: | |
832: | |
833: | |
834: | |
835: | protected function headers(): Generator |
836: | { |
837: | foreach ($this->Headers as $header) { |
838: | $value = reset($header); |
839: | $key = key($header); |
840: | yield $key => $value; |
841: | } |
842: | } |
843: | |
844: | |
845: | |
846: | |
847: | |
848: | protected function compareItems($a, $b): int |
849: | { |
850: | return $a <=> $b; |
851: | } |
852: | |
853: | protected function filterName(string $name): string |
854: | { |
855: | if (!Regex::match(self::HTTP_HEADER_FIELD_NAME, $name)) { |
856: | throw new InvalidArgumentException(sprintf('Invalid header name: %s', $name)); |
857: | } |
858: | return $name; |
859: | } |
860: | |
861: | protected function filterValue(string $value): string |
862: | { |
863: | $value = Regex::replace('/\r\n\h+/', ' ', trim($value, " \t")); |
864: | if (!Regex::match(self::HTTP_HEADER_FIELD_VALUE, $value)) { |
865: | throw new InvalidArgumentException(sprintf('Invalid header value: %s', $value)); |
866: | } |
867: | return $value; |
868: | } |
869: | |
870: | |
871: | |
872: | |
873: | |
874: | |
875: | protected function maybeReplaceHeaders(?array $headers, array $index, bool $filterHeaders = false) |
876: | { |
877: | $headers ??= $this->getIndexHeaders($index); |
878: | |
879: | if ($filterHeaders) { |
880: | $headers = $this->filterHeaders($headers); |
881: | } |
882: | |
883: | if ($headers === $this->Headers && $index === $this->Index) { |
884: | return $this; |
885: | } |
886: | |
887: | return $this->replaceHeaders($headers, $index); |
888: | } |
889: | |
890: | |
891: | |
892: | |
893: | |
894: | |
895: | protected function replaceHeaders(?array $headers, array $index) |
896: | { |
897: | $headers ??= $this->getIndexHeaders($index); |
898: | |
899: | $clone = clone $this; |
900: | $clone->Headers = $headers; |
901: | $clone->Index = $clone->filterIndex($index); |
902: | $clone->Items = $clone->doGetHeaders(); |
903: | return $clone; |
904: | } |
905: | |
906: | |
907: | |
908: | |
909: | protected function doGetHeaders(bool $preserveCase = false): array |
910: | { |
911: | foreach ($this->Index as $lower => $headerIndex) { |
912: | if ($preserveCase) { |
913: | unset($key); |
914: | } else { |
915: | $key = $lower; |
916: | } |
917: | foreach ($headerIndex as $i) { |
918: | $header = $this->Headers[$i]; |
919: | $value = reset($header); |
920: | $key ??= key($header); |
921: | $headers[$key][] = $value; |
922: | } |
923: | } |
924: | return $headers ?? []; |
925: | } |
926: | |
927: | |
928: | |
929: | |
930: | |
931: | protected function getIndexHeaders(array $index): array |
932: | { |
933: | foreach ($index as $headerIndex) { |
934: | foreach ($headerIndex as $i) { |
935: | $headers[$i] = null; |
936: | } |
937: | } |
938: | return array_intersect_key($this->Headers, $headers ?? []); |
939: | } |
940: | |
941: | |
942: | |
943: | |
944: | |
945: | private function filterHeaders(array $headers): array |
946: | { |
947: | $host = []; |
948: | foreach ($headers as $i => $header) { |
949: | if (isset(array_change_key_case($header)['host'])) { |
950: | $host[$i] = $header; |
951: | } |
952: | } |
953: | return $host + $headers; |
954: | } |
955: | |
956: | |
957: | |
958: | |
959: | |
960: | private function filterIndex(array $index): array |
961: | { |
962: | |
963: | |
964: | if (isset($index['host'])) { |
965: | $index = ['host' => $index['host']] + $index; |
966: | } |
967: | return $index; |
968: | } |
969: | |
970: | |
971: | |
972: | |
973: | |
974: | |
975: | protected function getIndexValues(array $headers, array $index): array |
976: | { |
977: | foreach ($index as $lower => $headerIndex) { |
978: | foreach ($headerIndex as $i) { |
979: | $_headers[$lower][] = $headers[$i]; |
980: | } |
981: | } |
982: | return $_headers ?? []; |
983: | } |
984: | |
985: | |
986: | |
987: | |
988: | |
989: | protected function generateItems($items): Generator |
990: | { |
991: | if ($items instanceof self) { |
992: | yield from $items->headers(); |
993: | } elseif ($items instanceof Arrayable) { |
994: | |
995: | $items = $items->toArray(); |
996: | yield from $this->filterItems($items); |
997: | } elseif (is_array($items)) { |
998: | yield from $this->filterItems($items); |
999: | } else { |
1000: | foreach ($items as $key => $value) { |
1001: | $values = (array) $value; |
1002: | if (!$values) { |
1003: | throw new InvalidArgumentException( |
1004: | sprintf('At least one value must be given for HTTP header: %s', $key) |
1005: | ); |
1006: | } |
1007: | $key = $this->filterName($key); |
1008: | foreach ($values as $value) { |
1009: | yield $key => $this->filterValue($value); |
1010: | } |
1011: | } |
1012: | } |
1013: | } |
1014: | |
1015: | |
1016: | |
1017: | |
1018: | |
1019: | protected function filterItems(array $items): array |
1020: | { |
1021: | foreach ($items as $key => $value) { |
1022: | $values = (array) $value; |
1023: | if (!$values) { |
1024: | throw new InvalidArgumentException( |
1025: | sprintf('At least one value must be given for HTTP header: %s', $key) |
1026: | ); |
1027: | } |
1028: | $key = $this->filterName($key); |
1029: | foreach ($values as $value) { |
1030: | $filtered[$key][] = $this->filterValue($value); |
1031: | } |
1032: | } |
1033: | return $filtered ?? []; |
1034: | } |
1035: | |
1036: | |
1037: | |
1038: | |
1039: | |
1040: | |
1041: | |
1042: | protected function getItems($items): array |
1043: | { |
1044: | throw new MethodNotImplementedException( |
1045: | static::class, |
1046: | __FUNCTION__, |
1047: | ReadableCollectionTrait::class, |
1048: | ); |
1049: | } |
1050: | |
1051: | |
1052: | |
1053: | |
1054: | private function extendLast(string $line) |
1055: | { |
1056: | if (!$this->Headers) { |
1057: | return $this; |
1058: | } |
1059: | $headers = $this->Headers; |
1060: | $i = array_key_last($headers); |
1061: | $header = $this->Headers[$i]; |
1062: | $value = reset($header); |
1063: | $key = key($header); |
1064: | $headers[$i][$key] = ltrim($value . $line); |
1065: | return $this->maybeReplaceHeaders($headers, $this->Index); |
1066: | } |
1067: | |
1068: | |
1069: | |
1070: | |
1071: | private function maybeIndexTrailer() |
1072: | { |
1073: | if (!$this->Headers) { |
1074: | |
1075: | throw new LogicException('No headers applied'); |
1076: | |
1077: | } |
1078: | if (!$this->Closed) { |
1079: | return $this; |
1080: | } |
1081: | $i = array_key_last($this->Headers); |
1082: | $trailers = $this->Trailers; |
1083: | $trailers[$i] = true; |
1084: | return $this->with('Trailers', $trailers); |
1085: | } |
1086: | |
1087: | |
1088: | |
1089: | |
1090: | private function whereIsTrailer(bool $value = true) |
1091: | { |
1092: | $headers = []; |
1093: | $index = []; |
1094: | foreach ($this->Index as $lower => $headerIndex) { |
1095: | foreach ($headerIndex as $i) { |
1096: | $isTrailer = $this->Trailers[$i] ?? false; |
1097: | if ($value xor $isTrailer) { |
1098: | continue; |
1099: | } |
1100: | $headers[$i] = $this->Headers[$i]; |
1101: | $index[$lower][] = $i; |
1102: | } |
1103: | } |
1104: | ksort($headers); |
1105: | return $this->maybeReplaceHeaders($headers, $index); |
1106: | } |
1107: | } |
1108: | |