diff --git a/UrlManager.php b/UrlManager.php index 2adb73c..01a252b 100644 --- a/UrlManager.php +++ b/UrlManager.php @@ -7,6 +7,7 @@ use yii\base\InvalidConfigException; use yii\web\Cookie; use yii\web\UrlManager as BaseUrlManager; +use yii\web\UrlNormalizerRedirectException; /** * UrlManager @@ -158,6 +159,7 @@ public function getDefaultLanguage() public function parseRequest($request) { if ($this->enableLocaleUrls && $this->languages) { + $this->_request = $request; $process = true; if ($this->ignoreLanguageUrlPatterns) { $pathInfo = $request->getPathInfo(); @@ -169,13 +171,17 @@ public function parseRequest($request) } } if ($process && !$this->_processed) { - // If a normalizer is configured, let it do it's job + // Check if a normalizer wants to redirect + $normalized = false; if (property_exists($this, 'normalizer') && $this->normalizer!==false) { - parent::parseRequest($request); + try { + parent::parseRequest($request); + } catch (UrlNormalizerRedirectException $e) { + $normalized = true; + } } - // Still here, so parent::parseRequest() didn't throw a UrlNormalizerRedirectException. $this->_processed = true; - $this->processLocaleUrl($request); + $this->processLocaleUrl($normalized); } } return parent::parseRequest($request); @@ -291,12 +297,11 @@ public function createUrl($params) * If no parameter is found it will try to detect the language from persistent storage (session / * cookie) or from browser settings. * - * @var \yii\web\Request $request + * @param bool $normalized whether a UrlNormalizer tried to redirect */ - protected function processLocaleUrl($request) + protected function processLocaleUrl($normalized) { - $this->_request = $request; - $pathInfo = $request->getPathInfo(); + $pathInfo = $this->_request->getPathInfo(); $parts = []; foreach ($this->languages as $k => $v) { $value = is_string($k) ? $k : $v; @@ -310,7 +315,7 @@ protected function processLocaleUrl($request) } $pattern = implode('|', $parts); if (preg_match("#^($pattern)\b(/?)#i", $pathInfo, $m)) { - $request->setPathInfo(mb_substr($pathInfo, mb_strlen($m[1].$m[2]))); + $this->_request->setPathInfo(mb_substr($pathInfo, mb_strlen($m[1].$m[2]))); $code = $m[1]; if (isset($this->languages[$code])) { // Replace alias with language code @@ -351,7 +356,9 @@ protected function processLocaleUrl($request) // "Reset" case: We called e.g. /fr/demo/page so the persisted language was set back to "fr". // Now we can redirect to the URL without language prefix, if default prefixes are disabled. - if (!$this->enableDefaultLanguageUrlCode && $language===$this->_defaultLanguage) { + $reset = !$this->enableDefaultLanguageUrlCode && $language===$this->_defaultLanguage; + + if ($reset || $normalized) { $this->redirectToLanguage(''); } } else { @@ -360,12 +367,12 @@ protected function processLocaleUrl($request) $language = Yii::$app->session->get($this->languageSessionKey); $language!==null && Yii::trace("Found persisted language '$language' in session.", __METHOD__); if ($language===null) { - $language = $request->getCookies()->getValue($this->languageCookieName); + $language = $this->_request->getCookies()->getValue($this->languageCookieName); $language!==null && Yii::trace("Found persisted language '$language' in cookie.", __METHOD__); } } if ($language===null && $this->enableLanguageDetection) { - foreach ($request->getAcceptableLanguages() as $acceptable) { + foreach ($this->_request->getAcceptableLanguages() as $acceptable) { list($language,$country) = $this->matchCode($acceptable); if ($language!==null) { $language = $country===null ? $language : "$language-$country"; @@ -390,7 +397,10 @@ protected function processLocaleUrl($request) if ($key && is_string($key)) { $language = $key; } - $this->redirectToLanguage($this->keepUppercaseLanguageCode ? $language : strtolower($language)); + if (!$this->keepUppercaseLanguageCode) { + $language = strtolower($language); + } + $this->redirectToLanguage($language); } } @@ -448,7 +458,12 @@ protected function matchCode($code) */ protected function redirectToLanguage($language) { - $result = parent::parseRequest($this->_request); + try { + $result = parent::parseRequest($this->_request); + } catch (UrlNormalizerRedirectException $e) { + $route = is_array($e->url) ? $e->url[0] : $e->url; + $result = [$route, $this->_request->getQueryParams()]; + } if ($result === false) { throw new \yii\web\NotFoundHttpException(Yii::t('yii', 'Page not found.')); } @@ -473,6 +488,5 @@ protected function redirectToLanguage($language) } else { Yii::$app->end(); } - } } diff --git a/tests/RedirectTest.php b/tests/RedirectTest.php index 79a5fca..cc947da 100644 --- a/tests/RedirectTest.php +++ b/tests/RedirectTest.php @@ -19,7 +19,8 @@ class RedirectTest extends TestCase * // - a string with a URL that should be redirected to, * // - `false` if there should be no redirect * // - an array of individual request/session/cookie configurations - * // each indexed by the expected redirect URL (or `false`) + * // of this form: + * // [$to, 'request' => .., 'session' => ..., 'cookie' => ...] * ], * ] * ``` @@ -40,34 +41,41 @@ class RedirectTest extends TestCase '/site/page' => [ // Acceptable languages in request - '/de/site/page' => ['request' => ['acceptableLanguages' => ['de']]], - '/at/site/page' => ['request' => ['acceptableLanguages' => ['de-at', 'de']]], - '/wc/site/page' => ['request' => ['acceptableLanguages' => ['wc']]], - '/es-bo/site/page' => ['request' => ['acceptableLanguages' => ['es-BO', 'es', 'en']]], - '/es-bo/site/page' => ['request' => ['acceptableLanguages' => ['es-bo', 'es', 'en']]], - '/wc-at/site/page' => ['request' => ['acceptableLanguages' => ['wc-AT', 'de', 'en']]], - '/pt/site/page' => ['request' => ['acceptableLanguages' => ['pt-br']]], - '/alias/site/page' => ['request' => ['acceptableLanguages' => ['fr']]], + ['/de/site/page', 'request' => ['acceptableLanguages' => ['de']]], + ['/at/site/page', 'request' => ['acceptableLanguages' => ['de-at', 'de']]], + ['/wc/site/page', 'request' => ['acceptableLanguages' => ['wc']]], + ['/es-bo/site/page', 'request' => ['acceptableLanguages' => ['es-BO', 'es', 'en']]], + ['/es-bo/site/page', 'request' => ['acceptableLanguages' => ['es-bo', 'es', 'en']]], + ['/wc-at/site/page', 'request' => ['acceptableLanguages' => ['wc-AT', 'de', 'en']]], + ['/pt/site/page', 'request' => ['acceptableLanguages' => ['pt-br']]], + ['/alias/site/page', 'request' => ['acceptableLanguages' => ['fr']]], // no redirect - false => ['request' => ['acceptableLanguages' => ['en']]], // default language + [false, 'request' => ['acceptableLanguages' => ['en']]], // default language // Language in session - '/de/site/page' => ['session' => ['_language' => 'de']], - '/at/site/page' => ['session' => ['_language' => 'de-AT']], - '/wc/site/page' => ['session' => ['_language' => 'wc']], - '/es-bo/site/page' => ['session' => ['_language' => 'es-BO']], - '/wc-at/site/page' => ['session' => ['_language' => 'wc-AT']], - '/pt/site/page' => ['session' => ['_language' => 'pt']], - '/alias/site/page' => ['session' => ['_language' => 'fr']], + ['/de/site/page', 'session' => ['_language' => 'de']], + ['/at/site/page', 'session' => ['_language' => 'de-AT']], + ['/wc/site/page', 'session' => ['_language' => 'wc']], + ['/es-bo/site/page', 'session' => ['_language' => 'es-BO']], + ['/wc-at/site/page', 'session' => ['_language' => 'wc-AT']], + ['/pt/site/page', 'session' => ['_language' => 'pt']], + ['/alias/site/page', 'session' => ['_language' => 'fr']], // Language in cookie - '/de/site/page' => ['cookie' => ['_language' => 'de']], - '/at/site/page' => ['cookie' => ['_language' => 'de-AT']], - '/wc/site/page' => ['cookie' => ['_language' => 'wc']], - '/es-bo/site/page' => ['cookie' => ['_language' => 'es-BO']], - '/wc-at/site/page' => ['cookie' => ['_language' => 'wc-AT']], - '/pt/site/page' => ['cookie' => ['_language' => 'pt']], - '/alias/site/page' => ['cookie' => ['_language' => 'fr']], + ['/de/site/page', 'cookie' => ['_language' => 'de']], + ['/at/site/page', 'cookie' => ['_language' => 'de-AT']], + ['/wc/site/page', 'cookie' => ['_language' => 'wc']], + ['/es-bo/site/page', 'cookie' => ['_language' => 'es-BO']], + ['/wc-at/site/page', 'cookie' => ['_language' => 'wc-AT']], + ['/pt/site/page', 'cookie' => ['_language' => 'pt']], + ['/alias/site/page', 'cookie' => ['_language' => 'fr']], + ], + + // Requests with other language in session, cookie or request headers + '/de/site/page' => [ + [false, 'session' => ['_language' => 'wc']], + [false, 'cookie' => ['_language' => 'wc']], + [false, 'request' => ['acceptableLanguages' => ['en']]], ], ], ], @@ -75,12 +83,26 @@ class RedirectTest extends TestCase // Default language uses language code [ 'urlManager' => [ - 'languages' => ['en-US', 'en', 'de'], + 'languages' => ['en-US', 'en', 'de', 'pt', 'at' => 'de-AT', 'alias' => 'fr', 'es-BO', 'wc-*'], 'enableDefaultLanguageUrlCode' => true, ], 'redirects' => [ - '/' => '/en', - '/site/page' => '/en/site/page', + '/' => [ + ['/en'], // default language + ['/de', 'session' => ['_language' => 'de']], + ['/alias', 'cookie' => ['_language' => 'fr']], + ], + '/site/page' => [ + ['/en/site/page', ], // default language + ['/de/site/page', 'session' => ['_language' => 'de']], + ['/alias/site/page', 'cookie' => ['_language' => 'fr']], + ], + // Requests with other language in session, cookie or request headers + '/de/site/page' => [ + [false, 'session' => ['_language' => 'wc']], + [false, 'cookie' => ['_language' => 'wc']], + [false, 'request' => ['acceptableLanguages' => ['en']]], + ], ], ], @@ -93,8 +115,8 @@ class RedirectTest extends TestCase 'redirects' => [ '/es-BO/site/page' => false, '/site/page' => [ - '/en-US/site/page' => ['session' => ['_language' => 'en-US']], - '/en-US/site/page' => ['cookie' => ['_language' => 'en-US']], + ['/en-US/site/page', 'session' => ['_language' => 'en-US']], + ['/en-US/site/page', 'cookie' => ['_language' => 'en-US']], ] ], ], @@ -127,13 +149,21 @@ class RedirectTest extends TestCase ], [ 'urlManager' => [ - 'languages' => ['en-US', 'en', 'de'], + 'languages' => ['en-US', 'en', 'de', 'pt', 'at' => 'de-AT', 'alias' => 'fr', 'es-BO', 'wc-*'], 'enableDefaultLanguageUrlCode' => true, 'suffix' => '/' ], 'redirects' => [ - '/' => '/en/', - '/site/page/' => '/en/site/page/', + '/' => [ + ['/en/'], // default language + ['/de/', 'session' => ['_language' => 'de']], + ['/alias/', 'cookie' => ['_language' => 'fr']], + ], + '/site/page/' => [ + ['/en/site/page/'], // default language + ['/de/site/page/', 'session' => ['_language' => 'de']], + ['/alias/site/page/', 'cookie' => ['_language' => 'fr']], + ], ], ], @@ -147,13 +177,17 @@ class RedirectTest extends TestCase ], ], 'redirects' => [ + '' => '', + '/site/page' => '/site/page/', + '/site/page/' => false, + '/de' => '/de/', // normalizer '/de/' => false, '/de/site/login' => '/de/site/login/', // normalizer '/de/site/login/' => false, - '/en/site/login' => '/en/site/login/', // normalizer + '/en/site/login' => '/site/login/', // normalizer '/en/site/login/' => '/site/login/', // localeurls ], ], @@ -165,14 +199,64 @@ class RedirectTest extends TestCase ], ], 'redirects' => [ + '' => '', + '/site/page/' => '/site/page', + '/site/page' => false, + '/de/' => '/de', // normalizer '/de' => false, '/de/site/login/' => '/de/site/login', // normalizer '/de/site/login' => false, - '/en/site/login/' => '/en/site/login', // normalizer - '/en/site/login' => '/site/login', // localeurls + '/en/site/login/' => '/site/login', // normalizer + '/en/site/login' => '/site/login', // localeurls + ], + ], + // Normalizer with default language code + [ + 'urlManager' => [ + 'languages' => ['en-US', 'en', 'de', 'pt', 'at' => 'de-AT', 'alias' => 'fr', 'es-BO', 'wc-*'], + 'enableDefaultLanguageUrlCode' => true, + 'suffix' => '/', + 'normalizer' => [ + 'class' => '\yii\web\UrlNormalizer', + ], + ], + 'redirects' => [ + '' => [ + ['/en/'], // default language + ['/de/', 'session' => ['_language' => 'de']], + ['/alias/', 'cookie' => ['_language' => 'fr']], + ], + '/site/page' => [ + ['/en/site/page/'], // default language + ['/de/site/page/', 'session' => ['_language' => 'de']], + ['/alias/site/page/', 'cookie' => ['_language' => 'fr']], + ], + '/en/site/page' => '/en/site/page/', + ], + ], + [ + 'urlManager' => [ + 'languages' => ['en-US', 'en', 'de', 'pt', 'at' => 'de-AT', 'alias' => 'fr', 'es-BO', 'wc-*'], + 'enableDefaultLanguageUrlCode' => true, + 'normalizer' => [ + 'class' => '\yii\web\UrlNormalizer', + ], + ], + 'redirects' => [ + '/' => [ + ['/en'], // default language + ['/de', 'session' => ['_language' => 'de']], + ['/alias', 'cookie' => ['_language' => 'fr']], + ], + '/site/page/' => [ + ['/en/site/page'], // default language + ['/de/site/page', 'session' => ['_language' => 'de']], + ['/alias/site/page', 'cookie' => ['_language' => 'fr']], + ], + '/en/site/page/' => '/en/site/page', ], ], ]; @@ -183,7 +267,8 @@ public function testRedirects() $urlManager = isset($config['urlManager']) ? $config['urlManager'] : []; foreach ($config['redirects'] as $from => $to) { if (is_array($to)) { - foreach ($to as $url => $params) { + foreach ($to as $params) { + $url = $params[0]; $request = isset($params['request']) ? $params['request'] : []; $session = isset($params['session']) ? $params['session'] : []; $cookie = isset($params['cookie']) ? $params['cookie'] : []; @@ -217,10 +302,18 @@ public function performRedirectTest($from, $to, $urlManager, $request = [], $ses if ($cookie!==null) { $_COOKIE = $cookie; } + $configMessage = print_r([ + 'from' => $from, + 'to' => $to, + 'urlManager' => $urlManager, + 'request' => $request, + 'session' => $session, + 'cookie' => $cookie, + ], true); try { $this->mockRequest($from, $request); if ($to) { - $this->fail("No redirect for $from to $to with urlManager config:\n" . print_r($urlManager, true)); + $this->fail("No redirect:\n$configMessage"); } } catch (\yii\web\UrlNormalizerRedirectException $e) { $url = $e->url; @@ -231,9 +324,11 @@ public function performRedirectTest($from, $to, $urlManager, $request = [], $ses } $url += Yii::$app->request->getQueryParams(); } - $this->assertEquals($this->prepareUrl($to), Url::to($url, $e->scheme)); + $message = "UrlNormalizerRedirectException:\n$configMessage"; + $this->assertEquals($this->prepareUrl($to), Url::to($url, $e->scheme), $message); } catch (\yii\base\Exception $e) { - $this->assertEquals($this->prepareUrl($to), $e->getMessage()); + $message = "Redirection:\n$configMessage"; + $this->assertEquals($this->prepareUrl($to), $e->getMessage(), $message); } }