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 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: * 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 = 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: * 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] = 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: * 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: // [RFC7230], Section 7: "a recipient MUST parse and ignore a reasonable
764: // number of empty list elements"
765: return Str::splitDelimited(',', implode(',', $values));
766: }
767:
768: /**
769: * @inheritDoc
770: */
771: public function getHeaderLine(string $name): string
772: {
773: return $this->doGetHeaderLine($name);
774: }
775:
776: /**
777: * @inheritDoc
778: */
779: public function getFirstHeaderLine(string $name): string
780: {
781: return $this->doGetHeaderLine($name, true);
782: }
783:
784: /**
785: * @inheritDoc
786: */
787: public function getLastHeaderLine(string $name): string
788: {
789: return $this->doGetHeaderLine($name, false, true);
790: }
791:
792: /**
793: * @inheritDoc
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: * @return Generator<string,string>
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: * @param string[] $a
846: * @param string[] $b
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: * @param array<int,non-empty-array<string,string>>|null $headers
872: * @param array<string,int[]> $index
873: * @return static
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: * @param array<int,non-empty-array<string,string>>|null $headers
892: * @param array<string,int[]> $index
893: * @return static
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: * @return array<string,string[]>
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: * @param array<string,int[]> $index
929: * @return array<int,non-empty-array<string,string>>
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: * @param array<int,non-empty-array<string,string>> $headers
943: * @return array<int,non-empty-array<string,string>>
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: * @param array<string,int[]> $index
958: * @return array<string,int[]>
959: */
960: private function filterIndex(array $index): array
961: {
962: // According to [RFC7230] Section 5.4, "a user agent SHOULD generate
963: // Host as the first header field following the request-line"
964: if (isset($index['host'])) {
965: $index = ['host' => $index['host']] + $index;
966: }
967: return $index;
968: }
969:
970: /**
971: * @param array<array<string,string>> $headers
972: * @param array<string,int[]> $index
973: * @return array<string,array<array<string,string>>>
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: * @param Arrayable<string,string[]|string>|iterable<string,string[]|string> $items
987: * @return Generator<string,string[]|string>
988: */
989: protected function generateItems($items): Generator
990: {
991: if ($items instanceof self) {
992: yield from $items->headers();
993: } elseif ($items instanceof Arrayable) {
994: /** @var array<string,string[]|string> */
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: * @param array<string,string[]|string> $items
1017: * @return array<string,string[]>
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: * @param Arrayable<string,string[]|string>|iterable<string,string[]|string> $items
1038: * @return never
1039: *
1040: * @codeCoverageIgnore
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: * @return static
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: * @return static
1070: */
1071: private function maybeIndexTrailer()
1072: {
1073: if (!$this->Headers) {
1074: // @codeCoverageIgnoreStart
1075: throw new LogicException('No headers applied');
1076: // @codeCoverageIgnoreEnd
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: * @return static
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: