aboutsummaryrefslogtreecommitdiffstats
path: root/apps/theming/lib
diff options
context:
space:
mode:
Diffstat (limited to 'apps/theming/lib')
-rw-r--r--apps/theming/lib/AppInfo/Application.php24
-rw-r--r--apps/theming/lib/Capabilities.php75
-rw-r--r--apps/theming/lib/Command/UpdateConfig.php49
-rw-r--r--apps/theming/lib/Controller/IconController.php70
-rw-r--r--apps/theming/lib/Controller/ThemingController.php191
-rw-r--r--apps/theming/lib/Controller/UserThemeController.php84
-rw-r--r--apps/theming/lib/ITheme.php20
-rw-r--r--apps/theming/lib/IconBuilder.php112
-rw-r--r--apps/theming/lib/ImageManager.php150
-rw-r--r--apps/theming/lib/Jobs/MigrateBackgroundImages.php44
-rw-r--r--apps/theming/lib/Jobs/RestoreBackgroundImageColor.php205
-rw-r--r--apps/theming/lib/Listener/BeforePreferenceListener.php43
-rw-r--r--apps/theming/lib/Listener/BeforeTemplateRenderedListener.php83
-rw-r--r--apps/theming/lib/Migration/InitBackgroundImagesMigration.php31
-rw-r--r--apps/theming/lib/Migration/Version2006Date20240905111627.php127
-rw-r--r--apps/theming/lib/ResponseDefinitions.php22
-rw-r--r--apps/theming/lib/Service/BackgroundService.php319
-rw-r--r--apps/theming/lib/Service/JSDataService.php50
-rw-r--r--apps/theming/lib/Service/ThemeInjectionService.php57
-rw-r--r--apps/theming/lib/Service/ThemesService.php75
-rw-r--r--apps/theming/lib/Settings/Admin.php42
-rw-r--r--apps/theming/lib/Settings/AdminSection.php39
-rw-r--r--apps/theming/lib/Settings/Personal.php61
-rw-r--r--apps/theming/lib/Settings/PersonalSection.php47
-rw-r--r--apps/theming/lib/SetupChecks/PhpImagickModule.php21
-rw-r--r--apps/theming/lib/Themes/CommonThemeTrait.php105
-rw-r--r--apps/theming/lib/Themes/DarkHighContrastTheme.php24
-rw-r--r--apps/theming/lib/Themes/DarkTheme.php26
-rw-r--r--apps/theming/lib/Themes/DefaultTheme.php138
-rw-r--r--apps/theming/lib/Themes/DyslexiaFont.php36
-rw-r--r--apps/theming/lib/Themes/HighContrastTheme.php27
-rw-r--r--apps/theming/lib/Themes/LightTheme.php22
-rw-r--r--apps/theming/lib/ThemingDefaults.php217
-rw-r--r--apps/theming/lib/Util.php79
34 files changed, 1319 insertions, 1396 deletions
diff --git a/apps/theming/lib/AppInfo/Application.php b/apps/theming/lib/AppInfo/Application.php
index ec4c2940871..d08a1903265 100644
--- a/apps/theming/lib/AppInfo/Application.php
+++ b/apps/theming/lib/AppInfo/Application.php
@@ -1,26 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2019 Robin Appelman <robin@icewind.nl>
- *
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Theming\AppInfo;
diff --git a/apps/theming/lib/Capabilities.php b/apps/theming/lib/Capabilities.php
index b0c6a71731b..d5d6e415e75 100644
--- a/apps/theming/lib/Capabilities.php
+++ b/apps/theming/lib/Capabilities.php
@@ -1,29 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, Joas Schilling <coding@schilljs.com>
- *
- * @author Guillaume COMPAGNON <gcompagnon@outlook.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Julien Veyssier <eneiluj@posteo.net>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Kate Döen <kate.doeen@nextcloud.com>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Theming;
@@ -42,32 +21,19 @@ use OCP\IUserSession;
*/
class Capabilities implements IPublicCapability {
- /** @var ThemingDefaults */
- protected $theming;
-
- /** @var Util */
- protected $util;
-
- /** @var IURLGenerator */
- protected $url;
-
- /** @var IConfig */
- protected $config;
-
- protected IUserSession $userSession;
-
/**
* @param ThemingDefaults $theming
* @param Util $util
* @param IURLGenerator $url
* @param IConfig $config
*/
- public function __construct(ThemingDefaults $theming, Util $util, IURLGenerator $url, IConfig $config, IUserSession $userSession) {
- $this->theming = $theming;
- $this->util = $util;
- $this->url = $url;
- $this->config = $config;
- $this->userSession = $userSession;
+ public function __construct(
+ protected ThemingDefaults $theming,
+ protected Util $util,
+ protected IURLGenerator $url,
+ protected IConfig $config,
+ protected IUserSession $userSession,
+ ) {
}
/**
@@ -76,6 +42,7 @@ class Capabilities implements IPublicCapability {
* @return array{
* theming: array{
* name: string,
+ * productName: string,
* url: string,
* slogan: string,
* color: string,
@@ -85,6 +52,7 @@ class Capabilities implements IPublicCapability {
* color-element-dark: string,
* logo: string,
* background: string,
+ * background-text: string,
* background-plain: bool,
* background-default: bool,
* logoheader: string,
@@ -94,15 +62,13 @@ class Capabilities implements IPublicCapability {
*/
public function getCapabilities() {
$color = $this->theming->getDefaultColorPrimary();
- // Same as in DefaultTheme
- if ($color === BackgroundService::DEFAULT_COLOR) {
- $color = BackgroundService::DEFAULT_ACCESSIBLE_COLOR;
- }
$colorText = $this->util->invertTextColor($color) ? '#000000' : '#ffffff';
$backgroundLogo = $this->config->getAppValue('theming', 'backgroundMime', '');
- $backgroundPlain = $backgroundLogo === 'backgroundColor' || ($backgroundLogo === '' && $color !== '#0082c9');
- $background = $backgroundPlain ? $color : $this->url->getAbsoluteURL($this->theming->getBackground());
+ $backgroundColor = $this->theming->getColorBackground();
+ $backgroundText = $this->theming->getTextColorBackground();
+ $backgroundPlain = $backgroundLogo === 'backgroundColor' || ($backgroundLogo === '' && $backgroundColor !== BackgroundService::DEFAULT_COLOR);
+ $background = $backgroundPlain ? $backgroundColor : $this->url->getAbsoluteURL($this->theming->getBackground());
$user = $this->userSession->getUser();
if ($user instanceof IUser) {
@@ -112,10 +78,7 @@ class Capabilities implements IPublicCapability {
* @see \OCA\Theming\Themes\CommonThemeTrait::generateUserBackgroundVariables()
*/
$color = $this->theming->getColorPrimary();
- if ($color === BackgroundService::DEFAULT_COLOR) {
- $color = BackgroundService::DEFAULT_ACCESSIBLE_COLOR;
- }
- $colorText = $this->util->invertTextColor($color) ? '#000000' : '#ffffff';
+ $colorText = $this->theming->getTextColorPrimary();
$backgroundImage = $this->config->getUserValue($user->getUID(), Application::APP_ID, 'background_image', BackgroundService::BACKGROUND_DEFAULT);
if ($backgroundImage === BackgroundService::BACKGROUND_CUSTOM) {
@@ -126,13 +89,14 @@ class Capabilities implements IPublicCapability {
$background = $this->url->linkTo(Application::APP_ID, "img/background/$backgroundImage");
} elseif ($backgroundImage !== BackgroundService::BACKGROUND_DEFAULT) {
$backgroundPlain = true;
- $background = $color;
+ $background = $backgroundColor;
}
}
return [
'theming' => [
'name' => $this->theming->getName(),
+ 'productName' => $this->theming->getProductName(),
'url' => $this->theming->getBaseUrl(),
'slogan' => $this->theming->getSlogan(),
'color' => $color,
@@ -142,6 +106,7 @@ class Capabilities implements IPublicCapability {
'color-element-dark' => $this->util->elementColor($color, false),
'logo' => $this->url->getAbsoluteURL($this->theming->getLogo()),
'background' => $background,
+ 'background-text' => $backgroundText,
'background-plain' => $backgroundPlain,
'background-default' => !$this->util->isBackgroundThemed(),
'logoheader' => $this->url->getAbsoluteURL($this->theming->getLogo()),
diff --git a/apps/theming/lib/Command/UpdateConfig.php b/apps/theming/lib/Command/UpdateConfig.php
index de180db6ce9..6236f866445 100644
--- a/apps/theming/lib/Command/UpdateConfig.php
+++ b/apps/theming/lib/Command/UpdateConfig.php
@@ -1,24 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net>
- *
- * @author Julius Härtl <jus@bitgrid.net>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Theming\Command;
@@ -33,19 +17,15 @@ use Symfony\Component\Console\Output\OutputInterface;
class UpdateConfig extends Command {
public const SUPPORTED_KEYS = [
- 'name', 'url', 'imprintUrl', 'privacyUrl', 'slogan', 'color', 'disable-user-theming'
+ 'name', 'url', 'imprintUrl', 'privacyUrl', 'slogan', 'color', 'primary_color', 'background_color', 'disable-user-theming'
];
- private $themingDefaults;
- private $imageManager;
- private $config;
-
- public function __construct(ThemingDefaults $themingDefaults, ImageManager $imageManager, IConfig $config) {
+ public function __construct(
+ private ThemingDefaults $themingDefaults,
+ private ImageManager $imageManager,
+ private IConfig $config,
+ ) {
parent::__construct();
-
- $this->themingDefaults = $themingDefaults;
- $this->imageManager = $imageManager;
- $this->config = $config;
}
protected function configure() {
@@ -55,8 +35,8 @@ class UpdateConfig extends Command {
->addArgument(
'key',
InputArgument::OPTIONAL,
- 'Key to update the theming app configuration (leave empty to get a list of all configured values)' . PHP_EOL .
- 'One of: ' . implode(', ', self::SUPPORTED_KEYS)
+ 'Key to update the theming app configuration (leave empty to get a list of all configured values)' . PHP_EOL
+ . 'One of: ' . implode(', ', self::SUPPORTED_KEYS)
)
->addArgument(
'value',
@@ -129,7 +109,12 @@ class UpdateConfig extends Command {
$key = $key . 'Mime';
}
- if ($key === 'color' && !preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $value)) {
+ if ($key === 'color') {
+ $output->writeln('<comment>Using "color" is deprecated, use "primary_color" instead</comment>');
+ $key = 'primary_color';
+ }
+
+ if ($key === 'primary_color' && !preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $value)) {
$output->writeln('<error>The given color is invalid: ' . $value . '</error>');
return 1;
}
diff --git a/apps/theming/lib/Controller/IconController.php b/apps/theming/lib/Controller/IconController.php
index 216ca88d375..e82faf78a79 100644
--- a/apps/theming/lib/Controller/IconController.php
+++ b/apps/theming/lib/Controller/IconController.php
@@ -1,30 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Julius Haertl <jus@bitgrid.net>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Julius Haertl <jus@bitgrid.net>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Michael Weimann <mail@michael-weimann.eu>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Kate Döen <kate.doeen@nextcloud.com>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Theming\Controller;
@@ -35,6 +13,9 @@ use OCA\Theming\ThemingDefaults;
use OCP\App\IAppManager;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
+use OCP\AppFramework\Http\Attribute\OpenAPI;
+use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\DataDisplayResponse;
use OCP\AppFramework\Http\FileDisplayResponse;
use OCP\AppFramework\Http\NotFoundResponse;
@@ -43,39 +24,23 @@ use OCP\Files\NotFoundException;
use OCP\IRequest;
class IconController extends Controller {
- /** @var ThemingDefaults */
- private $themingDefaults;
- /** @var IconBuilder */
- private $iconBuilder;
- /** @var ImageManager */
- private $imageManager;
/** @var FileAccessHelper */
private $fileAccessHelper;
- /** @var IAppManager */
- private $appManager;
public function __construct(
$appName,
IRequest $request,
- ThemingDefaults $themingDefaults,
- IconBuilder $iconBuilder,
- ImageManager $imageManager,
+ private ThemingDefaults $themingDefaults,
+ private IconBuilder $iconBuilder,
+ private ImageManager $imageManager,
FileAccessHelper $fileAccessHelper,
- IAppManager $appManager
+ private IAppManager $appManager,
) {
parent::__construct($appName, $request);
-
- $this->themingDefaults = $themingDefaults;
- $this->iconBuilder = $iconBuilder;
- $this->imageManager = $imageManager;
$this->fileAccessHelper = $fileAccessHelper;
- $this->appManager = $appManager;
}
/**
- * @PublicPage
- * @NoCSRFRequired
- *
* Get a themed icon
*
* @param string $app ID of the app
@@ -86,6 +51,9 @@ class IconController extends Controller {
* 200: Themed icon returned
* 404: Themed icon not found
*/
+ #[PublicPage]
+ #[NoCSRFRequired]
+ #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
public function getThemedIcon(string $app, string $image): Response {
if ($app !== 'core' && !$this->appManager->isEnabledForUser($app)) {
$app = 'core';
@@ -110,9 +78,6 @@ class IconController extends Controller {
/**
* Return a 32x32 favicon as png
*
- * @PublicPage
- * @NoCSRFRequired
- *
* @param string $app ID of the app
* @return DataDisplayResponse<Http::STATUS_OK, array{Content-Type: 'image/x-icon'}>|FileDisplayResponse<Http::STATUS_OK, array{Content-Type: 'image/x-icon'}>|NotFoundResponse<Http::STATUS_NOT_FOUND, array{}>
* @throws \Exception
@@ -120,6 +85,9 @@ class IconController extends Controller {
* 200: Favicon returned
* 404: Favicon not found
*/
+ #[PublicPage]
+ #[NoCSRFRequired]
+ #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
public function getFavicon(string $app = 'core'): Response {
if ($app !== 'core' && !$this->appManager->isEnabledForUser($app)) {
$app = 'core';
@@ -156,9 +124,6 @@ class IconController extends Controller {
/**
* Return a 512x512 icon for touch devices
*
- * @PublicPage
- * @NoCSRFRequired
- *
* @param string $app ID of the app
* @return DataDisplayResponse<Http::STATUS_OK, array{Content-Type: 'image/png'}>|FileDisplayResponse<Http::STATUS_OK, array{Content-Type: 'image/x-icon'|'image/png'}>|NotFoundResponse<Http::STATUS_NOT_FOUND, array{}>
* @throws \Exception
@@ -166,6 +131,9 @@ class IconController extends Controller {
* 200: Touch icon returned
* 404: Touch icon not found
*/
+ #[PublicPage]
+ #[NoCSRFRequired]
+ #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
public function getTouchIcon(string $app = 'core'): Response {
if ($app !== 'core' && !$this->appManager->isEnabledForUser($app)) {
$app = 'core';
diff --git a/apps/theming/lib/Controller/ThemingController.php b/apps/theming/lib/Controller/ThemingController.php
index 91012d1e37a..e5cee254fe8 100644
--- a/apps/theming/lib/Controller/ThemingController.php
+++ b/apps/theming/lib/Controller/ThemingController.php
@@ -1,64 +1,38 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Bjoern Schiessle <bjoern@schiessle.org>
- * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch>
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Bjoern Schiessle <bjoern@schiessle.org>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Calviño Sánchez <danxuliu@gmail.com>
- * @author Jan-Christoph Borchardt <hey@jancborchardt.net>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Julius Haertl <jus@bitgrid.net>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Kyle Fazzari <kyrofa@ubuntu.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author nhirokinet <nhirokinet@nhiroki.net>
- * @author rakekniven <mark.ziegler@rakekniven.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- * @author Kate Döen <kate.doeen@nextcloud.com>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Theming\Controller;
use InvalidArgumentException;
use OCA\Theming\ImageManager;
use OCA\Theming\Service\ThemesService;
+use OCA\Theming\Settings\Admin;
use OCA\Theming\ThemingDefaults;
use OCP\App\IAppManager;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting;
+use OCP\AppFramework\Http\Attribute\BruteForceProtection;
+use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
+use OCP\AppFramework\Http\Attribute\OpenAPI;
+use OCP\AppFramework\Http\Attribute\PublicPage;
+use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\DataDisplayResponse;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\FileDisplayResponse;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\NotFoundResponse;
-use OCP\Files\IAppData;
+use OCP\AppFramework\Services\IAppConfig;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\IConfig;
use OCP\IL10N;
+use OCP\INavigationManager;
use OCP\IRequest;
-use OCP\ITempManager;
use OCP\IURLGenerator;
-use ScssPhp\ScssPhp\Compiler;
/**
* Class ThemingController
@@ -70,52 +44,33 @@ use ScssPhp\ScssPhp\Compiler;
class ThemingController extends Controller {
public const VALID_UPLOAD_KEYS = ['header', 'logo', 'logoheader', 'background', 'favicon'];
- private ThemingDefaults $themingDefaults;
- private IL10N $l10n;
- private IConfig $config;
- private ITempManager $tempManager;
- private IAppData $appData;
- private IURLGenerator $urlGenerator;
- private IAppManager $appManager;
- private ImageManager $imageManager;
- private ThemesService $themesService;
-
public function __construct(
- $appName,
+ string $appName,
IRequest $request,
- IConfig $config,
- ThemingDefaults $themingDefaults,
- IL10N $l,
- ITempManager $tempManager,
- IAppData $appData,
- IURLGenerator $urlGenerator,
- IAppManager $appManager,
- ImageManager $imageManager,
- ThemesService $themesService
+ private IConfig $config,
+ private IAppConfig $appConfig,
+ private ThemingDefaults $themingDefaults,
+ private IL10N $l10n,
+ private IURLGenerator $urlGenerator,
+ private IAppManager $appManager,
+ private ImageManager $imageManager,
+ private ThemesService $themesService,
+ private INavigationManager $navigationManager,
) {
parent::__construct($appName, $request);
-
- $this->themingDefaults = $themingDefaults;
- $this->l10n = $l;
- $this->config = $config;
- $this->tempManager = $tempManager;
- $this->appData = $appData;
- $this->urlGenerator = $urlGenerator;
- $this->appManager = $appManager;
- $this->imageManager = $imageManager;
- $this->themesService = $themesService;
}
/**
- * @AuthorizedAdminSetting(settings=OCA\Theming\Settings\Admin)
* @param string $setting
* @param string $value
* @return DataResponse
* @throws NotPermittedException
*/
+ #[AuthorizedAdminSetting(settings: Admin::class)]
public function updateStylesheet($setting, $value) {
$value = trim($value);
$error = null;
+ $saved = false;
switch ($setting) {
case 'name':
if (strlen($value) > 250) {
@@ -151,14 +106,28 @@ class ThemingController extends Controller {
$error = $this->l10n->t('The given slogan is too long');
}
break;
- case 'color':
+ case 'primary_color':
if (!preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $value)) {
$error = $this->l10n->t('The given color is invalid');
+ } else {
+ $this->appConfig->setAppValueString('primary_color', $value);
+ $saved = true;
+ }
+ break;
+ case 'background_color':
+ if (!preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $value)) {
+ $error = $this->l10n->t('The given color is invalid');
+ } else {
+ $this->appConfig->setAppValueString('background_color', $value);
+ $saved = true;
}
break;
case 'disable-user-theming':
- if ($value !== 'yes' && $value !== 'no') {
+ if (!in_array($value, ['yes', 'true', 'no', 'false'])) {
$error = $this->l10n->t('Disable-user-theming should be true or false');
+ } else {
+ $this->appConfig->setAppValueBool('disable-user-theming', $value === 'yes' || $value === 'true');
+ $saved = true;
}
break;
}
@@ -171,7 +140,9 @@ class ThemingController extends Controller {
], Http::STATUS_BAD_REQUEST);
}
- $this->themingDefaults->set($setting, $value);
+ if (!$saved) {
+ $this->themingDefaults->set($setting, $value);
+ }
return new DataResponse([
'data' => [
@@ -182,19 +153,19 @@ class ThemingController extends Controller {
}
/**
- * @AuthorizedAdminSetting(settings=OCA\Theming\Settings\Admin)
* @param string $setting
* @param mixed $value
* @return DataResponse
* @throws NotPermittedException
*/
+ #[AuthorizedAdminSetting(settings: Admin::class)]
public function updateAppMenu($setting, $value) {
$error = null;
switch ($setting) {
case 'defaultApps':
if (is_array($value)) {
try {
- $this->appManager->setDefaultApps($value);
+ $this->navigationManager->setDefaultEntryIds($value);
} catch (InvalidArgumentException $e) {
$error = $this->l10n->t('Invalid app given');
}
@@ -223,18 +194,20 @@ class ThemingController extends Controller {
}
/**
- * Check that a string is a valid http/https url
+ * Check that a string is a valid http/https url.
+ * Also validates that there is no way for XSS through HTML
*/
private function isValidUrl(string $url): bool {
- return ((str_starts_with($url, 'http://') || str_starts_with($url, 'https://')) &&
- filter_var($url, FILTER_VALIDATE_URL) !== false);
+ return ((str_starts_with($url, 'http://') || str_starts_with($url, 'https://'))
+ && filter_var($url, FILTER_VALIDATE_URL) !== false)
+ && !str_contains($url, '"');
}
/**
- * @AuthorizedAdminSetting(settings=OCA\Theming\Settings\Admin)
* @return DataResponse
* @throws NotPermittedException
*/
+ #[AuthorizedAdminSetting(settings: Admin::class)]
public function uploadImage(): DataResponse {
$key = $this->request->getParam('key');
if (!in_array($key, self::VALID_UPLOAD_KEYS, true)) {
@@ -298,8 +271,8 @@ class ThemingController extends Controller {
return new DataResponse(
[
- 'data' =>
- [
+ 'data'
+ => [
'name' => $name,
'url' => $this->imageManager->getImageUrl($key),
'message' => $this->l10n->t('Saved'),
@@ -311,19 +284,19 @@ class ThemingController extends Controller {
/**
* Revert setting to default value
- * @AuthorizedAdminSetting(settings=OCA\Theming\Settings\Admin)
*
* @param string $setting setting which should be reverted
* @return DataResponse
* @throws NotPermittedException
*/
+ #[AuthorizedAdminSetting(settings: Admin::class)]
public function undo(string $setting): DataResponse {
$value = $this->themingDefaults->undo($setting);
return new DataResponse(
[
- 'data' =>
- [
+ 'data'
+ => [
'value' => $value,
'message' => $this->l10n->t('Saved'),
],
@@ -334,19 +307,19 @@ class ThemingController extends Controller {
/**
* Revert all theming settings to their default values
- * @AuthorizedAdminSetting(settings=OCA\Theming\Settings\Admin)
*
* @return DataResponse
* @throws NotPermittedException
*/
+ #[AuthorizedAdminSetting(settings: Admin::class)]
public function undoAll(): DataResponse {
$this->themingDefaults->undoAll();
- $this->appManager->setDefaultApps([]);
+ $this->navigationManager->setDefaultEntryIds([]);
return new DataResponse(
[
- 'data' =>
- [
+ 'data'
+ => [
'message' => $this->l10n->t('Saved'),
],
'status' => 'success'
@@ -355,8 +328,6 @@ class ThemingController extends Controller {
}
/**
- * @PublicPage
- * @NoCSRFRequired
* @NoSameSiteCookieRequired
*
* Get an image
@@ -369,6 +340,9 @@ class ThemingController extends Controller {
* 200: Image returned
* 404: Image not found
*/
+ #[PublicPage]
+ #[NoCSRFRequired]
+ #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
public function getImage(string $key, bool $useSvg = true) {
try {
$file = $this->imageManager->getImage($key, $useSvg);
@@ -377,7 +351,7 @@ class ThemingController extends Controller {
}
$response = new FileDisplayResponse($file);
- $csp = new Http\ContentSecurityPolicy();
+ $csp = new ContentSecurityPolicy();
$csp->allowInlineStyle();
$response->setContentSecurityPolicy($csp);
$response->cacheFor(3600);
@@ -392,8 +366,6 @@ class ThemingController extends Controller {
}
/**
- * @NoCSRFRequired
- * @PublicPage
* @NoSameSiteCookieRequired
* @NoTwoFactorRequired
*
@@ -407,6 +379,9 @@ class ThemingController extends Controller {
* 200: Stylesheet returned
* 404: Theme not found
*/
+ #[PublicPage]
+ #[NoCSRFRequired]
+ #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
public function getThemeStylesheet(string $themeId, bool $plain = false, bool $withCustomCss = false) {
$themes = $this->themesService->getThemes();
if (!in_array($themeId, array_keys($themes))) {
@@ -427,10 +402,17 @@ class ThemingController extends Controller {
$css = ":root { $variables } " . $customCss;
} else {
// If not set, we'll rely on the body class
- $compiler = new Compiler();
- $compiledCss = $compiler->compileString("[data-theme-$themeId] { $variables $customCss }");
- $css = $compiledCss->getCss();
- ;
+ // We need to separate @-rules from normal selectors, as they can't be nested
+ // This is a replacement for the SCSS compiler that did this automatically before f1448fcf0777db7d4254cb0a3ef94d63be9f7a24
+ // We need a better way to handle this, but for now we just remove comments and split the at-rules
+ // from the rest of the CSS.
+ $customCssWithoutComments = preg_replace('!/\*.*?\*/!s', '', $customCss);
+ $customCssWithoutComments = preg_replace('!//.*!', '', $customCssWithoutComments);
+ preg_match_all('/(@[^{]+{(?:[^{}]*|(?R))*})/', $customCssWithoutComments, $atRules);
+ $atRulesCss = implode('', $atRules[0]);
+ $scopedCss = preg_replace('/(@[^{]+{(?:[^{}]*|(?R))*})/', '', $customCssWithoutComments);
+
+ $css = "$atRulesCss [data-theme-$themeId] { $variables $scopedCss }";
}
try {
@@ -443,19 +425,19 @@ class ThemingController extends Controller {
}
/**
- * @NoCSRFRequired
- * @PublicPage
- * @BruteForceProtection(action=manifest)
- *
* Get the manifest for an app
*
* @param string $app ID of the app
* @psalm-suppress LessSpecificReturnStatement The content of the Manifest doesn't need to be described in the return type
- * @return JSONResponse<Http::STATUS_OK, array{name: string, short_name: string, start_url: string, theme_color: string, background_color: string, description: string, icons: array{src: non-empty-string, type: string, sizes: string}[], display: string}, array{}>|JSONResponse<Http::STATUS_NOT_FOUND, array{}, array{}>
+ * @return JSONResponse<Http::STATUS_OK, array{name: string, short_name: string, start_url: string, theme_color: string, background_color: string, description: string, icons: list<array{src: non-empty-string, type: string, sizes: string}>, display_override: list<string>, display: string}, array{}>|JSONResponse<Http::STATUS_NOT_FOUND, array{}, array{}>
*
* 200: Manifest returned
* 404: App not found
*/
+ #[PublicPage]
+ #[NoCSRFRequired]
+ #[BruteForceProtection(action: 'manifest')]
+ #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
public function getManifest(string $app): JSONResponse {
$cacheBusterValue = $this->config->getAppValue('theming', 'cachebuster', '0');
if ($app === 'core' || $app === 'settings') {
@@ -491,8 +473,8 @@ class ThemingController extends Controller {
'theme_color' => $this->themingDefaults->getColorPrimary(),
'background_color' => $this->themingDefaults->getColorPrimary(),
'description' => $description,
- 'icons' =>
- [
+ 'icons'
+ => [
[
'src' => $this->urlGenerator->linkToRoute('theming.Icon.getTouchIcon',
['app' => $app]) . '?v=' . $cacheBusterValue,
@@ -506,7 +488,8 @@ class ThemingController extends Controller {
'sizes' => '16x16'
]
],
- 'display' => 'standalone'
+ 'display_override' => [$this->config->getSystemValueBool('theming.standalone_window.enabled', true) ? 'minimal-ui' : ''],
+ 'display' => $this->config->getSystemValueBool('theming.standalone_window.enabled', true) ? 'standalone' : 'browser'
];
$response = new JSONResponse($responseJS);
$response->cacheFor(3600);
diff --git a/apps/theming/lib/Controller/UserThemeController.php b/apps/theming/lib/Controller/UserThemeController.php
index 657c20036d3..770f2ca922f 100644
--- a/apps/theming/lib/Controller/UserThemeController.php
+++ b/apps/theming/lib/Controller/UserThemeController.php
@@ -3,31 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2018 John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
- * @copyright Copyright (c) 2019 Janis Köhr <janiskoehr@icloud.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Kesselberg <mail@danielkesselberg.de>
- * @author Janis Köhr <janis.koehr@novatec-gmbh.de>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Kate Döen <kate.doeen@nextcloud.com>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Theming\Controller;
@@ -38,10 +15,14 @@ use OCA\Theming\Service\BackgroundService;
use OCA\Theming\Service\ThemesService;
use OCA\Theming\ThemingDefaults;
use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\Attribute\NoAdminRequired;
+use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
+use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\FileDisplayResponse;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\NotFoundResponse;
+use OCP\AppFramework\Http\Response;
use OCP\AppFramework\OCS\OCSBadRequestException;
use OCP\AppFramework\OCS\OCSForbiddenException;
use OCP\AppFramework\OCSController;
@@ -57,25 +38,16 @@ class UserThemeController extends OCSController {
protected ?string $userId = null;
- private IConfig $config;
- private IUserSession $userSession;
- private ThemesService $themesService;
- private ThemingDefaults $themingDefaults;
- private BackgroundService $backgroundService;
-
- public function __construct(string $appName,
+ public function __construct(
+ string $appName,
IRequest $request,
- IConfig $config,
+ private IConfig $config,
IUserSession $userSession,
- ThemesService $themesService,
- ThemingDefaults $themingDefaults,
- BackgroundService $backgroundService) {
+ private ThemesService $themesService,
+ private ThemingDefaults $themingDefaults,
+ private BackgroundService $backgroundService,
+ ) {
parent::__construct($appName, $request);
- $this->config = $config;
- $this->userSession = $userSession;
- $this->themesService = $themesService;
- $this->themingDefaults = $themingDefaults;
- $this->backgroundService = $backgroundService;
$user = $userSession->getUser();
if ($user !== null) {
@@ -84,17 +56,16 @@ class UserThemeController extends OCSController {
}
/**
- * @NoAdminRequired
- *
* Enable theme
*
* @param string $themeId the theme ID
- * @return DataResponse<Http::STATUS_OK, array<empty>, array{}>
+ * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
* @throws OCSBadRequestException Enabling theme is not possible
* @throws PreConditionNotMetException
*
* 200: Theme enabled successfully
*/
+ #[NoAdminRequired]
public function enableTheme(string $themeId): DataResponse {
$theme = $this->validateTheme($themeId);
@@ -104,17 +75,16 @@ class UserThemeController extends OCSController {
}
/**
- * @NoAdminRequired
- *
* Disable theme
*
* @param string $themeId the theme ID
- * @return DataResponse<Http::STATUS_OK, array<empty>, array{}>
+ * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
* @throws OCSBadRequestException Disabling theme is not possible
* @throws PreConditionNotMetException
*
* 200: Theme disabled successfully
*/
+ #[NoAdminRequired]
public function disableTheme(string $themeId): DataResponse {
$theme = $this->validateTheme($themeId);
@@ -153,16 +123,16 @@ class UserThemeController extends OCSController {
}
/**
- * @NoAdminRequired
- * @NoCSRFRequired
- *
* Get the background image
* @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|NotFoundResponse<Http::STATUS_NOT_FOUND, array{}>
*
* 200: Background image returned
* 404: Background image not found
*/
- public function getBackground(): Http\Response {
+ #[NoAdminRequired]
+ #[NoCSRFRequired]
+ #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
+ public function getBackground(): Response {
$file = $this->backgroundService->getBackground();
if ($file !== null) {
$response = new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => $file->getMimeType()]);
@@ -173,27 +143,25 @@ class UserThemeController extends OCSController {
}
/**
- * @NoAdminRequired
- *
* Delete the background
*
* @return JSONResponse<Http::STATUS_OK, ThemingBackground, array{}>
*
* 200: Background deleted successfully
*/
+ #[NoAdminRequired]
public function deleteBackground(): JSONResponse {
$currentVersion = (int)$this->config->getUserValue($this->userId, Application::APP_ID, 'userCacheBuster', '0');
$this->backgroundService->deleteBackgroundImage();
return new JSONResponse([
'backgroundImage' => null,
- 'backgroundColor' => $this->themingDefaults->getColorPrimary(),
+ 'backgroundColor' => $this->themingDefaults->getColorBackground(),
+ 'primaryColor' => $this->themingDefaults->getColorPrimary(),
'version' => $currentVersion,
]);
}
/**
- * @NoAdminRequired
- *
* Set the background
*
* @param string $type Type of background
@@ -204,6 +172,7 @@ class UserThemeController extends OCSController {
* 200: Background set successfully
* 400: Setting background is not possible
*/
+ #[NoAdminRequired]
public function setBackground(string $type = BackgroundService::BACKGROUND_DEFAULT, string $value = '', ?string $color = null): JSONResponse {
$currentVersion = (int)$this->config->getUserValue($this->userId, Application::APP_ID, 'userCacheBuster', '0');
@@ -241,7 +210,8 @@ class UserThemeController extends OCSController {
return new JSONResponse([
'backgroundImage' => $this->config->getUserValue($this->userId, Application::APP_ID, 'background_image', BackgroundService::BACKGROUND_DEFAULT),
- 'backgroundColor' => $this->themingDefaults->getColorPrimary(),
+ 'backgroundColor' => $this->themingDefaults->getColorBackground(),
+ 'primaryColor' => $this->themingDefaults->getColorPrimary(),
'version' => $currentVersion,
]);
}
diff --git a/apps/theming/lib/ITheme.php b/apps/theming/lib/ITheme.php
index 4ff455005a2..2e40e8e489b 100644
--- a/apps/theming/lib/ITheme.php
+++ b/apps/theming/lib/ITheme.php
@@ -2,24 +2,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2022 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * 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, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Theming;
diff --git a/apps/theming/lib/IconBuilder.php b/apps/theming/lib/IconBuilder.php
index 8d6546cdcce..63f4559970d 100644
--- a/apps/theming/lib/IconBuilder.php
+++ b/apps/theming/lib/IconBuilder.php
@@ -1,28 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Jan-Christoph Borchardt <hey@jancborchardt.net>
- * @author Julius Haertl <jus@bitgrid.net>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Morris Jobke <hey@morrisjobke.de>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Theming;
@@ -31,13 +11,6 @@ use ImagickPixel;
use OCP\Files\SimpleFS\ISimpleFile;
class IconBuilder {
- /** @var ThemingDefaults */
- private $themingDefaults;
- /** @var Util */
- private $util;
- /** @var ImageManager */
- private $imageManager;
-
/**
* IconBuilder constructor.
*
@@ -46,13 +19,10 @@ class IconBuilder {
* @param ImageManager $imageManager
*/
public function __construct(
- ThemingDefaults $themingDefaults,
- Util $util,
- ImageManager $imageManager
+ private ThemingDefaults $themingDefaults,
+ private Util $util,
+ private ImageManager $imageManager,
) {
- $this->themingDefaults = $themingDefaults;
- $this->util = $util;
- $this->imageManager = $imageManager;
}
/**
@@ -65,12 +35,12 @@ class IconBuilder {
}
try {
$favicon = new Imagick();
- $favicon->setFormat("ico");
+ $favicon->setFormat('ico');
$icon = $this->renderAppIcon($app, 128);
if ($icon === false) {
return false;
}
- $icon->setImageFormat("png32");
+ $icon->setImageFormat('png32');
$clone = clone $icon;
$clone->scaleImage(16, 0);
@@ -108,7 +78,7 @@ class IconBuilder {
if ($icon === false) {
return false;
}
- $icon->setImageFormat("png32");
+ $icon->setImageFormat('png32');
$data = $icon->getImageBlob();
$icon->destroy();
return $data;
@@ -121,58 +91,53 @@ class IconBuilder {
* Render app icon on themed background color
* fallback to logo
*
- * @param $app string app name
- * @param $size int size of the icon in px
+ * @param string $app app name
+ * @param int $size size of the icon in px
* @return Imagick|false
*/
public function renderAppIcon($app, $size) {
$appIcon = $this->util->getAppIcon($app);
- if ($appIcon === false) {
- return false;
- }
if ($appIcon instanceof ISimpleFile) {
$appIconContent = $appIcon->getContent();
$mime = $appIcon->getMimeType();
+ } elseif (!file_exists($appIcon)) {
+ return false;
} else {
$appIconContent = file_get_contents($appIcon);
$mime = mime_content_type($appIcon);
}
- if ($appIconContent === false || $appIconContent === "") {
+ if ($appIconContent === false || $appIconContent === '') {
return false;
}
$color = $this->themingDefaults->getColorPrimary();
// generate background image with rounded corners
- $background = '<?xml version="1.0" encoding="UTF-8"?>' .
- '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:cc="http://creativecommons.org/ns#" width="512" height="512" xmlns:xlink="http://www.w3.org/1999/xlink">' .
- '<rect x="0" y="0" rx="100" ry="100" width="512" height="512" style="fill:' . $color . ';" />' .
- '</svg>';
+ $cornerRadius = 0.2 * $size;
+ $background = '<?xml version="1.0" encoding="UTF-8"?>'
+ . '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:cc="http://creativecommons.org/ns#" width="' . $size . '" height="' . $size . '" xmlns:xlink="http://www.w3.org/1999/xlink">'
+ . '<rect x="0" y="0" rx="' . $cornerRadius . '" ry="' . $cornerRadius . '" width="' . $size . '" height="' . $size . '" style="fill:' . $color . ';" />'
+ . '</svg>';
// resize svg magic as this seems broken in Imagemagick
- if ($mime === "image/svg+xml" || substr($appIconContent, 0, 4) === "<svg") {
- if (substr($appIconContent, 0, 5) !== "<?xml") {
- $svg = "<?xml version=\"1.0\"?>".$appIconContent;
+ if ($mime === 'image/svg+xml' || substr($appIconContent, 0, 4) === '<svg') {
+ if (substr($appIconContent, 0, 5) !== '<?xml') {
+ $svg = '<?xml version="1.0"?>' . $appIconContent;
} else {
$svg = $appIconContent;
}
$tmp = new Imagick();
+ $tmp->setBackgroundColor(new ImagickPixel('transparent'));
+ $tmp->setResolution(72, 72);
$tmp->readImageBlob($svg);
$x = $tmp->getImageWidth();
$y = $tmp->getImageHeight();
- $res = $tmp->getImageResolution();
$tmp->destroy();
- if ($x > $y) {
- $max = $x;
- } else {
- $max = $y;
- }
-
// convert svg to resized image
$appIconFile = new Imagick();
- $resX = (int)(512 * $res['x'] / $max * 2.53);
- $resY = (int)(512 * $res['y'] / $max * 2.53);
+ $resX = (int)(72 * $size / $x);
+ $resY = (int)(72 * $size / $y);
$appIconFile->setResolution($resX, $resY);
$appIconFile->setBackgroundColor(new ImagickPixel('transparent'));
$appIconFile->readImageBlob($svg);
@@ -183,35 +148,34 @@ class IconBuilder {
*/
if ($this->util->isBrightColor($color)
&& !$appIcon instanceof ISimpleFile
- && $app !== "core"
+ && $app !== 'core'
) {
$appIconFile->negateImage(false);
}
- $appIconFile->scaleImage(512, 512, true);
} else {
$appIconFile = new Imagick();
$appIconFile->setBackgroundColor(new ImagickPixel('transparent'));
$appIconFile->readImageBlob($appIconContent);
- $appIconFile->scaleImage(512, 512, true);
}
// offset for icon positioning
- $border_w = (int)($appIconFile->getImageWidth() * 0.05);
- $border_h = (int)($appIconFile->getImageHeight() * 0.05);
+ $padding = 0.15;
+ $border_w = (int)($appIconFile->getImageWidth() * $padding);
+ $border_h = (int)($appIconFile->getImageHeight() * $padding);
$innerWidth = ($appIconFile->getImageWidth() - $border_w * 2);
$innerHeight = ($appIconFile->getImageHeight() - $border_h * 2);
$appIconFile->adaptiveResizeImage($innerWidth, $innerHeight);
// center icon
- $offset_w = (int)(512 / 2 - $innerWidth / 2);
- $offset_h = (int)(512 / 2 - $innerHeight / 2);
+ $offset_w = (int)($size / 2 - $innerWidth / 2);
+ $offset_h = (int)($size / 2 - $innerHeight / 2);
$finalIconFile = new Imagick();
$finalIconFile->setBackgroundColor(new ImagickPixel('transparent'));
$finalIconFile->readImageBlob($background);
$finalIconFile->setImageVirtualPixelMethod(Imagick::VIRTUALPIXELMETHOD_TRANSPARENT);
- $finalIconFile->setImageArtifact('compose:args', "1,0,-0.5,0.5");
+ $finalIconFile->setImageArtifact('compose:args', '1,0,-0.5,0.5');
$finalIconFile->compositeImage($appIconFile, Imagick::COMPOSITE_ATOP, $offset_w, $offset_h);
$finalIconFile->setImageFormat('png24');
- if (defined("Imagick::INTERPOLATE_BICUBIC") === true) {
+ if (defined('Imagick::INTERPOLATE_BICUBIC') === true) {
$filter = Imagick::INTERPOLATE_BICUBIC;
} else {
$filter = Imagick::FILTER_LANCZOS;
@@ -223,17 +187,17 @@ class IconBuilder {
}
/**
- * @param $app string app name
- * @param $image string relative path to svg file in app directory
+ * @param string $app app name
+ * @param string $image relative path to svg file in app directory
* @return string|false content of a colorized svg file
*/
public function colorSvg($app, $image) {
$imageFile = $this->util->getAppImage($app, $image);
- if ($imageFile === false || $imageFile === "") {
+ if ($imageFile === false || $imageFile === '' || !file_exists($imageFile)) {
return false;
}
$svg = file_get_contents($imageFile);
- if ($svg !== false && $svg !== "") {
+ if ($svg !== false && $svg !== '') {
$color = $this->util->elementColor($this->themingDefaults->getColorPrimary());
$svg = $this->util->colorizeSvg($svg, $color);
return $svg;
diff --git a/apps/theming/lib/ImageManager.php b/apps/theming/lib/ImageManager.php
index 994e3f35118..309bf192bc3 100644
--- a/apps/theming/lib/ImageManager.php
+++ b/apps/theming/lib/ImageManager.php
@@ -1,35 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Kesselberg <mail@danielkesselberg.de>
- * @author Gary Kim <gary@garykim.dev>
- * @author Jacob Neplokh <me@jacobneplokh.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Julien Veyssier <eneiluj@posteo.net>
- * @author Julius Haertl <jus@bitgrid.net>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Michael Weimann <mail@michael-weimann.eu>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author ste101 <stephan_bauer@gmx.de>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Theming;
@@ -56,6 +29,7 @@ class ImageManager {
private ICacheFactory $cacheFactory,
private LoggerInterface $logger,
private ITempManager $tempManager,
+ private BackgroundService $backgroundService,
) {
}
@@ -69,6 +43,9 @@ class ImageManager {
$cacheBusterCounter = $this->config->getAppValue(Application::APP_ID, 'cachebuster', '0');
if ($this->hasImage($key)) {
return $this->urlGenerator->linkToRoute('theming.Theming.getImage', [ 'key' => $key ]) . '?v=' . $cacheBusterCounter;
+ } elseif ($key === 'backgroundDark' && $this->hasImage('background')) {
+ // Fall back to light variant
+ return $this->urlGenerator->linkToRoute('theming.Theming.getImage', [ 'key' => 'background' ]) . '?v=' . $cacheBusterCounter;
}
switch ($key) {
@@ -76,8 +53,17 @@ class ImageManager {
case 'logoheader':
case 'favicon':
return $this->urlGenerator->imagePath('core', 'logo/logo.png') . '?v=' . $cacheBusterCounter;
+ case 'backgroundDark':
case 'background':
- return $this->urlGenerator->linkTo(Application::APP_ID, 'img/background/' . BackgroundService::DEFAULT_BACKGROUND_IMAGE);
+ // Removing the background defines its mime as 'backgroundColor'
+ $mimeSetting = $this->config->getAppValue('theming', 'backgroundMime', '');
+ if ($mimeSetting !== 'backgroundColor') {
+ $image = BackgroundService::DEFAULT_BACKGROUND_IMAGE;
+ if ($key === 'backgroundDark') {
+ $image = BackgroundService::SHIPPED_BACKGROUNDS[$image]['dark_variant'] ?? $image;
+ }
+ return $this->urlGenerator->linkTo(Application::APP_ID, "img/background/$image");
+ }
}
return '';
}
@@ -167,7 +153,7 @@ class ImageManager {
*
* @param string $filename
* @throws NotFoundException
- * @return \OCP\Files\SimpleFS\ISimpleFile
+ * @return ISimpleFile
* @throws NotPermittedException
*/
public function getCachedImage(string $filename): ISimpleFile {
@@ -180,7 +166,7 @@ class ImageManager {
*
* @param string $filename
* @param string $data
- * @return \OCP\Files\SimpleFS\ISimpleFile
+ * @return ISimpleFile
* @throws NotFoundException
* @throws NotPermittedException
*/
@@ -209,6 +195,10 @@ class ImageManager {
} catch (NotFoundException $e) {
} catch (NotPermittedException $e) {
}
+
+ if ($key === 'logo') {
+ $this->config->deleteAppValue('theming', 'logoDimensions');
+ }
}
public function updateImage(string $key, string $tmpFile): string {
@@ -227,51 +217,79 @@ class ImageManager {
throw new \Exception('Unsupported image type: ' . $detectedMimeType);
}
- if ($key === 'background' && $this->shouldOptimizeBackgroundImage($detectedMimeType, filesize($tmpFile))) {
- try {
- // Optimize the image since some people may upload images that will be
- // either to big or are not progressive rendering.
- $newImage = @imagecreatefromstring(file_get_contents($tmpFile));
- if ($newImage === false) {
- throw new \Exception('Could not read background image, possibly corrupted.');
- }
+ if ($key === 'background') {
+ if ($this->shouldOptimizeBackgroundImage($detectedMimeType, filesize($tmpFile))) {
+ try {
+ // Optimize the image since some people may upload images that will be
+ // either to big or are not progressive rendering.
+ $newImage = @imagecreatefromstring(file_get_contents($tmpFile));
+ if ($newImage === false) {
+ throw new \Exception('Could not read background image, possibly corrupted.');
+ }
- // Preserve transparency
- imagesavealpha($newImage, true);
- imagealphablending($newImage, true);
+ // Preserve transparency
+ imagesavealpha($newImage, true);
+ imagealphablending($newImage, true);
- $newWidth = (imagesx($newImage) < 4096 ? imagesx($newImage) : 4096);
- $newHeight = (int)(imagesy($newImage) / (imagesx($newImage) / $newWidth));
- $outputImage = imagescale($newImage, $newWidth, $newHeight);
- if ($outputImage === false) {
- throw new \Exception('Could not scale uploaded background image.');
- }
+ $imageWidth = imagesx($newImage);
+ $imageHeight = imagesy($newImage);
- $newTmpFile = $this->tempManager->getTemporaryFile();
- imageinterlace($outputImage, true);
- // Keep jpeg images encoded as jpeg
- if (str_contains($detectedMimeType, 'image/jpeg')) {
- if (!imagejpeg($outputImage, $newTmpFile, 90)) {
- throw new \Exception('Could not recompress background image as JPEG');
+ /** @var int */
+ $newWidth = min(4096, $imageWidth);
+ $newHeight = intval($imageHeight / ($imageWidth / $newWidth));
+ $outputImage = imagescale($newImage, $newWidth, $newHeight);
+ if ($outputImage === false) {
+ throw new \Exception('Could not scale uploaded background image.');
}
- } else {
- if (!imagepng($outputImage, $newTmpFile, 8)) {
- throw new \Exception('Could not recompress background image as PNG');
+
+ $newTmpFile = $this->tempManager->getTemporaryFile();
+ imageinterlace($outputImage, true);
+ // Keep jpeg images encoded as jpeg
+ if (str_contains($detectedMimeType, 'image/jpeg')) {
+ if (!imagejpeg($outputImage, $newTmpFile, 90)) {
+ throw new \Exception('Could not recompress background image as JPEG');
+ }
+ } else {
+ if (!imagepng($outputImage, $newTmpFile, 8)) {
+ throw new \Exception('Could not recompress background image as PNG');
+ }
}
- }
- $tmpFile = $newTmpFile;
- imagedestroy($outputImage);
- } catch (\Exception $e) {
- if (is_resource($outputImage) || $outputImage instanceof \GdImage) {
+ $tmpFile = $newTmpFile;
imagedestroy($outputImage);
- }
+ } catch (\Exception $e) {
+ if (isset($outputImage) && is_resource($outputImage) || $outputImage instanceof \GdImage) {
+ imagedestroy($outputImage);
+ }
- $this->logger->debug($e->getMessage());
+ $this->logger->debug($e->getMessage());
+ }
}
+
+ // For background images we need to announce it
+ $this->backgroundService->setGlobalBackground($tmpFile);
}
$target->putContent(file_get_contents($tmpFile));
+ if ($key === 'logo') {
+ $content = file_get_contents($tmpFile);
+ $newImage = @imagecreatefromstring($content);
+ if ($newImage !== false) {
+ $this->config->setAppValue('theming', 'logoDimensions', imagesx($newImage) . 'x' . imagesy($newImage));
+ } elseif (str_starts_with($detectedMimeType, 'image/svg')) {
+ $matched = preg_match('/viewbox=["\']\d* \d* (\d*\.?\d*) (\d*\.?\d*)["\']/i', $content, $matches);
+ if ($matched) {
+ $this->config->setAppValue('theming', 'logoDimensions', $matches[1] . 'x' . $matches[2]);
+ } else {
+ $this->logger->warning('Could not read logo image dimensions to optimize for mail header');
+ $this->config->deleteAppValue('theming', 'logoDimensions');
+ }
+ } else {
+ $this->logger->warning('Could not read logo image dimensions to optimize for mail header');
+ $this->config->deleteAppValue('theming', 'logoDimensions');
+ }
+ }
+
return $detectedMimeType;
}
diff --git a/apps/theming/lib/Jobs/MigrateBackgroundImages.php b/apps/theming/lib/Jobs/MigrateBackgroundImages.php
index 62179e46a4b..62e58f5e722 100644
--- a/apps/theming/lib/Jobs/MigrateBackgroundImages.php
+++ b/apps/theming/lib/Jobs/MigrateBackgroundImages.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2022 Arthur Schiwon <blizzz@arthur-schiwon.de>
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- *
- * @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 <https://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Theming\Jobs;
@@ -47,31 +30,20 @@ class MigrateBackgroundImages extends QueuedJob {
// will be saved in appdata/theming/global/
protected const STATE_FILE_NAME = '25_dashboard_to_theming_migration_users.json';
- private IAppDataFactory $appDataFactory;
- private IJobList $jobList;
- private IDBConnection $dbc;
- private IAppData $appData;
- private LoggerInterface $logger;
-
public function __construct(
ITimeFactory $time,
- IAppDataFactory $appDataFactory,
- IJobList $jobList,
- IDBConnection $dbc,
- IAppData $appData,
- LoggerInterface $logger
+ private IAppDataFactory $appDataFactory,
+ private IJobList $jobList,
+ private IDBConnection $dbc,
+ private IAppData $appData,
+ private LoggerInterface $logger,
) {
parent::__construct($time);
- $this->appDataFactory = $appDataFactory;
- $this->jobList = $jobList;
- $this->dbc = $dbc;
- $this->appData = $appData;
- $this->logger = $logger;
}
protected function run(mixed $argument): void {
if (!is_array($argument) || !isset($argument['stage'])) {
- throw new \Exception('Job '.self::class.' called with wrong argument');
+ throw new \Exception('Job ' . self::class . ' called with wrong argument');
}
switch ($argument['stage']) {
diff --git a/apps/theming/lib/Jobs/RestoreBackgroundImageColor.php b/apps/theming/lib/Jobs/RestoreBackgroundImageColor.php
new file mode 100644
index 00000000000..42662dacef2
--- /dev/null
+++ b/apps/theming/lib/Jobs/RestoreBackgroundImageColor.php
@@ -0,0 +1,205 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Theming\Jobs;
+
+use OCA\Theming\AppInfo\Application;
+use OCA\Theming\Service\BackgroundService;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\IJobList;
+use OCP\BackgroundJob\QueuedJob;
+use OCP\Files\IAppData;
+use OCP\Files\NotFoundException;
+use OCP\Files\NotPermittedException;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use Psr\Log\LoggerInterface;
+
+class RestoreBackgroundImageColor extends QueuedJob {
+
+ public const STAGE_PREPARE = 'prepare';
+ public const STAGE_EXECUTE = 'execute';
+ // will be saved in appdata/theming/global/
+ protected const STATE_FILE_NAME = '30_background_image_color_restoration.json';
+
+ public function __construct(
+ ITimeFactory $time,
+ private IConfig $config,
+ private IAppData $appData,
+ private IJobList $jobList,
+ private IDBConnection $dbc,
+ private LoggerInterface $logger,
+ private BackgroundService $service,
+ ) {
+ parent::__construct($time);
+ }
+
+ protected function run(mixed $argument): void {
+ if (!is_array($argument) || !isset($argument['stage'])) {
+ throw new \Exception('Job ' . self::class . ' called with wrong argument');
+ }
+
+ switch ($argument['stage']) {
+ case self::STAGE_PREPARE:
+ $this->runPreparation();
+ break;
+ case self::STAGE_EXECUTE:
+ $this->runMigration();
+ break;
+ default:
+ break;
+ }
+ }
+
+ protected function runPreparation(): void {
+ try {
+ $qb = $this->dbc->getQueryBuilder();
+ $qb2 = $this->dbc->getQueryBuilder();
+
+ $innerSQL = $qb2->select('userid')
+ ->from('preferences')
+ ->where($qb2->expr()->eq('configkey', $qb->createNamedParameter('background_color')));
+
+ // Get those users, that have a background_image set - not the default, but no background_color.
+ $result = $qb->selectDistinct('a.userid')
+ ->from('preferences', 'a')
+ ->leftJoin('a', $qb->createFunction('(' . $innerSQL->getSQL() . ')'), 'b', 'a.userid = b.userid')
+ ->where($qb2->expr()->eq('a.configkey', $qb->createNamedParameter('background_image')))
+ ->andWhere($qb2->expr()->neq('a.configvalue', $qb->createNamedParameter(BackgroundService::BACKGROUND_DEFAULT)))
+ ->andWhere($qb2->expr()->isNull('b.userid'))
+ ->executeQuery();
+
+ $userIds = $result->fetchAll(\PDO::FETCH_COLUMN);
+ $this->logger->info('Prepare to restore background information for {users} users', ['users' => count($userIds)]);
+ $this->storeUserIdsToProcess($userIds);
+ } catch (\Throwable $t) {
+ $this->jobList->add(self::class, ['stage' => self::STAGE_PREPARE]);
+ throw $t;
+ }
+ $this->jobList->add(self::class, ['stage' => self::STAGE_EXECUTE]);
+ }
+
+ /**
+ * @throws NotPermittedException
+ * @throws NotFoundException
+ */
+ protected function runMigration(): void {
+ $allUserIds = $this->readUserIdsToProcess();
+ $notSoFastMode = count($allUserIds) > 1000;
+
+ $userIds = array_slice($allUserIds, 0, 1000);
+ foreach ($userIds as $userId) {
+ $backgroundColor = $this->config->getUserValue($userId, Application::APP_ID, 'background_color');
+ if ($backgroundColor !== '') {
+ continue;
+ }
+
+ $background = $this->config->getUserValue($userId, Application::APP_ID, 'background_image');
+ switch ($background) {
+ case BackgroundService::BACKGROUND_DEFAULT:
+ $this->service->setDefaultBackground($userId);
+ break;
+ case BackgroundService::BACKGROUND_COLOR:
+ break;
+ case BackgroundService::BACKGROUND_CUSTOM:
+ $this->service->recalculateMeanColor($userId);
+ break;
+ default:
+ // shipped backgrounds
+ // do not alter primary color
+ $primary = $this->config->getUserValue($userId, Application::APP_ID, 'primary_color');
+ if (isset(BackgroundService::SHIPPED_BACKGROUNDS[$background])) {
+ $this->service->setShippedBackground($background, $userId);
+ } else {
+ $this->service->setDefaultBackground($userId);
+ }
+ // Restore primary
+ if ($primary !== '') {
+ $this->config->setUserValue($userId, Application::APP_ID, 'primary_color', $primary);
+ }
+ }
+ }
+
+ if ($notSoFastMode) {
+ $remainingUserIds = array_slice($allUserIds, 1000);
+ $this->storeUserIdsToProcess($remainingUserIds);
+ $this->jobList->add(self::class, ['stage' => self::STAGE_EXECUTE]);
+ } else {
+ $this->deleteStateFile();
+ }
+ }
+
+ /**
+ * @throws NotPermittedException
+ * @throws NotFoundException
+ */
+ protected function readUserIdsToProcess(): array {
+ $globalFolder = $this->appData->getFolder('global');
+ if ($globalFolder->fileExists(self::STATE_FILE_NAME)) {
+ $file = $globalFolder->getFile(self::STATE_FILE_NAME);
+ try {
+ $userIds = \json_decode($file->getContent(), true);
+ } catch (NotFoundException $e) {
+ $userIds = [];
+ }
+ if ($userIds === null) {
+ $userIds = [];
+ }
+ } else {
+ $userIds = [];
+ }
+ return $userIds;
+ }
+
+ /**
+ * @throws NotFoundException
+ */
+ protected function storeUserIdsToProcess(array $userIds): void {
+ $storableUserIds = \json_encode($userIds);
+ $globalFolder = $this->appData->getFolder('global');
+ try {
+ if ($globalFolder->fileExists(self::STATE_FILE_NAME)) {
+ $file = $globalFolder->getFile(self::STATE_FILE_NAME);
+ } else {
+ $file = $globalFolder->newFile(self::STATE_FILE_NAME);
+ }
+ $file->putContent($storableUserIds);
+ } catch (NotFoundException $e) {
+ } catch (NotPermittedException $e) {
+ $this->logger->warning('Lacking permissions to create {file}',
+ [
+ 'app' => 'theming',
+ 'file' => self::STATE_FILE_NAME,
+ 'exception' => $e,
+ ]
+ );
+ }
+ }
+
+ /**
+ * @throws NotFoundException
+ */
+ protected function deleteStateFile(): void {
+ $globalFolder = $this->appData->getFolder('global');
+ if ($globalFolder->fileExists(self::STATE_FILE_NAME)) {
+ $file = $globalFolder->getFile(self::STATE_FILE_NAME);
+ try {
+ $file->delete();
+ } catch (NotPermittedException $e) {
+ $this->logger->info('Could not delete {file} due to permissions. It is safe to delete manually inside data -> appdata -> theming -> global.',
+ [
+ 'app' => 'theming',
+ 'file' => $file->getName(),
+ 'exception' => $e,
+ ]
+ );
+ }
+ }
+ }
+}
diff --git a/apps/theming/lib/Listener/BeforePreferenceListener.php b/apps/theming/lib/Listener/BeforePreferenceListener.php
index 47b7d3fb6ff..048deae50ce 100644
--- a/apps/theming/lib/Listener/BeforePreferenceListener.php
+++ b/apps/theming/lib/Listener/BeforePreferenceListener.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Theming\Listener;
@@ -34,6 +17,12 @@ use OCP\EventDispatcher\IEventListener;
/** @template-implements IEventListener<BeforePreferenceDeletedEvent|BeforePreferenceSetEvent> */
class BeforePreferenceListener implements IEventListener {
+
+ /**
+ * @var string[]
+ */
+ private const ALLOWED_KEYS = ['force_enable_blur_filter', 'shortcuts_disabled', 'primary_color'];
+
public function __construct(
private IAppManager $appManager,
) {
@@ -55,13 +44,25 @@ class BeforePreferenceListener implements IEventListener {
}
private function handleThemingValues(BeforePreferenceSetEvent|BeforePreferenceDeletedEvent $event): void {
- if ($event->getConfigKey() !== 'shortcuts_disabled') {
+ if (!in_array($event->getConfigKey(), self::ALLOWED_KEYS)) {
// Not allowed config key
return;
}
if ($event instanceof BeforePreferenceSetEvent) {
- $event->setValid($event->getConfigValue() === 'yes');
+ switch ($event->getConfigKey()) {
+ case 'force_enable_blur_filter':
+ $event->setValid($event->getConfigValue() === 'yes' || $event->getConfigValue() === 'no');
+ break;
+ case 'shortcuts_disabled':
+ $event->setValid($event->getConfigValue() === 'yes');
+ break;
+ case 'primary_color':
+ $event->setValid(preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $event->getConfigValue()) === 1);
+ break;
+ default:
+ $event->setValid(false);
+ }
return;
}
diff --git a/apps/theming/lib/Listener/BeforeTemplateRenderedListener.php b/apps/theming/lib/Listener/BeforeTemplateRenderedListener.php
index d845c9a1091..18ab9392b97 100644
--- a/apps/theming/lib/Listener/BeforeTemplateRenderedListener.php
+++ b/apps/theming/lib/Listener/BeforeTemplateRenderedListener.php
@@ -3,30 +3,12 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020 Morris Jobke <hey@morrisjobke.de>
- *
- * @author Morris Jobke <hey@morrisjobke.de>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Theming\Listener;
use OCA\Theming\AppInfo\Application;
-use OCA\Theming\Service\BackgroundService;
use OCA\Theming\Service\JSDataService;
use OCA\Theming\Service\ThemeInjectionService;
use OCP\AppFramework\Http\Events\BeforeLoginTemplateRenderedEvent;
@@ -37,29 +19,19 @@ use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\IConfig;
use OCP\IUserSession;
+use OCP\Util;
use Psr\Container\ContainerInterface;
/** @template-implements IEventListener<BeforeTemplateRenderedEvent|BeforeLoginTemplateRenderedEvent> */
class BeforeTemplateRenderedListener implements IEventListener {
- private IInitialState $initialState;
- private ContainerInterface $container;
- private ThemeInjectionService $themeInjectionService;
- private IUserSession $userSession;
- private IConfig $config;
-
public function __construct(
- IInitialState $initialState,
- ContainerInterface $container,
- ThemeInjectionService $themeInjectionService,
- IUserSession $userSession,
- IConfig $config
+ private IInitialState $initialState,
+ private ContainerInterface $container,
+ private ThemeInjectionService $themeInjectionService,
+ private IUserSession $userSession,
+ private IConfig $config,
) {
- $this->initialState = $initialState;
- $this->container = $container;
- $this->themeInjectionService = $themeInjectionService;
- $this->userSession = $userSession;
- $this->config = $config;
}
public function handle(Event $event): void {
@@ -81,44 +53,7 @@ class BeforeTemplateRenderedListener implements IEventListener {
$this->themeInjectionService->injectHeaders();
- $user = $this->userSession->getUser();
-
- if (!empty($user)) {
- $userId = $user->getUID();
-
- /** User background */
- $this->initialState->provideInitialState(
- 'backgroundImage',
- $this->config->getUserValue($userId, Application::APP_ID, 'background_image', BackgroundService::BACKGROUND_DEFAULT),
- );
-
- /** User color */
- $this->initialState->provideInitialState(
- 'backgroundColor',
- $this->config->getUserValue($userId, Application::APP_ID, 'background_color', BackgroundService::DEFAULT_COLOR),
- );
-
- /**
- * Admin background. `backgroundColor` if disabled,
- * mime type if defined and empty by default
- */
- $this->initialState->provideInitialState(
- 'themingDefaultBackground',
- $this->config->getAppValue('theming', 'backgroundMime', ''),
- );
- $this->initialState->provideInitialState(
- 'defaultShippedBackground',
- BackgroundService::DEFAULT_BACKGROUND_IMAGE,
- );
-
- /** List of all shipped backgrounds */
- $this->initialState->provideInitialState(
- 'shippedBackgrounds',
- BackgroundService::SHIPPED_BACKGROUNDS,
- );
- }
-
// Making sure to inject just after core
- \OCP\Util::addScript('theming', 'theming', 'core');
+ Util::addScript('theming', 'theming', 'core');
}
}
diff --git a/apps/theming/lib/Migration/InitBackgroundImagesMigration.php b/apps/theming/lib/Migration/InitBackgroundImagesMigration.php
index ff8783196ac..dea1bb3aa83 100644
--- a/apps/theming/lib/Migration/InitBackgroundImagesMigration.php
+++ b/apps/theming/lib/Migration/InitBackgroundImagesMigration.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2022 Arthur Schiwon <blizzz@arthur-schiwon.de>
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- *
- * @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 <https://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Theming\Migration;
@@ -29,13 +12,13 @@ namespace OCA\Theming\Migration;
use OCA\Theming\Jobs\MigrateBackgroundImages;
use OCP\BackgroundJob\IJobList;
use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
-class InitBackgroundImagesMigration implements \OCP\Migration\IRepairStep {
+class InitBackgroundImagesMigration implements IRepairStep {
- private IJobList $jobList;
-
- public function __construct(IJobList $jobList) {
- $this->jobList = $jobList;
+ public function __construct(
+ private IJobList $jobList,
+ ) {
}
public function getName() {
diff --git a/apps/theming/lib/Migration/Version2006Date20240905111627.php b/apps/theming/lib/Migration/Version2006Date20240905111627.php
new file mode 100644
index 00000000000..8f4130cba46
--- /dev/null
+++ b/apps/theming/lib/Migration/Version2006Date20240905111627.php
@@ -0,0 +1,127 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Theming\Migration;
+
+use Closure;
+use OCA\Theming\AppInfo\Application;
+use OCA\Theming\Jobs\RestoreBackgroundImageColor;
+use OCP\BackgroundJob\IJobList;
+use OCP\IAppConfig;
+use OCP\IDBConnection;
+use OCP\Migration\IMigrationStep;
+use OCP\Migration\IOutput;
+
+// This can only be executed once because `background_color` is again used with Nextcloud 30,
+// so this part only works when updating -> Nextcloud 29 -> 30
+class Version2006Date20240905111627 implements IMigrationStep {
+
+ public function __construct(
+ private IJobList $jobList,
+ private IAppConfig $appConfig,
+ private IDBConnection $connection,
+ ) {
+ }
+
+ public function name(): string {
+ return 'Restore custom primary color';
+ }
+
+ public function description(): string {
+ return 'Restore custom primary color after separating primary color from background color';
+ }
+
+ public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
+ // nop
+ }
+
+ public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
+ $this->restoreSystemColors($output);
+
+ $userThemingEnabled = $this->appConfig->getValueBool('theming', 'disable-user-theming') === false;
+ if ($userThemingEnabled) {
+ $this->restoreUserColors($output);
+ }
+
+ return null;
+ }
+
+ public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
+ $output->info('Initialize restoring of background colors for custom background images');
+ // This is done in a background job as this can take a lot of time for large instances
+ $this->jobList->add(RestoreBackgroundImageColor::class, ['stage' => RestoreBackgroundImageColor::STAGE_PREPARE]);
+ }
+
+ private function restoreSystemColors(IOutput $output): void {
+ $defaultColor = $this->appConfig->getValueString(Application::APP_ID, 'color', '');
+ if ($defaultColor === '') {
+ $output->info('No custom system color configured - skipping');
+ } else {
+ // Restore legacy value into new field
+ $this->appConfig->setValueString(Application::APP_ID, 'background_color', $defaultColor);
+ $this->appConfig->setValueString(Application::APP_ID, 'primary_color', $defaultColor);
+ // Delete legacy field
+ $this->appConfig->deleteKey(Application::APP_ID, 'color');
+ // give some feedback
+ $output->info('Global primary color restored');
+ }
+ }
+
+ private function restoreUserColors(IOutput $output): void {
+ $output->info('Restoring user primary color');
+ // For performance let the DB handle this
+ $qb = $this->connection->getQueryBuilder();
+ // Rename the `background_color` config to `primary_color` as this was the behavior on Nextcloud 29 and older
+ // with Nextcloud 30 `background_color` is a new option to define the background color independent of the primary color.
+ $qb->update('preferences')
+ ->set('configkey', $qb->createNamedParameter('primary_color'))
+ ->where($qb->expr()->eq('appid', $qb->createNamedParameter(Application::APP_ID)))
+ ->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter('background_color')));
+
+ try {
+ $qb->executeStatement();
+ } catch (\Exception) {
+ $output->debug('Some users already configured the background color');
+ $this->restoreUserColorsFallback($output);
+ }
+
+ $output->info('Primary color of users restored');
+ }
+
+ /**
+ * Similar to restoreUserColors but also works if some users already setup a new value.
+ * This is only called if the first approach fails as this takes much longer on the DB.
+ */
+ private function restoreUserColorsFallback(IOutput $output): void {
+ $qb = $this->connection->getQueryBuilder();
+ $qb2 = $this->connection->getQueryBuilder();
+
+ $qb2->select('userid')
+ ->from('preferences')
+ ->where($qb->expr()->eq('appid', $qb->createNamedParameter(Application::APP_ID)))
+ ->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter('primary_color')));
+
+ // MySQL does not update on select of the same table, so this is a workaround:
+ if ($this->connection->getDatabaseProvider() === IDBConnection::PLATFORM_MYSQL) {
+ $subquery = 'SELECT * from ( ' . $qb2->getSQL() . ' ) preferences_alias';
+ } else {
+ $subquery = $qb2->getSQL();
+ }
+
+ $qb->update('preferences')
+ ->set('configkey', $qb->createNamedParameter('primary_color'))
+ ->where($qb->expr()->eq('appid', $qb->createNamedParameter(Application::APP_ID)))
+ ->andWhere(
+ $qb->expr()->eq('configkey', $qb->createNamedParameter('background_color')),
+ $qb->expr()->notIn('userid', $qb->createFunction($subquery)),
+ );
+
+ $qb->executeStatement();
+ }
+}
diff --git a/apps/theming/lib/ResponseDefinitions.php b/apps/theming/lib/ResponseDefinitions.php
index f1b0d09790a..948fc792133 100644
--- a/apps/theming/lib/ResponseDefinitions.php
+++ b/apps/theming/lib/ResponseDefinitions.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2023 Kate Döen <kate.doeen@nextcloud.com>
- *
- * @author Kate Döen <kate.doeen@nextcloud.com>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Theming;
@@ -30,6 +13,7 @@ namespace OCA\Theming;
* @psalm-type ThemingBackground = array{
* backgroundImage: ?string,
* backgroundColor: string,
+ * primaryColor: string,
* version: int,
* }
*/
diff --git a/apps/theming/lib/Service/BackgroundService.php b/apps/theming/lib/Service/BackgroundService.php
index 1b948c7300a..ee9466c3a36 100644
--- a/apps/theming/lib/Service/BackgroundService.php
+++ b/apps/theming/lib/Service/BackgroundService.php
@@ -3,34 +3,14 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net>
- *
- * @author Jan C. Borchardt <hey@jancborchardt.net>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Christopher Ng <chrng8@gmail.com>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Theming\Service;
use InvalidArgumentException;
use OC\User\NoUserException;
use OCA\Theming\AppInfo\Application;
-use OCA\Theming\ThemingDefaults;
use OCP\Files\File;
use OCP\Files\IAppData;
use OCP\Files\IRootFolder;
@@ -38,170 +18,202 @@ use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\Files\SimpleFS\ISimpleFile;
use OCP\Files\SimpleFS\ISimpleFolder;
+use OCP\IAppConfig;
use OCP\IConfig;
+use OCP\Image;
use OCP\Lock\LockedException;
use OCP\PreConditionNotMetException;
+use RuntimeException;
class BackgroundService {
- // true when the background is bright and need dark icons
- public const THEMING_MODE_DARK = 'dark';
- public const DEFAULT_COLOR = '#0082c9';
- public const DEFAULT_ACCESSIBLE_COLOR = '#00679e';
+ public const DEFAULT_COLOR = '#00679e';
+ public const DEFAULT_BACKGROUND_COLOR = '#00679e';
+ /**
+ * One of our shipped background images is used
+ */
public const BACKGROUND_SHIPPED = 'shipped';
+ /**
+ * A custom background image is used
+ */
public const BACKGROUND_CUSTOM = 'custom';
+ /**
+ * The default background image is used
+ */
public const BACKGROUND_DEFAULT = 'default';
- public const BACKGROUND_DISABLED = 'disabled';
+ /**
+ * Just a background color is used
+ */
+ public const BACKGROUND_COLOR = 'color';
+
+ public const DEFAULT_BACKGROUND_IMAGE = 'jenna-kim-the-globe.webp';
- public const DEFAULT_BACKGROUND_IMAGE = 'kamil-porembinski-clouds.jpg';
+ /**
+ * 'attribution': Name, artist and license
+ * 'description': Alternative text
+ * 'attribution_url': URL for attribution
+ * 'background_color': Cached mean color of the top part to calculate app menu colors and use as fallback
+ * 'primary_color': Recommended primary color for this theme / image
+ */
public const SHIPPED_BACKGROUNDS = [
+ 'jenna-kim-the-globe.webp' => [
+ 'attribution' => 'Globe (Jenna Kim - Nextcloud GmbH, CC-BY-SA-4.0)',
+ 'description' => 'Background picture of white clouds on in front of a blue sky',
+ 'attribution_url' => 'https://nextcloud.com/trademarks/',
+ 'dark_variant' => 'jenna-kim-the-globe-dark.webp',
+ 'background_color' => self::DEFAULT_BACKGROUND_COLOR,
+ 'primary_color' => self::DEFAULT_COLOR,
+ ],
+ 'kamil-porembinski-clouds.jpg' => [
+ 'attribution' => 'Clouds (Kamil Porembiński, CC BY-SA)',
+ 'description' => 'Background picture of white clouds on in front of a blue sky',
+ 'attribution_url' => 'https://www.flickr.com/photos/paszczak000/8715851521/',
+ 'background_color' => self::DEFAULT_BACKGROUND_COLOR,
+ 'primary_color' => self::DEFAULT_COLOR,
+ ],
'hannah-maclean-soft-floral.jpg' => [
'attribution' => 'Soft floral (Hannah MacLean, CC0)',
'description' => 'Abstract background picture in yellow and white color whith a flower on it',
'attribution_url' => 'https://stocksnap.io/photo/soft-floral-XOYWCCW5PA',
- 'theming' => self::THEMING_MODE_DARK,
- 'primary_color' => '#D8A06C',
+ 'background_color' => '#e4d2c1',
+ 'primary_color' => '#9f652f',
],
'ted-moravec-morning-fog.jpg' => [
'attribution' => 'Morning fog (Ted Moravec, Public Domain)',
'description' => 'Background picture of a forest shrouded in fog',
'attribution_url' => 'https://flickr.com/photos/tmoravec/52392410261',
- 'theming' => self::THEMING_MODE_DARK,
- 'primary_color' => '#38A084',
+ 'background_color' => '#f6f7f6',
+ 'primary_color' => '#114c3b',
],
'stefanus-martanto-setyo-husodo-underwater-ocean.jpg' => [
'attribution' => 'Underwater ocean (Stefanus Martanto Setyo Husodo, CC0)',
'description' => 'Background picture of an underwater ocean',
'attribution_url' => 'https://stocksnap.io/photo/underwater-ocean-TJA9LBH4WS',
+ 'background_color' => '#003351',
'primary_color' => '#04577e',
],
'zoltan-voros-rhythm-and-blues.jpg' => [
'attribution' => 'Rhythm and blues (Zoltán Vörös, CC BY)',
'description' => 'Abstract background picture of sand dunes during night',
'attribution_url' => 'https://flickr.com/photos/v923z/51634409289/',
+ 'background_color' => '#1c2437',
'primary_color' => '#1c243c',
],
'anatoly-mikhaltsov-butterfly-wing-scale.jpg' => [
'attribution' => 'Butterfly wing scale (Anatoly Mikhaltsov, CC BY-SA)',
'description' => 'Background picture of a red-ish butterfly wing under microscope',
'attribution_url' => 'https://commons.wikimedia.org/wiki/File:%D0%A7%D0%B5%D1%88%D1%83%D0%B9%D0%BA%D0%B8_%D0%BA%D1%80%D1%8B%D0%BB%D0%B0_%D0%B1%D0%B0%D0%B1%D0%BE%D1%87%D0%BA%D0%B8.jpg',
+ 'background_color' => '#652e11',
'primary_color' => '#a53c17',
],
'bernie-cetonia-aurata-take-off-composition.jpg' => [
'attribution' => 'Cetonia aurata take off composition (Bernie, Public Domain)',
'description' => 'Montage of a cetonia aurata bug that takes off with white background',
'attribution_url' => 'https://commons.wikimedia.org/wiki/File:Cetonia_aurata_take_off_composition_05172009.jpg',
- 'theming' => self::THEMING_MODE_DARK,
- 'primary_color' => '#869171',
+ 'background_color' => '#dee0d3',
+ 'primary_color' => '#56633d',
],
'dejan-krsmanovic-ribbed-red-metal.jpg' => [
'attribution' => 'Ribbed red metal (Dejan Krsmanovic, CC BY)',
'description' => 'Abstract background picture of red ribbed metal with two horizontal white elements on top of it',
'attribution_url' => 'https://www.flickr.com/photos/dejankrsmanovic/42971456774/',
+ 'background_color' => '#9b171c',
'primary_color' => '#9c4236',
],
'eduardo-neves-pedra-azul.jpg' => [
'attribution' => 'Pedra azul milky way (Eduardo Neves, CC BY-SA)',
'description' => 'Background picture of the milky way during night with a mountain in front of it',
'attribution_url' => 'https://commons.wikimedia.org/wiki/File:Pedra_Azul_Milky_Way.jpg',
+ 'background_color' => '#1d242d',
'primary_color' => '#4f6071',
],
'european-space-agency-barents-bloom.jpg' => [
'attribution' => 'Barents bloom (European Space Agency, CC BY-SA)',
'description' => 'Abstract background picture of blooming barents in blue and green colors',
'attribution_url' => 'https://www.esa.int/ESA_Multimedia/Images/2016/08/Barents_bloom',
+ 'background_color' => '#1c383d',
'primary_color' => '#396475',
],
'hannes-fritz-flippity-floppity.jpg' => [
'attribution' => 'Flippity floppity (Hannes Fritz, CC BY-SA)',
'description' => 'Abstract background picture of many pairs of flip flops hanging on a wall in multiple colors',
'attribution_url' => 'http://hannes.photos/flippity-floppity',
+ 'background_color' => '#5b2d53',
'primary_color' => '#98415a',
],
'hannes-fritz-roulette.jpg' => [
'attribution' => 'Roulette (Hannes Fritz, CC BY-SA)',
'description' => 'Background picture of a rotating giant wheel during night',
'attribution_url' => 'http://hannes.photos/roulette',
+ 'background_color' => '#000000',
'primary_color' => '#845334',
],
'hannes-fritz-sea-spray.jpg' => [
'attribution' => 'Sea spray (Hannes Fritz, CC BY-SA)',
'description' => 'Background picture of a stone coast with fog and sea behind it',
'attribution_url' => 'http://hannes.photos/sea-spray',
+ 'background_color' => '#333f47',
'primary_color' => '#4f6071',
],
- 'kamil-porembinski-clouds.jpg' => [
- 'attribution' => 'Clouds (Kamil Porembiński, CC BY-SA)',
- 'description' => 'Background picture of white clouds on in front of a blue sky',
- 'attribution_url' => 'https://www.flickr.com/photos/paszczak000/8715851521/',
- 'primary_color' => self::DEFAULT_COLOR,
- ],
'bernard-spragg-new-zealand-fern.jpg' => [
'attribution' => 'New zealand fern (Bernard Spragg, CC0)',
'description' => 'Abstract background picture of fern leafes',
'attribution_url' => 'https://commons.wikimedia.org/wiki/File:NZ_Fern.(Blechnum_chambersii)_(11263534936).jpg',
+ 'background_color' => '#0c3c03',
'primary_color' => '#316b26',
],
'rawpixel-pink-tapioca-bubbles.jpg' => [
'attribution' => 'Pink tapioca bubbles (Rawpixel, CC BY)',
'description' => 'Abstract background picture of pink tapioca bubbles',
'attribution_url' => 'https://www.flickr.com/photos/byrawpixel/27665140298/in/photostream/',
- 'theming' => self::THEMING_MODE_DARK,
- 'primary_color' => '#b17ab4',
+ 'background_color' => '#c56e95',
+ 'primary_color' => '#7b4e7e',
],
'nasa-waxing-crescent-moon.jpg' => [
'attribution' => 'Waxing crescent moon (NASA, Public Domain)',
'description' => 'Background picture of glowing earth in foreground and moon in the background',
'attribution_url' => 'https://www.nasa.gov/image-feature/a-waxing-crescent-moon',
+ 'background_color' => '#000002',
'primary_color' => '#005ac1',
],
'tommy-chau-already.jpg' => [
'attribution' => 'Cityscape (Tommy Chau, CC BY)',
'description' => 'Background picture of a skyscraper city during night',
'attribution_url' => 'https://www.flickr.com/photos/90975693@N05/16910999368',
+ 'background_color' => '#35229f',
'primary_color' => '#6a2af4',
],
'tommy-chau-lion-rock-hill.jpg' => [
'attribution' => 'Lion rock hill (Tommy Chau, CC BY)',
'description' => 'Background picture of mountains during sunset or sunrise',
'attribution_url' => 'https://www.flickr.com/photos/90975693@N05/17136440246',
- 'theming' => self::THEMING_MODE_DARK,
- 'primary_color' => '#c074a9',
+ 'background_color' => '#cb92b7',
+ 'primary_color' => '#7f4f70',
],
'lali-masriera-yellow-bricks.jpg' => [
'attribution' => 'Yellow bricks (Lali Masriera, CC BY)',
'description' => 'Background picture of yellow bricks with some yellow tubes',
'attribution_url' => 'https://www.flickr.com/photos/visualpanic/3982464447',
- 'theming' => self::THEMING_MODE_DARK,
- 'primary_color' => '#bc8210',
+ 'background_color' => '#c78a19',
+ 'primary_color' => '#7f5700',
],
];
- private IRootFolder $rootFolder;
- private IAppData $appData;
- private IConfig $config;
- private string $userId;
- private ThemingDefaults $themingDefaults;
-
- public function __construct(IRootFolder $rootFolder,
- IAppData $appData,
- IConfig $config,
- ?string $userId,
- ThemingDefaults $themingDefaults) {
- if ($userId === null) {
- return;
- }
-
- $this->rootFolder = $rootFolder;
- $this->config = $config;
- $this->userId = $userId;
- $this->appData = $appData;
- $this->themingDefaults = $themingDefaults;
+ public function __construct(
+ private IRootFolder $rootFolder,
+ private IAppData $appData,
+ private IAppConfig $appConfig,
+ private IConfig $config,
+ private ?string $userId,
+ ) {
}
- public function setDefaultBackground(): void {
- $this->config->deleteUserValue($this->userId, Application::APP_ID, 'background_image');
- $this->config->deleteUserValue($this->userId, Application::APP_ID, 'background_color');
+ public function setDefaultBackground(?string $userId = null): void {
+ $userId = $userId ?? $this->getUserId();
+
+ $this->config->deleteUserValue($userId, Application::APP_ID, 'background_image');
+ $this->config->deleteUserValue($userId, Application::APP_ID, 'background_color');
+ $this->config->deleteUserValue($userId, Application::APP_ID, 'primary_color');
}
/**
@@ -212,46 +224,81 @@ class BackgroundService {
* @throws PreConditionNotMetException
* @throws NoUserException
*/
- public function setFileBackground($path): void {
- $this->config->setUserValue($this->userId, Application::APP_ID, 'background_image', self::BACKGROUND_CUSTOM);
- $userFolder = $this->rootFolder->getUserFolder($this->userId);
+ public function setFileBackground(string $path, ?string $userId = null): void {
+ $userId = $userId ?? $this->getUserId();
+ $userFolder = $this->rootFolder->getUserFolder($userId);
/** @var File $file */
$file = $userFolder->get($path);
- $image = new \OCP\Image();
+ $handle = $file->fopen('r');
+ if ($handle === false) {
+ throw new InvalidArgumentException('Invalid image file');
+ }
+ $this->getAppDataFolder()->newFile('background.jpg', $handle);
- if ($image->loadFromFileHandle($file->fopen('r')) === false) {
+ $this->recalculateMeanColor();
+ }
+
+ public function recalculateMeanColor(?string $userId = null): void {
+ $userId = $userId ?? $this->getUserId();
+
+ $image = new Image();
+ $handle = $this->getAppDataFolder($userId)->getFile('background.jpg')->read();
+ if ($handle === false || $image->loadFromFileHandle($handle) === false) {
throw new InvalidArgumentException('Invalid image file');
}
- $this->getAppDataFolder()->newFile('background.jpg', $file->fopen('r'));
+ $meanColor = $this->calculateMeanColor($image);
+ if ($meanColor !== false) {
+ $this->setColorBackground($meanColor);
+ }
+ $this->config->setUserValue($userId, Application::APP_ID, 'background_image', self::BACKGROUND_CUSTOM);
}
- public function setShippedBackground($fileName): void {
- if (!array_key_exists($fileName, self::SHIPPED_BACKGROUNDS)) {
+ /**
+ * Set background of user to a shipped background identified by the filename
+ * @param string $filename The shipped background filename
+ * @param null|string $userId The user to set - defaults to currently logged in user
+ * @throws RuntimeException If neither $userId is specified nor a user is logged in
+ * @throws InvalidArgumentException If the specified filename does not match any shipped background
+ */
+ public function setShippedBackground(string $filename, ?string $userId = null): void {
+ $userId = $userId ?? $this->getUserId();
+
+ if (!array_key_exists($filename, self::SHIPPED_BACKGROUNDS)) {
throw new InvalidArgumentException('The given file name is invalid');
}
- $this->config->setUserValue($this->userId, Application::APP_ID, 'background_image', $fileName);
- $this->setColorBackground(self::SHIPPED_BACKGROUNDS[$fileName]['primary_color']);
+ $this->setColorBackground(self::SHIPPED_BACKGROUNDS[$filename]['background_color'], $userId);
+ $this->config->setUserValue($userId, Application::APP_ID, 'background_image', $filename);
+ $this->config->setUserValue($userId, Application::APP_ID, 'primary_color', self::SHIPPED_BACKGROUNDS[$filename]['primary_color']);
}
- public function setColorBackground(string $color): void {
+ /**
+ * Set the background to color only
+ * @param string|null $userId The user to set the color - default to current logged-in user
+ */
+ public function setColorBackground(string $color, ?string $userId = null): void {
+ $userId = $userId ?? $this->getUserId();
+
if (!preg_match('/^#([0-9a-f]{3}|[0-9a-f]{6})$/i', $color)) {
throw new InvalidArgumentException('The given color is invalid');
}
- $this->config->setUserValue($this->userId, Application::APP_ID, 'background_color', $color);
+ $this->config->setUserValue($userId, Application::APP_ID, 'background_color', $color);
+ $this->config->setUserValue($userId, Application::APP_ID, 'background_image', self::BACKGROUND_COLOR);
}
- public function deleteBackgroundImage(): void {
- $this->config->setUserValue($this->userId, Application::APP_ID, 'background_image', self::BACKGROUND_DISABLED);
+ public function deleteBackgroundImage(?string $userId = null): void {
+ $userId = $userId ?? $this->getUserId();
+ $this->config->setUserValue($userId, Application::APP_ID, 'background_image', self::BACKGROUND_COLOR);
}
- public function getBackground(): ?ISimpleFile {
- $background = $this->config->getUserValue($this->userId, Application::APP_ID, 'background_image', self::BACKGROUND_DEFAULT);
+ public function getBackground(?string $userId = null): ?ISimpleFile {
+ $userId = $userId ?? $this->getUserId();
+ $background = $this->config->getUserValue($userId, Application::APP_ID, 'background_image', self::BACKGROUND_DEFAULT);
if ($background === self::BACKGROUND_CUSTOM) {
try {
return $this->getAppDataFolder()->getFile('background.jpg');
- } catch (NotFoundException | NotPermittedException $e) {
+ } catch (NotFoundException|NotPermittedException $e) {
return null;
}
}
@@ -259,21 +306,109 @@ class BackgroundService {
}
/**
+ * Called when a new global background (backgroundMime) is uploaded (admin setting)
+ * This sets all necessary app config values
+ * @param resource|string $path
+ * @return string|null The fallback background color - if any
+ */
+ public function setGlobalBackground($path): ?string {
+ $image = new Image();
+ $handle = is_resource($path) ? $path : fopen($path, 'rb');
+
+ if ($handle && $image->loadFromFileHandle($handle) !== false) {
+ $meanColor = $this->calculateMeanColor($image);
+ if ($meanColor !== false) {
+ $this->appConfig->setValueString(Application::APP_ID, 'background_color', $meanColor);
+ return $meanColor;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Calculate mean color of an given image
+ * It only takes the upper part into account so that a matching text color can be derived for the app menu
+ */
+ private function calculateMeanColor(Image $image): false|string {
+ /**
+ * Small helper to ensure one channel is returned as 8byte hex
+ */
+ function toHex(int $channel): string {
+ $hex = dechex($channel);
+ return match (strlen($hex)) {
+ 0 => '00',
+ 1 => '0' . $hex,
+ 2 => $hex,
+ default => 'ff',
+ };
+ }
+
+ $tempImage = new Image();
+
+ // Crop to only analyze top bar
+ $resource = $image->cropNew(0, 0, $image->width(), min(max(50, (int)($image->height() * 0.125)), $image->height()));
+ if ($resource === false) {
+ return false;
+ }
+
+ $tempImage->setResource($resource);
+ if (!$tempImage->preciseResize(100, 7)) {
+ return false;
+ }
+
+ $resource = $tempImage->resource();
+ if ($resource === false) {
+ return false;
+ }
+
+ $reds = [];
+ $greens = [];
+ $blues = [];
+ for ($y = 0; $y < 7; $y++) {
+ for ($x = 0; $x < 100; $x++) {
+ $value = imagecolorat($resource, $x, $y);
+ if ($value === false) {
+ continue;
+ }
+ $reds[] = ($value >> 16) & 0xFF;
+ $greens[] = ($value >> 8) & 0xFF;
+ $blues[] = $value & 0xFF;
+ }
+ }
+ $meanColor = '#' . toHex((int)(array_sum($reds) / count($reds)));
+ $meanColor .= toHex((int)(array_sum($greens) / count($greens)));
+ $meanColor .= toHex((int)(array_sum($blues) / count($blues)));
+ return $meanColor;
+ }
+
+ /**
* Storing the data in appdata/theming/users/USERID
*
- * @return ISimpleFolder
+ * @param string|null $userId The user to get the folder - default to current user
* @throws NotPermittedException
*/
- private function getAppDataFolder(): ISimpleFolder {
+ private function getAppDataFolder(?string $userId = null): ISimpleFolder {
+ $userId = $userId ?? $this->getUserId();
+
try {
$rootFolder = $this->appData->getFolder('users');
- } catch (NotFoundException $e) {
+ } catch (NotFoundException) {
$rootFolder = $this->appData->newFolder('users');
}
try {
- return $rootFolder->getFolder($this->userId);
- } catch (NotFoundException $e) {
- return $rootFolder->newFolder($this->userId);
+ return $rootFolder->getFolder($userId);
+ } catch (NotFoundException) {
+ return $rootFolder->newFolder($userId);
+ }
+ }
+
+ /**
+ * @throws RuntimeException Thrown if a method that needs a user is called without any logged-in user
+ */
+ private function getUserId(): string {
+ if ($this->userId === null) {
+ throw new RuntimeException('No currently logged-in user');
}
+ return $this->userId;
}
}
diff --git a/apps/theming/lib/Service/JSDataService.php b/apps/theming/lib/Service/JSDataService.php
index 5dc7b3fd632..81198f8b3f5 100644
--- a/apps/theming/lib/Service/JSDataService.php
+++ b/apps/theming/lib/Service/JSDataService.php
@@ -3,63 +3,47 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Julien Veyssier <eneiluj@posteo.net>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Theming\Service;
use OCA\Theming\ThemingDefaults;
use OCA\Theming\Util;
-use OCP\IConfig;
class JSDataService implements \JsonSerializable {
- private ThemingDefaults $themingDefaults;
- private Util $util;
- private IConfig $appConfig;
- private ThemesService $themesService;
public function __construct(
- ThemingDefaults $themingDefaults,
- Util $util,
- IConfig $appConfig,
- ThemesService $themesService
+ private ThemingDefaults $themingDefaults,
+ private Util $util,
+ private ThemesService $themesService,
) {
$this->themingDefaults = $themingDefaults;
$this->util = $util;
- $this->appConfig = $appConfig;
$this->themesService = $themesService;
}
public function jsonSerialize(): array {
return [
'name' => $this->themingDefaults->getName(),
- 'url' => $this->themingDefaults->getBaseUrl(),
'slogan' => $this->themingDefaults->getSlogan(),
- 'color' => $this->themingDefaults->getColorPrimary(),
- 'defaultColor' => $this->themingDefaults->getDefaultColorPrimary(),
+
+ 'url' => $this->themingDefaults->getBaseUrl(),
'imprintUrl' => $this->themingDefaults->getImprintUrl(),
'privacyUrl' => $this->themingDefaults->getPrivacyUrl(),
+
+ 'primaryColor' => $this->themingDefaults->getColorPrimary(),
+ 'backgroundColor' => $this->themingDefaults->getColorBackground(),
+ 'defaultPrimaryColor' => $this->themingDefaults->getDefaultColorPrimary(),
+ 'defaultBackgroundColor' => $this->themingDefaults->getDefaultColorBackground(),
'inverted' => $this->util->invertTextColor($this->themingDefaults->getColorPrimary()),
+
'cacheBuster' => $this->util->getCacheBuster(),
'enabledThemes' => $this->themesService->getEnabledThemes(),
+
+ // deprecated use primaryColor
+ 'color' => $this->themingDefaults->getColorPrimary(),
+ '' => 'color is deprecated since Nextcloud 29, use primaryColor instead'
];
}
}
diff --git a/apps/theming/lib/Service/ThemeInjectionService.php b/apps/theming/lib/Service/ThemeInjectionService.php
index c6ef6b9614d..873d388081c 100644
--- a/apps/theming/lib/Service/ThemeInjectionService.php
+++ b/apps/theming/lib/Service/ThemeInjectionService.php
@@ -1,24 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2022 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Theming\Service;
@@ -31,25 +15,16 @@ use OCP\IUserSession;
class ThemeInjectionService {
- private IURLGenerator $urlGenerator;
- private ThemesService $themesService;
- private DefaultTheme $defaultTheme;
- private Util $util;
- private IConfig $config;
private ?string $userId;
- public function __construct(IURLGenerator $urlGenerator,
- ThemesService $themesService,
- DefaultTheme $defaultTheme,
- Util $util,
- IConfig $config,
- IUserSession $userSession) {
- $this->urlGenerator = $urlGenerator;
- $this->themesService = $themesService;
- $this->defaultTheme = $defaultTheme;
- $this->util = $util;
- $this->config = $config;
-
+ public function __construct(
+ private IURLGenerator $urlGenerator,
+ private ThemesService $themesService,
+ private DefaultTheme $defaultTheme,
+ private Util $util,
+ private IConfig $config,
+ IUserSession $userSession,
+ ) {
if ($userSession->getUser() !== null) {
$this->userId = $userSession->getUser()->getUID();
} else {
@@ -69,12 +44,12 @@ class ThemeInjectionService {
$this->addThemeHeaders($defaultTheme);
// Themes applied by media queries
- foreach($mediaThemes as $theme) {
+ foreach ($mediaThemes as $theme) {
$this->addThemeHeaders($theme, true, $theme->getMediaQuery());
}
// Themes
- foreach($this->themesService->getThemes() as $theme) {
+ foreach ($this->themesService->getThemes() as $theme) {
// Ignore default theme as already processed first
if ($theme->getId() === $this->defaultTheme->getId()) {
continue;
@@ -116,9 +91,9 @@ class ThemeInjectionService {
$metaHeaders = [];
// Meta headers
- foreach($this->themesService->getThemes() as $theme) {
+ foreach ($this->themesService->getThemes() as $theme) {
if (!empty($theme->getMeta())) {
- foreach($theme->getMeta() as $meta) {
+ foreach ($theme->getMeta() as $meta) {
if (!isset($meta['name']) || !isset($meta['content'])) {
continue;
}
@@ -131,7 +106,7 @@ class ThemeInjectionService {
}
}
- foreach($metaHeaders as $name => $content) {
+ foreach ($metaHeaders as $name => $content) {
\OCP\Util::addHeader('meta', [
'name' => $name,
'content' => join(' ', array_unique($content)),
diff --git a/apps/theming/lib/Service/ThemesService.php b/apps/theming/lib/Service/ThemesService.php
index 0d252e96431..f49524cb62c 100644
--- a/apps/theming/lib/Service/ThemesService.php
+++ b/apps/theming/lib/Service/ThemesService.php
@@ -1,24 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2022 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Theming\Service;
@@ -33,24 +17,23 @@ use OCA\Theming\Themes\LightTheme;
use OCP\IConfig;
use OCP\IUser;
use OCP\IUserSession;
+use Psr\Log\LoggerInterface;
class ThemesService {
- private IUserSession $userSession;
- private IConfig $config;
-
/** @var ITheme[] */
private array $themesProviders;
- public function __construct(IUserSession $userSession,
- IConfig $config,
- DefaultTheme $defaultTheme,
+ public function __construct(
+ private IUserSession $userSession,
+ private IConfig $config,
+ private LoggerInterface $logger,
+ private DefaultTheme $defaultTheme,
LightTheme $lightTheme,
- DarkTheme $darkTheme,
+ private DarkTheme $darkTheme,
HighContrastTheme $highContrastTheme,
DarkHighContrastTheme $darkHighContrastTheme,
- DyslexiaFont $dyslexiaFont) {
- $this->userSession = $userSession;
- $this->config = $config;
+ DyslexiaFont $dyslexiaFont,
+ ) {
// Register themes
$this->themesProviders = [
@@ -69,6 +52,28 @@ class ThemesService {
* @return ITheme[]
*/
public function getThemes(): array {
+ // Enforced theme if configured
+ $enforcedTheme = $this->config->getSystemValueString('enforce_theme', '');
+ if ($enforcedTheme !== '') {
+ if (!isset($this->themesProviders[$enforcedTheme])) {
+ $this->logger->error('Enforced theme not found', ['theme' => $enforcedTheme]);
+ return $this->themesProviders;
+ }
+
+ $defaultTheme = $this->themesProviders[$this->defaultTheme->getId()];
+ $darkTheme = $this->themesProviders[$this->darkTheme->getId()];
+ $theme = $this->themesProviders[$enforcedTheme];
+ return [
+ // Leave the default theme as a fallback
+ $defaultTheme->getId() => $defaultTheme,
+ // Make sure we also have the dark theme to allow apps
+ // to scope sections of their UI to the dark theme
+ $darkTheme->getId() => $darkTheme,
+ // Finally, the enforced theme
+ $theme->getId() => $theme,
+ ];
+ }
+
return $this->themesProviders;
}
@@ -123,7 +128,7 @@ class ThemesService {
$this->setEnabledThemes($enabledThemes);
return $enabledThemes;
}
-
+
return $themesIds;
}
@@ -144,19 +149,21 @@ class ThemesService {
}
/**
- * Get the list of all enabled themes IDs
- * for the logged-in user
+ * Get the list of all enabled themes IDs for the current user.
*
* @return string[]
*/
public function getEnabledThemes(): array {
+ $enforcedTheme = $this->config->getSystemValueString('enforce_theme', '');
$user = $this->userSession->getUser();
if ($user === null) {
+ if ($enforcedTheme !== '') {
+ return [$enforcedTheme];
+ }
return [];
}
- $enforcedTheme = $this->config->getSystemValueString('enforce_theme', '');
- $enabledThemes = json_decode($this->config->getUserValue($user->getUID(), Application::APP_ID, 'enabled-themes', '[]'));
+ $enabledThemes = json_decode($this->config->getUserValue($user->getUID(), Application::APP_ID, 'enabled-themes', '["default"]'));
if ($enforcedTheme !== '') {
return array_merge([$enforcedTheme], $enabledThemes);
}
diff --git a/apps/theming/lib/Settings/Admin.php b/apps/theming/lib/Settings/Admin.php
index dc5d9a2a6a4..9fa0f2bb0e7 100644
--- a/apps/theming/lib/Settings/Admin.php
+++ b/apps/theming/lib/Settings/Admin.php
@@ -1,40 +1,21 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Arthur Schiwon <blizzz@arthur-schiwon.de>
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Bjoern Schiessle <bjoern@schiessle.org>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Theming\Settings;
use OCA\Theming\AppInfo\Application;
use OCA\Theming\Controller\ThemingController;
use OCA\Theming\ImageManager;
+use OCA\Theming\Service\BackgroundService;
use OCA\Theming\ThemingDefaults;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\IConfig;
use OCP\IL10N;
+use OCP\INavigationManager;
use OCP\IURLGenerator;
use OCP\Settings\IDelegatedSettings;
use OCP\Util;
@@ -49,6 +30,7 @@ class Admin implements IDelegatedSettings {
private IInitialState $initialState,
private IURLGenerator $urlGenerator,
private ImageManager $imageManager,
+ private INavigationManager $navigationManager,
) {
}
@@ -75,9 +57,13 @@ class Admin implements IDelegatedSettings {
'name' => $this->themingDefaults->getEntity(),
'url' => $this->themingDefaults->getBaseUrl(),
'slogan' => $this->themingDefaults->getSlogan(),
- 'color' => $this->themingDefaults->getDefaultColorPrimary(),
+ 'primaryColor' => $this->themingDefaults->getDefaultColorPrimary(),
+ 'backgroundColor' => $this->themingDefaults->getDefaultColorBackground(),
'logoMime' => $this->config->getAppValue(Application::APP_ID, 'logoMime', ''),
'allowedMimeTypes' => $allowedMimeTypes,
+ 'backgroundURL' => $this->imageManager->getImageUrl('background'),
+ 'defaultBackgroundURL' => $this->urlGenerator->linkTo(Application::APP_ID, 'img/background/' . BackgroundService::DEFAULT_BACKGROUND_IMAGE),
+ 'defaultBackgroundColor' => BackgroundService::DEFAULT_BACKGROUND_COLOR,
'backgroundMime' => $this->config->getAppValue(Application::APP_ID, 'backgroundMime', ''),
'logoheaderMime' => $this->config->getAppValue(Application::APP_ID, 'logoheaderMime', ''),
'faviconMime' => $this->config->getAppValue(Application::APP_ID, 'faviconMime', ''),
@@ -87,7 +73,7 @@ class Admin implements IDelegatedSettings {
'docUrlIcons' => $this->urlGenerator->linkToDocs('admin-theming-icons'),
'canThemeIcons' => $this->imageManager->shouldReplaceIcons(),
'userThemingDisabled' => $this->themingDefaults->isUserThemingDisabled(),
- 'defaultApps' => array_filter(explode(',', $this->config->getSystemValueString('defaultapp', ''))),
+ 'defaultApps' => $this->navigationManager->getDefaultEntryIds(),
]);
Util::addScript($this->appName, 'admin-theming');
@@ -104,8 +90,8 @@ class Admin implements IDelegatedSettings {
/**
* @return int whether the form should be rather on the top or bottom of
- * the admin section. The forms are arranged in ascending order of the
- * priority values. It is required to return a value between 0 and 100.
+ * the admin section. The forms are arranged in ascending order of the
+ * priority values. It is required to return a value between 0 and 100.
*
* E.g.: 70
*/
diff --git a/apps/theming/lib/Settings/AdminSection.php b/apps/theming/lib/Settings/AdminSection.php
index 2fcc81a9279..a1ea568d9f2 100644
--- a/apps/theming/lib/Settings/AdminSection.php
+++ b/apps/theming/lib/Settings/AdminSection.php
@@ -1,24 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Arthur Schiwon <blizzz@arthur-schiwon.de>
- *
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Theming\Settings;
@@ -27,14 +11,11 @@ use OCP\IURLGenerator;
use OCP\Settings\IIconSection;
class AdminSection implements IIconSection {
- private string $appName;
- private IL10N $l;
- private IURLGenerator $url;
-
- public function __construct(string $appName, IURLGenerator $url, IL10N $l) {
- $this->appName = $appName;
- $this->url = $url;
- $this->l = $l;
+ public function __construct(
+ private string $appName,
+ private IURLGenerator $url,
+ private IL10N $l,
+ ) {
}
/**
@@ -59,8 +40,8 @@ class AdminSection implements IIconSection {
/**
* @return int whether the form should be rather on the top or bottom of
- * the settings navigation. The sections are arranged in ascending order of
- * the priority values. It is required to return a value between 0 and 99.
+ * the settings navigation. The sections are arranged in ascending order of
+ * the priority values. It is required to return a value between 0 and 99.
*
* E.g.: 70
*/
diff --git a/apps/theming/lib/Settings/Personal.php b/apps/theming/lib/Settings/Personal.php
index f24aaa2f8f8..f14deeb35f0 100644
--- a/apps/theming/lib/Settings/Personal.php
+++ b/apps/theming/lib/Settings/Personal.php
@@ -1,37 +1,19 @@
<?php
+
/**
- * @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
- * @copyright Copyright (c) 2019 Janis Köhr <janiskoehr@icloud.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Theming\Settings;
use OCA\Theming\ITheme;
+use OCA\Theming\Service\BackgroundService;
use OCA\Theming\Service\ThemesService;
use OCA\Theming\ThemingDefaults;
-use OCP\App\IAppManager;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\IConfig;
+use OCP\INavigationManager;
use OCP\Settings\ISettings;
use OCP\Util;
@@ -44,7 +26,7 @@ class Personal implements ISettings {
private ThemesService $themesService,
private IInitialState $initialStateService,
private ThemingDefaults $themingDefaults,
- private IAppManager $appManager,
+ private INavigationManager $navigationManager,
) {
}
@@ -68,15 +50,36 @@ class Personal implements ISettings {
});
}
- // Get the default app enforced by admin
- $forcedDefaultApp = $this->appManager->getDefaultAppForUser(null, false);
+ // Get the default entry enforced by admin
+ $forcedDefaultEntry = $this->navigationManager->getDefaultEntryIdForUser(null, false);
+
+ /** List of all shipped backgrounds */
+ $this->initialStateService->provideInitialState('shippedBackgrounds', BackgroundService::SHIPPED_BACKGROUNDS);
+
+ /**
+ * Admin theming
+ */
+ $this->initialStateService->provideInitialState('themingDefaults', [
+ /** URL of admin configured background image */
+ 'backgroundImage' => $this->themingDefaults->getBackground(),
+ /** `backgroundColor` if disabled, mime type if defined and empty by default */
+ 'backgroundMime' => $this->config->getAppValue('theming', 'backgroundMime', ''),
+ /** Admin configured background color */
+ 'backgroundColor' => $this->themingDefaults->getDefaultColorBackground(),
+ /** Admin configured primary color */
+ 'primaryColor' => $this->themingDefaults->getDefaultColorPrimary(),
+ /** Nextcloud default background image */
+ 'defaultShippedBackground' => BackgroundService::DEFAULT_BACKGROUND_IMAGE,
+ ]);
+ $this->initialStateService->provideInitialState('userBackgroundImage', $this->config->getUserValue($this->userId, 'theming', 'background_image', BackgroundService::BACKGROUND_DEFAULT));
$this->initialStateService->provideInitialState('themes', array_values($themes));
$this->initialStateService->provideInitialState('enforceTheme', $enforcedTheme);
$this->initialStateService->provideInitialState('isUserThemingDisabled', $this->themingDefaults->isUserThemingDisabled());
+ $this->initialStateService->provideInitialState('enableBlurFilter', $this->config->getUserValue($this->userId, 'theming', 'force_enable_blur_filter', ''));
$this->initialStateService->provideInitialState('navigationBar', [
'userAppOrder' => json_decode($this->config->getUserValue($this->userId, 'core', 'apporder', '[]'), true, flags:JSON_THROW_ON_ERROR),
- 'enforcedDefaultApp' => $forcedDefaultApp
+ 'enforcedDefaultApp' => $forcedDefaultEntry
]);
Util::addScript($this->appName, 'personal-theming');
@@ -94,8 +97,8 @@ class Personal implements ISettings {
/**
* @return int whether the form should be rather on the top or bottom of
- * the admin section. The forms are arranged in ascending order of the
- * priority values. It is required to return a value between 0 and 100.
+ * the admin section. The forms are arranged in ascending order of the
+ * priority values. It is required to return a value between 0 and 100.
*
* E.g.: 70
* @since 9.1
diff --git a/apps/theming/lib/Settings/PersonalSection.php b/apps/theming/lib/Settings/PersonalSection.php
index bfaa8bcaa32..0a9361d5533 100644
--- a/apps/theming/lib/Settings/PersonalSection.php
+++ b/apps/theming/lib/Settings/PersonalSection.php
@@ -1,25 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Theming\Settings;
@@ -29,15 +12,6 @@ use OCP\Settings\IIconSection;
class PersonalSection implements IIconSection {
- /** @var string */
- protected $appName;
-
- /** @var IURLGenerator */
- private $urlGenerator;
-
- /** @var IL10N */
- private $l;
-
/**
* Personal Section constructor.
*
@@ -45,12 +19,11 @@ class PersonalSection implements IIconSection {
* @param IURLGenerator $urlGenerator
* @param IL10N $l
*/
- public function __construct(string $appName,
- IURLGenerator $urlGenerator,
- IL10N $l) {
- $this->appName = $appName;
- $this->urlGenerator = $urlGenerator;
- $this->l = $l;
+ public function __construct(
+ protected string $appName,
+ private IURLGenerator $urlGenerator,
+ private IL10N $l,
+ ) {
}
/**
@@ -88,8 +61,8 @@ class PersonalSection implements IIconSection {
/**
* @return int whether the form should be rather on the top or bottom of
- * the settings navigation. The sections are arranged in ascending order of
- * the priority values. It is required to return a value between 0 and 99.
+ * the settings navigation. The sections are arranged in ascending order of
+ * the priority values. It is required to return a value between 0 and 99.
*
* E.g.: 70
* @since 9.1
diff --git a/apps/theming/lib/SetupChecks/PhpImagickModule.php b/apps/theming/lib/SetupChecks/PhpImagickModule.php
index 7d08aa8e954..cf9e51eee33 100644
--- a/apps/theming/lib/SetupChecks/PhpImagickModule.php
+++ b/apps/theming/lib/SetupChecks/PhpImagickModule.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2023 Côme Chilliet <come.chilliet@nextcloud.com>
- *
- * @author Côme Chilliet <come.chilliet@nextcloud.com>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Theming\SetupChecks;
diff --git a/apps/theming/lib/Themes/CommonThemeTrait.php b/apps/theming/lib/Themes/CommonThemeTrait.php
index dd5b25e1a29..74979770b70 100644
--- a/apps/theming/lib/Themes/CommonThemeTrait.php
+++ b/apps/theming/lib/Themes/CommonThemeTrait.php
@@ -2,36 +2,22 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Theming\Themes;
use OCA\Theming\AppInfo\Application;
use OCA\Theming\ImageManager;
use OCA\Theming\Service\BackgroundService;
+use OCA\Theming\ThemingDefaults;
use OCA\Theming\Util;
trait CommonThemeTrait {
public Util $util;
+ public ThemingDefaults $themingDefaults;
+
+ protected bool $isDarkVariant = false;
/**
* Generate primary-related variables
@@ -58,7 +44,6 @@ trait CommonThemeTrait {
'--primary-invert-if-dark' => $this->util->invertTextColor($colorPrimaryElement) ? 'no' : 'invert(100%)',
'--color-primary' => $this->primaryColor,
- '--color-primary-default' => $this->defaultPrimaryColor,
'--color-primary-text' => $this->util->invertTextColor($this->primaryColor) ? '#000000' : '#ffffff',
'--color-primary-hover' => $this->util->mix($this->primaryColor, $colorMainBackground, 60),
'--color-primary-light' => $colorPrimaryLight,
@@ -88,34 +73,33 @@ trait CommonThemeTrait {
protected function generateGlobalBackgroundVariables(): array {
$backgroundDeleted = $this->config->getAppValue(Application::APP_ID, 'backgroundMime', '') === 'backgroundColor';
$hasCustomLogoHeader = $this->util->isLogoThemed();
- $isPrimaryBright = $this->util->invertTextColor($this->primaryColor);
-
- $variables = [];
+ $backgroundColor = $this->themingDefaults->getColorBackground();
// Default last fallback values
- $variables['--image-background-default'] = "url('" . $this->themingDefaults->getBackground() . "')";
- $variables['--color-background-plain'] = $this->primaryColor;
+ $variables = [
+ '--color-background-plain' => $backgroundColor,
+ '--color-background-plain-text' => $this->util->invertTextColor($backgroundColor) ? '#000000' : '#ffffff',
+ '--background-image-invert-if-bright' => $this->util->invertTextColor($backgroundColor) ? 'invert(100%)' : 'no',
+ ];
// Register image variables only if custom-defined
foreach (ImageManager::SUPPORTED_IMAGE_KEYS as $image) {
if ($this->imageManager->hasImage($image)) {
$imageUrl = $this->imageManager->getImageUrl($image);
- // --image-background is overridden by user theming if logged in
$variables["--image-$image"] = "url('" . $imageUrl . "')";
+ } elseif ($image === 'background') {
+ // Apply default background if nothing is configured
+ $variables['--image-background'] = "url('" . $this->themingDefaults->getBackground($this->isDarkVariant) . "')";
}
}
- // If primary as background has been request or if we have a custom primary colour
- // let's not define the background image
+ // If a background has been requested let's not define the background image
if ($backgroundDeleted) {
- $variables['--color-background-plain'] = $this->primaryColor;
- $variables['--image-background-plain'] = 'yes';
- $variables['--image-background'] = 'no';
- // If no background image is set, we need to check against the shown primary colour
- $variables['--background-image-invert-if-bright'] = $isPrimaryBright ? 'invert(100%)' : 'no';
+ $variables['--image-background'] = 'none';
}
if ($hasCustomLogoHeader) {
+ // prevent inverting the logo on bright colors if customized
$variables['--image-logoheader-custom'] = 'true';
}
@@ -130,48 +114,41 @@ trait CommonThemeTrait {
if ($user !== null
&& !$this->themingDefaults->isUserThemingDisabled()
&& $this->appManager->isEnabledForUser(Application::APP_ID)) {
- $adminBackgroundDeleted = $this->config->getAppValue(Application::APP_ID, 'backgroundMime', '') === 'backgroundColor';
$backgroundImage = $this->config->getUserValue($user->getUID(), Application::APP_ID, 'background_image', BackgroundService::BACKGROUND_DEFAULT);
+ $backgroundColor = $this->config->getUserValue($user->getUID(), Application::APP_ID, 'background_color', $this->themingDefaults->getColorBackground());
+
$currentVersion = (int)$this->config->getUserValue($user->getUID(), Application::APP_ID, 'userCacheBuster', '0');
- $isPrimaryBright = $this->util->invertTextColor($this->primaryColor);
-
- // The user removed the background
- if ($backgroundImage === BackgroundService::BACKGROUND_DISABLED) {
- return [
- // Might be defined already by admin theming, needs to be overridden
- '--image-background' => 'none',
- '--color-background-plain' => $this->primaryColor,
- // If no background image is set, we need to check against the shown primary colour
- '--background-image-invert-if-bright' => $isPrimaryBright ? 'invert(100%)' : 'no',
- ];
+ $isBackgroundBright = $this->util->invertTextColor($backgroundColor);
+ $backgroundTextColor = $this->util->invertTextColor($backgroundColor) ? '#000000' : '#ffffff';
+
+ $variables = [
+ '--color-background-plain' => $backgroundColor,
+ '--color-background-plain-text' => $backgroundTextColor,
+ '--background-image-invert-if-bright' => $isBackgroundBright ? 'invert(100%)' : 'no',
+ ];
+
+ // Only use a background color without an image
+ if ($backgroundImage === BackgroundService::BACKGROUND_COLOR) {
+ // Might be defined already by admin theming, needs to be overridden
+ $variables['--image-background'] = 'none';
}
// The user uploaded a custom background
if ($backgroundImage === BackgroundService::BACKGROUND_CUSTOM) {
$cacheBuster = substr(sha1($user->getUID() . '_' . $currentVersion), 0, 8);
- return [
- '--image-background' => "url('" . $this->urlGenerator->linkToRouteAbsolute('theming.userTheme.getBackground') . "?v=$cacheBuster')",
- '--color-background-plain' => $this->primaryColor,
- ];
- }
-
- // The user is using the default background and admin removed the background image
- if ($backgroundImage === BackgroundService::BACKGROUND_DEFAULT && $adminBackgroundDeleted) {
- return [
- // --image-background is not defined in this case
- '--color-background-plain' => $this->primaryColor,
- '--background-image-invert-if-bright' => $isPrimaryBright ? 'invert(100%)' : 'no',
- ];
+ $variables['--image-background'] = "url('" . $this->urlGenerator->linkToRouteAbsolute('theming.userTheme.getBackground') . "?v=$cacheBuster')";
}
// The user picked a shipped background
if (isset(BackgroundService::SHIPPED_BACKGROUNDS[$backgroundImage])) {
- return [
- '--image-background' => "url('" . $this->urlGenerator->linkTo(Application::APP_ID, "img/background/$backgroundImage") . "')",
- '--color-background-plain' => $this->primaryColor,
- '--background-image-invert-if-bright' => BackgroundService::SHIPPED_BACKGROUNDS[$backgroundImage]['theming'] ?? null === BackgroundService::THEMING_MODE_DARK ? 'invert(100%)' : 'no',
- ];
+ $shippedBackground = BackgroundService::SHIPPED_BACKGROUNDS[$backgroundImage];
+ if ($this->isDarkVariant && isset($shippedBackground['dark_variant'])) {
+ $backgroundImage = $shippedBackground['dark_variant'];
+ }
+ $variables['--image-background'] = "url('" . $this->urlGenerator->linkTo(Application::APP_ID, "img/background/$backgroundImage") . "')";
}
+
+ return $variables;
}
return [];
diff --git a/apps/theming/lib/Themes/DarkHighContrastTheme.php b/apps/theming/lib/Themes/DarkHighContrastTheme.php
index e6f1da94b4e..0c8b436d660 100644
--- a/apps/theming/lib/Themes/DarkHighContrastTheme.php
+++ b/apps/theming/lib/Themes/DarkHighContrastTheme.php
@@ -2,26 +2,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Theming\Themes;
@@ -107,7 +89,7 @@ class DarkHighContrastTheme extends DarkTheme implements ITheme {
'--color-info-hover' => $this->util->lighten($colorInfo, 10),
'--color-info-text' => $this->util->lighten($colorInfo, 20),
- '--color-scrollbar' => $this->util->lighten($colorMainBackground, 35),
+ '--color-scrollbar' => 'auto transparent',
// used for the icon loading animation
'--color-loading-light' => '#000000',
diff --git a/apps/theming/lib/Themes/DarkTheme.php b/apps/theming/lib/Themes/DarkTheme.php
index 8d88de75834..fd273d4697d 100644
--- a/apps/theming/lib/Themes/DarkTheme.php
+++ b/apps/theming/lib/Themes/DarkTheme.php
@@ -2,26 +2,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Theming\Themes;
@@ -29,6 +11,8 @@ use OCA\Theming\ITheme;
class DarkTheme extends DefaultTheme implements ITheme {
+ protected bool $isDarkVariant = true;
+
public function getId(): string {
return 'dark';
}
@@ -82,8 +66,6 @@ class DarkTheme extends DefaultTheme implements ITheme {
'--color-main-background-rgb' => $colorMainBackgroundRGB,
'--color-main-background-blur' => 'rgba(var(--color-main-background-rgb), .85)',
- '--color-scrollbar' => $this->util->lighten($colorMainBackground, 15),
-
'--color-background-hover' => $this->util->lighten($colorMainBackground, 4),
'--color-background-dark' => $this->util->lighten($colorMainBackground, 7),
'--color-background-darker' => $this->util->lighten($colorMainBackground, 14),
diff --git a/apps/theming/lib/Themes/DefaultTheme.php b/apps/theming/lib/Themes/DefaultTheme.php
index 68038f053e3..bdd3048a498 100644
--- a/apps/theming/lib/Themes/DefaultTheme.php
+++ b/apps/theming/lib/Themes/DefaultTheme.php
@@ -2,79 +2,42 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Theming\Themes;
+use OC\AppFramework\Http\Request;
use OCA\Theming\ImageManager;
use OCA\Theming\ITheme;
-use OCA\Theming\Service\BackgroundService;
use OCA\Theming\ThemingDefaults;
use OCA\Theming\Util;
use OCP\App\IAppManager;
use OCP\IConfig;
use OCP\IL10N;
+use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\IUserSession;
class DefaultTheme implements ITheme {
use CommonThemeTrait;
- public Util $util;
- public ThemingDefaults $themingDefaults;
- public IUserSession $userSession;
- public IURLGenerator $urlGenerator;
- public ImageManager $imageManager;
- public IConfig $config;
- public IL10N $l;
- public IAppManager $appManager;
-
public string $defaultPrimaryColor;
public string $primaryColor;
- public function __construct(Util $util,
- ThemingDefaults $themingDefaults,
- IUserSession $userSession,
- IURLGenerator $urlGenerator,
- ImageManager $imageManager,
- IConfig $config,
- IL10N $l,
- IAppManager $appManager) {
- $this->util = $util;
- $this->themingDefaults = $themingDefaults;
- $this->userSession = $userSession;
- $this->urlGenerator = $urlGenerator;
- $this->imageManager = $imageManager;
- $this->config = $config;
- $this->l = $l;
- $this->appManager = $appManager;
-
+ public function __construct(
+ public Util $util,
+ public ThemingDefaults $themingDefaults,
+ public IUserSession $userSession,
+ public IURLGenerator $urlGenerator,
+ public ImageManager $imageManager,
+ public IConfig $config,
+ public IL10N $l,
+ public IAppManager $appManager,
+ private ?IRequest $request,
+ ) {
$this->defaultPrimaryColor = $this->themingDefaults->getDefaultColorPrimary();
$this->primaryColor = $this->themingDefaults->getColorPrimary();
-
- // Override primary colors (if set) to improve accessibility
- if ($this->primaryColor === BackgroundService::DEFAULT_COLOR) {
- $this->primaryColor = BackgroundService::DEFAULT_ACCESSIBLE_COLOR;
- }
}
public function getId(): string {
@@ -120,12 +83,29 @@ class DefaultTheme implements ITheme {
$colorSuccess = '#2d7b41';
$colorInfo = '#0071ad';
+ $user = $this->userSession->getUser();
+ // Chromium based browsers currently (2024) have huge performance issues with blur filters
+ $isChromium = $this->request !== null && $this->request->isUserAgent([Request::USER_AGENT_CHROME, Request::USER_AGENT_MS_EDGE]);
+ // Ignore MacOS because they always have hardware accelartion
+ $isChromium = $isChromium && !$this->request->isUserAgent(['/Macintosh/']);
+ // Allow to force the blur filter
+ $forceEnableBlur = $user === null ? false : $this->config->getUserValue(
+ $user->getUID(),
+ 'theming',
+ 'force_enable_blur_filter',
+ );
+ $workingBlur = match($forceEnableBlur) {
+ 'yes' => true,
+ 'no' => false,
+ default => !$isChromium
+ };
+
$variables = [
'--color-main-background' => $colorMainBackground,
'--color-main-background-rgb' => $colorMainBackgroundRGB,
'--color-main-background-translucent' => 'rgba(var(--color-main-background-rgb), .97)',
'--color-main-background-blur' => 'rgba(var(--color-main-background-rgb), .8)',
- '--filter-background-blur' => 'blur(25px)',
+ '--filter-background-blur' => $workingBlur ? 'blur(25px)' : 'none',
// to use like this: background-image: linear-gradient(0, var('--gradient-main-background));
'--gradient-main-background' => 'var(--color-main-background) 0%, var(--color-main-background-translucent) 85%, transparent 100%',
@@ -146,7 +126,7 @@ class DefaultTheme implements ITheme {
'--color-text-light' => 'var(--color-main-text)', // deprecated
'--color-text-lighter' => 'var(--color-text-maxcontrast)', // deprecated
- '--color-scrollbar' => 'rgba(' . $colorMainTextRgb . ', .15)',
+ '--color-scrollbar' => 'var(--color-border-maxcontrast) transparent',
// error/warning/success/info feedback colours
'--color-error' => $colorError,
@@ -172,7 +152,7 @@ class DefaultTheme implements ITheme {
'--color-loading-dark' => '#444444',
'--color-box-shadow-rgb' => $colorBoxShadowRGB,
- '--color-box-shadow' => "rgba(var(--color-box-shadow-rgb), 0.5)",
+ '--color-box-shadow' => 'rgba(var(--color-box-shadow-rgb), 0.5)',
'--color-border' => $this->util->darken($colorMainBackground, 7),
'--color-border-dark' => $this->util->darken($colorMainBackground, 14),
@@ -180,33 +160,59 @@ class DefaultTheme implements ITheme {
'--font-face' => "system-ui, -apple-system, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, Ubuntu, 'Helvetica Neue', 'Noto Sans', 'Liberation Sans', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'",
'--default-font-size' => '15px',
+ '--font-size-small' => '13px',
+ // 1.5 * font-size for accessibility
+ '--default-line-height' => '1.5',
// TODO: support "(prefers-reduced-motion)"
'--animation-quick' => '100ms',
'--animation-slow' => '300ms',
// Default variables --------------------------------------------
- '--border-radius' => '3px',
- '--border-radius-large' => '10px',
+ // Border width for input elements such as text fields and selects
+ '--border-width-input' => '1px',
+ '--border-width-input-focused' => '2px',
+
+ // Border radii (new values)
+ '--border-radius-small' => '4px', // For smaller elements
+ '--border-radius-element' => '8px', // For interactive elements such as buttons, input, navigation and list items
+ '--border-radius-container' => '12px', // For smaller containers like action menus
+ '--border-radius-container-large' => '16px', // For bigger containers like body or modals
+
+ // Border radii (deprecated)
+ '--border-radius' => 'var(--border-radius-small)',
+ '--border-radius-large' => 'var(--border-radius-element)',
'--border-radius-rounded' => '28px',
- // pill-style button, value is large so big buttons also have correct roundness
'--border-radius-pill' => '100px',
- '--default-clickable-area' => '44px',
- '--default-line-height' => '24px',
+ '--default-clickable-area' => '34px',
+ '--clickable-area-large' => '48px',
+ '--clickable-area-small' => '24px',
+
'--default-grid-baseline' => '4px',
- // various structure data
+ // header / navigation bar
'--header-height' => '50px',
+ '--header-menu-item-height' => '44px',
+ /* An alpha mask to be applied to all icons on the navigation bar (header menu).
+ * Icons are have a size of 20px but usually we use MDI which have a content of 16px so 2px padding top bottom,
+ * for better gradient we must at first begin at those 2px (10% of height) as start and stop positions.
+ */
+ '--header-menu-icon-mask' => 'linear-gradient(var(--color-background-plain-text) 25%, color-mix(in srgb, var(--color-background-plain-text), 55% transparent) 90%) alpha',
+
+ // various structure data
'--navigation-width' => '300px',
'--sidebar-min-width' => '300px',
'--sidebar-max-width' => '500px',
- '--list-min-width' => '200px',
- '--list-max-width' => '300px',
- '--header-menu-item-height' => '44px',
- '--header-menu-profile-item-height' => '66px',
- // mobile. Keep in sync with core/js/js.js
+ // Border radius of the body container
+ '--body-container-radius' => 'var(--border-radius-container-large)',
+ // Margin of the body container
+ '--body-container-margin' => 'calc(var(--default-grid-baseline) * 2)',
+ // Height of the body container to fully fill the view port
+ '--body-height' => 'calc(100% - env(safe-area-inset-bottom) - var(--header-height) - var(--body-container-margin))',
+
+ // mobile. Keep in sync with core/src/init.js
'--breakpoint-mobile' => '1024px',
'--background-invert-if-dark' => 'no',
'--background-invert-if-bright' => 'invert(100%)',
diff --git a/apps/theming/lib/Themes/DyslexiaFont.php b/apps/theming/lib/Themes/DyslexiaFont.php
index 3275a005c8d..2448de7b3c8 100644
--- a/apps/theming/lib/Themes/DyslexiaFont.php
+++ b/apps/theming/lib/Themes/DyslexiaFont.php
@@ -2,26 +2,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Theming\Themes;
@@ -61,30 +43,22 @@ class DyslexiaFont extends DefaultTheme implements ITheme {
}
public function getCustomCss(): string {
- $fontPathWoff = $this->urlGenerator->linkTo('theming', 'fonts/OpenDyslexic-Regular.woff');
$fontPathOtf = $this->urlGenerator->linkTo('theming', 'fonts/OpenDyslexic-Regular.otf');
- $fontPathTtf = $this->urlGenerator->linkTo('theming', 'fonts/OpenDyslexic-Regular.ttf');
- $boldFontPathWoff = $this->urlGenerator->linkTo('theming', 'fonts/OpenDyslexic-Bold.woff');
$boldFontPathOtf = $this->urlGenerator->linkTo('theming', 'fonts/OpenDyslexic-Bold.otf');
- $boldFontPathTtf = $this->urlGenerator->linkTo('theming', 'fonts/OpenDyslexic-Bold.ttf');
return "
@font-face {
font-family: 'OpenDyslexic';
font-style: normal;
font-weight: 400;
- src: url('$fontPathWoff') format('woff'),
- url('$fontPathOtf') format('opentype'),
- url('$fontPathTtf') format('truetype');
+ src: url('$fontPathOtf') format('opentype');
}
-
+
@font-face {
font-family: 'OpenDyslexic';
font-style: normal;
font-weight: 700;
- src: url('$boldFontPathWoff') format('woff'),
- url('$boldFontPathOtf') format('opentype'),
- url('$boldFontPathTtf') format('truetype');
+ src: url('$boldFontPathOtf') format('opentype');
}
";
}
diff --git a/apps/theming/lib/Themes/HighContrastTheme.php b/apps/theming/lib/Themes/HighContrastTheme.php
index e21e802fbe3..5b51114a32f 100644
--- a/apps/theming/lib/Themes/HighContrastTheme.php
+++ b/apps/theming/lib/Themes/HighContrastTheme.php
@@ -2,26 +2,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Theming\Themes;
@@ -112,7 +94,7 @@ class HighContrastTheme extends DefaultTheme implements ITheme {
'--color-favorite' => '#936B06',
- '--color-scrollbar' => $this->util->darken($colorMainBackground, 25),
+ '--color-scrollbar' => 'auto transparent',
// used for the icon loading animation
'--color-loading-light' => '#dddddd',
@@ -124,6 +106,9 @@ class HighContrastTheme extends DefaultTheme implements ITheme {
'--color-border' => $this->util->darken($colorMainBackground, 50),
'--color-border-dark' => $this->util->darken($colorMainBackground, 50),
'--color-border-maxcontrast' => $this->util->darken($colorMainBackground, 56),
+
+ // remove the gradient from the app icons
+ '--header-menu-icon-mask' => 'none',
]
);
}
diff --git a/apps/theming/lib/Themes/LightTheme.php b/apps/theming/lib/Themes/LightTheme.php
index 7e6773992a1..714156d4721 100644
--- a/apps/theming/lib/Themes/LightTheme.php
+++ b/apps/theming/lib/Themes/LightTheme.php
@@ -2,26 +2,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Theming\Themes;
diff --git a/apps/theming/lib/ThemingDefaults.php b/apps/theming/lib/ThemingDefaults.php
index 210029ae636..04f56895fa3 100644
--- a/apps/theming/lib/ThemingDefaults.php
+++ b/apps/theming/lib/ThemingDefaults.php
@@ -1,42 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Bjoern Schiessle <bjoern@schiessle.org>
- * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Bjoern Schiessle <bjoern@schiessle.org>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Kesselberg <mail@danielkesselberg.de>
- * @author Guillaume COMPAGNON <gcompagnon@outlook.com>
- * @author Jan-Christoph Borchardt <hey@jancborchardt.net>
- * @author Joachim Bauch <bauch@struktur.de>
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Julien Veyssier <eneiluj@posteo.net>
- * @author Julius Haertl <jus@bitgrid.net>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Michael Weimann <mail@michael-weimann.eu>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Patrik Kernstock <info@pkern.at>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Theming;
@@ -46,6 +12,7 @@ use OCP\App\AppPathNotFoundException;
use OCP\App\IAppManager;
use OCP\Files\NotFoundException;
use OCP\Files\SimpleFS\ISimpleFile;
+use OCP\IAppConfig;
use OCP\ICacheFactory;
use OCP\IConfig;
use OCP\IL10N;
@@ -55,22 +22,13 @@ use OCP\IUserSession;
class ThemingDefaults extends \OC_Defaults {
- private IConfig $config;
- private IL10N $l;
- private ImageManager $imageManager;
- private IUserSession $userSession;
- private IURLGenerator $urlGenerator;
- private ICacheFactory $cacheFactory;
- private Util $util;
- private IAppManager $appManager;
- private INavigationManager $navigationManager;
-
private string $name;
private string $title;
private string $entity;
private string $productName;
private string $url;
- private string $color;
+ private string $backgroundColor;
+ private string $primaryColor;
private string $docBaseUrl;
private string $iTunesAppId;
@@ -80,43 +38,29 @@ class ThemingDefaults extends \OC_Defaults {
/**
* ThemingDefaults constructor.
- *
- * @param IConfig $config
- * @param IL10N $l
- * @param ImageManager $imageManager
- * @param IUserSession $userSession
- * @param IURLGenerator $urlGenerator
- * @param ICacheFactory $cacheFactory
- * @param Util $util
- * @param IAppManager $appManager
*/
- public function __construct(IConfig $config,
- IL10N $l,
- IUserSession $userSession,
- IURLGenerator $urlGenerator,
- ICacheFactory $cacheFactory,
- Util $util,
- ImageManager $imageManager,
- IAppManager $appManager,
- INavigationManager $navigationManager
+ public function __construct(
+ private IConfig $config,
+ private IAppConfig $appConfig,
+ private IL10N $l,
+ private IUserSession $userSession,
+ private IURLGenerator $urlGenerator,
+ private ICacheFactory $cacheFactory,
+ private Util $util,
+ private ImageManager $imageManager,
+ private IAppManager $appManager,
+ private INavigationManager $navigationManager,
+ private BackgroundService $backgroundService,
) {
parent::__construct();
- $this->config = $config;
- $this->l = $l;
- $this->imageManager = $imageManager;
- $this->userSession = $userSession;
- $this->urlGenerator = $urlGenerator;
- $this->cacheFactory = $cacheFactory;
- $this->util = $util;
- $this->appManager = $appManager;
- $this->navigationManager = $navigationManager;
$this->name = parent::getName();
$this->title = parent::getTitle();
$this->entity = parent::getEntity();
$this->productName = parent::getProductName();
$this->url = parent::getBaseUrl();
- $this->color = parent::getColorPrimary();
+ $this->primaryColor = parent::getColorPrimary();
+ $this->backgroundColor = parent::getColorBackground();
$this->iTunesAppId = parent::getiTunesAppId();
$this->iOSClientUrl = parent::getiOSClientUrl();
$this->AndroidClientUrl = parent::getAndroidClientUrl();
@@ -177,10 +121,10 @@ class ThemingDefaults extends \OC_Defaults {
if ($entity !== '') {
if ($baseUrl !== '') {
- $footer = '<a href="' . $baseUrl . '" target="_blank"' .
- ' rel="noreferrer noopener" class="entity-name">' . $entity . '</a>';
+ $footer = '<a href="' . $baseUrl . '" target="_blank"'
+ . ' rel="noreferrer noopener" class="entity-name">' . $entity . '</a>';
} else {
- $footer = '<span class="entity-name">' .$entity . '</span>';
+ $footer = '<span class="entity-name">' . $entity . '</span>';
}
}
$footer .= ($slogan !== '' ? ' – ' . $slogan : '');
@@ -211,20 +155,21 @@ class ThemingDefaults extends \OC_Defaults {
if ($link['url'] !== ''
&& filter_var($link['url'], FILTER_VALIDATE_URL)
) {
- $legalLinks .= $divider . '<a href="' . $link['url'] . '" class="legal" target="_blank"' .
- ' rel="noreferrer noopener">' . $link['text'] . '</a>';
+ $legalLinks .= $divider . '<a href="' . $link['url'] . '" class="legal" target="_blank"'
+ . ' rel="noreferrer noopener">' . $link['text'] . '</a>';
$divider = ' · ';
}
}
if ($legalLinks !== '') {
- $footer .= '<br/>' . $legalLinks;
+ $footer .= '<br/><span class="footer__legal-links">' . $legalLinks . '</span>';
}
return $footer;
}
/**
- * Color that is used for the header as well as for mail headers
+ * Color that is used for highlighting elements like important buttons
+ * If user theming is enabled then the user defined value is returned
*/
public function getColorPrimary(): string {
$user = $this->userSession->getUser();
@@ -238,32 +183,66 @@ class ThemingDefaults extends \OC_Defaults {
// user-defined primary color
if (!empty($user)) {
- $themingBackgroundColor = $this->config->getUserValue($user->getUID(), Application::APP_ID, 'background_color', '');
- // If the user selected a specific colour
- if (preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $themingBackgroundColor)) {
- return $themingBackgroundColor;
+ $userPrimaryColor = $this->config->getUserValue($user->getUID(), Application::APP_ID, 'primary_color', '');
+ if (preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $userPrimaryColor)) {
+ return $userPrimaryColor;
}
}
- // If the default color is not valid, return the default background one
- if (!preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $defaultColor)) {
- return BackgroundService::DEFAULT_COLOR;
+ // Finally, return the system global primary color
+ return $defaultColor;
+ }
+
+ /**
+ * Color that is used for the page background (e.g. the header)
+ * If user theming is enabled then the user defined value is returned
+ */
+ public function getColorBackground(): string {
+ $user = $this->userSession->getUser();
+
+ // admin-defined background color
+ $defaultColor = $this->getDefaultColorBackground();
+
+ if ($this->isUserThemingDisabled()) {
+ return $defaultColor;
}
- // Finally, return the system global primary color
+ // user-defined background color
+ if (!empty($user)) {
+ $userBackgroundColor = $this->config->getUserValue($user->getUID(), Application::APP_ID, 'background_color', '');
+ if (preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $userBackgroundColor)) {
+ return $userBackgroundColor;
+ }
+ }
+
+ // Finally, return the system global background color
return $defaultColor;
}
/**
- * Return the default color primary
+ * Return the default primary color - only taking admin setting into account
*/
public function getDefaultColorPrimary(): string {
- $color = $this->config->getAppValue(Application::APP_ID, 'color', '');
- if (!preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $color)) {
- return BackgroundService::DEFAULT_COLOR;
+ // try admin color
+ $defaultColor = $this->appConfig->getValueString(Application::APP_ID, 'primary_color', '');
+ if (preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $defaultColor)) {
+ return $defaultColor;
+ }
+
+ // fall back to default primary color
+ return $this->primaryColor;
+ }
+
+ /**
+ * Default background color only taking admin setting into account
+ */
+ public function getDefaultColorBackground(): string {
+ $defaultColor = $this->appConfig->getValueString(Application::APP_ID, 'background_color');
+ if (preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $defaultColor)) {
+ return $defaultColor;
}
- return $color;
+ return $this->backgroundColor;
}
/**
@@ -308,10 +287,11 @@ class ThemingDefaults extends \OC_Defaults {
/**
* Themed background image url
*
+ * @param bool $darkVariant if the dark variant (if available) of the background should be used
* @return string
*/
- public function getBackground(): string {
- return $this->imageManager->getImageUrl('background');
+ public function getBackground(bool $darkVariant = false): string {
+ return $this->imageManager->getImageUrl('background' . ($darkVariant ? 'Dark' : ''));
}
/**
@@ -344,6 +324,7 @@ class ThemingDefaults extends \OC_Defaults {
/**
* @return array scss variables to overwrite
+ * @deprecated since Nextcloud 22 - https://github.com/nextcloud/server/issues/9940
*/
public function getScssVariables() {
$cacheBuster = $this->config->getAppValue('theming', 'cachebuster', '0');
@@ -360,13 +341,13 @@ class ThemingDefaults extends \OC_Defaults {
'theming-favicon-mime' => "'" . $this->config->getAppValue('theming', 'faviconMime') . "'"
];
- $variables['image-logo'] = "url('".$this->imageManager->getImageUrl('logo')."')";
- $variables['image-logoheader'] = "url('".$this->imageManager->getImageUrl('logoheader')."')";
- $variables['image-favicon'] = "url('".$this->imageManager->getImageUrl('favicon')."')";
- $variables['image-login-background'] = "url('".$this->imageManager->getImageUrl('background')."')";
+ $variables['image-logo'] = "url('" . $this->imageManager->getImageUrl('logo') . "')";
+ $variables['image-logoheader'] = "url('" . $this->imageManager->getImageUrl('logoheader') . "')";
+ $variables['image-favicon'] = "url('" . $this->imageManager->getImageUrl('favicon') . "')";
+ $variables['image-login-background'] = "url('" . $this->imageManager->getImageUrl('background') . "')";
$variables['image-login-plain'] = 'false';
- if ($this->config->getAppValue('theming', 'color', '') !== '') {
+ if ($this->appConfig->getValueString(Application::APP_ID, 'primary_color', '') !== '') {
$variables['color-primary'] = $this->getColorPrimary();
$variables['color-primary-text'] = $this->getTextColorPrimary();
$variables['color-primary-element'] = $this->util->elementColor($this->getColorPrimary());
@@ -460,12 +441,16 @@ class ThemingDefaults extends \OC_Defaults {
* Revert all settings to the default value
*/
public function undoAll(): void {
+ // Remember the current cachebuster value, as we do not want to reset this value
+ // Otherwise this can lead to caching issues as the value might be known to a browser already
+ $cacheBusterKey = $this->config->getAppValue('theming', 'cachebuster', '0');
$this->config->deleteAppValues('theming');
+ $this->config->setAppValue('theming', 'cachebuster', $cacheBusterKey);
$this->increaseCacheBuster();
}
/**
- * Revert settings to the default value
+ * Revert admin settings to the default value
*
* @param string $setting setting which should be reverted
* @return string default value
@@ -485,8 +470,15 @@ class ThemingDefaults extends \OC_Defaults {
case 'slogan':
$returnValue = $this->getSlogan();
break;
- case 'color':
- $returnValue = $this->getDefaultColorPrimary();
+ case 'primary_color':
+ $returnValue = BackgroundService::DEFAULT_COLOR;
+ break;
+ case 'background_color':
+ // If a background image is set we revert to the mean image color
+ if ($this->imageManager->hasImage('background')) {
+ $file = $this->imageManager->getImage('background');
+ $returnValue = $this->backgroundService->setGlobalBackground($file->read()) ?? '';
+ }
break;
case 'logo':
case 'logoheader':
@@ -501,7 +493,16 @@ class ThemingDefaults extends \OC_Defaults {
}
/**
- * Color of text in the header and primary buttons
+ * Color of text in the header menu
+ *
+ * @return string
+ */
+ public function getTextColorBackground() {
+ return $this->util->invertTextColor($this->getColorBackground()) ? '#000000' : '#ffffff';
+ }
+
+ /**
+ * Color of text on primary buttons and other elements
*
* @return string
*/
@@ -522,6 +523,6 @@ class ThemingDefaults extends \OC_Defaults {
* Has the admin disabled user customization
*/
public function isUserThemingDisabled(): bool {
- return $this->config->getAppValue('theming', 'disable-user-theming', 'no') === 'yes';
+ return $this->appConfig->getValueBool(Application::APP_ID, 'disable-user-theming');
}
}
diff --git a/apps/theming/lib/Util.php b/apps/theming/lib/Util.php
index 71ab0a6dc6c..797456632fc 100644
--- a/apps/theming/lib/Util.php
+++ b/apps/theming/lib/Util.php
@@ -1,30 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Kesselberg <mail@danielkesselberg.de>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Julien Veyssier <eneiluj@posteo.net>
- * @author Julius Haertl <jus@bitgrid.net>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Michael Weimann <mail@michael-weimann.eu>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Theming;
@@ -36,19 +14,17 @@ use OCP\Files\NotFoundException;
use OCP\Files\SimpleFS\ISimpleFile;
use OCP\IConfig;
use OCP\IUserSession;
+use OCP\Server;
+use OCP\ServerVersion;
class Util {
-
- private IConfig $config;
- private IAppManager $appManager;
- private IAppData $appData;
- private ImageManager $imageManager;
-
- public function __construct(IConfig $config, IAppManager $appManager, IAppData $appData, ImageManager $imageManager) {
- $this->config = $config;
- $this->appManager = $appManager;
- $this->appData = $appData;
- $this->imageManager = $imageManager;
+ public function __construct(
+ private ServerVersion $serverVersion,
+ private IConfig $config,
+ private IAppManager $appManager,
+ private IAppData $appData,
+ private ImageManager $imageManager,
+ ) {
}
/**
@@ -93,7 +69,7 @@ class Util {
$contrast = $this->colorContrast($color, $blurredBackground);
// Min. element contrast is 3:1 but we need to keep hover states in mind -> min 3.2:1
- $minContrast = $highContrast ? 5.5 : 3.2;
+ $minContrast = $highContrast ? 5.6 : 3.2;
while ($contrast < $minContrast && $iteration++ < 100) {
$hsl = Color::hexToHsl($color);
@@ -211,18 +187,18 @@ class Util {
* @return string base64 encoded radio button svg
*/
public function generateRadioButton($color) {
- $radioButtonIcon = '<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16">' .
- '<path d="M8 1a7 7 0 0 0-7 7 7 7 0 0 0 7 7 7 7 0 0 0 7-7 7 7 0 0 0-7-7zm0 1a6 6 0 0 1 6 6 6 6 0 0 1-6 6 6 6 0 0 1-6-6 6 6 0 0 1 6-6zm0 2a4 4 0 1 0 0 8 4 4 0 0 0 0-8z" fill="'.$color.'"/></svg>';
+ $radioButtonIcon = '<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16">'
+ . '<path d="M8 1a7 7 0 0 0-7 7 7 7 0 0 0 7 7 7 7 0 0 0 7-7 7 7 0 0 0-7-7zm0 1a6 6 0 0 1 6 6 6 6 0 0 1-6 6 6 6 0 0 1-6-6 6 6 0 0 1 6-6zm0 2a4 4 0 1 0 0 8 4 4 0 0 0 0-8z" fill="' . $color . '"/></svg>';
return base64_encode($radioButtonIcon);
}
/**
- * @param $app string app name
+ * @param string $app app name
* @return string|ISimpleFile path to app icon / file of logo
*/
public function getAppIcon($app) {
- $app = str_replace(['\0', '/', '\\', '..'], '', $app);
+ $app = $this->appManager->cleanAppId($app);
try {
$appPath = $this->appManager->getAppPath($app);
$icon = $appPath . '/img/' . $app . '.svg';
@@ -248,14 +224,17 @@ class Util {
}
/**
- * @param $app string app name
- * @param $image string relative path to image in app folder
+ * @param string $app app name
+ * @param string $image relative path to image in app folder
* @return string|false absolute path to image
*/
public function getAppImage($app, $image) {
- $app = str_replace(['\0', '/', '\\', '..'], '', $app);
+ $app = $this->appManager->cleanAppId($app);
+ /**
+ * @psalm-taint-escape file
+ */
$image = str_replace(['\0', '\\', '..'], '', $image);
- if ($app === "core") {
+ if ($app === 'core') {
$icon = \OC::$SERVERROOT . '/core/img/' . $image;
if (file_exists($icon)) {
return $icon;
@@ -295,8 +274,8 @@ class Util {
/**
* replace default color with a custom one
*
- * @param $svg string content of a svg file
- * @param $color string color to match
+ * @param string $svg content of a svg file
+ * @param string $color color to match
* @return string
*/
public function colorizeSvg($svg, $color) {
@@ -328,18 +307,20 @@ class Util {
}
public function getCacheBuster(): string {
- $userSession = \OC::$server->get(IUserSession::class);
+ $userSession = Server::get(IUserSession::class);
$userId = '';
$user = $userSession->getUser();
if (!is_null($user)) {
$userId = $user->getUID();
}
+ $serverVersion = $this->serverVersion->getVersionString();
+ $themingAppVersion = $this->appManager->getAppVersion('theming');
$userCacheBuster = '';
if ($userId) {
$userCacheBusterValue = (int)$this->config->getUserValue($userId, 'theming', 'userCacheBuster', '0');
$userCacheBuster = $userId . '_' . $userCacheBusterValue;
}
$systemCacheBuster = $this->config->getAppValue('theming', 'cachebuster', '0');
- return substr(sha1($userCacheBuster . $systemCacheBuster), 0, 8);
+ return substr(sha1($serverVersion . $themingAppVersion . $userCacheBuster . $systemCacheBuster), 0, 8);
}
}