Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit eee2a20

Browse filesBrowse files
committed
[HttpFoundation] Add HeaderUtility class
1 parent b43bdf3 commit eee2a20
Copy full SHA for eee2a20

11 files changed

+332
-75
lines changed

‎src/Symfony/Component/HttpFoundation/AcceptHeader.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpFoundation/AcceptHeader.php
+8-3Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpFoundation/AcceptHeaderItem.php
+6-20Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -59,24 +59,12 @@ public function __construct($value, array $attributes = array())
5959
*/
6060
public static function fromString($itemValue)
6161
{
62-
$bits = preg_split('/\s*(?:;*("[^"]+");*|;*(\'[^\']+\');*|;+)\s*/', $itemValue, 0, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
63-
$value = array_shift($bits);
64-
$attributes = array();
65-
66-
$lastNullAttribute = null;
67-
foreach ($bits as $bit) {
68-
if (($start = substr($bit, 0, 1)) === ($end = substr($bit, -1)) && ('"' === $start || '\'' === $start)) {
69-
$attributes[$lastNullAttribute] = substr($bit, 1, -1);
70-
} elseif ('=' === $end) {
71-
$lastNullAttribute = $bit = substr($bit, 0, -1);
72-
$attributes[$bit] = null;
73-
} else {
74-
$parts = explode('=', $bit);
75-
$attributes[$parts[0]] = isset($parts[1]) && strlen($parts[1]) > 0 ? $parts[1] : '';
76-
}
77-
}
62+
$parts = HeaderUtils::split($itemValue, ';=');
63+
64+
$part = array_shift($parts);
65+
$attributes = HeaderUtils::combineParts($parts, 1);
7866

79-
return new self(($start = substr($value, 0, 1)) === ($end = substr($value, -1)) && ('"' === $start || '\'' === $start) ? substr($value, 1, -1) : $value, $attributes);
67+
return new self($part[0], $attributes);
8068
}
8169

8270
/**
@@ -88,9 +76,7 @@ public function __toString()
8876
{
8977
$string = $this->value.($this->quality < 1 ? ';q='.$this->quality : '');
9078
if (count($this->attributes) > 0) {
91-
$string .= ';'.implode(';', array_map(function ($name, $value) {
92-
return sprintf(preg_match('/[,;=]/', $value) ? '%s="%s"' : '%s=%s', $name, $value);
93-
}, array_keys($this->attributes), $this->attributes));
79+
$string .= '; '.HeaderUtils::joinAssoc($this->attributes, ';');
9480
}
9581

9682
return $string;

‎src/Symfony/Component/HttpFoundation/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpFoundation/CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ CHANGELOG
99
* deprecated setting session save handlers that do not implement `\SessionHandlerInterface` in `NativeSessionStorage::setSaveHandler()`
1010
* deprecated using `MongoDbSessionHandler` with the legacy mongo extension; use it with the mongodb/mongodb package and ext-mongodb instead
1111
* deprecated `MemcacheSessionHandler`; use `MemcachedSessionHandler` instead
12+
* added `HeaderUtility`
1213

1314
3.3.0
1415
-----

‎src/Symfony/Component/HttpFoundation/Cookie.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpFoundation/Cookie.php
+12-26Lines 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];
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

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpFoundation/HeaderBag.php
+3-19Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -286,21 +286,9 @@ public function count()
286286

287287
protected function getCacheControlHeader()
288288
{
289-
$parts = array();
290289
ksort($this->cacheControl);
291-
foreach ($this->cacheControl as $key => $value) {
292-
if (true === $value) {
293-
$parts[] = $key;
294-
} else {
295-
if (preg_match('#[^a-zA-Z0-9._-]#', $value)) {
296-
$value = '"'.$value.'"';
297-
}
298-
299-
$parts[] = "$key=$value";
300-
}
301-
}
302290

303-
return implode(', ', $parts);
291+
return HeaderUtils::joinAssoc($this->cacheControl, ',');
304292
}
305293

306294
/**
@@ -312,12 +300,8 @@ protected function getCacheControlHeader()
312300
*/
313301
protected function parseCacheControl($header)
314302
{
315-
$cacheControl = array();
316-
preg_match_all('#([a-zA-Z][a-zA-Z_-]*)\s*(?:=(?:"([^"]*)"|([^ \t",;]*)))?#', $header, $matches, PREG_SET_ORDER);
317-
foreach ($matches as $match) {
318-
$cacheControl[strtolower($match[1])] = isset($match[3]) ? $match[3] : (isset($match[2]) ? $match[2] : true);
319-
}
303+
$parts = HeaderUtils::split($header, ',=');
320304

321-
return $cacheControl;
305+
return HeaderUtils::combineParts($parts);
322306
}
323307
}
+191Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
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, $separators)
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+
private static function groupParts(array $matches, $separators)
69+
{
70+
$separator = $separators[0];
71+
$partSeparators = substr($separators, 1);
72+
73+
$i = 0;
74+
$partMatches = array();
75+
foreach ($matches as $match) {
76+
if (isset($match['separator']) && $match['separator'] === $separator) {
77+
++$i;
78+
} else {
79+
$partMatches[$i][] = $match;
80+
}
81+
}
82+
83+
$parts = array();
84+
if ($partSeparators) {
85+
foreach ($partMatches as $matches) {
86+
$parts[] = self::groupParts($matches, $partSeparators);
87+
}
88+
} else {
89+
foreach ($partMatches as $matches) {
90+
$parts[] = self::unquote($matches[0][0]);
91+
}
92+
}
93+
94+
return $parts;
95+
}
96+
97+
/**
98+
* Combines an array of arrays into one associative array.
99+
*
100+
* Each of the nested arrays should have one or two elements. The first
101+
* value will be used as the keys in the associative array, and the second
102+
* will be used as the values, or true if the nested array only contains one
103+
* element.
104+
*
105+
* Example:
106+
*
107+
* HeaderUtils::combineParts(array(array("foo", "abc"), array("bar")))
108+
* // => array("foo" => "abc", "bar" => true)
109+
*
110+
* @param array $parts Array of arrays
111+
*
112+
* @return array Associative array
113+
*/
114+
public static function combineParts(array $parts)
115+
{
116+
$assoc = array();
117+
foreach ($parts as $part) {
118+
$name = strtolower($part[0]);
119+
$value = isset($part[1]) ? $part[1] : true;
120+
$assoc[$name] = $value;
121+
}
122+
123+
return $assoc;
124+
}
125+
126+
/**
127+
* Joins an associative array into a string.
128+
*
129+
* The key and value of each entry are joined with "=", and all entries
130+
* is joined with the specified separator and an additional space (for
131+
* readability). Values are quoted if necessary.
132+
*
133+
* Example:
134+
*
135+
* HeaderUtils::joinAssoc(array("foo" => "abc", "bar" => true, "baz" => "a b c"), ",")
136+
* // => 'foo=bar, baz, baz="a b c"'
137+
*
138+
* @param array $assoc The associative array
139+
* @param string $separator Separator between each array entry
140+
*
141+
* @return string A string formatted for use in an HTTP header
142+
*/
143+
public static function joinAssoc(array $assoc, $separator)
144+
{
145+
$parts = array();
146+
foreach ($assoc as $name => $value) {
147+
if (true === $value) {
148+
$parts[] = $name;
149+
} else {
150+
$parts[] = $name.'='.self::quote($value);
151+
}
152+
}
153+
154+
return implode($separator.' ', $parts);
155+
}
156+
157+
/**
158+
* Encodes a string as a quoted string, if necessary.
159+
*
160+
* If a string contains characters not allowed by the "token" construct in
161+
* the HTTP specification, it is backslash-escaped and enclosed in quotes
162+
* to match the "quoted-string" construct.
163+
*
164+
* @param string $s The raw string
165+
*
166+
* @return return The quoted string
167+
*/
168+
public static function quote($s)
169+
{
170+
if (preg_match('/^[a-z0-9!#$%&\'*.^_`|~-]+$/i', $s)) {
171+
return $s;
172+
}
173+
174+
return '"'.addcslashes($s, '"\\"').'"';
175+
}
176+
177+
/**
178+
* Decodes a quoted string.
179+
*
180+
* If passed an unquoted string that matches the "token" construct (as
181+
* defined in the HTTP specification), it is passed through verbatimly.
182+
*
183+
* @param string $s The quoted string
184+
*
185+
* @return string The raw string
186+
*/
187+
public static function unquote($s)
188+
{
189+
return preg_replace('/\\\\(.)|"/', '$1', $s);
190+
}
191+
}

