Skip to content

Commit b7ca7d0

Browse files
committed
feat(theming): allow request-scoped light/dark theme override
Support ?theme=light and ?theme=dark as a request-scoped override that is not persisted to user settings and never bypasses an admin-configured enforce_theme. Invalid, empty, non-string or unknown values are ignored, and non-visual themes such as the OpenDyslexic font theme are preserved. The override is applied only in ThemesService::getEnabledThemes() so that getThemes() and theme registration remain unchanged. Assisted-by: Cline Signed-off-by: Jack Arru <giacomo@beta.srl>
1 parent fea2f67 commit b7ca7d0

2 files changed

Lines changed: 145 additions & 1 deletion

File tree

apps/theming/lib/Service/ThemesService.php

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,25 @@
1616
use OCA\Theming\Themes\HighContrastTheme;
1717
use OCA\Theming\Themes\LightTheme;
1818
use OCP\IConfig;
19+
use OCP\IRequest;
1920
use OCP\IUser;
2021
use OCP\IUserSession;
2122
use Psr\Log\LoggerInterface;
2223

2324
class ThemesService {
25+
private const REQUEST_THEME_PARAM = 'theme';
26+
private const REQUEST_THEME_OVERRIDES = [
27+
'light',
28+
'dark',
29+
];
30+
2431
/** @var ITheme[] */
2532
private array $themesProviders;
2633

2734
public function __construct(
2835
private IUserSession $userSession,
2936
private IConfig $config,
37+
private IRequest $request,
3038
private LoggerInterface $logger,
3139
private DefaultTheme $defaultTheme,
3240
LightTheme $lightTheme,
@@ -161,6 +169,12 @@ public function getEnabledThemes() {
161169
if ($enforcedTheme !== '') {
162170
return [$enforcedTheme];
163171
}
172+
173+
$requestThemeOverride = $this->getRequestThemeOverride();
174+
if ($requestThemeOverride !== null) {
175+
return [$requestThemeOverride];
176+
}
177+
164178
return [];
165179
}
166180

@@ -171,12 +185,51 @@ public function getEnabledThemes() {
171185
}
172186

173187
try {
174-
return $enabledThemes;
188+
return $this->applyRequestThemeOverride($enabledThemes);
175189
} catch (\Exception $e) {
176190
return [];
177191
}
178192
}
179193

194+
/**
195+
* Apply a request-scoped light/dark theme override without persisting it.
196+
*
197+
* @param list<string> $themes
198+
* @return list<string>
199+
*/
200+
private function applyRequestThemeOverride(array $themes): array {
201+
$requestThemeOverride = $this->getRequestThemeOverride();
202+
if ($requestThemeOverride === null) {
203+
return $themes;
204+
}
205+
206+
$theme = $this->themesProviders[$requestThemeOverride];
207+
$themes = array_filter($themes, function (string $themeId) use ($theme): bool {
208+
return !isset($this->themesProviders[$themeId])
209+
|| $this->themesProviders[$themeId]->getType() !== $theme->getType();
210+
});
211+
212+
return array_values(array_unique(array_merge($themes, [$requestThemeOverride])));
213+
}
214+
215+
private function getRequestThemeOverride(): ?string {
216+
$requestThemeOverride = $this->request->getParam(self::REQUEST_THEME_PARAM, '');
217+
if (!is_string($requestThemeOverride)) {
218+
return null;
219+
}
220+
221+
$requestThemeOverride = strtolower(trim($requestThemeOverride));
222+
if (!in_array($requestThemeOverride, self::REQUEST_THEME_OVERRIDES, true)) {
223+
return null;
224+
}
225+
226+
if (!isset($this->themesProviders[$requestThemeOverride])) {
227+
return null;
228+
}
229+
230+
return $requestThemeOverride;
231+
}
232+
180233
/**
181234
* Set the list of enabled themes
182235
* for the logged-in user

apps/theming/tests/Service/ThemesServiceTest.php

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use OCP\App\IAppManager;
2424
use OCP\IConfig;
2525
use OCP\IL10N;
26+
use OCP\IRequest;
2627
use OCP\IURLGenerator;
2728
use OCP\IUser;
2829
use OCP\IUserSession;
@@ -33,6 +34,7 @@
3334
class ThemesServiceTest extends TestCase {
3435
private IUserSession&MockObject $userSession;
3536
private IConfig&MockObject $config;
37+
private IRequest&MockObject $request;
3638
private LoggerInterface&MockObject $logger;
3739

3840
private ThemingDefaults&MockObject $themingDefaults;
@@ -44,6 +46,7 @@ class ThemesServiceTest extends TestCase {
4446
protected function setUp(): void {
4547
$this->userSession = $this->createMock(IUserSession::class);
4648
$this->config = $this->createMock(IConfig::class);
49+
$this->request = $this->createMock(IRequest::class);
4750
$this->logger = $this->createMock(LoggerInterface::class);
4851
$this->themingDefaults = $this->createMock(ThemingDefaults::class);
4952

@@ -60,6 +63,7 @@ protected function setUp(): void {
6063
$this->themesService = new ThemesService(
6164
$this->userSession,
6265
$this->config,
66+
$this->request,
6367
$this->logger,
6468
...array_values($this->themes)
6569
);
@@ -232,6 +236,93 @@ public function testGetEnabledThemes(): void {
232236
$this->assertEquals(['default'], $this->themesService->getEnabledThemes());
233237
}
234238

239+
public function testGetEnabledThemesRequestThemeForGuest(): void {
240+
$this->userSession->expects($this->any())
241+
->method('getUser')
242+
->willReturn(null);
243+
$this->config->expects($this->once())
244+
->method('getSystemValueString')
245+
->with('enforce_theme', '')
246+
->willReturn('');
247+
$this->request->expects($this->once())
248+
->method('getParam')
249+
->with('theme', '')
250+
->willReturn('dark');
251+
252+
$this->assertEquals(['dark'], $this->themesService->getEnabledThemes());
253+
}
254+
255+
public function testGetEnabledThemesRequestThemeOverridesUserTheme(): void {
256+
$user = $this->createMock(IUser::class);
257+
$this->userSession->expects($this->any())
258+
->method('getUser')
259+
->willReturn($user);
260+
$user->expects($this->any())
261+
->method('getUID')
262+
->willReturn('user');
263+
264+
$this->config->expects($this->once())
265+
->method('getUserValue')
266+
->with('user', Application::APP_ID, 'enabled-themes', '["default"]')
267+
->willReturn(json_encode(['dark', 'opendyslexic']));
268+
$this->config->expects($this->once())
269+
->method('getSystemValueString')
270+
->with('enforce_theme', '')
271+
->willReturn('');
272+
$this->request->expects($this->once())
273+
->method('getParam')
274+
->with('theme', '')
275+
->willReturn('light');
276+
277+
$this->assertEquals(['opendyslexic', 'light'], $this->themesService->getEnabledThemes());
278+
}
279+
280+
public function testGetEnabledThemesInvalidRequestTheme(): void {
281+
$this->userSession->expects($this->any())
282+
->method('getUser')
283+
->willReturn(null);
284+
$this->config->expects($this->once())
285+
->method('getSystemValueString')
286+
->with('enforce_theme', '')
287+
->willReturn('');
288+
$this->request->expects($this->once())
289+
->method('getParam')
290+
->with('theme', '')
291+
->willReturn('sepia');
292+
293+
$this->assertEquals([], $this->themesService->getEnabledThemes());
294+
}
295+
296+
public function testGetEnabledThemesNonStringRequestThemeIsIgnored(): void {
297+
$this->userSession->expects($this->any())
298+
->method('getUser')
299+
->willReturn(null);
300+
$this->config->expects($this->once())
301+
->method('getSystemValueString')
302+
->with('enforce_theme', '')
303+
->willReturn('');
304+
$this->request->expects($this->once())
305+
->method('getParam')
306+
->with('theme', '')
307+
->willReturn(['dark']);
308+
309+
$this->assertEquals([], $this->themesService->getEnabledThemes());
310+
}
311+
312+
public function testGetEnabledThemesRequestThemeDoesNotOverrideEnforcedTheme(): void {
313+
$this->userSession->expects($this->any())
314+
->method('getUser')
315+
->willReturn(null);
316+
$this->config->expects($this->once())
317+
->method('getSystemValueString')
318+
->with('enforce_theme', '')
319+
->willReturn('light');
320+
$this->request->expects($this->never())
321+
->method('getParam');
322+
323+
$this->assertEquals(['light'], $this->themesService->getEnabledThemes());
324+
}
325+
235326
public function testGetEnabledThemesEnforced(): void {
236327
$user = $this->createMock(IUser::class);
237328
$this->userSession->expects($this->any())

0 commit comments

Comments
 (0)