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

Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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;
}
}
2 changes: 1 addition & 1 deletion src/Symfony/Bundle/FrameworkBundle/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions src/Symfony/Component/HttpFoundation/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
-----

Expand Down
58 changes: 58 additions & 0 deletions src/Symfony/Component/HttpFoundation/HeaderUtils.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

@dunglas dunglas Jun 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we just use the native parse_str() if $ignoreBracket is set to false? And so rename $ignoreBracketsto something else such as$phpCompat = true` or something like that?

Many PHP libraries rely on the fact that dot are replaced by underscores (CGI-like) and we may introduce issues by changing this. I would prefer to have a pure PHP mode (with all the oddities, including the dots replacement etc), and a "strict" mode doing nothing more than the URL class in JS for instance. It feels wrong to me to have some intermediary modes such as "replace the dots but ignore brackets".

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sorry I'm missing your logic. To me, it feels wrong to parse dots (replacing them with _) but ignore brackets.
The purpose of the method is to give you access to the original names and have a toggle to accept multiple values with or without using [].
Libs that expect keys mangled by php should be given arrays mangled by php, ie you wouldn't use this function with them.

{
$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];
Expand Down
4 changes: 2 additions & 2 deletions src/Symfony/Component/HttpFoundation/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
37 changes: 37 additions & 0 deletions src/Symfony/Component/HttpFoundation/Tests/HeaderUtilsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@drupol this is the test case with a space in the name

["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));
}
}
2 changes: 1 addition & 1 deletion src/Symfony/Component/HttpFoundation/Tests/RequestTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down