1212use Lkrms \Http \Catalog \HttpHeader ;
1313use Lkrms \Http \Contract \AccessTokenInterface ;
1414use Lkrms \Http \Contract \HttpHeadersInterface ;
15- use Lkrms \Support \Catalog \RegularExpression as Regex ;
1615use Lkrms \Utility \Arr ;
1716use Lkrms \Utility \Pcre ;
1817use Generator ;
1918use LogicException ;
2019
2120/**
22- * A collection of HTTP headers
21+ * A collection of [RFC7230]-compliant HTTP headers
2322 */
2423class HttpHeaders implements HttpHeadersInterface, IImmutable
2524{
@@ -31,6 +30,25 @@ class HttpHeaders implements HttpHeadersInterface, IImmutable
3130 withPropertyValue as with;
3231 }
3332
33+ private const HTTP_HEADER_FIELD_NAME = '/^[-0-9a-z!#$%& \'*+.^_`|~]++$/iD ' ;
34+
35+ private const HTTP_HEADER_FIELD_VALUE = '/^([\x21-\x7e\x80-\xff]++(?:\h++[\x21-\x7e\x80-\xff]++)*+)?$/D ' ;
36+
37+ private const HTTP_HEADER_FIELD = <<<'REGEX'
38+ / ^
39+ (?(DEFINE)
40+ (?<token> [-0-9a-z!#$%&'*+.^_`|~]++ )
41+ (?<field_vchar> [\x21-\x7e\x80-\xff]++ )
42+ (?<field_content> (?&field_vchar) (?: \h++ (?&field_vchar) )*+ )
43+ )
44+ (?:
45+ (?<name> (?&token) ) (?<bad_whitespace> \h++ )?+ : \h*+ (?<value> (?&field_content)? ) |
46+ \h++ (?<extended> (?&field_content)? )
47+ )
48+ (?<carry> \h++ )?
49+ $ /xiD
50+ REGEX;
51+
3452 /**
3553 * [ [ Name => value ], ... ]
3654 *
@@ -61,6 +79,31 @@ class HttpHeaders implements HttpHeadersInterface, IImmutable
6179 */
6280 protected ?string $ Carry = null ;
6381
82+ /**
83+ * @param Arrayable<string,string[]|string>|iterable<string,string[]|string> $items
84+ */
85+ public function __construct ($ items = [])
86+ {
87+ $ headers = [];
88+ $ index = [];
89+ $ i = -1 ;
90+ foreach ($ items as $ key => $ value ) {
91+ $ values = (array ) $ value ;
92+ if (!$ values ) {
93+ continue ;
94+ }
95+ $ lower = strtolower ($ key );
96+ $ key = $ this ->filterName ($ key );
97+ foreach ($ values as $ value ) {
98+ $ headers [++$ i ] = [$ key => $ this ->filterValue ($ value )];
99+ $ index [$ lower ][] = $ i ;
100+ }
101+ }
102+ $ this ->Headers = $ headers ;
103+ $ this ->Index = $ this ->filterIndex ($ index );
104+ $ this ->Items = $ this ->doGetHeaders ();
105+ }
106+
64107 /**
65108 * @inheritDoc
66109 */
@@ -82,8 +125,7 @@ public function addLine(string $line, bool $strict = false)
82125 $ value = null ;
83126 if ($ strict ) {
84127 $ line = substr ($ line , 0 , -2 );
85- $ regex = Regex::anchorAndDelimit (Regex::HTTP_HEADER_FIELD );
86- if (!Pcre::match ($ regex , $ line , $ matches , \PREG_UNMATCHED_AS_NULL ) ||
128+ if (!Pcre::match (self ::HTTP_HEADER_FIELD , $ line , $ matches , \PREG_UNMATCHED_AS_NULL ) ||
87129 $ matches ['bad_whitespace ' ] !== null ) {
88130 throw new InvalidArgumentException (sprintf ('Invalid HTTP header field: %s ' , $ line ));
89131 }
@@ -134,9 +176,9 @@ public function add($key, $value)
134176 $ lower = strtolower ($ key );
135177 $ headers = $ this ->Headers ;
136178 $ index = $ this ->Index ;
137- $ key = $ this ->normaliseName ($ key );
179+ $ key = $ this ->filterName ($ key );
138180 foreach ($ values as $ value ) {
139- $ headers [] = [$ key => $ this ->normaliseValue ($ value )];
181+ $ headers [] = [$ key => $ this ->filterValue ($ value )];
140182 $ index [$ lower ][] = array_key_last ($ headers );
141183 }
142184 return $ this ->replaceHeaders ($ headers , $ index );
@@ -172,9 +214,9 @@ public function set($key, $value)
172214 }
173215 unset($ index [$ lower ]);
174216 }
175- $ key = $ this ->normaliseName ($ key );
217+ $ key = $ this ->filterName ($ key );
176218 foreach ($ values as $ value ) {
177- $ headers [] = [$ key => $ this ->normaliseValue ($ value )];
219+ $ headers [] = [$ key => $ this ->filterValue ($ value )];
178220 $ index [$ lower ][] = array_key_last ($ headers );
179221 }
180222 return $ this ->replaceHeaders ($ headers , $ index );
@@ -233,10 +275,10 @@ public function merge($items, bool $preserveExisting = false)
233275 // Maintain the order of $index for comparison
234276 $ index [$ lower ] = [];
235277 }
236- $ key = $ this ->normaliseName ($ key );
278+ $ key = $ this ->filterName ($ key );
237279 foreach ($ values as $ value ) {
238280 $ applied = true ;
239- $ headers [] = [$ key => $ this ->normaliseValue ($ value )];
281+ $ headers [] = [$ key => $ this ->filterValue ($ value )];
240282 $ index [$ lower ][] = array_key_last ($ headers );
241283 }
242284 }
@@ -486,18 +528,18 @@ protected function compareItems($a, $b): int
486528 return $ a <=> $ b ;
487529 }
488530
489- protected function normaliseName (string $ name ): string
531+ protected function filterName (string $ name ): string
490532 {
491- if (!Pcre::match (Regex:: anchorAndDelimit (Regex:: HTTP_HEADER_FIELD_NAME ) , $ name )) {
533+ if (!Pcre::match (self :: HTTP_HEADER_FIELD_NAME , $ name )) {
492534 throw new InvalidArgumentException (sprintf ('Invalid header name: %s ' , $ name ));
493535 }
494536 return $ name ;
495537 }
496538
497- protected function normaliseValue (string $ value ): string
539+ protected function filterValue (string $ value ): string
498540 {
499- $ value = Pcre::replace ('/\r\n\h+/ ' , ' ' , trim ($ value ));
500- if (!Pcre::match (Regex:: anchorAndDelimit (Regex:: HTTP_HEADER_FIELD_VALUE ) , $ value )) {
541+ $ value = Pcre::replace ('/\r\n\h+/ ' , ' ' , trim ($ value, " \t" ));
542+ if (!Pcre::match (self :: HTTP_HEADER_FIELD_VALUE , $ value )) {
501543 throw new InvalidArgumentException (sprintf ('Invalid header value: %s ' , $ value ));
502544 }
503545 return $ value ;
@@ -534,7 +576,7 @@ protected function replaceHeaders(?array $headers, array $index)
534576
535577 $ clone = $ this ->clone ();
536578 $ clone ->Headers = $ headers ;
537- $ clone ->Index = $ index ;
579+ $ clone ->Index = $ clone -> filterIndex ( $ index) ;
538580 $ clone ->Items = $ clone ->doGetHeaders ();
539581 return $ clone ;
540582 }
@@ -574,6 +616,20 @@ protected function getIndexHeaders(array $index): array
574616 return array_intersect_key ($ this ->Headers , $ headers ?? []);
575617 }
576618
619+ /**
620+ * @param array<string,int[]> $index
621+ * @return array<string,int[]>
622+ */
623+ private function filterIndex (array $ index ): array
624+ {
625+ // According to [RFC7230] Section 5.4, "a user agent SHOULD generate
626+ // Host as the first header field following the request-line"
627+ if (isset ($ index ['host ' ])) {
628+ $ index = ['host ' => $ index ['host ' ]] + $ index ;
629+ }
630+ return $ index ;
631+ }
632+
577633 /**
578634 * @param array<array<string,string>> $headers
579635 * @param array<string,int[]> $index
0 commit comments