Skip to content

Commit 54305d6

Browse files
committed
feature #26220 [HttpFoundation] Use parse_str() for query strings normalization (nicolas-grekas)
This PR was merged into the 4.1-dev branch. Discussion ---------- [HttpFoundation] Use parse_str() for query strings normalization | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | - | License | MIT | Doc PR | - Follow up of #26214 and #26202 The current normalization logic is both too loose and too broad: it changes the order of recursive data structures, while not normalizing keys. Since the normalization logic varies by query string parser, I'd like to propose a logic that exactly matches the native PHP one, which is exposed to userland via `parse_str()`. Using this, we accurately remove all useless information, while preserving all the meaningful one. (The change in `overrideGlobals()` is a bug fix to me btw, the current logic breaks the interpretation of legitimate query strings.) Commits ------- 5133536 [HttpFoundation] Use parse_str() for query strings normalization
2 parents e157ded + 5133536 commit 54305d6

File tree

3 files changed

+16
-30
lines changed

3 files changed

+16
-30
lines changed

src/Symfony/Component/HttpFoundation/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
4.1.0
55
-----
66

7+
* Query string normalization uses `parse_str()` instead of custom parsing logic.
78
* Passing the file size to the constructor of the `UploadedFile` class is deprecated.
89
* The `getClientSize()` method of the `UploadedFile` class is deprecated. Use `getSize()` instead.
910
* added `RedisSessionHandler` to use Redis as a session storage

src/Symfony/Component/HttpFoundation/Request.php

Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -629,31 +629,10 @@ public static function normalizeQueryString($qs)
629629
return '';
630630
}
631631

632-
$parts = array();
633-
$order = array();
634-
635-
foreach (explode('&', $qs) as $param) {
636-
if ('' === $param || '=' === $param[0]) {
637-
// Ignore useless delimiters, e.g. "x=y&".
638-
// Also ignore pairs with empty key, even if there was a value, e.g. "=value", as such nameless values cannot be retrieved anyway.
639-
// PHP also does not include them when building _GET.
640-
continue;
641-
}
642-
643-
$keyValuePair = explode('=', $param, 2);
644-
645-
// GET parameters, that are submitted from a HTML form, encode spaces as "+" by default (as defined in enctype application/x-www-form-urlencoded).
646-
// PHP also converts "+" to spaces when filling the global _GET or when using the function parse_str. This is why we use urldecode and then normalize to
647-
// RFC 3986 with rawurlencode.
648-
$parts[] = isset($keyValuePair[1]) ?
649-
rawurlencode(urldecode($keyValuePair[0])).'='.rawurlencode(urldecode($keyValuePair[1])) :
650-
rawurlencode(urldecode($keyValuePair[0]));
651-
$order[] = urldecode($keyValuePair[0]);
652-
}
653-
654-
array_multisort($order, SORT_ASC, $parts);
632+
parse_str($qs, $qs);
633+
ksort($qs);
655634

656-
return implode('&', $parts);
635+
return http_build_query($qs, '', '&', PHP_QUERY_RFC3986);
657636
}
658637

659638
/**

src/Symfony/Component/HttpFoundation/Tests/RequestTest.php

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -675,7 +675,7 @@ public function testGetQueryString($query, $expectedQuery, $msg)
675675
public function getQueryStringNormalizationData()
676676
{
677677
return array(
678-
array('foo', 'foo', 'works with valueless parameters'),
678+
array('foo', 'foo=', 'works with valueless parameters'),
679679
array('foo=', 'foo=', 'includes a dangling equal sign'),
680680
array('bar=&foo=bar', 'bar=&foo=bar', '->works with empty parameters'),
681681
array('foo=bar&bar=', 'bar=&foo=bar', 'sorts keys alphabetically'),
@@ -684,18 +684,24 @@ public function getQueryStringNormalizationData()
684684
// PHP also converts "+" to spaces when filling the global _GET or when using the function parse_str.
685685
array('him=John%20Doe&her=Jane+Doe', 'her=Jane%20Doe&him=John%20Doe', 'normalizes spaces in both encodings "%20" and "+"'),
686686

687-
array('foo[]=1&foo[]=2', 'foo%5B%5D=1&foo%5B%5D=2', 'allows array notation'),
688-
array('foo=1&foo=2', 'foo=1&foo=2', 'allows repeated parameters'),
687+
array('foo[]=1&foo[]=2', 'foo%5B0%5D=1&foo%5B1%5D=2', 'allows array notation'),
688+
array('foo=1&foo=2', 'foo=2', 'merges repeated parameters'),
689689
array('pa%3Dram=foo%26bar%3Dbaz&test=test', 'pa%3Dram=foo%26bar%3Dbaz&test=test', 'works with encoded delimiters'),
690-
array('0', '0', 'allows "0"'),
691-
array('Jane Doe&John%20Doe', 'Jane%20Doe&John%20Doe', 'normalizes encoding in keys'),
690+
array('0', '0=', 'allows "0"'),
691+
array('Jane Doe&John%20Doe', 'Jane_Doe=&John_Doe=', 'normalizes encoding in keys'),
692692
array('her=Jane Doe&him=John%20Doe', 'her=Jane%20Doe&him=John%20Doe', 'normalizes encoding in values'),
693-
array('foo=bar&&&test&&', 'foo=bar&test', 'removes unneeded delimiters'),
693+
array('foo=bar&&&test&&', 'foo=bar&test=', 'removes unneeded delimiters'),
694694
array('formula=e=m*c^2', 'formula=e%3Dm%2Ac%5E2', 'correctly treats only the first "=" as delimiter and the next as value'),
695695

696696
// Ignore pairs with empty key, even if there was a value, e.g. "=value", as such nameless values cannot be retrieved anyway.
697697
// PHP also does not include them when building _GET.
698698
array('foo=bar&=a=b&=x=y', 'foo=bar', 'removes params with empty key'),
699+
700+
// Don't reorder nested query string keys
701+
array('foo[]=Z&foo[]=A', 'foo%5B0%5D=Z&foo%5B1%5D=A', 'keeps order of values'),
702+
array('foo[Z]=B&foo[A]=B', 'foo%5BZ%5D=B&foo%5BA%5D=B', 'keeps order of keys'),
703+
704+
array('utf8=✓', 'utf8=%E2%9C%93', 'encodes UTF-8'),
699705
);
700706
}
701707

0 commit comments

Comments
 (0)