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.php1
-rw-r--r--apps/theming/lib/Capabilities.php30
-rw-r--r--apps/theming/lib/Command/UpdateConfig.php25
-rw-r--r--apps/theming/lib/Controller/IconController.php43
-rw-r--r--apps/theming/lib/Controller/ThemingController.php139
-rw-r--r--apps/theming/lib/Controller/UserThemeController.php49
-rw-r--r--apps/theming/lib/IconBuilder.php87
-rw-r--r--apps/theming/lib/ImageManager.php38
-rw-r--r--apps/theming/lib/Jobs/MigrateBackgroundImages.php23
-rw-r--r--apps/theming/lib/Jobs/RestoreBackgroundImageColor.php205
-rw-r--r--apps/theming/lib/Listener/BeforePreferenceListener.php14
-rw-r--r--apps/theming/lib/Listener/BeforeTemplateRenderedListener.php24
-rw-r--r--apps/theming/lib/Migration/InitBackgroundImagesMigration.php10
-rw-r--r--apps/theming/lib/Migration/Version2006Date20240905111627.php127
-rw-r--r--apps/theming/lib/Service/BackgroundService.php150
-rw-r--r--apps/theming/lib/Service/ThemeInjectionService.php36
-rw-r--r--apps/theming/lib/Service/ThemesService.php54
-rw-r--r--apps/theming/lib/Settings/Admin.php9
-rw-r--r--apps/theming/lib/Settings/AdminSection.php18
-rw-r--r--apps/theming/lib/Settings/Personal.php16
-rw-r--r--apps/theming/lib/Settings/PersonalSection.php25
-rw-r--r--apps/theming/lib/Themes/CommonThemeTrait.php8
-rw-r--r--apps/theming/lib/Themes/DarkHighContrastTheme.php2
-rw-r--r--apps/theming/lib/Themes/DarkTheme.php4
-rw-r--r--apps/theming/lib/Themes/DefaultTheme.php108
-rw-r--r--apps/theming/lib/Themes/DyslexiaFont.php14
-rw-r--r--apps/theming/lib/Themes/HighContrastTheme.php5
-rw-r--r--apps/theming/lib/ThemingDefaults.php46
-rw-r--r--apps/theming/lib/Util.php40
29 files changed, 882 insertions, 468 deletions
diff --git a/apps/theming/lib/AppInfo/Application.php b/apps/theming/lib/AppInfo/Application.php
index c031243361f..d08a1903265 100644
--- a/apps/theming/lib/AppInfo/Application.php
+++ b/apps/theming/lib/AppInfo/Application.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
diff --git a/apps/theming/lib/Capabilities.php b/apps/theming/lib/Capabilities.php
index bd3dd45741d..d5d6e415e75 100644
--- a/apps/theming/lib/Capabilities.php
+++ b/apps/theming/lib/Capabilities.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -20,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,
+ ) {
}
/**
@@ -54,6 +42,7 @@ class Capabilities implements IPublicCapability {
* @return array{
* theming: array{
* name: string,
+ * productName: string,
* url: string,
* slogan: string,
* color: string,
@@ -107,6 +96,7 @@ class Capabilities implements IPublicCapability {
return [
'theming' => [
'name' => $this->theming->getName(),
+ 'productName' => $this->theming->getProductName(),
'url' => $this->theming->getBaseUrl(),
'slogan' => $this->theming->getSlogan(),
'color' => $color,
diff --git a/apps/theming/lib/Command/UpdateConfig.php b/apps/theming/lib/Command/UpdateConfig.php
index d3131fcb58f..6236f866445 100644
--- a/apps/theming/lib/Command/UpdateConfig.php
+++ b/apps/theming/lib/Command/UpdateConfig.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -16,19 +17,15 @@ use Symfony\Component\Console\Output\OutputInterface;
class UpdateConfig extends Command {
public const SUPPORTED_KEYS = [
- 'name', 'url', 'imprintUrl', 'privacyUrl', 'slogan', 'color', 'primary_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() {
@@ -38,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',
@@ -111,9 +108,9 @@ class UpdateConfig extends Command {
$value = $this->imageManager->updateImage($key, $value);
$key = $key . 'Mime';
}
-
+
if ($key === 'color') {
- $output->writeln('<warning>Using "color" is depreacted, use "primary_color" instead');
+ $output->writeln('<comment>Using "color" is deprecated, use "primary_color" instead</comment>');
$key = 'primary_color';
}
diff --git a/apps/theming/lib/Controller/IconController.php b/apps/theming/lib/Controller/IconController.php
index acbb24e0883..e82faf78a79 100644
--- a/apps/theming/lib/Controller/IconController.php
+++ b/apps/theming/lib/Controller/IconController.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -12,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;
@@ -20,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
@@ -63,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';
@@ -87,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
@@ -97,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';
@@ -133,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
@@ -143,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 8fdb020e614..e5cee254fe8 100644
--- a/apps/theming/lib/Controller/ThemingController.php
+++ b/apps/theming/lib/Controller/ThemingController.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -8,22 +9,30 @@ 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\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\IURLGenerator;
-use ScssPhp\ScssPhp\Compiler;
/**
* Class ThemingController
@@ -35,46 +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 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,
- 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->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) {
@@ -113,16 +109,25 @@ class ThemingController extends Controller {
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;
}
@@ -135,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' => [
@@ -146,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');
}
@@ -187,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)) {
@@ -262,8 +271,8 @@ class ThemingController extends Controller {
return new DataResponse(
[
- 'data' =>
- [
+ 'data'
+ => [
'name' => $name,
'url' => $this->imageManager->getImageUrl($key),
'message' => $this->l10n->t('Saved'),
@@ -275,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'),
],
@@ -298,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'
@@ -319,8 +328,6 @@ class ThemingController extends Controller {
}
/**
- * @PublicPage
- * @NoCSRFRequired
* @NoSameSiteCookieRequired
*
* Get an image
@@ -333,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);
@@ -341,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);
@@ -356,8 +366,6 @@ class ThemingController extends Controller {
}
/**
- * @NoCSRFRequired
- * @PublicPage
* @NoSameSiteCookieRequired
* @NoTwoFactorRequired
*
@@ -371,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))) {
@@ -391,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 {
@@ -407,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') {
@@ -455,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,
@@ -470,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 33c6c5c8a3b..770f2ca922f 100644
--- a/apps/theming/lib/Controller/UserThemeController.php
+++ b/apps/theming/lib/Controller/UserThemeController.php
@@ -15,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;
@@ -34,23 +38,16 @@ class UserThemeController extends OCSController {
protected ?string $userId = null;
- private IConfig $config;
- 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->themesService = $themesService;
- $this->themingDefaults = $themingDefaults;
- $this->backgroundService = $backgroundService;
$user = $userSession->getUser();
if ($user !== null) {
@@ -59,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);
@@ -79,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);
@@ -128,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()]);
@@ -148,14 +143,13 @@ 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();
@@ -168,8 +162,6 @@ class UserThemeController extends OCSController {
}
/**
- * @NoAdminRequired
- *
* Set the background
*
* @param string $type Type of background
@@ -180,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');
diff --git a/apps/theming/lib/IconBuilder.php b/apps/theming/lib/IconBuilder.php
index a10e7442c46..63f4559970d 100644
--- a/apps/theming/lib/IconBuilder.php
+++ b/apps/theming/lib/IconBuilder.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -10,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.
*
@@ -25,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;
}
/**
@@ -44,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);
@@ -87,7 +78,7 @@ class IconBuilder {
if ($icon === false) {
return false;
}
- $icon->setImageFormat("png32");
+ $icon->setImageFormat('png32');
$data = $icon->getImageBlob();
$icon->destroy();
return $data;
@@ -100,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);
@@ -162,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;
@@ -202,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 c44d6de128b..309bf192bc3 100644
--- a/apps/theming/lib/ImageManager.php
+++ b/apps/theming/lib/ImageManager.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -42,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) {
@@ -49,11 +53,16 @@ class ImageManager {
case 'logoheader':
case 'favicon':
return $this->urlGenerator->imagePath('core', 'logo/logo.png') . '?v=' . $cacheBusterCounter;
+ case 'backgroundDark':
case 'background':
// Removing the background defines its mime as 'backgroundColor'
$mimeSetting = $this->config->getAppValue('theming', 'backgroundMime', '');
if ($mimeSetting !== 'backgroundColor') {
- return $this->urlGenerator->linkTo(Application::APP_ID, 'img/background/' . BackgroundService::DEFAULT_BACKGROUND_IMAGE);
+ $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 '';
@@ -144,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 {
@@ -157,7 +166,7 @@ class ImageManager {
*
* @param string $filename
* @param string $data
- * @return \OCP\Files\SimpleFS\ISimpleFile
+ * @return ISimpleFile
* @throws NotFoundException
* @throws NotPermittedException
*/
@@ -186,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 {
@@ -258,6 +271,25 @@ class ImageManager {
$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 aff13fc2910..62e58f5e722 100644
--- a/apps/theming/lib/Jobs/MigrateBackgroundImages.php
+++ b/apps/theming/lib/Jobs/MigrateBackgroundImages.php
@@ -30,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 f21ccc94a1a..048deae50ce 100644
--- a/apps/theming/lib/Listener/BeforePreferenceListener.php
+++ b/apps/theming/lib/Listener/BeforePreferenceListener.php
@@ -17,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,
) {
@@ -38,15 +44,16 @@ class BeforePreferenceListener implements IEventListener {
}
private function handleThemingValues(BeforePreferenceSetEvent|BeforePreferenceDeletedEvent $event): void {
- $allowedKeys = ['shortcuts_disabled', 'primary_color'];
-
- if (!in_array($event->getConfigKey(), $allowedKeys)) {
+ if (!in_array($event->getConfigKey(), self::ALLOWED_KEYS)) {
// Not allowed config key
return;
}
if ($event instanceof BeforePreferenceSetEvent) {
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;
@@ -56,6 +63,7 @@ class BeforePreferenceListener implements IEventListener {
default:
$event->setValid(false);
}
+ return;
}
$event->setValid(true);
diff --git a/apps/theming/lib/Listener/BeforeTemplateRenderedListener.php b/apps/theming/lib/Listener/BeforeTemplateRenderedListener.php
index cb72e46360e..18ab9392b97 100644
--- a/apps/theming/lib/Listener/BeforeTemplateRenderedListener.php
+++ b/apps/theming/lib/Listener/BeforeTemplateRenderedListener.php
@@ -19,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 {
@@ -64,6 +54,6 @@ class BeforeTemplateRenderedListener implements IEventListener {
$this->themeInjectionService->injectHeaders();
// 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 a070ebb3d56..dea1bb3aa83 100644
--- a/apps/theming/lib/Migration/InitBackgroundImagesMigration.php
+++ b/apps/theming/lib/Migration/InitBackgroundImagesMigration.php
@@ -12,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/Service/BackgroundService.php b/apps/theming/lib/Service/BackgroundService.php
index 52925fdf980..ee9466c3a36 100644
--- a/apps/theming/lib/Service/BackgroundService.php
+++ b/apps/theming/lib/Service/BackgroundService.php
@@ -18,7 +18,9 @@ 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;
@@ -44,7 +46,7 @@ class BackgroundService {
*/
public const BACKGROUND_COLOR = 'color';
- public const DEFAULT_BACKGROUND_IMAGE = 'kamil-porembinski-clouds.jpg';
+ public const DEFAULT_BACKGROUND_IMAGE = 'jenna-kim-the-globe.webp';
/**
* 'attribution': Name, artist and license
@@ -54,6 +56,21 @@ class BackgroundService {
* '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',
@@ -138,13 +155,6 @@ class BackgroundService {
'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/',
- 'background_color' => self::DEFAULT_BACKGROUND_COLOR,
- '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',
@@ -192,15 +202,18 @@ class BackgroundService {
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');
- $this->config->deleteUserValue($this->userId, Application::APP_ID, 'primary_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');
}
/**
@@ -211,17 +224,27 @@ class BackgroundService {
* @throws PreConditionNotMetException
* @throws NoUserException
*/
- public function setFileBackground($path): void {
- if ($this->userId === null) {
- throw new RuntimeException('No currently logged-in user');
- }
- $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');
}
@@ -229,50 +252,53 @@ class BackgroundService {
if ($meanColor !== false) {
$this->setColorBackground($meanColor);
}
-
- $this->getAppDataFolder()->newFile('background.jpg', $file->fopen('r'));
- $this->config->setUserValue($this->userId, Application::APP_ID, 'background_image', self::BACKGROUND_CUSTOM);
+ $this->config->setUserValue($userId, Application::APP_ID, 'background_image', self::BACKGROUND_CUSTOM);
}
- public function setShippedBackground($fileName): void {
- if ($this->userId === null) {
- throw new RuntimeException('No currently logged-in user');
- }
- 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->setColorBackground(self::SHIPPED_BACKGROUNDS[$fileName]['background_color']);
- $this->config->setUserValue($this->userId, Application::APP_ID, 'background_image', $fileName);
- $this->config->setUserValue($this->userId, Application::APP_ID, 'primary_color', 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']);
}
/**
* 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): void {
- if ($this->userId === null) {
- throw new RuntimeException('No currently 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($this->userId, Application::APP_ID, 'background_image', self::BACKGROUND_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 {
- if ($this->userId === null) {
- throw new RuntimeException('No currently logged-in user');
- }
- $this->config->setUserValue($this->userId, Application::APP_ID, 'background_image', self::BACKGROUND_COLOR);
+ 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;
}
}
@@ -285,14 +311,14 @@ class BackgroundService {
* @param resource|string $path
* @return string|null The fallback background color - if any
*/
- public function setGlobalBackground($path): string|null {
- $image = new \OCP\Image();
+ 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->config->setAppValue(Application::APP_ID, 'background_color', $meanColor);
+ $this->appConfig->setValueString(Application::APP_ID, 'background_color', $meanColor);
return $meanColor;
}
}
@@ -303,7 +329,7 @@ class BackgroundService {
* 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(\OCP\Image $image): false|string {
+ private function calculateMeanColor(Image $image): false|string {
/**
* Small helper to ensure one channel is returned as 8byte hex
*/
@@ -311,13 +337,13 @@ class BackgroundService {
$hex = dechex($channel);
return match (strlen($hex)) {
0 => '00',
- 1 => '0'.$hex,
+ 1 => '0' . $hex,
2 => $hex,
default => 'ff',
};
}
- $tempImage = new \OCP\Image();
+ $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()));
@@ -358,19 +384,31 @@ class BackgroundService {
/**
* 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/ThemeInjectionService.php b/apps/theming/lib/Service/ThemeInjectionService.php
index f65dde076bc..873d388081c 100644
--- a/apps/theming/lib/Service/ThemeInjectionService.php
+++ b/apps/theming/lib/Service/ThemeInjectionService.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -14,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 {
@@ -52,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;
@@ -99,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;
}
@@ -114,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 d6e14b6ffcb..f49524cb62c 100644
--- a/apps/theming/lib/Service/ThemesService.php
+++ b/apps/theming/lib/Service/ThemesService.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -16,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 = [
@@ -52,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;
}
@@ -106,7 +128,7 @@ class ThemesService {
$this->setEnabledThemes($enabledThemes);
return $enabledThemes;
}
-
+
return $themesIds;
}
@@ -127,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 8a7b47a44a0..9fa0f2bb0e7 100644
--- a/apps/theming/lib/Settings/Admin.php
+++ b/apps/theming/lib/Settings/Admin.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -14,6 +15,7 @@ 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;
@@ -28,6 +30,7 @@ class Admin implements IDelegatedSettings {
private IInitialState $initialState,
private IURLGenerator $urlGenerator,
private ImageManager $imageManager,
+ private INavigationManager $navigationManager,
) {
}
@@ -70,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');
@@ -87,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 3170a826310..a1ea568d9f2 100644
--- a/apps/theming/lib/Settings/AdminSection.php
+++ b/apps/theming/lib/Settings/AdminSection.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -10,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,
+ ) {
}
/**
@@ -42,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 cdf0399946f..f14deeb35f0 100644
--- a/apps/theming/lib/Settings/Personal.php
+++ b/apps/theming/lib/Settings/Personal.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -9,10 +10,10 @@ 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;
@@ -25,7 +26,7 @@ class Personal implements ISettings {
private ThemesService $themesService,
private IInitialState $initialStateService,
private ThemingDefaults $themingDefaults,
- private IAppManager $appManager,
+ private INavigationManager $navigationManager,
) {
}
@@ -49,8 +50,8 @@ 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);
@@ -75,9 +76,10 @@ class Personal implements ISettings {
$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');
@@ -95,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 4411f605903..0a9361d5533 100644
--- a/apps/theming/lib/Settings/PersonalSection.php
+++ b/apps/theming/lib/Settings/PersonalSection.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -11,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.
*
@@ -27,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,
+ ) {
}
/**
@@ -70,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/Themes/CommonThemeTrait.php b/apps/theming/lib/Themes/CommonThemeTrait.php
index 99ad919abbf..74979770b70 100644
--- a/apps/theming/lib/Themes/CommonThemeTrait.php
+++ b/apps/theming/lib/Themes/CommonThemeTrait.php
@@ -17,6 +17,8 @@ trait CommonThemeTrait {
public Util $util;
public ThemingDefaults $themingDefaults;
+ protected bool $isDarkVariant = false;
+
/**
* Generate primary-related variables
* This is shared between multiple themes because colorMainBackground and colorMainText
@@ -87,7 +89,7 @@ trait CommonThemeTrait {
$variables["--image-$image"] = "url('" . $imageUrl . "')";
} elseif ($image === 'background') {
// Apply default background if nothing is configured
- $variables['--image-background'] = "url('" . $this->themingDefaults->getBackground() . "')";
+ $variables['--image-background'] = "url('" . $this->themingDefaults->getBackground($this->isDarkVariant) . "')";
}
}
@@ -139,6 +141,10 @@ trait CommonThemeTrait {
// The user picked a shipped background
if (isset(BackgroundService::SHIPPED_BACKGROUNDS[$backgroundImage])) {
+ $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") . "')";
}
diff --git a/apps/theming/lib/Themes/DarkHighContrastTheme.php b/apps/theming/lib/Themes/DarkHighContrastTheme.php
index 64804d24e76..0c8b436d660 100644
--- a/apps/theming/lib/Themes/DarkHighContrastTheme.php
+++ b/apps/theming/lib/Themes/DarkHighContrastTheme.php
@@ -89,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 661656f2d70..fd273d4697d 100644
--- a/apps/theming/lib/Themes/DarkTheme.php
+++ b/apps/theming/lib/Themes/DarkTheme.php
@@ -11,6 +11,8 @@ use OCA\Theming\ITheme;
class DarkTheme extends DefaultTheme implements ITheme {
+ protected bool $isDarkVariant = true;
+
public function getId(): string {
return 'dark';
}
@@ -64,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 fa5dcb98c12..bdd3048a498 100644
--- a/apps/theming/lib/Themes/DefaultTheme.php
+++ b/apps/theming/lib/Themes/DefaultTheme.php
@@ -7,6 +7,7 @@ declare(strict_types=1);
*/
namespace OCA\Theming\Themes;
+use OC\AppFramework\Http\Request;
use OCA\Theming\ImageManager;
use OCA\Theming\ITheme;
use OCA\Theming\ThemingDefaults;
@@ -14,41 +15,27 @@ 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();
}
@@ -96,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%',
@@ -122,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,
@@ -148,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),
@@ -156,6 +160,9 @@ 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',
@@ -165,28 +172,47 @@ class DefaultTheme implements ITheme {
// Border width for input elements such as text fields and selects
'--border-width-input' => '1px',
'--border-width-input-focused' => '2px',
- '--border-radius' => '3px',
- '--border-radius-large' => '10px',
+
+ // 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',
- '--border-radius-element' => '10px',
- // 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 2552fc65724..2448de7b3c8 100644
--- a/apps/theming/lib/Themes/DyslexiaFont.php
+++ b/apps/theming/lib/Themes/DyslexiaFont.php
@@ -43,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 6f33c0bbcd9..5b51114a32f 100644
--- a/apps/theming/lib/Themes/HighContrastTheme.php
+++ b/apps/theming/lib/Themes/HighContrastTheme.php
@@ -94,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',
@@ -106,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/ThemingDefaults.php b/apps/theming/lib/ThemingDefaults.php
index 1b31f91eeff..04f56895fa3 100644
--- a/apps/theming/lib/ThemingDefaults.php
+++ b/apps/theming/lib/ThemingDefaults.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -11,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;
@@ -39,6 +41,7 @@ class ThemingDefaults extends \OC_Defaults {
*/
public function __construct(
private IConfig $config,
+ private IAppConfig $appConfig,
private IL10N $l,
private IUserSession $userSession,
private IURLGenerator $urlGenerator,
@@ -118,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 : '');
@@ -152,13 +155,13 @@ 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;
@@ -206,9 +209,9 @@ class ThemingDefaults extends \OC_Defaults {
// user-defined background color
if (!empty($user)) {
- $userPrimaryColor = $this->config->getUserValue($user->getUID(), Application::APP_ID, 'background_color', '');
- if (preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $userPrimaryColor)) {
- return $userPrimaryColor;
+ $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;
}
}
@@ -221,7 +224,7 @@ class ThemingDefaults extends \OC_Defaults {
*/
public function getDefaultColorPrimary(): string {
// try admin color
- $defaultColor = $this->config->getAppValue(Application::APP_ID, 'primary_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;
}
@@ -234,7 +237,7 @@ class ThemingDefaults extends \OC_Defaults {
* Default background color only taking admin setting into account
*/
public function getDefaultColorBackground(): string {
- $defaultColor = $this->config->getAppValue(Application::APP_ID, 'background_color', '');
+ $defaultColor = $this->appConfig->getValueString(Application::APP_ID, 'background_color');
if (preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $defaultColor)) {
return $defaultColor;
}
@@ -284,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' : ''));
}
/**
@@ -337,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', 'primary_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());
@@ -437,7 +441,11 @@ 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();
}
@@ -515,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 ec85120413f..797456632fc 100644
--- a/apps/theming/lib/Util.php
+++ b/apps/theming/lib/Util.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -13,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,
+ ) {
}
/**
@@ -188,8 +187,8 @@ 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);
}
@@ -199,7 +198,7 @@ class Util {
* @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';
@@ -230,9 +229,12 @@ class Util {
* @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;
@@ -305,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);
}
}