From 24c5d994c7652f266f62563f11bab55defc41dae Mon Sep 17 00:00:00 2001 From: John Molakvoæ Date: Fri, 29 Apr 2022 11:54:25 +0200 Subject: Allow to override the default theme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: John Molakvoæ --- .../theming/lib/Controller/UserThemeController.php | 42 +++-- apps/theming/lib/Service/ThemesService.php | 8 +- apps/theming/lib/Settings/Personal.php | 10 + apps/theming/src/UserThemes.vue | 11 +- apps/theming/src/components/ItemPreview.vue | 30 ++- apps/theming/tests/Service/ThemesServiceTest.php | 46 ++++- apps/theming/tests/Settings/AdminSectionTest.php | 78 ++++++++ apps/theming/tests/Settings/PersonalTest.php | 209 +++++++++++++++++++++ apps/theming/tests/Settings/SectionTest.php | 78 -------- 9 files changed, 406 insertions(+), 106 deletions(-) create mode 100644 apps/theming/tests/Settings/AdminSectionTest.php create mode 100644 apps/theming/tests/Settings/PersonalTest.php delete mode 100644 apps/theming/tests/Settings/SectionTest.php (limited to 'apps/theming') diff --git a/apps/theming/lib/Controller/UserThemeController.php b/apps/theming/lib/Controller/UserThemeController.php index ec379d2e6fa..71d78db4b3d 100644 --- a/apps/theming/lib/Controller/UserThemeController.php +++ b/apps/theming/lib/Controller/UserThemeController.php @@ -30,9 +30,11 @@ declare(strict_types=1); */ namespace OCA\Theming\Controller; +use OCA\Theming\ITheme; use OCA\Theming\Service\ThemesService; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSBadRequestException; +use OCP\AppFramework\OCS\OCSForbiddenException; use OCP\AppFramework\OCSController; use OCP\IConfig; use OCP\IRequest; @@ -71,17 +73,10 @@ class UserThemeController extends OCSController { * @throws OCSBadRequestException|PreConditionNotMetException */ public function enableTheme(string $themeId): DataResponse { - if ($themeId === '' || !$themeId) { - throw new OCSBadRequestException('Invalid theme id: ' . $themeId); - } + $theme = $this->validateTheme($themeId); - $themes = $this->themesService->getThemes(); - if (!isset($themes[$themeId])) { - throw new OCSBadRequestException('Invalid theme id: ' . $themeId); - } - // Enable selected theme - $this->themesService->enableTheme($themes[$themeId]); + $this->themesService->enableTheme($theme); return new DataResponse(); } @@ -95,6 +90,23 @@ class UserThemeController extends OCSController { * @throws OCSBadRequestException|PreConditionNotMetException */ public function disableTheme(string $themeId): DataResponse { + $theme = $this->validateTheme($themeId); + + // Enable selected theme + $this->themesService->disableTheme($theme); + return new DataResponse(); + } + + /** + * Validate and return the matching ITheme + * + * Disable theme + * + * @param string $themeId the theme ID + * @return ITheme + * @throws OCSBadRequestException|PreConditionNotMetException + */ + private function validateTheme(string $themeId): ITheme { if ($themeId === '' || !$themeId) { throw new OCSBadRequestException('Invalid theme id: ' . $themeId); } @@ -103,9 +115,13 @@ class UserThemeController extends OCSController { if (!isset($themes[$themeId])) { throw new OCSBadRequestException('Invalid theme id: ' . $themeId); } - - // Enable selected theme - $this->themesService->disableTheme($themes[$themeId]); - return new DataResponse(); + + // If trying to toggle another theme but this is enforced + if ($this->config->getSystemValueString('enforce_theme', '') !== '' + && $themes[$themeId]->getType() === ITheme::TYPE_THEME) { + throw new OCSForbiddenException('Theme switching is disabled'); + } + + return $themes[$themeId]; } } diff --git a/apps/theming/lib/Service/ThemesService.php b/apps/theming/lib/Service/ThemesService.php index 43977721e76..283b2e9c9ee 100644 --- a/apps/theming/lib/Service/ThemesService.php +++ b/apps/theming/lib/Service/ThemesService.php @@ -155,8 +155,14 @@ class ThemesService { return []; } + $enforcedTheme = $this->config->getSystemValueString('enforce_theme', ''); + $enabledThemes = json_decode($this->config->getUserValue($user->getUID(), Application::APP_ID, 'enabled-themes', '[]')); + if ($enforcedTheme !== '') { + return array_merge([$enforcedTheme], $enabledThemes); + } + try { - return json_decode($this->config->getUserValue($user->getUID(), Application::APP_ID, 'enabled-themes', '[]')); + return $enabledThemes; } catch (\Exception $e) { return []; } diff --git a/apps/theming/lib/Settings/Personal.php b/apps/theming/lib/Settings/Personal.php index 6dd865b9cf6..790c0fd7f39 100644 --- a/apps/theming/lib/Settings/Personal.php +++ b/apps/theming/lib/Settings/Personal.php @@ -25,6 +25,7 @@ */ namespace OCA\Theming\Settings; +use OCA\Theming\ITheme; use OCA\Theming\Service\ThemesService; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Services\IInitialState; @@ -54,6 +55,8 @@ class Personal implements ISettings { } public function getForm(): TemplateResponse { + $enforcedTheme = $this->config->getSystemValueString('enforce_theme', ''); + $themes = array_map(function($theme) { return [ 'id' => $theme->getId(), @@ -65,7 +68,14 @@ class Personal implements ISettings { ]; }, $this->themesService->getThemes()); + if ($enforcedTheme !== '') { + $themes = array_filter($themes, function($theme) use ($enforcedTheme) { + return $theme['type'] !== ITheme::TYPE_THEME || $theme['id'] === $enforcedTheme; + }); + } + $this->initialStateService->provideInitialState('themes', array_values($themes)); + $this->initialStateService->provideInitialState('enforceTheme', $enforcedTheme); Util::addScript($this->appName, 'theming-settings'); return new TemplateResponse($this->appName, 'settings-personal'); diff --git a/apps/theming/src/UserThemes.vue b/apps/theming/src/UserThemes.vue index 1fd6cb20866..f78e63484d6 100644 --- a/apps/theming/src/UserThemes.vue +++ b/apps/theming/src/UserThemes.vue @@ -6,16 +6,17 @@
@@ -31,6 +32,7 @@ import SettingsSection from '@nextcloud/vue/dist/Components/SettingsSection' import ItemPreview from './components/ItemPreview' const availableThemes = loadState('theming', 'themes', []) +const enforceTheme = loadState('theming', 'enforceTheme', '') console.debug('Available themes', availableThemes) @@ -44,6 +46,7 @@ export default { data() { return { availableThemes, + enforceTheme, } }, diff --git a/apps/theming/src/components/ItemPreview.vue b/apps/theming/src/components/ItemPreview.vue index 82d588059a2..e7c5866b662 100644 --- a/apps/theming/src/components/ItemPreview.vue +++ b/apps/theming/src/components/ItemPreview.vue @@ -4,8 +4,12 @@

{{ theme.title }}

{{ theme.description }}

+ + {{ t('theming', 'Theme selection is enforced') }} + {{ theme.enableLabel }} @@ -24,30 +28,34 @@ export default { CheckboxRadioSwitch, }, props: { - theme: { - type: Object, - required: true, + enforced: { + type: Boolean, + default: false, }, selected: { type: Boolean, default: false, }, + theme: { + type: Object, + required: true, + }, type: { type: String, default: '', }, - themes: { - type: Array, - default: () => [], + unique: { + type: Boolean, + default: false, }, }, computed: { switchType() { - return this.themes.length === 1 ? 'switch' : 'radio' + return this.unique ? 'switch' : 'radio' }, name() { - return this.switchType === 'radio' ? this.type : null + return !this.unique ? this.type : null }, img() { @@ -62,7 +70,7 @@ export default { console.debug('Selecting theme', this.theme, checked) // If this is a radio, we can only enable - if (this.switchType === 'radio') { + if (!this.unique) { this.$emit('change', { enabled: true, id: this.theme.id }) return } @@ -109,6 +117,10 @@ $ratio: 16; padding: 12px 0; } } + + &-warning { + color: var(--color-warning); + } } @media (max-width: (1024px / 1.5)) { diff --git a/apps/theming/tests/Service/ThemesServiceTest.php b/apps/theming/tests/Service/ThemesServiceTest.php index 5865875cbb8..657418db471 100644 --- a/apps/theming/tests/Service/ThemesServiceTest.php +++ b/apps/theming/tests/Service/ThemesServiceTest.php @@ -177,7 +177,7 @@ class ThemesServiceTest extends TestCase { * @param string $toEnable * @param string[] $enabledThemes */ - public function testisEnabled(string $themeId, array $enabledThemes, $expected) { + public function testIsEnabled(string $themeId, array $enabledThemes, $expected) { $user = $this->createMock(IUser::class); $this->userSession->expects($this->any()) ->method('getUser') @@ -195,6 +195,50 @@ class ThemesServiceTest extends TestCase { $this->assertEquals($expected, $this->themesService->isEnabled($this->themes[$themeId])); } + public function testGetEnabledThemes() { + $user = $this->createMock(IUser::class); + $this->userSession->expects($this->any()) + ->method('getUser') + ->willReturn($user); + $user->expects($this->any()) + ->method('getUID') + ->willReturn('user'); + + + $this->config->expects($this->once()) + ->method('getUserValue') + ->with('user', Application::APP_ID, 'enabled-themes', '[]') + ->willReturn(json_encode([])); + $this->config->expects($this->once()) + ->method('getSystemValueString') + ->with('enforce_theme', '') + ->willReturn(''); + + $this->assertEquals([], $this->themesService->getEnabledThemes()); + } + + public function testGetEnabledThemesEnforced() { + $user = $this->createMock(IUser::class); + $this->userSession->expects($this->any()) + ->method('getUser') + ->willReturn($user); + $user->expects($this->any()) + ->method('getUID') + ->willReturn('user'); + + + $this->config->expects($this->once()) + ->method('getUserValue') + ->with('user', Application::APP_ID, 'enabled-themes', '[]') + ->willReturn(json_encode([])); + $this->config->expects($this->once()) + ->method('getSystemValueString') + ->with('enforce_theme', '') + ->willReturn('light'); + + $this->assertEquals(['light'], $this->themesService->getEnabledThemes()); + } + private function initThemes() { $util = $this->createMock(Util::class); $urlGenerator = $this->createMock(IURLGenerator::class); diff --git a/apps/theming/tests/Settings/AdminSectionTest.php b/apps/theming/tests/Settings/AdminSectionTest.php new file mode 100644 index 00000000000..80223664ce4 --- /dev/null +++ b/apps/theming/tests/Settings/AdminSectionTest.php @@ -0,0 +1,78 @@ + + * + * @author Morris Jobke + * @author Roeland Jago Douma + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\Theming\Tests\Settings; + +use OCA\Theming\AppInfo\Application; +use OCA\Theming\Settings\AdminSection; +use OCP\IL10N; +use OCP\IURLGenerator; +use Test\TestCase; + +class AdminSectionTest extends TestCase { + /** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */ + private $url; + /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */ + private $l; + /** @var AdminSection */ + private $section; + + protected function setUp(): void { + parent::setUp(); + $this->url = $this->createMock(IURLGenerator::class); + $this->l = $this->createMock(IL10N::class); + + $this->section = new AdminSection( + Application::APP_ID, + $this->url, + $this->l + ); + } + + public function testGetID() { + $this->assertSame('theming', $this->section->getID()); + } + + public function testGetName() { + $this->l + ->expects($this->once()) + ->method('t') + ->with('Theming') + ->willReturn('Theming'); + + $this->assertSame('Theming', $this->section->getName()); + } + + public function testGetPriority() { + $this->assertSame(30, $this->section->getPriority()); + } + + public function testGetIcon() { + $this->url->expects($this->once()) + ->method('imagePath') + ->with('theming', 'app-dark.svg') + ->willReturn('icon'); + + $this->assertSame('icon', $this->section->getIcon()); + } +} diff --git a/apps/theming/tests/Settings/PersonalTest.php b/apps/theming/tests/Settings/PersonalTest.php new file mode 100644 index 00000000000..5f0585911bb --- /dev/null +++ b/apps/theming/tests/Settings/PersonalTest.php @@ -0,0 +1,209 @@ + + * + * @author Arthur Schiwon + * @author Jan-Christoph Borchardt + * @author Julius Härtl + * @author Lukas Reschke + * @author Morris Jobke + * @author Roeland Jago Douma + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\Theming\Tests\Settings; + +use OCA\Theming\AppInfo\Application; +use OCA\Theming\ImageManager; +use OCA\Theming\Service\ThemesService; +use OCA\Theming\Settings\Personal; +use OCA\Theming\Themes\DarkHighContrastTheme; +use OCA\Theming\Themes\DarkTheme; +use OCA\Theming\Themes\DefaultTheme; +use OCA\Theming\Themes\DyslexiaFont; +use OCA\Theming\Themes\HighContrastTheme; +use OCA\Theming\Themes\LightTheme; +use OCA\Theming\ThemingDefaults; +use OCA\Theming\Util; +use OCA\Theming\ITheme; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Services\IInitialState; +use OCP\IConfig; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\IUserSession; +use Test\TestCase; + +class PersonalTest extends TestCase { + private IConfig $config; + private IUserSession $userSession; + private ThemesService $themesService; + private IInitialState $initialStateService; + + /** @var ITheme[] */ + private $themes; + + protected function setUp(): void { + parent::setUp(); + $this->config = $this->createMock(IConfig::class); + $this->userSession = $this->createMock(IUserSession::class); + $this->themesService = $this->createMock(ThemesService::class); + $this->initialStateService = $this->createMock(IInitialState::class); + + $this->initThemes(); + + $this->themesService + ->expects($this->any()) + ->method('getThemes') + ->willReturn($this->themes); + + $this->admin = new Personal( + Application::APP_ID, + $this->config, + $this->userSession, + $this->themesService, + $this->initialStateService + ); + } + + + public function dataTestGetForm() { + return [ + ['', [ + $this->formatThemeForm('default'), + $this->formatThemeForm('light'), + $this->formatThemeForm('dark'), + $this->formatThemeForm('highcontrast'), + $this->formatThemeForm('dark-highcontrast'), + $this->formatThemeForm('opendyslexic'), + ]], + ['dark', [ + $this->formatThemeForm('dark'), + $this->formatThemeForm('opendyslexic'), + ]], + ]; + } + + /** + * @dataProvider dataTestGetForm + * + * @param string $toEnable + * @param string[] $enabledThemes + */ + public function testGetForm(string $enforcedTheme, $themesState) { + $this->config->expects($this->once()) + ->method('getSystemValueString') + ->with('enforce_theme', '') + ->willReturn($enforcedTheme); + + $this->initialStateService->expects($this->at(0)) + ->method('provideInitialState') + ->with('themes', $themesState); + $this->initialStateService->expects($this->at(1)) + ->method('provideInitialState') + ->with('enforceTheme', $enforcedTheme); + + $expected = new TemplateResponse('theming', 'settings-personal'); + $this->assertEquals($expected, $this->admin->getForm()); + } + + public function testGetSection() { + $this->assertSame('theming', $this->admin->getSection()); + } + + public function testGetPriority() { + $this->assertSame(40, $this->admin->getPriority()); + } + + private function initThemes() { + $util = $this->createMock(Util::class); + $themingDefaults = $this->createMock(ThemingDefaults::class); + $urlGenerator = $this->createMock(IURLGenerator::class); + $imageManager = $this->createMock(ImageManager::class); + $config = $this->createMock(IConfig::class); + $l10n = $this->createMock(IL10N::class); + + $themingDefaults->expects($this->any()) + ->method('getColorPrimary') + ->willReturn('#0082c9'); + + $this->themes = [ + 'default' => new DefaultTheme( + $util, + $themingDefaults, + $urlGenerator, + $imageManager, + $config, + $l10n, + ), + 'light' => new LightTheme( + $util, + $themingDefaults, + $urlGenerator, + $imageManager, + $config, + $l10n, + ), + 'dark' => new DarkTheme( + $util, + $themingDefaults, + $urlGenerator, + $imageManager, + $config, + $l10n, + ), + 'highcontrast' => new HighContrastTheme( + $util, + $themingDefaults, + $urlGenerator, + $imageManager, + $config, + $l10n, + ), + 'dark-highcontrast' => new DarkHighContrastTheme( + $util, + $themingDefaults, + $urlGenerator, + $imageManager, + $config, + $l10n, + ), + 'opendyslexic' => new DyslexiaFont( + $util, + $themingDefaults, + $urlGenerator, + $imageManager, + $config, + $l10n, + ), + ]; + } + + private function formatThemeForm(string $themeId): array { + $this->initThemes(); + + $theme = $this->themes[$themeId]; + return [ + 'id' => $theme->getId(), + 'type' => $theme->getType(), + 'title' => $theme->getTitle(), + 'enableLabel' => $theme->getEnableLabel(), + 'description' => $theme->getDescription(), + 'enabled' => false, + ]; + } +} diff --git a/apps/theming/tests/Settings/SectionTest.php b/apps/theming/tests/Settings/SectionTest.php deleted file mode 100644 index c168f13728d..00000000000 --- a/apps/theming/tests/Settings/SectionTest.php +++ /dev/null @@ -1,78 +0,0 @@ - - * - * @author Morris Jobke - * @author Roeland Jago Douma - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ -namespace OCA\Theming\Tests\Settings; - -use OCA\Theming\AppInfo\Application; -use OCA\Theming\Settings\AdminSection; -use OCP\IL10N; -use OCP\IURLGenerator; -use Test\TestCase; - -class SectionTest extends TestCase { - /** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */ - private $url; - /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */ - private $l; - /** @var AdminSection */ - private $section; - - protected function setUp(): void { - parent::setUp(); - $this->url = $this->createMock(IURLGenerator::class); - $this->l = $this->createMock(IL10N::class); - - $this->section = new AdminSection( - Application::APP_ID, - $this->url, - $this->l - ); - } - - public function testGetID() { - $this->assertSame('theming', $this->section->getID()); - } - - public function testGetName() { - $this->l - ->expects($this->once()) - ->method('t') - ->with('Theming') - ->willReturn('Theming'); - - $this->assertSame('Theming', $this->section->getName()); - } - - public function testGetPriority() { - $this->assertSame(30, $this->section->getPriority()); - } - - public function testGetIcon() { - $this->url->expects($this->once()) - ->method('imagePath') - ->with('theming', 'app-dark.svg') - ->willReturn('icon'); - - $this->assertSame('icon', $this->section->getIcon()); - } -} -- cgit v1.2.3