diff --git a/src/Symfony/Component/HttpFoundation/AcceptHeader.php b/src/Symfony/Component/HttpFoundation/AcceptHeader.php index 12ca50651f74d..972cb9d299133 100644 --- a/src/Symfony/Component/HttpFoundation/AcceptHeader.php +++ b/src/Symfony/Component/HttpFoundation/AcceptHeader.php @@ -52,12 +52,17 @@ public static function fromString($headerValue) { $index = 0; - return new self(array_map(function ($itemValue) use (&$index) { - $item = AcceptHeaderItem::fromString($itemValue); + $parts = HeaderUtils::split((string) $headerValue, ',;='); + + return new self(array_map(function ($subParts) use (&$index) { + $part = array_shift($subParts); + $attributes = HeaderUtils::combineParts($subParts); + + $item = new AcceptHeaderItem($part[0], $attributes); $item->setIndex($index++); return $item; - }, preg_split('/\s*(?:,*("[^"]+"),*|,*(\'[^\']+\'),*|,+)\s*/', $headerValue, 0, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE))); + }, $parts)); } /** diff --git a/src/Symfony/Component/HttpFoundation/AcceptHeaderItem.php b/src/Symfony/Component/HttpFoundation/AcceptHeaderItem.php index 367531496e23f..761a9df60f528 100644 --- a/src/Symfony/Component/HttpFoundation/AcceptHeaderItem.php +++ b/src/Symfony/Component/HttpFoundation/AcceptHeaderItem.php @@ -40,24 +40,12 @@ public function __construct(string $value, array $attributes = array()) */ public static function fromString($itemValue) { - $bits = preg_split('/\s*(?:;*("[^"]+");*|;*(\'[^\']+\');*|;+)\s*/', $itemValue, 0, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); - $value = array_shift($bits); - $attributes = array(); - - $lastNullAttribute = null; - foreach ($bits as $bit) { - if (($start = substr($bit, 0, 1)) === ($end = substr($bit, -1)) && ('"' === $start || '\'' === $start)) { - $attributes[$lastNullAttribute] = substr($bit, 1, -1); - } elseif ('=' === $end) { - $lastNullAttribute = $bit = substr($bit, 0, -1); - $attributes[$bit] = null; - } else { - $parts = explode('=', $bit); - $attributes[$parts[0]] = isset($parts[1]) && strlen($parts[1]) > 0 ? $parts[1] : ''; - } - } + $parts = HeaderUtils::split($itemValue, ';='); + + $part = array_shift($parts); + $attributes = HeaderUtils::combineParts($parts, 1); - return new self(($start = substr($value, 0, 1)) === ($end = substr($value, -1)) && ('"' === $start || '\'' === $start) ? substr($value, 1, -1) : $value, $attributes); + return new self($part[0], $attributes); } /** @@ -69,9 +57,7 @@ public function __toString() { $string = $this->value.($this->quality < 1 ? ';q='.$this->quality : ''); if (count($this->attributes) > 0) { - $string .= ';'.implode(';', array_map(function ($name, $value) { - return sprintf(preg_match('/[,;=]/', $value) ? '%s="%s"' : '%s=%s', $name, $value); - }, array_keys($this->attributes), $this->attributes)); + $string .= '; '.HeaderUtils::joinAssoc($this->attributes, ';'); } return $string; diff --git a/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php b/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php index bee8b82687996..be032c4b0bb49 100644 --- a/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php +++ b/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php @@ -218,17 +218,12 @@ public function prepare(Request $request) if ('x-accel-redirect' === strtolower($type)) { // Do X-Accel-Mapping substitutions. // @link http://wiki.nginx.org/X-accel#X-Accel-Redirect - foreach (explode(',', $request->headers->get('X-Accel-Mapping', '')) as $mapping) { - $mapping = explode('=', $mapping, 2); - - if (2 === count($mapping)) { - $pathPrefix = trim($mapping[0]); - $location = trim($mapping[1]); - - if (substr($path, 0, strlen($pathPrefix)) === $pathPrefix) { - $path = $location.substr($path, strlen($pathPrefix)); - break; - } + $parts = HeaderUtils::split($request->headers->get('X-Accel-Mapping', ''), ',='); + $mappings = HeaderUtils::combineParts($parts); + foreach ($mappings as $pathPrefix => $location) { + if (substr($path, 0, strlen($pathPrefix)) === $pathPrefix) { + $path = $location.substr($path, strlen($pathPrefix)); + break; } } } diff --git a/src/Symfony/Component/HttpFoundation/CHANGELOG.md b/src/Symfony/Component/HttpFoundation/CHANGELOG.md index e4e8f9603ebfe..83e98a0e4a0a7 100644 --- a/src/Symfony/Component/HttpFoundation/CHANGELOG.md +++ b/src/Symfony/Component/HttpFoundation/CHANGELOG.md @@ -16,6 +16,7 @@ CHANGELOG `IniSizeFileException`, `NoFileException`, `NoTmpDirFileException`, `PartialFileException` to handle failed `UploadedFile`. * added `MigratingSessionHandler` for migrating between two session handlers without losing sessions + * added `HeaderUtils`. 4.0.0 ----- diff --git a/src/Symfony/Component/HttpFoundation/Cookie.php b/src/Symfony/Component/HttpFoundation/Cookie.php index 78b67bb3d5d44..94d406ce71f18 100644 --- a/src/Symfony/Component/HttpFoundation/Cookie.php +++ b/src/Symfony/Component/HttpFoundation/Cookie.php @@ -50,34 +50,20 @@ public static function fromString($cookie, $decode = false) 'raw' => !$decode, 'samesite' => null, ); - foreach (explode(';', $cookie) as $part) { - if (false === strpos($part, '=')) { - $key = trim($part); - $value = true; - } else { - list($key, $value) = explode('=', trim($part), 2); - $key = trim($key); - $value = trim($value); - } - if (!isset($data['name'])) { - $data['name'] = $decode ? urldecode($key) : $key; - $data['value'] = true === $value ? null : ($decode ? urldecode($value) : $value); - continue; - } - switch ($key = strtolower($key)) { - case 'name': - case 'value': - break; - case 'max-age': - $data['expires'] = time() + (int) $value; - break; - default: - $data[$key] = $value; - break; - } + + $parts = HeaderUtils::split($cookie, ';='); + $part = array_shift($parts); + + $name = $decode ? urldecode($part[0]) : $part[0]; + $value = isset($part[1]) ? ($decode ? urldecode($part[1]) : $part[1]) : null; + + $data = HeaderUtils::combineParts($parts) + $data; + + if (isset($data['max-age'])) { + $data['expires'] = time() + (int) $data['max-age']; } - return new static($data['name'], $data['value'], $data['expires'], $data['path'], $data['domain'], $data['secure'], $data['httponly'], $data['raw'], $data['samesite']); + return new static($name, $value, $data['expires'], $data['path'], $data['domain'], $data['secure'], $data['httponly'], $data['raw'], $data['samesite']); } /** diff --git a/src/Symfony/Component/HttpFoundation/HeaderBag.php b/src/Symfony/Component/HttpFoundation/HeaderBag.php index 27fa1c7cca990..4f9a6e8aa35b3 100644 --- a/src/Symfony/Component/HttpFoundation/HeaderBag.php +++ b/src/Symfony/Component/HttpFoundation/HeaderBag.php @@ -294,21 +294,9 @@ public function count() protected function getCacheControlHeader() { - $parts = array(); ksort($this->cacheControl); - foreach ($this->cacheControl as $key => $value) { - if (true === $value) { - $parts[] = $key; - } else { - if (preg_match('#[^a-zA-Z0-9._-]#', $value)) { - $value = '"'.$value.'"'; - } - $parts[] = "$key=$value"; - } - } - - return implode(', ', $parts); + return HeaderUtils::joinAssoc($this->cacheControl, ','); } /** @@ -320,12 +308,8 @@ protected function getCacheControlHeader() */ protected function parseCacheControl($header) { - $cacheControl = array(); - preg_match_all('#([a-zA-Z][a-zA-Z_-]*)\s*(?:=(?:"([^"]*)"|([^ \t",;]*)))?#', $header, $matches, PREG_SET_ORDER); - foreach ($matches as $match) { - $cacheControl[strtolower($match[1])] = isset($match[3]) ? $match[3] : (isset($match[2]) ? $match[2] : true); - } + $parts = HeaderUtils::split($header, ',='); - return $cacheControl; + return HeaderUtils::combineParts($parts); } } diff --git a/src/Symfony/Component/HttpFoundation/HeaderUtils.php b/src/Symfony/Component/HttpFoundation/HeaderUtils.php new file mode 100644 index 0000000000000..8f1193e51cadd --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/HeaderUtils.php @@ -0,0 +1,174 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * HTTP header utility functions. + * + * @author Christian Schmidt + */ +class HeaderUtils +{ + /** + * This class should not be instantiated. + */ + private function __construct() + { + } + + /** + * Splits an HTTP header by one or more separators. + * + * Example: + * + * HeaderUtils::split("da, en-gb;q=0.8", ",;") + * // => array(array("da"), array("en-gb"), array("q", "0.8")) + * + * @param string $header HTTP header value + * @param string $separators List of characters to split on, ordered by + * precedence, e.g. ",", ";=", or ",;=" + * + * @return array Nested array with as many levels as there are characters in + * $separators + */ + public static function split(string $header, string $separators): array + { + $quotedSeparators = preg_quote($separators); + + preg_match_all(' + / + (?!\s) + (?: + # quoted-string + "(?:[^"\\\\]|\\\\.)*(?:"|\\\\|$) + | + # token + [^"'.$quotedSeparators.']+ + )+ + (?['.$quotedSeparators.']) + \s* + /x', trim($header), $matches, PREG_SET_ORDER); + + return self::groupParts($matches, $separators); + } + + /** + * Combines an array of arrays into one associative array. + * + * Each of the nested arrays should have one or two elements. The first + * value will be used as the keys in the associative array, and the second + * will be used as the values, or true if the nested array only contains one + * element. + * + * Example: + * + * HeaderUtils::combineParts(array(array("foo", "abc"), array("bar"))) + * // => array("foo" => "abc", "bar" => true) + */ + public static function combineParts(array $parts): array + { + $assoc = array(); + foreach ($parts as $part) { + $name = strtolower($part[0]); + $value = $part[1] ?? true; + $assoc[$name] = $value; + } + + return $assoc; + } + + /** + * Joins an associative array into a string for use in an HTTP header. + * + * The key and value of each entry are joined with "=", and all entries + * is joined with the specified separator and an additional space (for + * readability). Values are quoted if necessary. + * + * Example: + * + * HeaderUtils::joinAssoc(array("foo" => "abc", "bar" => true, "baz" => "a b c"), ",") + * // => 'foo=bar, baz, baz="a b c"' + */ + public static function joinAssoc(array $assoc, string $separator): string + { + $parts = array(); + foreach ($assoc as $name => $value) { + if (true === $value) { + $parts[] = $name; + } else { + $parts[] = $name.'='.self::quote($value); + } + } + + return implode($separator.' ', $parts); + } + + /** + * Encodes a string as a quoted string, if necessary. + * + * If a string contains characters not allowed by the "token" construct in + * the HTTP specification, it is backslash-escaped and enclosed in quotes + * to match the "quoted-string" construct. + */ + public static function quote(string $s): string + { + if (preg_match('/^[a-z0-9!#$%&\'*.^_`|~-]+$/i', $s)) { + return $s; + } + + return '"'.addcslashes($s, '"\\"').'"'; + } + + /** + * Decodes a quoted string. + * + * If passed an unquoted string that matches the "token" construct (as + * defined in the HTTP specification), it is passed through verbatimly. + */ + public static function unquote(string $s): string + { + return preg_replace('/\\\\(.)|"/', '$1', $s); + } + + private static function groupParts(array $matches, string $separators): array + { + $separator = $separators[0]; + $partSeparators = substr($separators, 1); + + $i = 0; + $partMatches = array(); + foreach ($matches as $match) { + if (isset($match['separator']) && $match['separator'] === $separator) { + ++$i; + } else { + $partMatches[$i][] = $match; + } + } + + $parts = array(); + if ($partSeparators) { + foreach ($partMatches as $matches) { + $parts[] = self::groupParts($matches, $partSeparators); + } + } else { + foreach ($partMatches as $matches) { + $parts[] = self::unquote($matches[0][0]); + } + } + + return $parts; + } +} diff --git a/src/Symfony/Component/HttpFoundation/Request.php b/src/Symfony/Component/HttpFoundation/Request.php index 007682ed0ed7f..5b106d33d6971 100644 --- a/src/Symfony/Component/HttpFoundation/Request.php +++ b/src/Symfony/Component/HttpFoundation/Request.php @@ -1944,8 +1944,16 @@ private function getTrustedValues($type, $ip = null) } if ((self::$trustedHeaderSet & self::HEADER_FORWARDED) && $this->headers->has(self::$trustedHeaders[self::HEADER_FORWARDED])) { - $forwardedValues = $this->headers->get(self::$trustedHeaders[self::HEADER_FORWARDED]); - $forwardedValues = preg_match_all(sprintf('{(?:%s)=(?:"?\[?)([a-zA-Z0-9\.:_\-/]*+)}', self::$forwardedParams[$type]), $forwardedValues, $matches) ? $matches[1] : array(); + $forwarded = $this->headers->get(self::$trustedHeaders[self::HEADER_FORWARDED]); + $parts = HeaderUtils::split($forwarded, ',;='); + $forwardedValues = array(); + $param = self::$forwardedParams[$type]; + foreach ($parts as $subParts) { + $assoc = HeaderUtils::combineParts($subParts); + if (isset($assoc[$param])) { + $forwardedValues[] = $assoc[$param]; + } + } } if (null !== $ip) { @@ -1978,9 +1986,17 @@ private function normalizeAndFilterClientIps(array $clientIps, $ip) $firstTrustedIp = null; foreach ($clientIps as $key => $clientIp) { - // Remove port (unfortunately, it does happen) - if (preg_match('{((?:\d+\.){3}\d+)\:\d+}', $clientIp, $match)) { - $clientIps[$key] = $clientIp = $match[1]; + if (strpos($clientIp, '.')) { + // Strip :port from IPv4 addresses. This is allowed in Forwarded + // and may occur in X-Forwarded-For. + $i = strpos($clientIp, ':'); + if ($i) { + $clientIps[$key] = $clientIp = substr($clientIp, 0, $i); + } + } elseif ('[' == $clientIp[0]) { + // Strip brackets and :port from IPv6 addresses. + $i = strpos($clientIp, ']', 1); + $clientIps[$key] = $clientIp = substr($clientIp, 1, $i - 1); } if (!filter_var($clientIp, FILTER_VALIDATE_IP)) { diff --git a/src/Symfony/Component/HttpFoundation/ResponseHeaderBag.php b/src/Symfony/Component/HttpFoundation/ResponseHeaderBag.php index 11a859326b047..ba896a47c8e51 100644 --- a/src/Symfony/Component/HttpFoundation/ResponseHeaderBag.php +++ b/src/Symfony/Component/HttpFoundation/ResponseHeaderBag.php @@ -290,13 +290,12 @@ public function makeDisposition($disposition, $filename, $filenameFallback = '') throw new \InvalidArgumentException('The filename and the fallback cannot contain the "/" and "\\" characters.'); } - $output = sprintf('%s; filename="%s"', $disposition, str_replace('"', '\\"', $filenameFallback)); - + $params = array('filename' => $filenameFallback); if ($filename !== $filenameFallback) { - $output .= sprintf("; filename*=utf-8''%s", rawurlencode($filename)); + $params['filename*'] = "utf-8''".rawurlencode($filename); } - return $output; + return $disposition.'; '.HeaderUtils::joinAssoc($params, ';'); } /** diff --git a/src/Symfony/Component/HttpFoundation/Tests/AcceptHeaderItemTest.php b/src/Symfony/Component/HttpFoundation/Tests/AcceptHeaderItemTest.php index cb43bb35168a7..1a660247c8fbe 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/AcceptHeaderItemTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/AcceptHeaderItemTest.php @@ -66,7 +66,7 @@ public function provideToStringData() ), array( 'text/plain', array('charset' => 'utf-8', 'param' => 'this;should,not=matter', 'footnotes' => 'true'), - 'text/plain;charset=utf-8;param="this;should,not=matter";footnotes=true', + 'text/plain; charset=utf-8; param="this;should,not=matter"; footnotes=true', ), ); } diff --git a/src/Symfony/Component/HttpFoundation/Tests/BinaryFileResponseTest.php b/src/Symfony/Component/HttpFoundation/Tests/BinaryFileResponseTest.php index 1b9e58991cc6d..ce47157fe10dc 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/BinaryFileResponseTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/BinaryFileResponseTest.php @@ -32,7 +32,7 @@ public function testConstruction() $response = BinaryFileResponse::create($file, 404, array(), true, ResponseHeaderBag::DISPOSITION_INLINE); $this->assertEquals(404, $response->getStatusCode()); $this->assertFalse($response->headers->has('ETag')); - $this->assertEquals('inline; filename="README.md"', $response->headers->get('Content-Disposition')); + $this->assertEquals('inline; filename=README.md', $response->headers->get('Content-Disposition')); } public function testConstructWithNonAsciiFilename() @@ -66,7 +66,7 @@ public function testSetContentDispositionGeneratesSafeFallbackFilename() $response = new BinaryFileResponse(__FILE__); $response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, 'föö.html'); - $this->assertSame('attachment; filename="f__.html"; filename*=utf-8\'\'f%C3%B6%C3%B6.html', $response->headers->get('Content-Disposition')); + $this->assertSame('attachment; filename=f__.html; filename*=utf-8\'\'f%C3%B6%C3%B6.html', $response->headers->get('Content-Disposition')); } public function testSetContentDispositionGeneratesSafeFallbackFilenameForWronglyEncodedFilename() @@ -77,7 +77,7 @@ public function testSetContentDispositionGeneratesSafeFallbackFilenameForWrongly $response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $iso88591EncodedFilename); // the parameter filename* is invalid in this case (rawurldecode('f%F6%F6') does not provide a UTF-8 string but an ISO-8859-1 encoded one) - $this->assertSame('attachment; filename="f__.html"; filename*=utf-8\'\'f%F6%F6.html', $response->headers->get('Content-Disposition')); + $this->assertSame('attachment; filename=f__.html; filename*=utf-8\'\'f%F6%F6.html', $response->headers->get('Content-Disposition')); } /** diff --git a/src/Symfony/Component/HttpFoundation/Tests/CookieTest.php b/src/Symfony/Component/HttpFoundation/Tests/CookieTest.php index 264fafa097596..f225d23714661 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/CookieTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/CookieTest.php @@ -201,6 +201,9 @@ public function testFromString() $cookie = Cookie::fromString('foo=bar', true); $this->assertEquals(new Cookie('foo', 'bar', 0, '/', null, false, false), $cookie); + + $cookie = Cookie::fromString('foo', true); + $this->assertEquals(new Cookie('foo', null, 0, '/', null, false, false), $cookie); } public function testFromStringWithHttpOnly() diff --git a/src/Symfony/Component/HttpFoundation/Tests/HeaderUtilsTest.php b/src/Symfony/Component/HttpFoundation/Tests/HeaderUtilsTest.php new file mode 100644 index 0000000000000..8bb9bc35b95e6 --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Tests/HeaderUtilsTest.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\HeaderUtils; + +class HeaderUtilsTest extends TestCase +{ + public function testSplit() + { + $this->assertSame(array('foo=123', 'bar'), HeaderUtils::split('foo=123,bar', ',')); + $this->assertSame(array('foo=123', 'bar'), HeaderUtils::split('foo=123, bar', ',')); + $this->assertSame(array(array('foo=123', 'bar')), HeaderUtils::split('foo=123; bar', ',;')); + $this->assertSame(array(array('foo=123'), array('bar')), HeaderUtils::split('foo=123, bar', ',;')); + $this->assertSame(array('foo', '123, bar'), HeaderUtils::split('foo=123, bar', '=')); + $this->assertSame(array('foo', '123, bar'), HeaderUtils::split(' foo = 123, bar ', '=')); + $this->assertSame(array(array('foo', '123'), array('bar')), HeaderUtils::split('foo=123, bar', ',=')); + $this->assertSame(array(array(array('foo', '123')), array(array('bar'), array('foo', '456'))), HeaderUtils::split('foo=123, bar; foo=456', ',;=')); + $this->assertSame(array(array(array('foo', 'a,b;c=d'))), HeaderUtils::split('foo="a,b;c=d"', ',;=')); + + $this->assertSame(array('foo', 'bar'), HeaderUtils::split('foo,,,, bar', ',')); + $this->assertSame(array('foo', 'bar'), HeaderUtils::split(',foo, bar,', ',')); + $this->assertSame(array('foo', 'bar'), HeaderUtils::split(' , foo, bar, ', ',')); + $this->assertSame(array('foo bar'), HeaderUtils::split('foo "bar"', ',')); + $this->assertSame(array('foo bar'), HeaderUtils::split('"foo" bar', ',')); + $this->assertSame(array('foo bar'), HeaderUtils::split('"foo" "bar"', ',')); + + // These are not a valid header values. We test that they parse anyway, + // and that both the valid and invalid parts are returned. + $this->assertSame(array(), HeaderUtils::split('', ',')); + $this->assertSame(array(), HeaderUtils::split(',,,', ',')); + $this->assertSame(array('foo', 'bar', 'baz'), HeaderUtils::split('foo, "bar", "baz', ',')); + $this->assertSame(array('foo', 'bar, baz'), HeaderUtils::split('foo, "bar, baz', ',')); + $this->assertSame(array('foo', 'bar, baz\\'), HeaderUtils::split('foo, "bar, baz\\', ',')); + $this->assertSame(array('foo', 'bar, baz\\'), HeaderUtils::split('foo, "bar, baz\\\\', ',')); + } + + public function testCombineAssoc() + { + $this->assertSame(array('foo' => '123'), HeaderUtils::combineParts(array(array('foo', '123')))); + $this->assertSame(array('foo' => true), HeaderUtils::combineParts(array(array('foo')))); + $this->assertSame(array('foo' => true), HeaderUtils::combineParts(array(array('Foo')))); + $this->assertSame(array('foo' => '123', 'bar' => true), HeaderUtils::combineParts(array(array('foo', '123'), array('bar')))); + } + + public function testJoinAssoc() + { + $this->assertSame('foo', HeaderUtils::joinAssoc(array('foo' => true), ',')); + $this->assertSame('foo; bar', HeaderUtils::joinAssoc(array('foo' => true, 'bar' => true), ';')); + $this->assertSame('foo=123', HeaderUtils::joinAssoc(array('foo' => '123'), ',')); + $this->assertSame('foo="1 2 3"', HeaderUtils::joinAssoc(array('foo' => '1 2 3'), ',')); + $this->assertSame('foo="1 2 3", bar', HeaderUtils::joinAssoc(array('foo' => '1 2 3', 'bar' => true), ',')); + } + + public function testQuote() + { + $this->assertSame('foo', HeaderUtils::quote('foo')); + $this->assertSame('az09!#$%&\'*.^_`|~-', HeaderUtils::quote('az09!#$%&\'*.^_`|~-')); + $this->assertSame('"foo bar"', HeaderUtils::quote('foo bar')); + $this->assertSame('"foo [bar]"', HeaderUtils::quote('foo [bar]')); + $this->assertSame('"foo \"bar\""', HeaderUtils::quote('foo "bar"')); + $this->assertSame('"foo \\\\ bar"', HeaderUtils::quote('foo \\ bar')); + } + + public function testUnquote() + { + $this->assertEquals('foo', HeaderUtils::unquote('foo')); + $this->assertEquals('az09!#$%&\'*.^_`|~-', HeaderUtils::unquote('az09!#$%&\'*.^_`|~-')); + $this->assertEquals('foo bar', HeaderUtils::unquote('"foo bar"')); + $this->assertEquals('foo [bar]', HeaderUtils::unquote('"foo [bar]"')); + $this->assertEquals('foo "bar"', HeaderUtils::unquote('"foo \"bar\""')); + $this->assertEquals('foo "bar"', HeaderUtils::unquote('"foo \"\b\a\r\""')); + $this->assertEquals('foo \\ bar', HeaderUtils::unquote('"foo \\\\ bar"')); + } +} diff --git a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php index 902a5ba4f1bdb..b0baa779a93c8 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php @@ -894,7 +894,7 @@ public function getClientIpsForwardedProvider() array(array('127.0.0.1'), '127.0.0.1', 'for="_gazonk"', array('127.0.0.1')), array(array('88.88.88.88'), '127.0.0.1', 'for="88.88.88.88:80"', array('127.0.0.1')), array(array('192.0.2.60'), '::1', 'for=192.0.2.60;proto=http;by=203.0.113.43', array('::1')), - array(array('2620:0:1cfe:face:b00c::3', '192.0.2.43'), '::1', 'for=192.0.2.43, for=2620:0:1cfe:face:b00c::3', array('::1')), + array(array('2620:0:1cfe:face:b00c::3', '192.0.2.43'), '::1', 'for=192.0.2.43, for="[2620:0:1cfe:face:b00c::3]"', array('::1')), array(array('2001:db8:cafe::17'), '::1', 'for="[2001:db8:cafe::17]:4711', array('::1')), ); } diff --git a/src/Symfony/Component/HttpFoundation/Tests/ResponseHeaderBagTest.php b/src/Symfony/Component/HttpFoundation/Tests/ResponseHeaderBagTest.php index ce8553590dcdb..74bb7c0b77429 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/ResponseHeaderBagTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/ResponseHeaderBagTest.php @@ -287,12 +287,12 @@ public function testToStringDoesntMessUpHeaders() public function provideMakeDisposition() { return array( - array('attachment', 'foo.html', 'foo.html', 'attachment; filename="foo.html"'), - array('attachment', 'foo.html', '', 'attachment; filename="foo.html"'), + array('attachment', 'foo.html', 'foo.html', 'attachment; filename=foo.html'), + array('attachment', 'foo.html', '', 'attachment; filename=foo.html'), array('attachment', 'foo bar.html', '', 'attachment; filename="foo bar.html"'), array('attachment', 'foo "bar".html', '', 'attachment; filename="foo \\"bar\\".html"'), array('attachment', 'foo%20bar.html', 'foo bar.html', 'attachment; filename="foo bar.html"; filename*=utf-8\'\'foo%2520bar.html'), - array('attachment', 'föö.html', 'foo.html', 'attachment; filename="foo.html"; filename*=utf-8\'\'f%C3%B6%C3%B6.html'), + array('attachment', 'föö.html', 'foo.html', 'attachment; filename=foo.html; filename*=utf-8\'\'f%C3%B6%C3%B6.html'), ); }