feature #37272 [HttpFoundation] add HeaderUtils::parseQuery(): it does the same as parse_str() but preserves dots in variable names (nicolas-grekas)

This PR was merged into the 5.2-dev branch.

Discussion
----------

[HttpFoundation] add `HeaderUtils::parseQuery()`: it does the same as `parse_str()` but preserves dots in variable names

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Tickets       | -
| License       | MIT
| Doc PR        | -

Inspired by https://github.com/symfony/psr-http-message-bridge/pull/80
/cc @drupol

Related to #9009, #29664, #26220 but also https://github.com/api-platform/core/issues/509 and https://www.drupal.org/project/drupal/issues/2984272
/cc @dunglas @alexpott

Commits
-------

dd81e32ec1 [HttpFoundation] add `HeaderUtils::parseQuery()`: it does the same as `parse_str()` but preserves dots in variable names
This commit is contained in:
Fabien Potencier 2020-06-24 15:46:31 +02:00
commit fb123e4fca
7 changed files with 106 additions and 50 deletions

View File

@ -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 @@ class RedirectController
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 @@ class RedirectController
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;
}
}

View File

@ -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",

View File

@ -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
-----

View File

@ -193,6 +193,64 @@ class HeaderUtils
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];

View File

@ -399,7 +399,7 @@ class Request
$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 @@ class Request
return '';
}
parse_str($qs, $qs);
$qs = HeaderUtils::parseQuery($qs);
ksort($qs);
return http_build_query($qs, '', '&', PHP_QUERY_RFC3986);

View File

@ -129,4 +129,41 @@ class HeaderUtilsTest extends TestCase
['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));
}
}

View File

@ -807,7 +807,7 @@ class RequestTest extends TestCase
['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'],