From efdbe721ed65f95201e00288d99e64b2181ef76b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Tue, 30 Jun 2026 23:04:12 +0200 Subject: [PATCH 1/2] fix: don't cache the 'en' language fallback on anonymous pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Anonymous pages (login page, password-protected public share page) always rendered in English even when default_language was set or the browser sent a matching Accept-Language header, while authenticated pages were translated correctly. Root cause: OC\L10N\Factory is a per-request singleton. When the first no-app findLanguage() ran without a usable Accept-Language match (e.g. the header not forwarded by the web server), setLanguageFromRequest() cached the 'en' last-resort fallback in $requestLanguage. findLanguage() short-circuits on a non-empty $requestLanguage (languageExists(_, 'en') is always true), so every later lookup returned 'en' before default_language or Accept-Language could be consulted. Authenticated requests are unaffected because they seed $requestLanguage from the stored user language first. Stop caching the 'en' fallback so default_language and Accept-Language are re-evaluated on subsequent lookups. The FactoryTest data rows that enshrined the 'en' caching are updated to assert the fallback is returned but not stored. Fixes #41618 Co-Authored-By: Claude Opus 4.8 Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> --- changelog/unreleased/41618 | 11 +++++++++++ lib/private/L10N/Factory.php | 9 ++++++--- tests/lib/L10N/FactoryTest.php | 9 +++++++-- 3 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 changelog/unreleased/41618 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..776c09cf8882 100644 --- a/tests/lib/L10N/FactoryTest.php +++ b/tests/lib/L10N/FactoryTest.php @@ -404,8 +404,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 From 928906020f4abfe96435288961958359da436045 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Thu, 9 Jul 2026 09:34:14 +0200 Subject: [PATCH 2/2] test: add end-to-end regression guard for #41618 anon language fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reproduces the two-call scenario the fix targets: on an anonymous request the first no-app findLanguage() falls through to the 'en' last-resort fallback (no Accept-Language match, no default_language). This must not poison the per-request Factory singleton, so a later findLanguage() honors default_language instead of short-circuiting on a cached 'en'. Co-Authored-By: Claude Opus 4.8 Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> --- tests/lib/L10N/FactoryTest.php | 50 ++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/lib/L10N/FactoryTest.php b/tests/lib/L10N/FactoryTest.php index 776c09cf8882..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 *