8000 [HttpFoundation] Add HeaderUtility class · symfony/symfony@96f7606 · GitHub
[go: up one dir, main page]

Skip to content

Commit 96f7606

Browse files
lyrixxc960657
authored andcommitted
[HttpFoundation] Add HeaderUtility class
1 parent 306c599 commit 96f7606

15 files changed

+330
-96
lines changed

src/Symfony/Component/HttpFoundation/AcceptHeader.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,17 @@ public static function fromString($headerValue)
5252
{
5353
$index = 0;
5454

55-
return new self(array_map(function ($itemValue) use (&$index) {
56-
$item = AcceptHeaderItem::fromString($itemValue);
55+
$parts = HeaderUtils::split((string) $headerValue, ',;=');
56+
57+
return new self(array_map(function ($subParts) use (&$index) {
58+
$part = array_shift($subParts);
59+
$attributes = HeaderUtils::combineParts($subParts);
60+
61+
$item = new AcceptHeaderItem($part[0], $attributes);
5762
$item->setIndex($index++);
5863

5964
return $item;
60-
}, preg_split('/\s*(?:,*("[^"]+"),*|,*(\'[^\']+\'),*|,+)\s*/', $headerValue, 0, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE)));
65+
}, $parts));
6166
}
6267

6368
/**

src/Symfony/Component/HttpFoundation/AcceptHeaderItem.php

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -40,24 +40,12 @@ public function __construct(string $value, array $attributes = array())
4040
*/
4141
public static function fromString($itemValue)
4242
{
43-
$bits = preg_split('/\s*(?:;*("[^"]+");*|;*(\'[^\']+\');*|;+)\s*/', $itemValue, 0, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
44-
$value = array_shift($bits);
45-
$attributes = array();
46-
47-
$lastNullAttribute = null;
48-
foreach ($bits as $bit) {
49-
if (($start = substr($bit, 0, 1)) === ($end = substr($bit, -1)) && ('"' === $start || '\'' === $start)) {
50-
$attributes[$lastNullAttribute] = substr($bit, 1, -1);
51-
} elseif ('=' === $end) {
52-
$lastNullAttribute = $bit = substr($bit, 0, -1);
53-
$attributes[$bit] = null;
54-
} else {
55-
$parts = explode('=', $bit);
56-
$attributes[$parts[0]] = isset($parts[1]) && strlen($parts[1]) > 0 ? $parts[1] : '';
57-
}
58-
}
43+
$parts = HeaderUtils::split($itemValue, ';=');
44+
45+
$part = array_shift($parts);
46+
$attributes = HeaderUtils::combineParts($parts, 1);
5947

60-
return new self(($start = substr($value, 0, 1)) === ($end = substr($value, -1)) && ('"' === $start || '\'' === $start) ? substr($value, 1, -1) : $value, $attributes);
48+
return new self($part[0], $attributes);
6149
}
6250

6351
/**
@@ -69,9 +57,7 @@ public function __toString()
6957
{
7058
$string = $this->value.($this->quality < 1 ? ';q='.$this->quality : '');
7159
if (count($this->attributes) > 0) {
72-
$string .= ';'.implode(';', array_map(function ($name, $value) {
73-
return sprintf(preg_match('/[,;=]/', $value) ? '%s="%s"' : '%s=%s', $name, $value);
74-
}, array_keys($this->attributes), $this->attributes));
60+
$string .= '; '.HeaderUtils::joinAssoc($this->attributes, ';');
7561
}
7662

7763
return $string;

src/Symfony/Component/HttpFoundation/BinaryFileResponse.php

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -218,17 +218,12 @@ public function prepare(Request $request)
218218
if ('x-accel-redirect' === strtolower($type)) {
219219
// Do X-Accel-Mapping substitutions.
220220
// @link http://wiki.nginx.org/X-accel#X-Accel-Redirect
221-
foreach (explode(',', $request->headers->get('X-Accel-Mapping', '')) as $mapping) {
222-
$mapping = explode('=', $mapping, 2);
223-
224-
if (2 === count($mapping)) {
225-
$pathPrefix = trim($mapping[0]);
226-
$location = trim($mapping[1]);
227-
228-
if (substr($path, 0, strlen($pathPrefix)) === $pathPrefix) {
229-
$path = $location.substr($path, strlen($pathPrefix));
230-
break;
231-
}
221+
$parts = HeaderUtils::split($request->headers->get('X-Accel-Mapping', ''), ',=');
222+
$mappings = HeaderUtils::combineParts($parts);
223+
foreach ($mappings as $pathPrefix => $location) {
224+
if (substr($path, 0, strlen($pathPrefix)) === $pathPrefix) {
225+
$path = $location.substr($path, strlen($pathPrefix));
226+
break;
232227
}
233228
}
234229
}

src/Symfony/Component/HttpFoundation/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ CHANGELOG
1616
`IniSizeFileException`, `NoFileException`, `NoTmpDirFileException`, `PartialFileException` to
1717
handle failed `UploadedFile`.
1818
* added `MigratingSessionHandler` for migrating between two session handlers without losing sessions
19+
* added `HeaderUtils`.
1920

2021
4.0.0
2122
-----

src/Symfony/Component/HttpFoundation/Cookie.php

Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -50,34 +50,20 @@ public static function fromString($cookie, $decode = false)
5050
'raw' => !$decode,
5151
'samesite' => null,
5252
);
53-
foreach (explode(';', $cookie) as $part) {
54-
if (false === strpos($part, '=')) {
55-
$key = trim($part);
56-
$value = true;
57-
} else {
58-
list($key, $value) = explode('=', trim($part), 2);
59-
$key = trim($key);
60-
$value = trim($value);
61-
}
62-
if (!isset($data['name'])) {
63-
$data['name'] = $decode ? urldecode($key) : $key;
64-
$data['value'] = true === $value ? null : ($decode ? urldecode($value) : $value);
65-
continue;
66-
}
67-
switch ($key = strtolower($key)) {
68-
case 'name':
69-
case 'value':
70-
break;
71-
case 'max-age':
72-
$data['expires'] = time() + (int) $value;
73-
break;
74-
default:
75-
$data[$key] = $value;
76-
break;
77-
}
53+
54+
$parts = HeaderUtils::split($cookie, ';=');
55+
$part = array_shift($parts);
56+
57+
$name = $decode ? urldecode($part[0]) : $part[0];
< 1CF5 /td>58+
$value = isset($part[1]) ? ($decode ? urldecode($part[1]) : $part[1]) : null;
59+
60+
$data = HeaderUtils::combineParts($parts) + $data;
61+
62+
if (isset($data['max-age'])) {
63+
$data['expires'] = time() + (int) $data['max-age'];
7864
}
7965

80-
return new static($data['name'], $data['value'], $data['expires'], $data['path'], $data['domain'], $data['secure'], $data['httponly'], $data['raw'], $data['samesite']);
66+
return new static($name, $value, $data['expires'], $data['path'], $data['domain'], $data['secure'], $data['httponly'], $data['raw'], $data['samesite']);
8167
}
8268

8369
/**

src/Symfony/Component/HttpFoundation/HeaderBag.php

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -294,21 +294,9 @@ public function count()
294294

295295
protected function getCacheControlHeader()
296296
{
297-
$parts = array();
298297
ksort($this->cacheControl);
299-
foreach ($this->cacheControl as $key => $value) {
300-
if (true === $value) {
301-
$parts[] = $key;
302-
} else {
303-
if (preg_match('#[^a-zA-Z0-9._-]#', $value)) {
304-
$value = '"'.$value.'"';
305-
}
306298

307-
$parts[] = "$key=$value";
308-
}
309-
}
310-
311-
return implode(', ', $parts);
299+
return HeaderUtils::joinAssoc($this->cacheControl, ',');
312300
}
313301

314302
/**
@@ -320,12 +308,8 @@ protected function getCacheControlHeader()
320308
*/
321309
protected function parseCacheControl($header)
322310
{
323-
$cacheControl = array();
324-
preg_match_all('#([a-zA-Z][a-zA-Z_-]*)\s*(?:=(?:"([^"]*)"|([^ \t",;]*)))?#', $header, $matches, PREG_SET_ORDER);
325-
foreach ($matches as $match) {
326-
$cacheControl[strtolower($match[1])] = isset($match[3]) ? $match[3] : (isset($match[2]) ? $match[2] : true);
327-
}
311+
$parts = HeaderUtils::split($header, ',=');
328312

329-
return $cacheControl;
313+
return HeaderUtils::combineParts($parts);
330314
}
331315
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpFoundation;
13+
14+
/**
15+
* HTTP header utility functions.
16+
*
17+
* @author Christian Schmidt <github@chsc.dk>
18+
*/
19+
class HeaderUtils
20+
{
21+
/**
22+
* This class should not be instantiated.
23+
*/
24+
private function __construct()
25+
{
26+
}
27+
28+
/**
29+
* Splits an HTTP header by one or more separators.
30+
*
31+
* Example:
32+
*
33+
* HeaderUtils::split("da, en-gb;q=0.8", ",;")
34+
* // => array(array("da"), array("en-gb"), array("q", "0.8"))
35+
*
36+
* @param string $header HTTP header value
37+
* @param string $separators List of characters to split on, ordered by
38+
* precedence, e.g. ",", ";=", or ",;="
39+
*
40+
* @return array Nested array with as many levels as there are characters in
41+
* $separators
42+
*/
43+
public static function split(string $header, string $separators): array
44+
{
45+
$quotedSeparators = preg_quote($separators);
46+
47+
preg_match_all('
48+
/
49+
(?!\s)
50+
(?:
51+
# quoted-string
52+
"(?:[^"\\\\]|\\\\.)*(?:"|\\\\|$)
53+
|
54+
# token
55+
[^"'.$quotedSeparators.']+
56+
)+
57+
(?<!\s)
58+
|
59+
# separator
60+
\s*
61+
(?<separator>['.$quotedSeparators.'])
62+
\s*
63+
/x', trim($header), $matches, PREG_SET_ORDER);
64+
65+
return self::groupParts($matches, $separators);
66+
}
67+
68+
/**
69+
* Combines an array of arrays into one associative array.
70+
*
71+
* Each of the nested arrays should have one or two elements. The first
72+
* value will be used as the keys in the associative array, and the second
73+
* will be used as the values, or true if the nested array only contains one
74+
* element.
75+
*
76+
* Example:
77+
*
78+
* HeaderUtils::combineParts(array(array("foo", "abc"), array("bar")))
79+
* // => array("foo" => "abc", "bar" => true)
80+
*/
81+
public static function combineParts(array $parts): array
82+
{
83+
$assoc = array();
84+
foreach ($parts as $part) {
85+
$name = strtolower($part[0]);
86+
$value = $part[1] ?? true;
87+
$assoc[$name] = $value;
88+
}
89+
90+
return $assoc;
91+
}
92+
93+
/**
94+
* Joins an associative array into a string for use in an HTTP header.
95+
*
96+
* The key and value of each entry are joined with "=", and all entries
97+
* is joined with the specified separator and an additional space (for
98+
* readability). Values are quoted if necessary.
99+
*
100+
* Example:
101+
*
102+
* HeaderUtils::joinAssoc(array("foo" => "abc", "bar" => true, "baz" => "a b c"), ",")
103+
* // => 'foo=bar, baz, baz="a b c"'
104+
*/
105+
public static function joinAssoc(array $assoc, string $separator): string
106+
{
107+
$parts = array();
108+
foreach ($assoc as $name => $value) {
109+
if (true === $value) {
110+
$parts[] = $name;
111+
} else {
112+
$parts[] = $name.'='.self::quote($value);
113+
}
114+
}
115+
116+
return implode($separator.' ', $parts);
117+
}
118+
119+
/**
120+
* Encodes a string as a quoted string, if necessary.
121+
*
122+
* If a string contains characters not allowed by the "token" construct in
123+
* the HTTP specification, it is backslash-escaped and enclosed in quotes
124+
* to match the "quoted-string" construct.
125+
*/
126+
public static function quote(string $s): string
127+
{
128+
if (preg_match('/^[a-z0-9!#$%&\'*.^_`|~-]+$/i', $s)) {
129+
return $s;
130+
}
131+
132+
return '"'.addcslashes($s, '"\\"').'"';
133+
}
134+
135+
/**
136+
* Decodes a quoted string.
137+
*
138+
* If passed an unquoted string that matches the "token" construct (as
139+
* defined in the HTTP specification), it is passed through verbatimly.
140+
*/
141+
public static function unquote(string $s): string
142+
{
143+
return preg_replace('/\\\\(.)|"/', '$1', $s);
144+
}
145+
146+
private static function groupParts(array $matches, string $separators): array
147+
{
148+
$separator = $separators[0];
149+
$partSeparators = substr($separators, 1);
150+
151+
$i = 0;
152+
$partMatches = array();
153+
foreach ($matches as $match) {
154+
if (isset($match['separator']) && $match['separator'] === $separator) {
155+
++$i;
156+
} else {
157+
$partMatches[$i][] = $match;
158+
}
159+
}
160+
161+
$parts = array();
162+
if ($partSeparators) {
163+
foreach ($partMatches as $matches) {
164+
$parts[] = self::groupParts($matches, $partSeparators);
165+
}
166+
} else {
167+
foreach ($partMatches as $matches) {
168+
$parts[] = self::unquote($matches[0][0]);
169+
}
170+
}
171+
172+
return $parts;
173+
}
174+
}

src/Symfony/Component/HttpFoundation/Request.php

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1944,8 +1944,16 @@ private function getTrustedValues($type, $ip = null)
19441944
}
19451945

19461946
if ((self::$trustedHeaderSet & self::HEADER_FORWARDED) && $this->headers->has(self::$trustedHeaders[self::HEADER_FORWARDED])) {
1947-
$forwardedValues = $this->headers->get(self::$trustedHeaders[self::HEADER_FORWARDED]);
1948-
$forwardedValues = preg_match_all(sprintf('{(?:%s)=(?:"?\[?)([a-zA-Z0-9\.:_\-/]*+)}', self::$forwardedParams[$type]), $forwardedValues, $matches) ? $matches[1] : array();
1947+
$forwarded = $this->headers->get(self::$trustedHeaders[self::HEADER_FORWARDED]);
1948+
$parts = HeaderUtils::split($forwarded, ',;=');
1949+
$forwardedValues = array();
1950+
$param = self::$forwardedParams[$type];
1951+
foreach ($parts as $subParts) {
1952+
$assoc = HeaderUtils::combineParts($subParts);
1953+
if (isset($assoc[$param])) {
1954+
$forwardedValues[] = $assoc[$param];
1955+
}
1956+
}
19491957
}
19501958

19511959
if (null !== $ip) {
@@ -1978,9 +1986,17 @@ private function normalizeAndFilterClientIps(array $clientIps, $ip)
19781986
$firstTrustedIp = null;
19791987

19801988
foreach ($clientIps as $key => $clientIp) {
1981-
// Remove port (unfortunately, it does happen)
1982-
if (preg_match('{((?:\d+\.){3}\d+)\:\d+}', $clientIp, $match)) {
1983-
$clientIps[$key] = $clientIp = $match[1];
1989+
if (strpos($clientIp, '.')) {
1990+
// Strip :port from IPv4 addresses. This is allowed in Forwarded
1991+
// and may occur in X-Forwarded-For.
1992+
$i = strpos($clientIp, ':');
1993+
if ($i) {
1994+
$clientIps[$key] = $clientIp = substr($clientIp, 0, $i);
1995+
}
1996+
} elseif ('[' == $clientIp[0]) {
1997+
// Strip brackets and :port from IPv6 addresses.
1998+
$i = strpos($clientIp, ']', 1);
1999+
$clientIps[$key] = $clientIp = substr($clientIp, 1, $i - 1);
19842000
}
19852001

19862002
if (!filter_var($clientIp, FILTER_VALIDATE_IP)) {

0 commit comments

Comments
 (0)
0