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