diff --git a/changelog/unreleased/41618 b/changelog/unreleased/41618 new file mode 100644 index 000000000000..4f79a3adafb1 --- /dev/null +++ b/changelog/unreleased/41618 @@ -0,0 +1,11 @@ +Bugfix: Honor language on anonymous pages + +Anonymous pages (the login page and the password-protected public share page) +always rendered in English even when default_language was configured or the +browser sent a matching Accept-Language header, while authenticated pages were +translated correctly. The language factory cached the "en" last-resort fallback +as the per-request language, which then short-circuited every later lookup +before default_language or Accept-Language could be consulted. The fallback is +no longer cached, so anonymous pages honor default_language and Accept-Language. + +https://github.com/owncloud/core/issues/41618 diff --git a/lib/private/L10N/Factory.php b/lib/private/L10N/Factory.php index 59bdf33a7975..1ad4f43693ab 100644 --- a/lib/private/L10N/Factory.php +++ b/lib/private/L10N/Factory.php @@ -251,9 +251,12 @@ public function setLanguageFromRequest($app = null) { } } - if ($app === null && !$this->requestLanguage) { - $this->requestLanguage = 'en'; - } + // Do NOT cache the 'en' last-resort fallback in requestLanguage: doing so + // poisons the per-request Factory singleton so that a later findLanguage() + // returns 'en' before ever consulting default_language or Accept-Language. + // This broke anonymous pages (login / public-share password page) whenever + // the first no-app lookup ran without a visible Accept-Language header + // (e.g. the header not forwarded by the web server) — see issue #41618. return 'en'; // Last try: English } diff --git a/tests/lib/L10N/FactoryTest.php b/tests/lib/L10N/FactoryTest.php index 4d15c2ca113e..47fa4c9f63f4 100644 --- a/tests/lib/L10N/FactoryTest.php +++ b/tests/lib/L10N/FactoryTest.php @@ -270,6 +270,56 @@ public function testFindLanguageWithNotExistingRequestLanguageAndNotExistingStor $this->assertSame('en', $factory->findLanguage('MyApp')); } + /** + * Regression test for #41618: on an anonymous request the first no-app + * findLanguage() falls through to the 'en' last-resort fallback (no + * Accept-Language match, no default_language yet). This must NOT poison the + * per-request Factory singleton — a later findLanguage() has to re-evaluate + * default_language instead of short-circuiting on a cached 'en'. + */ + public function testFindLanguageDoesNotCacheFallbackAcrossAnonymousLookups(): void { + $factory = $this->getFactory(['languageExists']); + + // Only the second lookup resolves a language (default_language 'de'). + $factory + ->expects($this->once()) + ->method('languageExists') + ->with(null, 'de') + ->willReturn(true); + + // installed=true and getUser()=null on both lookups (anonymous). The + // first lookup sees no default_language, the second one does. + $this->config + ->expects($this->exactly(4)) + ->method('getSystemValue') + ->withConsecutive( + ['installed', false], + ['default_language', false], + ['installed', false], + ['default_language', false], + ) + ->willReturnOnConsecutiveCalls(true, false, true, 'de'); + $this->userSession + ->expects($this->exactly(2)) + ->method('getUser') + ->willReturn(null); + + // Accept-Language header is not forwarded (e.g. official Docker/Apache + // image), so the first lookup can only reach the 'en' fallback. + $this->request + ->expects($this->once()) + ->method('getHeader') + ->with('ACCEPT_LANGUAGE') + ->willReturn(''); + + // First lookup: nothing matchable -> last-resort 'en'. + $this->assertSame('en', $factory->findLanguage(), 'First lookup falls back to en'); + // The fallback must not have been cached. + $this->assertSame('', self::invokePrivate($factory, 'requestLanguage'), 'Fallback en is not cached'); + // Second lookup: default_language is now honored instead of cached 'en'. + $this->assertSame('de', $factory->findLanguage(), 'Second lookup honors default_language'); + } + /** * @dataProvider dataFindAvailableLanguages * @@ -404,8 +454,13 @@ public function dataSetLanguageFromRequest(): array { [null, 'de', null, ['de'], 'de', 'de'], [null, 'de,en', null, ['de'], 'de', 'de'], [null, 'de-DE,en-US;q=0.8,en;q=0.6', null, ['de'], 'de', 'de'], - // Language is not available - [null, 'de', null, ['ru'], 'en', 'en'], + // No matchable language and 'en' is neither requested nor available → + // last-resort 'en' is returned but must NOT be cached as the request + // language (caching it would defeat default_language on a later lookup + // — see issue #41618). + [null, 'de', null, ['ru'], 'en', ''], + // 'en' is explicitly requested AND available → a real match, which IS + // cached as the request language (correct behavior, not the #41618 bug). [null, 'de,en', null, ['ru', 'en'], 'en', 'en'], [null, 'de-DE,en-US;q=0.8,en;q=0.6', null, ['ru', 'en'], 'en', 'en'], // Language is available, but request language is set