Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit eee2a20

Browse files
committed
[HttpFoundation] Add HeaderUtility class
1 parent b43bdf3 commit eee2a20

11 files changed

+332
-75
lines changed

src/Symfony/Component/HttpFoundation/AcceptHeader.php

+8-3
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

+6-20
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

+1
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

+12-26
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

+3-19
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
}
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 <[email protected]>
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 <[email protected]>
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

+21-5
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

+1-1
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)