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:
commit
fb123e4fca
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
-----
|
||||
|
||||
|
@ -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];
|
||||
|
@ -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);
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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'],
|
||||
|
Reference in New Issue
Block a user