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