‎src/Symfony/Component/HttpFoundation/Request.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpFoundation/Request.php
+21-5Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2075,8 +2075,16 @@ private function getTrustedValues($type, $ip = null)
20752075
}
20762076

20772077
if (self::$trustedHeaders[self::HEADER_FORWARDED] && $this->headers->has(self::$trustedHeaders[self::HEADER_FORWARDED])) {
2078-
$forwardedValues = $this->headers->get(self::$trustedHeaders[self::HEADER_FORWARDED]);
2079-
$forwardedValues = preg_match_all(sprintf('{(?:%s)=(?:"?\[?)([a-zA-Z0-9\.:_\-/]*+)}', self::$forwardedParams[$type]), $forwardedValues, $matches) ? $matches[1] : array();
2078+
$forwarded = $this->headers->get(self::$trustedHeaders[self::HEADER_FORWARDED]);
2079+
$parts = HeaderUtils::split($forwarded, ',;=');
2080+
$forwardedValues = array();
2081+
$param = self::$forwardedParams[$type];
2082+
foreach ($parts as $subParts) {
2083+
$assoc = HeaderUtils::combineParts($subParts);
2084+
if (isset($assoc[$param])) {
2085+
$forwardedValues[] = $assoc[$param];
2086+
}
2087+
}
20802088
}
20812089

