Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions changelog/unreleased/41618
Original file line number Diff line number Diff line change
@@ -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
9 changes: 6 additions & 3 deletions lib/private/L10N/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
59 changes: 57 additions & 2 deletions tests/lib/L10N/FactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down Expand Up @@ -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
Expand Down