diff --git a/core/lib/Drupal/Component/Utility/UrlHelper.php b/core/lib/Drupal/Component/Utility/UrlHelper.php index 20c2748f2c..316f598c21 100644 --- a/core/lib/Drupal/Component/Utility/UrlHelper.php +++ b/core/lib/Drupal/Component/Utility/UrlHelper.php @@ -160,7 +160,7 @@ public static function parse($url) { } // If there is a query string, transform it into keyed query parameters. if (isset($parts[1])) { - parse_str($parts[1], $options['query']); + $options['query'] = static::parseQueryString($parts[1]); } } // Internal URLs. @@ -171,7 +171,7 @@ public static function parse($url) { // Strip the leading slash that was just added. $options['path'] = substr($parts['path'], 1); if (isset($parts['query'])) { - parse_str($parts['query'], $options['query']); + $options['query'] = static::parseQueryString($parts['query']); } if (isset($parts['fragment'])) { $options['fragment'] = $parts['fragment']; @@ -181,6 +181,42 @@ public static function parse($url) { return $options; } + /** + * A wrapper for parse_str() that restores original parameter names. + * + * The parse_str() function can be used to set variables in the current scope, + * although this is deprecated. Because variables in PHP cannot have periods + * and spaces in their names, parse_str() converts them to underscores. This + * breaks external links and front end libraries that require specific + * parameter names. It can also cause name collisions. + * + * @param string $query + * A query string to parse. + * + * @return array + * The result of parse_str() with original parameter names restored. + */ + public static function parseQueryString($query) { + // Create an array to map parsed parameters to original names. + $parsed = []; + foreach (explode('&', $query) as $param) { + list($name) = explode('=', $param, 2); + $name = rawurldecode($name); + // Extract the first item of a potentially nested parameter name. + $name = strstr($name, '[', true) ?: $name; + // Parse the query parameter. + parse_str($param, $param); + // Merge the parameter into the parsed array. + if (isset($parsed[$name])) { + if (!is_array($parsed[$name])) { + $parsed[$name] = [$parsed[$name]]; + } + $parsed[$name] = NestedArray::mergeDeepArray([$parsed[$name], reset($param)], TRUE); + } + else { + $parsed[$name] = reset($param); + } + } + return $parsed; + } + /** * Encodes a Drupal path for use in a URL. * diff --git a/core/lib/Drupal/Core/Url.php b/core/lib/Drupal/Core/Url.php index 82c2c600b3..1c9bab02e4 100644 --- a/core/lib/Drupal/Core/Url.php +++ b/core/lib/Drupal/Core/Url.php @@ -303,8 +303,7 @@ public static function fromUri($uri, $options = []) { } if (!empty($uri_parts['query'])) { - $uri_query = []; - parse_str($uri_parts['query'], $uri_query); + $uri_query = UrlHelper::parseQueryString($uri_parts['query']); $uri_options['query'] = isset($uri_options['query']) ? $uri_options['query'] + $uri_query : $uri_query; unset($uri_parts['query']); } diff --git a/core/tests/Drupal/Tests/Component/Utility/UrlHelperTest.php b/core/tests/Drupal/Tests/Component/Utility/UrlHelperTest.php index 613ecfffbe..8c583c84e2 100644 --- a/core/tests/Drupal/Tests/Component/Utility/UrlHelperTest.php +++ b/core/tests/Drupal/Tests/Component/Utility/UrlHelperTest.php @@ -269,6 +269,30 @@ public static function providerTestParse() { 'fragment' => 'footer', ], ], + 'query parameters with characters not allowed in PHP variable names' => [ + 'http://www.example.com/my/path?single.value=&single value=&single_value=&multi.value[first]=&multi.value[second]=&multi value[third]=&multi value[fourth]=&multi_value[fifth]=&multi_value[sixth]=', + [ + 'path' => 'http://www.example.com/my/path', + 'query' => [ + 'single.value' => '', + 'single value' => '', + 'single_value' => '', + 'multi.value' => [ + 'first' => '', + 'second' => '', + ], + 'multi value' => [ + 'third' => '', + 'fourth' => '', + ], + 'multi_value' => [ + 'fifth' => '', + 'sixth' => '', + ], + ], + 'fragment' => '', + ], + ], 'absolute fragment, no query' => [ 'http://www.example.com/my/path#footer', [ diff --git a/core/tests/Drupal/Tests/Core/UrlTest.php b/core/tests/Drupal/Tests/Core/UrlTest.php index 450f03407c..4004ff5c86 100644 --- a/core/tests/Drupal/Tests/Core/UrlTest.php +++ b/core/tests/Drupal/Tests/Core/UrlTest.php @@ -333,6 +333,16 @@ public function testGetUriForExternalUrl() { $this->assertEquals('http://example.com/test', $url->getUri()); } + /** + * Tests the getUri() method for external URLs with parameters. + * + * @covers ::getUri + */ + public function testGetUriForExternalUrlWithParameters() { + $url = Url::fromUri('http://www.example.com/my/path?single.value=&single value=&single_value=&multi.value[first]=&multi.value[second]=&multi value[third]=&multi value[fourth]=&multi_value[fifth]=&multi_value[sixth]='); + $this->assertEquals('http://www.example.com/my/path?single.value=&single value=&single_value=&multi.value[first]=&multi.value[second]=&multi value[third]=&multi value[fourth]=&multi_value[fifth]=&multi_value[sixth]=', $url->getUri()); + } + /** * Tests the getUri() and isExternal() methods for protocol-relative URLs. *