20822090
if (null !== $ip) {
@@ -2109,9 +2117,17 @@ private function normalizeAndFilterClientIps(array $clientIps, $ip)
21092117
$firstTrustedIp = null;
21102118

21112119
foreach ($clientIps as $key => $clientIp) {
2112-
// Remove port (unfortunately, it does happen)
2113-
if (preg_match('{((?:\d+\.){3}\d+)\:\d+}', $clientIp, $match)) {
2114-
$clientIps[$key] = $clientIp = $match[1];
2120+
if (strpos($clientIp, '.')) {
2121+
// Strip :port from IPv4 addresses. This is allowed in Forwarded
2122+
// and may occur in X-Forwarded-For.
2123+
$i = strpos($clientIp, ':');
2124+
if ($i) {
2125+
$clientIps[$key] = $clientIp = substr($clientIp, 0, $i);
2126+
}
2127+
} elseif ('[' == $clientIp[0]) {
2128+
// Strip brackets and :port from IPv6 addresses.
2129+
$i = strpos($clientIp, ']', 1);
2130+
$clientIps[$key] = $clientIp = substr($clientIp, 1, $i - 1);
21152131
}
21162132

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

‎src/Symfony/Component/HttpFoundation/Tests/AcceptHeaderItemTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpFoundation/Tests/AcceptHeaderItemTest.php
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public function provideToStringData()
6666
),
6767
array(
6868
'text/plain', array('charset' => 'utf-8', 'param' => 'this;should,not=matter', 'footnotes' => 'true'),
69-
'text/plain;charset=utf-8;param="this;should,not=matter";footnotes=true',
69+
'text/plain; charset=utf-8; param="this;should,not=matter"; footnotes=true',
7070
),
7171
);
7272
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.