diff --git a/core/core.services.yml b/core/core.services.yml index afccb31..1fb5f8b 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -1117,6 +1117,11 @@ services: arguments: ['@unrouted_url_assembler', '@router.request_context'] tags: - { name: event_subscriber } + route_normalizer_request_subscriber: + class: Drupal\Core\EventSubscriber\RouteNormalizerRequestSubscriber + arguments: ['@url_generator', '@path.matcher'] + tags: + - { name: event_subscriber } redirect_leading_slashes_subscriber: class: Drupal\Core\EventSubscriber\RedirectLeadingSlashesSubscriber tags: diff --git a/core/lib/Drupal/Core/EventSubscriber/RouteNormalizerRequestSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/RouteNormalizerRequestSubscriber.php new file mode 100644 index 0000000..14dce75 --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/RouteNormalizerRequestSubscriber.php @@ -0,0 +1,123 @@ +urlGenerator = $url_generator; + $this->pathMatcher = $path_matcher; + } + + /** + * Performs a redirect if the path changed in routing. + * + * For example, when language negotiation selected a different language for + * the page. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event + * The Event to process. + */ + public function onKernelRequestRedirect(GetResponseEvent $event) { + $request = $event->getRequest(); + $can_redirect = $event->isMasterRequest() + && $request->isMethod('GET') + && !$request->query->has('destination') + && !$this->isRequestChanged($request); + if ($can_redirect) { + // Construct URL to the current route. If it is different from the request + // URL, then we assume that it was changed on a purpose (for example, to + // match the detected language) and perform a redirect. + $route_name = $this->pathMatcher->isFrontPage() ? '' : ''; + $options = [ + 'query' => $request->query->all(), + 'absolute' => TRUE, + ]; + $redirect_uri = $this->urlGenerator->generateFromRoute($route_name, [], $options); + $original_uri = $request->getSchemeAndHttpHost() . $request->getRequestUri(); + if ($redirect_uri != $original_uri) { + $event->setResponse(new RedirectResponse($redirect_uri)); + } + } + } + + /** + * Registers the methods in this class that should be listeners. + * + * @return array + * An array of event listener definitions. + */ + static function getSubscribedEvents() { + // Execute after routes are initialized in + // \Drupal\Core\Routing\RoutePreloader::onRequest(). + $events[KernelEvents::REQUEST][] = array('onKernelRequestRedirect', -1); + + return $events; + } + + /** + * Checks if given request is different from the initial one. + * + * Some code can change the request object, for example, during the inbound + * path processing. Mostly the request attributes are used, but in some cases + * the request query can be changed. If this happens, we can't construct a + * redirect. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request object passed from the KernelEvents::REQUEST event. + * + * @return bool + */ + protected function isRequestChanged(Request $request) { + if (!isset($this->requestFromGlobals)) { + $this->requestFromGlobals = Request::createFromGlobals(); + } + return $request->query->all() != $this->requestFromGlobals->query->all(); + } + +} diff --git a/core/lib/Drupal/Core/Routing/UrlGenerator.php b/core/lib/Drupal/Core/Routing/UrlGenerator.php index 9979b73..c9521ed 100644 --- a/core/lib/Drupal/Core/Routing/UrlGenerator.php +++ b/core/lib/Drupal/Core/Routing/UrlGenerator.php @@ -235,10 +235,8 @@ protected function doGenerate(array $variables, array $defaults, array $tokens, // Add a query string if needed, including extra parameters. $query_params += array_diff_key($parameters, $variables, $defaults); - if ($query_params && $query = http_build_query($query_params, '', '&')) { - // "/" and "?" can be left decoded for better user experience, see - // http://tools.ietf.org/html/rfc3986#section-3.4 - $url .= '?'.strtr($query, array('%2F' => '/')); + if ($query_params && $query = UrlHelper::buildQuery($query_params)) { + $url .= '?' . $query; } return $url; @@ -300,7 +298,7 @@ public function generateFromRoute($name, $parameters = array(), $options = array // Generate a relative URL having no path, just query string and fragment. if ($route->getOption('_no_path')) { - $query = $query_params ? '?' . http_build_query($query_params, '', '&') : ''; + $query = $query_params ? '?' . UrlHelper::buildQuery($query_params) : ''; $url = $query . $fragment; return $collect_bubbleable_metadata ? $generated_url->setGeneratedUrl($url) : $url; } diff --git a/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationContentEntity.php b/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationContentEntity.php index fd9da35..126dc1c 100644 --- a/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationContentEntity.php +++ b/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationContentEntity.php @@ -123,10 +123,12 @@ public function processOutbound($path, &$options = [], Request $request = NULL, unset($options['language']); } - if (isset($options['query']) && is_string($options['query'])) { - $query = []; - parse_str($options['query'], $query); - $options['query'] = $query; + if (isset($options['query'])) { + if (is_string($options['query'])) { + $query = []; + parse_str($options['query'], $query); + $options['query'] = $query; + } } else { $options['query'] = []; diff --git a/core/modules/simpletest/src/WebTestBase.php b/core/modules/simpletest/src/WebTestBase.php index 1e3bfdb..c739fc5 100644 --- a/core/modules/simpletest/src/WebTestBase.php +++ b/core/modules/simpletest/src/WebTestBase.php @@ -2189,7 +2189,7 @@ protected function translatePostValues(array $values) { // The easiest and most straightforward way to translate values suitable for // WebTestBase::drupalPostForm() is to actually build the POST data string // and convert the resulting key/value pairs back into a flat array. - $query = http_build_query($values); + $query = UrlHelper::buildQuery($values); foreach (explode('&', $query) as $item) { list($key, $value) = explode('=', $item); $edit[urldecode($key)] = urldecode($value); diff --git a/core/modules/statistics/src/Tests/StatisticsReportsTest.php b/core/modules/statistics/src/Tests/StatisticsReportsTest.php index f2a79ff..2dbfa8d 100644 --- a/core/modules/statistics/src/Tests/StatisticsReportsTest.php +++ b/core/modules/statistics/src/Tests/StatisticsReportsTest.php @@ -6,6 +6,7 @@ */ namespace Drupal\statistics\Tests; +use Drupal\Component\Utility\UrlHelper; /** * Tests display of statistics report blocks. @@ -26,7 +27,7 @@ function testPopularContentBlock() { $this->drupalGet('node/' . $node->id()); // Manually calling statistics.php, simulating ajax behavior. $nid = $node->id(); - $post = http_build_query(array('nid' => $nid)); + $post = UrlHelper::buildQuery(array('nid' => $nid)); $headers = array('Content-Type' => 'application/x-www-form-urlencoded'); global $base_url; $stats_path = $base_url . '/' . drupal_get_path('module', 'statistics'). '/statistics.php'; diff --git a/core/modules/statistics/src/Tests/StatisticsTokenReplaceTest.php b/core/modules/statistics/src/Tests/StatisticsTokenReplaceTest.php index 792c40a..f7888d4 100644 --- a/core/modules/statistics/src/Tests/StatisticsTokenReplaceTest.php +++ b/core/modules/statistics/src/Tests/StatisticsTokenReplaceTest.php @@ -6,6 +6,7 @@ */ namespace Drupal\statistics\Tests; +use Drupal\Component\Utility\UrlHelper; /** * Generates text using placeholders for dummy content to check statistics token @@ -29,7 +30,7 @@ function testStatisticsTokenReplacement() { $this->drupalGet('node/' . $node->id()); // Manually calling statistics.php, simulating ajax behavior. $nid = $node->id(); - $post = http_build_query(array('nid' => $nid)); + $post = UrlHelper::buildQuery(array('nid' => $nid)); $headers = array('Content-Type' => 'application/x-www-form-urlencoded'); global $base_url; $stats_path = $base_url . '/' . drupal_get_path('module', 'statistics'). '/statistics.php'; diff --git a/core/modules/system/src/Tests/Common/UrlTest.php b/core/modules/system/src/Tests/Common/UrlTest.php index f52d022..3c43406 100644 --- a/core/modules/system/src/Tests/Common/UrlTest.php +++ b/core/modules/system/src/Tests/Common/UrlTest.php @@ -313,12 +313,12 @@ function testExternalUrls() { $url = $test_url; $query = array($this->randomMachineName(5) => $this->randomMachineName(5)); $result = Url::fromUri($url, array('query' => $query))->toString(); - $this->assertEqual($url . '?' . http_build_query($query, '', '&'), $result, 'External URL can be extended with a query string in $options.'); + $this->assertEqual($url . '?' . UrlHelper::buildQuery($query), $result, 'External URL can be extended with a query string in $options.'); // Verify query string can be extended in an external URL. $url = $test_url . '?drupal=awesome'; $query = array($this->randomMachineName(5) => $this->randomMachineName(5)); $result = Url::fromUri($url, array('query' => $query))->toString(); - $this->assertEqual($url . '&' . http_build_query($query, '', '&'), $result); + $this->assertEqual($url . '&' . UrlHelper::buildQuery($query), $result); } } diff --git a/core/modules/system/src/Tests/Routing/ContentNegotiationRoutingTest.php b/core/modules/system/src/Tests/Routing/ContentNegotiationRoutingTest.php index 98515b8..416b11c 100644 --- a/core/modules/system/src/Tests/Routing/ContentNegotiationRoutingTest.php +++ b/core/modules/system/src/Tests/Routing/ContentNegotiationRoutingTest.php @@ -7,6 +7,7 @@ namespace Drupal\system\Tests\Routing; +use Drupal\Component\Utility\UrlHelper; use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\simpletest\KernelTestBase; use Symfony\Component\HttpFoundation\Request; @@ -113,6 +114,18 @@ function testContentRouting() { // Verbose message since simpletest doesn't let us provide a message and // see the error. $this->assertTrue(TRUE, $message); + + // Handle redirect. + if ($response->isRedirect()) { + $parsed = parse_url($response->headers->get('Location')); + $path = $parsed['path']; + if (isset($parsed['query'])) { + $path .= '?' . $parsed['query']; + } + $request = Request::create($path); + $response = $kernel->handle($request); + } + $this->assertEqual($response->getStatusCode(), Response::HTTP_OK); $this->assertTrue(strpos($response->headers->get('Content-type'), $content_type) !== FALSE); }