From dd81e32ec119bfde74948db25f3cb6115058c3f0 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Sat, 13 Jun 2020 20:18:57 +0200 Subject: [PATCH] [HttpFoundation] add `HeaderUtils::parseQuery()`: it does the same as `parse_str()` but preserves dots in variable names --- .../Controller/RedirectController.php | 48 +-------------- .../Bundle/FrameworkBundle/composer.json | 2 +- .../Component/HttpFoundation/CHANGELOG.md | 5 ++ .../Component/HttpFoundation/HeaderUtils.php | 58 +++++++++++++++++++ .../Component/HttpFoundation/Request.php | 4 +- .../HttpFoundation/Tests/HeaderUtilsTest.php | 37 ++++++++++++ .../HttpFoundation/Tests/RequestTest.php | 2 +- 7 files changed, 106 insertions(+), 50 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/RedirectController.php b/src/Symfony/Bundle/FrameworkBundle/Controller/RedirectController.php index 58f06d5df9f05..a1f1c1697909b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/RedirectController.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/RedirectController.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Controller; +use Symfony\Component\HttpFoundation\HeaderUtils; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -65,7 +66,7 @@ public function redirectAction(Request $request, string $route, bool $permanent if ($keepQueryParams) { if ($query = $request->server->get('QUERY_STRING')) { - $query = self::parseQuery($query); + $query = HeaderUtils::parseQuery($query); } else { $query = $request->query->all(); } @@ -185,49 +186,4 @@ public function __invoke(Request $request): Response throw new \RuntimeException(sprintf('The parameter "path" or "route" is required to configure the redirect action in "%s" routing configuration.', $request->attributes->get('_route'))); } - - private static function parseQuery(string $query) - { - $q = []; - - foreach (explode('&', $query) as $v) { - if (false !== $i = strpos($v, "\0")) { - $v = substr($v, 0, $i); - } - - if (false === $i = strpos($v, '=')) { - $k = urldecode($v); - $v = ''; - } else { - $k = urldecode(substr($v, 0, $i)); - $v = substr($v, $i); - } - - if (false !== $i = strpos($k, "\0")) { - $k = substr($k, 0, $i); - } - - $k = ltrim($k, ' '); - - if (false === $i = strpos($k, '[')) { - $q[] = bin2hex($k).$v; - } else { - $q[] = substr_replace($k, bin2hex(substr($k, 0, $i)), 0, $i).$v; - } - } - - parse_str(implode('&', $q), $q); - - $query = []; - - foreach ($q as $k => $v) { - if (false !== $i = strpos($k, '_')) { - $query[substr_replace($k, hex2bin(substr($k, 0, $i)).'[', 0, 1 + $i)] = $v; - } else { - $query[hex2bin($k)] = $v; - } - } - - return $query; - } } diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index dacfa59a65a4b..3e60ddc2ed496 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -23,7 +23,7 @@ "symfony/dependency-injection": "^5.2", "symfony/event-dispatcher": "^5.1", "symfony/error-handler": "^4.4.1|^5.0.1", - "symfony/http-foundation": "^4.4|^5.0", + "symfony/http-foundation": "^5.2", "symfony/http-kernel": "^5.2", "symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-php80": "^1.15", diff --git a/src/Symfony/Component/HttpFoundation/CHANGELOG.md b/src/Symfony/Component/HttpFoundation/CHANGELOG.md index 60fab8381bf46..f806ee6993e84 100644 --- a/src/Symfony/Component/HttpFoundation/CHANGELOG.md +++ b/src/Symfony/Component/HttpFoundation/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +5.2.0 +----- + + * added `HeaderUtils::parseQuery()`: it does the same as `parse_str()` but preserves dots in variable names + 5.1.0 ----- diff --git a/src/Symfony/Component/HttpFoundation/HeaderUtils.php b/src/Symfony/Component/HttpFoundation/HeaderUtils.php index 5866e3b2b53e6..bd9d04dfac22a 100644 --- a/src/Symfony/Component/HttpFoundation/HeaderUtils.php +++ b/src/Symfony/Component/HttpFoundation/HeaderUtils.php @@ -193,6 +193,64 @@ public static function makeDisposition(string $disposition, string $filename, st return $disposition.'; '.self::toString($params, ';'); } + /** + * Like parse_str(), but preserves dots in variable names. + */ + public static function parseQuery(string $query, bool $ignoreBrackets = false, string $separator = '&'): array + { + $q = []; + + foreach (explode($separator, $query) as $v) { + if (false !== $i = strpos($v, "\0")) { + $v = substr($v, 0, $i); + } + + if (false === $i = strpos($v, '=')) { + $k = urldecode($v); + $v = ''; + } else { + $k = urldecode(substr($v, 0, $i)); + $v = substr($v, $i); + } + + if (false !== $i = strpos($k, "\0")) { + $k = substr($k, 0, $i); + } + + $k = ltrim($k, ' '); + + if ($ignoreBrackets) { + $q[$k][] = urldecode(substr($v, 1)); + + continue; + } + + if (false === $i = strpos($k, '[')) { + $q[] = bin2hex($k).$v; + } else { + $q[] = substr_replace($k, bin2hex(substr($k, 0, $i)), 0, $i).$v; + } + } + + if ($ignoreBrackets) { + return $q; + } + + parse_str(implode('&', $q), $q); + + $query = []; + + foreach ($q as $k => $v) { + if (false !== $i = strpos($k, '_')) { + $query[substr_replace($k, hex2bin(substr($k, 0, $i)).'[', 0, 1 + $i)] = $v; + } else { + $query[hex2bin($k)] = $v; + } + } + + return $query; + } + private static function groupParts(array $matches, string $separators): array { $separator = $separators[0]; diff --git a/src/Symfony/Component/HttpFoundation/Request.php b/src/Symfony/Component/HttpFoundation/Request.php index e737a7e58c832..ce1e779eaae65 100644 --- a/src/Symfony/Component/HttpFoundation/Request.php +++ b/src/Symfony/Component/HttpFoundation/Request.php @@ -399,7 +399,7 @@ public static function create(string $uri, string $method = 'GET', array $parame $queryString = ''; if (isset($components['query'])) { - parse_str(html_entity_decode($components['query']), $qs); + $qs = HeaderUtils::parseQuery(html_entity_decode($components['query'])); if ($query) { $query = array_replace($qs, $query); @@ -660,7 +660,7 @@ public static function normalizeQueryString(?string $qs) return ''; } - parse_str($qs, $qs); + $qs = HeaderUtils::parseQuery($qs); ksort($qs); return http_build_query($qs, '', '&', PHP_QUERY_RFC3986); diff --git a/src/Symfony/Component/HttpFoundation/Tests/HeaderUtilsTest.php b/src/Symfony/Component/HttpFoundation/Tests/HeaderUtilsTest.php index d2b19ca84d1c6..b8ad4dfd0a24a 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/HeaderUtilsTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/HeaderUtilsTest.php @@ -129,4 +129,41 @@ public function provideMakeDispositionFail() ['attachment', 'föö.html'], ]; } + + /** + * @dataProvider provideParseQuery + */ + public function testParseQuery(string $query, string $expected = null) + { + $this->assertSame($expected ?? $query, http_build_query(HeaderUtils::parseQuery($query), '', '&')); + } + + public function provideParseQuery() + { + return [ + ['a=b&c=d'], + ['a.b=c'], + ['a+b=c'], + ["a\0b=c", 'a='], + ['a%00b=c', 'a=c'], + ['a[b=c', 'a%5Bb=c'], + ['a]b=c', 'a%5Db=c'], + ['a[b]=c', 'a%5Bb%5D=c'], + ['a[b][c.d]=c', 'a%5Bb%5D%5Bc.d%5D=c'], + ['a%5Bb%5D=c'], + ]; + } + + public function testParseCookie() + { + $query = 'a.b=c; def%5Bg%5D=h'; + $this->assertSame($query, http_build_query(HeaderUtils::parseQuery($query, false, ';'), '', '; ')); + } + + public function testParseQueryIgnoreBrackets() + { + $this->assertSame(['a.b' => ['A', 'B']], HeaderUtils::parseQuery('a.b=A&a.b=B', true)); + $this->assertSame(['a.b[]' => ['A']], HeaderUtils::parseQuery('a.b[]=A', true)); + $this->assertSame(['a.b[]' => ['A']], HeaderUtils::parseQuery('a.b%5B%5D=A', true)); + } } diff --git a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php index 99cd54884f51c..8986be52c7735 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php @@ -807,7 +807,7 @@ public function getQueryStringNormalizationData() ['foo=1&foo=2', 'foo=2', 'merges repeated parameters'], ['pa%3Dram=foo%26bar%3Dbaz&test=test', 'pa%3Dram=foo%26bar%3Dbaz&test=test', 'works with encoded delimiters'], ['0', '0=', 'allows "0"'], - ['Foo Bar&Foo%20Baz', 'Foo_Bar=&Foo_Baz=', 'normalizes encoding in keys'], + ['Foo Bar&Foo%20Baz', 'Foo%20Bar=&Foo%20Baz=', 'normalizes encoding in keys'], ['bar=Foo Bar&baz=Foo%20Baz', 'bar=Foo%20Bar&baz=Foo%20Baz', 'normalizes encoding in values'], ['foo=bar&&&test&&', 'foo=bar&test=', 'removes unneeded delimiters'], ['formula=e=m*c^2', 'formula=e%3Dm%2Ac%5E2', 'correctly treats only the first "=" as delimiter and the next as value'],