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: * An [RFC7230]-compliant HTTP header collection
27: *
28: * Headers can be applied explicitly or by passing HTTP header fields to
29: * {@see HttpHeaders::addLine()}.
30: *
31: * @api
32: */
33: class HttpHeaders implements HttpHeadersInterface
34: {
35: /** @use ReadableCollectionTrait<string,string[]> */
36: use ReadableCollectionTrait;
37: /** @use ImmutableArrayAccessTrait<string,string[]> */
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: * [ [ Name => value ], ... ]
61: *
62: * @var array<int,non-empty-array<string,string>>
63: */
64: protected array $Headers = [];
65:
66: /**
67: * [ Lowercase name => [ index in $Headers, ... ] ]
68: *
69: * @var array<string,int[]>
70: */
71: protected array $Index = [];
72:
73: /**
74: * [ Index in $Headers => true ]
75: *
76: * @var array<int,true>
77: */
78: protected array $Trailers = [];
79:
80: protected bool $Closed = false;
81:
82: /**
83: * Trailing whitespace carried over from the previous line
84: *
85: * Applied before `obs-fold` when a header is extended over multiple lines.
86: */
87: protected ?string $Carry = null;
88:
89: /**
90: * Creates a new HttpHeaders object
91: *
92: * @param Arrayable<string,string[]|string>|iterable<string,string[]|string> $items
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: * Resolve a value to an HttpHeaders object
114: *
115: * If `$value` is a string, it is parsed as an HTTP message.
116: *
117: * @param MessageInterface|Arrayable<string,string[]|string>|iterable<string,string[]|string>|string $value
118: * @return static
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: // Remove start line
134: Arr::shift(
135: // Split on CRLF
136: explode(
137: "\r\n",
138: // Remove body if present
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: * Get the value of the Content-Length header, or null if it is not set
153: *
154: * @return int<0,max>|null
155: * @throws InvalidHeaderException if `Content-Length` is given multiple
156: * times or has an invalid value.
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: * Get the value of the Content-Type header's boundary parameter, or null if
178: * it is not set
179: *
180: * @throws InvalidHeaderException if `Content-Type` is given multiple times
181: * or has an invalid value.
182: */
183: public function getMultipartBoundary(): ?string
184: {
185: if (!$this->hasHeader(HttpHeader::CONTENT_TYPE)) {
186: return null;
187: }
188:
189: try {
190: return Http::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: * Get preferences applied to the Prefer header as per [RFC7240]
202: *
203: * @return array<string,array{value:string,parameters:array<string,string>}>
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: /** @var array<string,string> */
213: $params = Http::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: * Merge preferences into a Prefer header value as per [RFC7240]
228: *
229: * @param array<string,array{value:string,parameters?:array<string,string>}|string> $preferences
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] = Http::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: * Get the value of the Retry-After header in seconds, or null if it has an
250: * invalid value or is not set
251: *
252: * @return int<0,max>|null
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: * @inheritDoc
271: */
272: public function __toString(): string
273: {
274: return implode("\r\n", $this->getLines());
275: }
276:
277: /**
278: * @inheritDoc
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: // Section 3.2.4 of [RFC7230]: "A user agent that receives an
303: // obs-fold in a response message that is not within a message/http
304: // container MUST replace each received obs-fold with one or more SP
305: // octets prior to interpreting the field value."
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: // Whitespace after name is not allowed since [RFC7230] (see
329: // Section 3.2.4) and should be removed from upstream responses
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: /** @var string $name */
340: /** @var string $value */
341: return $this->add($name, $value)->maybeIndexTrailer()->with('Carry', $carry);
342: }
343:
344: /**
345: * @inheritDoc
346: */
347: public function hasLastLine(): bool
348: {
349: return $this->Closed;
350: }
351:
352: /**
353: * @inheritDoc
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: * @inheritDoc
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: // Return `$this` if existing values are being reapplied
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: * @inheritDoc
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: * @inheritDoc
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: // Checking against $this->Index instead of $index means any
449: // duplicates in $items will be preserved
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: // Maintain the order of $index for comparison
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: * @inheritDoc
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: * @inheritDoc
492: */
493: public function canonicalize()
494: {
495: return $this->maybeReplaceHeaders($this->Headers, $this->Index, true);
496: }
497:
498: /**
499: * @inheritDoc
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: * @inheritDoc
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: * @template T of string[]|string|array{string,string[]}
524: *
525: * @param callable(T, T|null $next, T|null $prev): string[] $callback
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: * @template T of string[]|string|array{string,string[]}
538: *
539: * @param callable(T, T|null $next, T|null $prev): bool $callback
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: /** @var T $item */
560: /** @var T $next */
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: /** @var T $item */
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: * @inheritDoc
581: */
582: public function only(array $keys)
583: {
584: return $this->onlyIn(Arr::toIndex($keys));
585: }
586:
587: /**
588: * @inheritDoc
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: * @inheritDoc
603: */
604: public function except(array $keys)
605: {
606: return $this->exceptIn(Arr::toIndex($keys));
607: }
608:
609: /**
610: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
636: */
637: public function toArray(): array
638: {
639: return $this->Items;
640: }
641:
642: /**
643: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
688: */
689: public function trailers()
690: {
691: return $this->whereIsTrailer();
692: }
693:
694: /**
695: * @inheritDoc
696: */
697: public function withoutTrailers()
698: {
699: return $this->whereIsTrailer(false);
700: }
701:
702: /**
703: * @inheritDoc
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: * @inheritDoc
721: */
722: public function getHeaders(): array
723: {
724: return $this->doGetHeaders(true);
725: }
726:
727: /**
728: * @inheritDoc
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: * @inheritDoc
740: */
741: public function hasHeader(string $name): bool
742: {
743: return isset($this->Items[Str::lower($name)]);
744: }
745:
746: /**
747: * @inheritDoc
748: */
749: public function getHeader(string $name): array
750: {
751: return $this->Items[Str::lower($name)] ?? [];
752: }
753:
754: /**
755: * @inheritDoc
756: */
757: public function getHeaderValues(string $name): array
758: {
759: $values = $this->Items[Str::lower($name)] ?? [];
760: if (!$values) {
761: return [];
762: }
763: return Str::splitDelimited(',', implode(',', $values));
764: }
765:
766: /**
767: * @inheritDoc
768: */
769: public function getHeaderLine(string $name): string
770: {
771: return $this->doGetHeaderLine($name);
772: }
773:
774: /**
775: * @inheritDoc
776: */
777: public function getFirstHeaderLine(string $name): string
778: {
779: return $this->doGetHeaderLine($name, true);
780: }
781:
782: /**
783: * @inheritDoc
784: */
785: public function getLastHeaderLine(string $name): string
786: {
787: return $this->doGetHeaderLine($name, false, true);
788: }
789:
790: /**
791: * @inheritDoc
792: */
793: public function getOneHeaderLine(string $name): string
794: {
795: return $this->doGetHeaderLine($name, false, false, true);
796: }
797:
798: private function doGetHeaderLine(
799: string $name,
800: bool $first = false,
801: bool $last = false,
802: bool $one = false
803: ): string {
804: $values = $this->Items[Str::lower($name)] ?? [];
805: if (!$values) {
806: return '';
807: }
808: $line = implode(', ', $values);
809: if (!($first || $last || $one)) {
810: return $line;
811: }
812: $values = Str::splitDelimited(',', $line);
813: if ($one && count($values) > 1) {
814: throw new InvalidHeaderException(sprintf(
815: 'HTTP header has more than one value: %s',
816: $name,
817: ));
818: }
819: if ($last) {
820: return end($values);
821: }
822: return reset($values);
823: }
824:
825: /**
826: * @return Generator<string,string>
827: */
828: protected function headers(): Generator
829: {
830: foreach ($this->Headers as $header) {
831: $value = reset($header);
832: $key = key($header);
833: yield $key => $value;
834: }
835: }
836:
837: /**
838: * @param string[] $a
839: * @param string[] $b
840: */
841: protected function compareItems($a, $b): int
842: {
843: return $a <=> $b;
844: }
845:
846: protected function filterName(string $name): string
847: {
848: if (!Regex::match(self::HTTP_HEADER_FIELD_NAME, $name)) {
849: throw new InvalidArgumentException(sprintf('Invalid header name: %s', $name));
850: }
851: return $name;
852: }
853:
854: protected function filterValue(string $value): string
855: {
856: $value = Regex::replace('/\r\n\h+/', ' ', trim($value, " \t"));
857: if (!Regex::match(self::HTTP_HEADER_FIELD_VALUE, $value)) {
858: throw new InvalidArgumentException(sprintf('Invalid header value: %s', $value));
859: }
860: return $value;
861: }
862:
863: /**
864: * @param array<int,non-empty-array<string,string>>|null $headers
865: * @param array<string,int[]> $index
866: * @return static
867: */
868: protected function maybeReplaceHeaders(?array $headers, array $index, bool $filterHeaders = false)
869: {
870: $headers ??= $this->getIndexHeaders($index);
871:
872: if ($filterHeaders) {
873: $headers = $this->filterHeaders($headers);
874: }
875:
876: if ($headers === $this->Headers && $index === $this->Index) {
877: return $this;
878: }
879:
880: return $this->replaceHeaders($headers, $index);
881: }
882:
883: /**
884: * @param array<int,non-empty-array<string,string>>|null $headers
885: * @param array<string,int[]> $index
886: * @return static
887: */
888: protected function replaceHeaders(?array $headers, array $index)
889: {
890: $headers ??= $this->getIndexHeaders($index);
891:
892: $clone = clone $this;
893: $clone->Headers = $headers;
894: $clone->Index = $clone->filterIndex($index);
895: $clone->Items = $clone->doGetHeaders();
896: return $clone;
897: }
898:
899: /**
900: * @return array<string,string[]>
901: */
902: protected function doGetHeaders(bool $preserveCase = false): array
903: {
904: foreach ($this->Index as $lower => $headerIndex) {
905: if ($preserveCase) {
906: unset($key);
907: } else {
908: $key = $lower;
909: }
910: foreach ($headerIndex as $i) {
911: $header = $this->Headers[$i];
912: $value = reset($header);
913: $key ??= key($header);
914: $headers[$key][] = $value;
915: }
916: }
917: return $headers ?? [];
918: }
919:
920: /**
921: * @param array<string,int[]> $index
922: * @return array<int,non-empty-array<string,string>>
923: */
924: protected function getIndexHeaders(array $index): array
925: {
926: foreach ($index as $headerIndex) {
927: foreach ($headerIndex as $i) {
928: $headers[$i] = null;
929: }
930: }
931: return array_intersect_key($this->Headers, $headers ?? []);
932: }
933:
934: /**
935: * @param array<int,non-empty-array<string,string>> $headers
936: * @return array<int,non-empty-array<string,string>>
937: */
938: private function filterHeaders(array $headers): array
939: {
940: $host = [];
941: foreach ($headers as $i => $header) {
942: if (isset(array_change_key_case($header)['host'])) {
943: $host[$i] = $header;
944: }
945: }
946: return $host + $headers;
947: }
948:
949: /**
950: * @param array<string,int[]> $index
951: * @return array<string,int[]>
952: */
953: private function filterIndex(array $index): array
954: {
955: // According to [RFC7230] Section 5.4, "a user agent SHOULD generate
956: // Host as the first header field following the request-line"
957: if (isset($index['host'])) {
958: $index = ['host' => $index['host']] + $index;
959: }
960: return $index;
961: }
962:
963: /**
964: * @param array<array<string,string>> $headers
965: * @param array<string,int[]> $index
966: * @return array<string,array<array<string,string>>>
967: */
968: protected function getIndexValues(array $headers, array $index): array
969: {
970: foreach ($index as $lower => $headerIndex) {
971: foreach ($headerIndex as $i) {
972: $_headers[$lower][] = $headers[$i];
973: }
974: }
975: return $_headers ?? [];
976: }
977:
978: /**
979: * @param Arrayable<string,string[]|string>|iterable<string,string[]|string> $items
980: * @return Generator<string,string[]|string>
981: */
982: protected function generateItems($items): Generator
983: {
984: if ($items instanceof self) {
985: yield from $items->headers();
986: } elseif ($items instanceof Arrayable) {
987: /** @var array<string,string[]|string> */
988: $items = $items->toArray();
989: yield from $this->filterItems($items);
990: } elseif (is_array($items)) {
991: yield from $this->filterItems($items);
992: } else {
993: foreach ($items as $key => $value) {
994: $values = (array) $value;
995: if (!$values) {
996: throw new InvalidArgumentException(
997: sprintf('At least one value must be given for HTTP header: %s', $key)
998: );
999: }
1000: $key = $this->filterName($key);
1001: foreach ($values as $value) {
1002: yield $key => $this->filterValue($value);
1003: }
1004: }
1005: }
1006: }
1007:
1008: /**
1009: * @param array<string,string[]|string> $items
1010: * @return array<string,string[]>
1011: */
1012: protected function filterItems(array $items): array
1013: {
1014: foreach ($items as $key => $value) {
1015: $values = (array) $value;
1016: if (!$values) {
1017: throw new InvalidArgumentException(
1018: sprintf('At least one value must be given for HTTP header: %s', $key)
1019: );
1020: }
1021: $key = $this->filterName($key);
1022: foreach ($values as $value) {
1023: $filtered[$key][] = $this->filterValue($value);
1024: }
1025: }
1026: return $filtered ?? [];
1027: }
1028:
1029: /**
1030: * @param Arrayable<string,string[]|string>|iterable<string,string[]|string> $items
1031: * @return never
1032: *
1033: * @codeCoverageIgnore
1034: */
1035: protected function getItems($items): array
1036: {
1037: throw new MethodNotImplementedException(
1038: static::class,
1039: __FUNCTION__,
1040: ReadableCollectionTrait::class,
1041: );
1042: }
1043:
1044: /**
1045: * @return static
1046: */
1047: private function extendLast(string $line)
1048: {
1049: if (!$this->Headers) {
1050: return $this;
1051: }
1052: $headers = $this->Headers;
1053: $i = array_key_last($headers);
1054: $header = $this->Headers[$i];
1055: $value = reset($header);
1056: $key = key($header);
1057: $headers[$i][$key] = ltrim($value . $line);
1058: return $this->maybeReplaceHeaders($headers, $this->Index);
1059: }
1060:
1061: /**
1062: * @return static
1063: */
1064: private function maybeIndexTrailer()
1065: {
1066: if (!$this->Headers) {
1067: // @codeCoverageIgnoreStart
1068: throw new LogicException('No headers applied');
1069: // @codeCoverageIgnoreEnd
1070: }
1071: if (!$this->Closed) {
1072: return $this;
1073: }
1074: $i = array_key_last($this->Headers);
1075: $trailers = $this->Trailers;
1076: $trailers[$i] = true;
1077: return $this->with('Trailers', $trailers);
1078: }
1079:
1080: /**
1081: * @return static
1082: */
1083: private function whereIsTrailer(bool $value = true)
1084: {
1085: $headers = [];
1086: $index = [];
1087: foreach ($this->Index as $lower => $headerIndex) {
1088: foreach ($headerIndex as $i) {
1089: $isTrailer = $this->Trailers[$i] ?? false;
1090: if ($value xor $isTrailer) {
1091: continue;
1092: }
1093: $headers[$i] = $this->Headers[$i];
1094: $index[$lower][] = $i;
1095: }
1096: }
1097: ksort($headers);
1098: return $this->maybeReplaceHeaders($headers, $index);
1099: }
1100: }
1101: