aboutsummaryrefslogtreecommitdiffstats
path: root/apps/theming/lib/Service
diff options
context:
space:
mode:
Diffstat (limited to 'apps/theming/lib/Service')
-rw-r--r--apps/theming/lib/Service/BackgroundService.php414
-rw-r--r--apps/theming/lib/Service/JSDataService.php52
-rw-r--r--apps/theming/lib/Service/ThemeInjectionService.php114
-rw-r--r--apps/theming/lib/Service/ThemesService.php120
4 files changed, 571 insertions, 129 deletions
diff --git a/apps/theming/lib/Service/BackgroundService.php b/apps/theming/lib/Service/BackgroundService.php
new file mode 100644
index 00000000000..ee9466c3a36
--- /dev/null
+++ b/apps/theming/lib/Service/BackgroundService.php
@@ -0,0 +1,414 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Theming\Service;
+
+use InvalidArgumentException;
+use OC\User\NoUserException;
+use OCA\Theming\AppInfo\Application;
+use OCP\Files\File;
+use OCP\Files\IAppData;
+use OCP\Files\IRootFolder;
+use OCP\Files\NotFoundException;
+use OCP\Files\NotPermittedException;
+use OCP\Files\SimpleFS\ISimpleFile;
+use OCP\Files\SimpleFS\ISimpleFolder;
+use OCP\IAppConfig;
+use OCP\IConfig;
+use OCP\Image;
+use OCP\Lock\LockedException;
+use OCP\PreConditionNotMetException;
+use RuntimeException;
+
+class BackgroundService {
+ public const DEFAULT_COLOR = '#00679e';
+ public const DEFAULT_BACKGROUND_COLOR = '#00679e';
+
+ /**
+ * One of our shipped background images is used
+ */
+ public const BACKGROUND_SHIPPED = 'shipped';
+ /**
+ * A custom background image is used
+ */
+ public const BACKGROUND_CUSTOM = 'custom';
+ /**
+ * The default background image is used
+ */
+ public const BACKGROUND_DEFAULT = 'default';
+ /**
+ * Just a background color is used
+ */
+ public const BACKGROUND_COLOR = 'color';
+
+ public const DEFAULT_BACKGROUND_IMAGE = 'jenna-kim-the-globe.webp';
+
+ /**
+ * 'attribution': Name, artist and license
+ * 'description': Alternative text
+ * 'attribution_url': URL for attribution
+ * 'background_color': Cached mean color of the top part to calculate app menu colors and use as fallback
+ * 'primary_color': Recommended primary color for this theme / image
+ */
+ public const SHIPPED_BACKGROUNDS = [
+ 'jenna-kim-the-globe.webp' => [
+ 'attribution' => 'Globe (Jenna Kim - Nextcloud GmbH, CC-BY-SA-4.0)',
+ 'description' => 'Background picture of white clouds on in front of a blue sky',
+ 'attribution_url' => 'https://nextcloud.com/trademarks/',
+ 'dark_variant' => 'jenna-kim-the-globe-dark.webp',
+ 'background_color' => self::DEFAULT_BACKGROUND_COLOR,
+ 'primary_color' => self::DEFAULT_COLOR,
+ ],
+ 'kamil-porembinski-clouds.jpg' => [
+ 'attribution' => 'Clouds (Kamil Porembiński, CC BY-SA)',
+ 'description' => 'Background picture of white clouds on in front of a blue sky',
+ 'attribution_url' => 'https://www.flickr.com/photos/paszczak000/8715851521/',
+ 'background_color' => self::DEFAULT_BACKGROUND_COLOR,
+ 'primary_color' => self::DEFAULT_COLOR,
+ ],
+ 'hannah-maclean-soft-floral.jpg' => [
+ 'attribution' => 'Soft floral (Hannah MacLean, CC0)',
+ 'description' => 'Abstract background picture in yellow and white color whith a flower on it',
+ 'attribution_url' => 'https://stocksnap.io/photo/soft-floral-XOYWCCW5PA',
+ 'background_color' => '#e4d2c1',
+ 'primary_color' => '#9f652f',
+ ],
+ 'ted-moravec-morning-fog.jpg' => [
+ 'attribution' => 'Morning fog (Ted Moravec, Public Domain)',
+ 'description' => 'Background picture of a forest shrouded in fog',
+ 'attribution_url' => 'https://flickr.com/photos/tmoravec/52392410261',
+ 'background_color' => '#f6f7f6',
+ 'primary_color' => '#114c3b',
+ ],
+ 'stefanus-martanto-setyo-husodo-underwater-ocean.jpg' => [
+ 'attribution' => 'Underwater ocean (Stefanus Martanto Setyo Husodo, CC0)',
+ 'description' => 'Background picture of an underwater ocean',
+ 'attribution_url' => 'https://stocksnap.io/photo/underwater-ocean-TJA9LBH4WS',
+ 'background_color' => '#003351',
+ 'primary_color' => '#04577e',
+ ],
+ 'zoltan-voros-rhythm-and-blues.jpg' => [
+ 'attribution' => 'Rhythm and blues (Zoltán Vörös, CC BY)',
+ 'description' => 'Abstract background picture of sand dunes during night',
+ 'attribution_url' => 'https://flickr.com/photos/v923z/51634409289/',
+ 'background_color' => '#1c2437',
+ 'primary_color' => '#1c243c',
+ ],
+ 'anatoly-mikhaltsov-butterfly-wing-scale.jpg' => [
+ 'attribution' => 'Butterfly wing scale (Anatoly Mikhaltsov, CC BY-SA)',
+ 'description' => 'Background picture of a red-ish butterfly wing under microscope',
+ 'attribution_url' => 'https://commons.wikimedia.org/wiki/File:%D0%A7%D0%B5%D1%88%D1%83%D0%B9%D0%BA%D0%B8_%D0%BA%D1%80%D1%8B%D0%BB%D0%B0_%D0%B1%D0%B0%D0%B1%D0%BE%D1%87%D0%BA%D0%B8.jpg',
+ 'background_color' => '#652e11',
+ 'primary_color' => '#a53c17',
+ ],
+ 'bernie-cetonia-aurata-take-off-composition.jpg' => [
+ 'attribution' => 'Cetonia aurata take off composition (Bernie, Public Domain)',
+ 'description' => 'Montage of a cetonia aurata bug that takes off with white background',
+ 'attribution_url' => 'https://commons.wikimedia.org/wiki/File:Cetonia_aurata_take_off_composition_05172009.jpg',
+ 'background_color' => '#dee0d3',
+ 'primary_color' => '#56633d',
+ ],
+ 'dejan-krsmanovic-ribbed-red-metal.jpg' => [
+ 'attribution' => 'Ribbed red metal (Dejan Krsmanovic, CC BY)',
+ 'description' => 'Abstract background picture of red ribbed metal with two horizontal white elements on top of it',
+ 'attribution_url' => 'https://www.flickr.com/photos/dejankrsmanovic/42971456774/',
+ 'background_color' => '#9b171c',
+ 'primary_color' => '#9c4236',
+ ],
+ 'eduardo-neves-pedra-azul.jpg' => [
+ 'attribution' => 'Pedra azul milky way (Eduardo Neves, CC BY-SA)',
+ 'description' => 'Background picture of the milky way during night with a mountain in front of it',
+ 'attribution_url' => 'https://commons.wikimedia.org/wiki/File:Pedra_Azul_Milky_Way.jpg',
+ 'background_color' => '#1d242d',
+ 'primary_color' => '#4f6071',
+ ],
+ 'european-space-agency-barents-bloom.jpg' => [
+ 'attribution' => 'Barents bloom (European Space Agency, CC BY-SA)',
+ 'description' => 'Abstract background picture of blooming barents in blue and green colors',
+ 'attribution_url' => 'https://www.esa.int/ESA_Multimedia/Images/2016/08/Barents_bloom',
+ 'background_color' => '#1c383d',
+ 'primary_color' => '#396475',
+ ],
+ 'hannes-fritz-flippity-floppity.jpg' => [
+ 'attribution' => 'Flippity floppity (Hannes Fritz, CC BY-SA)',
+ 'description' => 'Abstract background picture of many pairs of flip flops hanging on a wall in multiple colors',
+ 'attribution_url' => 'http://hannes.photos/flippity-floppity',
+ 'background_color' => '#5b2d53',
+ 'primary_color' => '#98415a',
+ ],
+ 'hannes-fritz-roulette.jpg' => [
+ 'attribution' => 'Roulette (Hannes Fritz, CC BY-SA)',
+ 'description' => 'Background picture of a rotating giant wheel during night',
+ 'attribution_url' => 'http://hannes.photos/roulette',
+ 'background_color' => '#000000',
+ 'primary_color' => '#845334',
+ ],
+ 'hannes-fritz-sea-spray.jpg' => [
+ 'attribution' => 'Sea spray (Hannes Fritz, CC BY-SA)',
+ 'description' => 'Background picture of a stone coast with fog and sea behind it',
+ 'attribution_url' => 'http://hannes.photos/sea-spray',
+ 'background_color' => '#333f47',
+ 'primary_color' => '#4f6071',
+ ],
+ 'bernard-spragg-new-zealand-fern.jpg' => [
+ 'attribution' => 'New zealand fern (Bernard Spragg, CC0)',
+ 'description' => 'Abstract background picture of fern leafes',
+ 'attribution_url' => 'https://commons.wikimedia.org/wiki/File:NZ_Fern.(Blechnum_chambersii)_(11263534936).jpg',
+ 'background_color' => '#0c3c03',
+ 'primary_color' => '#316b26',
+ ],
+ 'rawpixel-pink-tapioca-bubbles.jpg' => [
+ 'attribution' => 'Pink tapioca bubbles (Rawpixel, CC BY)',
+ 'description' => 'Abstract background picture of pink tapioca bubbles',
+ 'attribution_url' => 'https://www.flickr.com/photos/byrawpixel/27665140298/in/photostream/',
+ 'background_color' => '#c56e95',
+ 'primary_color' => '#7b4e7e',
+ ],
+ 'nasa-waxing-crescent-moon.jpg' => [
+ 'attribution' => 'Waxing crescent moon (NASA, Public Domain)',
+ 'description' => 'Background picture of glowing earth in foreground and moon in the background',
+ 'attribution_url' => 'https://www.nasa.gov/image-feature/a-waxing-crescent-moon',
+ 'background_color' => '#000002',
+ 'primary_color' => '#005ac1',
+ ],
+ 'tommy-chau-already.jpg' => [
+ 'attribution' => 'Cityscape (Tommy Chau, CC BY)',
+ 'description' => 'Background picture of a skyscraper city during night',
+ 'attribution_url' => 'https://www.flickr.com/photos/90975693@N05/16910999368',
+ 'background_color' => '#35229f',
+ 'primary_color' => '#6a2af4',
+ ],
+ 'tommy-chau-lion-rock-hill.jpg' => [
+ 'attribution' => 'Lion rock hill (Tommy Chau, CC BY)',
+ 'description' => 'Background picture of mountains during sunset or sunrise',
+ 'attribution_url' => 'https://www.flickr.com/photos/90975693@N05/17136440246',
+ 'background_color' => '#cb92b7',
+ 'primary_color' => '#7f4f70',
+ ],
+ 'lali-masriera-yellow-bricks.jpg' => [
+ 'attribution' => 'Yellow bricks (Lali Masriera, CC BY)',
+ 'description' => 'Background picture of yellow bricks with some yellow tubes',
+ 'attribution_url' => 'https://www.flickr.com/photos/visualpanic/3982464447',
+ 'background_color' => '#c78a19',
+ 'primary_color' => '#7f5700',
+ ],
+ ];
+
+ public function __construct(
+ private IRootFolder $rootFolder,
+ private IAppData $appData,
+ private IAppConfig $appConfig,
+ private IConfig $config,
+ private ?string $userId,
+ ) {
+ }
+
+ 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');
+ }
+
+ /**
+ * @param $path
+ * @throws NotFoundException
+ * @throws NotPermittedException
+ * @throws LockedException
+ * @throws PreConditionNotMetException
+ * @throws NoUserException
+ */
+ 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);
+ $handle = $file->fopen('r');
+ if ($handle === false) {
+ throw new InvalidArgumentException('Invalid image file');
+ }
+ $this->getAppDataFolder()->newFile('background.jpg', $handle);
+
+ $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');
+ }
+
+ $meanColor = $this->calculateMeanColor($image);
+ if ($meanColor !== false) {
+ $this->setColorBackground($meanColor);
+ }
+ $this->config->setUserValue($userId, Application::APP_ID, 'background_image', self::BACKGROUND_CUSTOM);
+ }
+
+ /**
+ * 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'], $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, ?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($userId, Application::APP_ID, 'background_color', $color);
+ $this->config->setUserValue($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(?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) {
+ return null;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Called when a new global background (backgroundMime) is uploaded (admin setting)
+ * This sets all necessary app config values
+ * @param resource|string $path
+ * @return string|null The fallback background color - if any
+ */
+ public function setGlobalBackground($path): ?string {
+ $image = new Image();
+ $handle = is_resource($path) ? $path : fopen($path, 'rb');
+
+ if ($handle && $image->loadFromFileHandle($handle) !== false) {
+ $meanColor = $this->calculateMeanColor($image);
+ if ($meanColor !== false) {
+ $this->appConfig->setValueString(Application::APP_ID, 'background_color', $meanColor);
+ return $meanColor;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Calculate mean color of an given image
+ * It only takes the upper part into account so that a matching text color can be derived for the app menu
+ */
+ private function calculateMeanColor(Image $image): false|string {
+ /**
+ * Small helper to ensure one channel is returned as 8byte hex
+ */
+ function toHex(int $channel): string {
+ $hex = dechex($channel);
+ return match (strlen($hex)) {
+ 0 => '00',
+ 1 => '0' . $hex,
+ 2 => $hex,
+ default => 'ff',
+ };
+ }
+
+ $tempImage = new Image();
+
+ // Crop to only analyze top bar
+ $resource = $image->cropNew(0, 0, $image->width(), min(max(50, (int)($image->height() * 0.125)), $image->height()));
+ if ($resource === false) {
+ return false;
+ }
+
+ $tempImage->setResource($resource);
+ if (!$tempImage->preciseResize(100, 7)) {
+ return false;
+ }
+
+ $resource = $tempImage->resource();
+ if ($resource === false) {
+ return false;
+ }
+
+ $reds = [];
+ $greens = [];
+ $blues = [];
+ for ($y = 0; $y < 7; $y++) {
+ for ($x = 0; $x < 100; $x++) {
+ $value = imagecolorat($resource, $x, $y);
+ if ($value === false) {
+ continue;
+ }
+ $reds[] = ($value >> 16) & 0xFF;
+ $greens[] = ($value >> 8) & 0xFF;
+ $blues[] = $value & 0xFF;
+ }
+ }
+ $meanColor = '#' . toHex((int)(array_sum($reds) / count($reds)));
+ $meanColor .= toHex((int)(array_sum($greens) / count($greens)));
+ $meanColor .= toHex((int)(array_sum($blues) / count($blues)));
+ return $meanColor;
+ }
+
+ /**
+ * Storing the data in appdata/theming/users/USERID
+ *
+ * @param string|null $userId The user to get the folder - default to current user
+ * @throws NotPermittedException
+ */
+ private function getAppDataFolder(?string $userId = null): ISimpleFolder {
+ $userId = $userId ?? $this->getUserId();
+
+ try {
+ $rootFolder = $this->appData->getFolder('users');
+ } catch (NotFoundException) {
+ $rootFolder = $this->appData->newFolder('users');
+ }
+ try {
+ return $rootFolder->getFolder($userId);
+ } catch (NotFoundException) {
+ return $rootFolder->newFolder($userId);
+ }
+ }
+
+ /**
+ * @throws RuntimeException Thrown if a method that needs a user is called without any logged-in user
+ */
+ private function getUserId(): string {
+ if ($this->userId === null) {
+ throw new RuntimeException('No currently logged-in user');
+ }
+ return $this->userId;
+ }
+}
diff --git a/apps/theming/lib/Service/JSDataService.php b/apps/theming/lib/Service/JSDataService.php
index fdc85ea445a..81198f8b3f5 100644
--- a/apps/theming/lib/Service/JSDataService.php
+++ b/apps/theming/lib/Service/JSDataService.php
@@ -3,63 +3,47 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Julien Veyssier <eneiluj@posteo.net>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Theming\Service;
-use OCA\Theming\AppInfo\Application;
use OCA\Theming\ThemingDefaults;
use OCA\Theming\Util;
-use OCP\IConfig;
class JSDataService implements \JsonSerializable {
- private ThemingDefaults $themingDefaults;
- private Util $util;
- private IConfig $appConfig;
- private ThemesService $themesService;
public function __construct(
- ThemingDefaults $themingDefaults,
- Util $util,
- IConfig $appConfig,
- ThemesService $themesService
+ private ThemingDefaults $themingDefaults,
+ private Util $util,
+ private ThemesService $themesService,
) {
$this->themingDefaults = $themingDefaults;
$this->util = $util;
- $this->appConfig = $appConfig;
$this->themesService = $themesService;
}
public function jsonSerialize(): array {
return [
'name' => $this->themingDefaults->getName(),
- 'url' => $this->themingDefaults->getBaseUrl(),
'slogan' => $this->themingDefaults->getSlogan(),
- 'color' => $this->themingDefaults->getColorPrimary(),
+
+ 'url' => $this->themingDefaults->getBaseUrl(),
'imprintUrl' => $this->themingDefaults->getImprintUrl(),
'privacyUrl' => $this->themingDefaults->getPrivacyUrl(),
+
+ 'primaryColor' => $this->themingDefaults->getColorPrimary(),
+ 'backgroundColor' => $this->themingDefaults->getColorBackground(),
+ 'defaultPrimaryColor' => $this->themingDefaults->getDefaultColorPrimary(),
+ 'defaultBackgroundColor' => $this->themingDefaults->getDefaultColorBackground(),
'inverted' => $this->util->invertTextColor($this->themingDefaults->getColorPrimary()),
- 'cacheBuster' => $this->appConfig->getAppValue(Application::APP_ID, 'cachebuster', '0'),
+
+ 'cacheBuster' => $this->util->getCacheBuster(),
'enabledThemes' => $this->themesService->getEnabledThemes(),
+
+ // deprecated use primaryColor
+ 'color' => $this->themingDefaults->getColorPrimary(),
+ '' => 'color is deprecated since Nextcloud 29, use primaryColor instead'
];
}
}
diff --git a/apps/theming/lib/Service/ThemeInjectionService.php b/apps/theming/lib/Service/ThemeInjectionService.php
index fec14de96cf..873d388081c 100644
--- a/apps/theming/lib/Service/ThemeInjectionService.php
+++ b/apps/theming/lib/Service/ThemeInjectionService.php
@@ -1,88 +1,116 @@
<?php
+
/**
- * @copyright Copyright (c) 2022 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Theming\Service;
+use OCA\Theming\ITheme;
use OCA\Theming\Themes\DefaultTheme;
+use OCA\Theming\Util;
+use OCP\IConfig;
use OCP\IURLGenerator;
-use OCP\Util;
+use OCP\IUserSession;
class ThemeInjectionService {
- private IURLGenerator $urlGenerator;
- private ThemesService $themesService;
- private DefaultTheme $defaultTheme;
+ private ?string $userId;
- public function __construct(IURLGenerator $urlGenerator,
- ThemesService $themesService,
- DefaultTheme $defaultTheme) {
- $this->urlGenerator = $urlGenerator;
- $this->themesService = $themesService;
- $this->defaultTheme = $defaultTheme;
+ 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 {
+ $this->userId = null;
+ }
}
- public function injectHeaders() {
+ public function injectHeaders(): void {
$themes = $this->themesService->getThemes();
$defaultTheme = $themes[$this->defaultTheme->getId()];
- $mediaThemes = array_filter($themes, function($theme) {
+ $mediaThemes = array_filter($themes, function ($theme) {
// Check if the theme provides a media query
return (bool)$theme->getMediaQuery();
});
// Default theme fallback
- $this->addThemeHeader($defaultTheme->getId());
-
+ $this->addThemeHeaders($defaultTheme);
+
// Themes applied by media queries
- foreach($mediaThemes as $theme) {
- $this->addThemeHeader($theme->getId(), true, $theme->getMediaQuery());
+ foreach ($mediaThemes as $theme) {
+ $this->addThemeHeaders($theme, true, $theme->getMediaQuery());
}
- // Themes
- foreach($this->themesService->getThemes() as $theme) {
+ // Themes
+ foreach ($this->themesService->getThemes() as $theme) {
// Ignore default theme as already processed first
if ($theme->getId() === $this->defaultTheme->getId()) {
continue;
}
- $this->addThemeHeader($theme->getId(), false);
+ $this->addThemeHeaders($theme, false);
}
+
+ // Meta headers
+ $this->addThemeMetaHeaders($themes);
}
/**
* Inject theme header into rendered page
- *
- * @param string $themeId the theme ID
+ *
+ * @param ITheme $theme the theme
* @param bool $plain request the :root syntax
* @param string $media media query to use in the <link> element
*/
- private function addThemeHeader(string $themeId, bool $plain = true, string $media = null) {
+ private function addThemeHeaders(ITheme $theme, bool $plain = true, ?string $media = null): void {
$linkToCSS = $this->urlGenerator->linkToRoute('theming.Theming.getThemeStylesheet', [
- 'themeId' => $themeId,
+ 'themeId' => $theme->getId(),
'plain' => $plain,
+ 'v' => $this->util->getCacheBuster(),
]);
- Util::addHeader('link', [
+ \OCP\Util::addHeader('link', [
'rel' => 'stylesheet',
'media' => $media,
'href' => $linkToCSS,
'class' => 'theme'
]);
}
+
+ /**
+ * Inject meta headers into rendered page
+ *
+ * @param ITheme[] $themes the theme
+ */
+ private function addThemeMetaHeaders(array $themes): void {
+ $metaHeaders = [];
+
+ // Meta headers
+ foreach ($this->themesService->getThemes() as $theme) {
+ if (!empty($theme->getMeta())) {
+ foreach ($theme->getMeta() as $meta) {
+ if (!isset($meta['name']) || !isset($meta['content'])) {
+ continue;
+ }
+
+ if (!isset($metaHeaders[$meta['name']])) {
+ $metaHeaders[$meta['name']] = [];
+ }
+ $metaHeaders[$meta['name']][] = $meta['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 01ebc3fb504..f49524cb62c 100644
--- a/apps/theming/lib/Service/ThemesService.php
+++ b/apps/theming/lib/Service/ThemesService.php
@@ -1,24 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2022 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Theming\Service;
@@ -29,49 +13,73 @@ use OCA\Theming\Themes\DarkTheme;
use OCA\Theming\Themes\DefaultTheme;
use OCA\Theming\Themes\DyslexiaFont;
use OCA\Theming\Themes\HighContrastTheme;
+use OCA\Theming\Themes\LightTheme;
use 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,
- DarkTheme $darkTheme,
- HighContrastTheme $highContrastTheme,
- DarkHighContrastTheme $darkHighContrastTheme,
- DyslexiaFont $dyslexiaFont) {
- $this->userSession = $userSession;
- $this->config = $config;
+ public function __construct(
+ private IUserSession $userSession,
+ private IConfig $config,
+ private LoggerInterface $logger,
+ private DefaultTheme $defaultTheme,
+ LightTheme $lightTheme,
+ private DarkTheme $darkTheme,
+ HighContrastTheme $highContrastTheme,
+ DarkHighContrastTheme $darkHighContrastTheme,
+ DyslexiaFont $dyslexiaFont,
+ ) {
// Register themes
$this->themesProviders = [
- $defaultTheme->getId() => $defaultTheme,
- $darkTheme->getId() => $darkTheme,
- $highContrastTheme->getId() => $highContrastTheme,
- $darkHighContrastTheme->getId() => $darkHighContrastTheme,
- $dyslexiaFont->getId() => $dyslexiaFont,
+ $defaultTheme->getId() => $defaultTheme,
+ $lightTheme->getId() => $lightTheme,
+ $darkTheme->getId() => $darkTheme,
+ $highContrastTheme->getId() => $highContrastTheme,
+ $darkHighContrastTheme->getId() => $darkHighContrastTheme,
+ $dyslexiaFont->getId() => $dyslexiaFont,
];
}
/**
* Get the list of all registered themes
- *
+ *
* @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;
}
/**
* Enable a theme for the logged-in user
- *
+ *
* @param ITheme $theme the theme to enable
* @return string[] the enabled themes
*/
@@ -84,18 +92,18 @@ class ThemesService {
}
/** @var ITheme[] */
- $themes = array_map(function($themeId) {
+ $themes = array_filter(array_map(function ($themeId) {
return $this->getThemes()[$themeId];
- }, $themesIds);
+ }, $themesIds));
// Filtering all themes with the same type
- $filteredThemes = array_filter($themes, function(ITheme $t) use ($theme) {
+ $filteredThemes = array_filter($themes, function (ITheme $t) use ($theme) {
return $theme->getType() === $t->getType();
});
// Retrieve IDs only
/** @var string[] */
- $filteredThemesIds = array_map(function(ITheme $t) {
+ $filteredThemesIds = array_map(function (ITheme $t) {
return $t->getId();
}, array_values($filteredThemes));
@@ -107,7 +115,7 @@ class ThemesService {
/**
* Disable a theme for the logged-in user
- *
+ *
* @param ITheme $theme the theme to disable
* @return string[] the enabled themes
*/
@@ -120,14 +128,14 @@ class ThemesService {
$this->setEnabledThemes($enabledThemes);
return $enabledThemes;
}
-
+
return $themesIds;
}
/**
* Check whether a theme is enabled or not
* for the logged-in user
- *
+ *
* @return bool
*/
public function isEnabled(ITheme $theme): bool {
@@ -141,32 +149,40 @@ 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 [];
}
+ $enabledThemes = json_decode($this->config->getUserValue($user->getUID(), Application::APP_ID, 'enabled-themes', '["default"]'));
+ if ($enforcedTheme !== '') {
+ return array_merge([$enforcedTheme], $enabledThemes);
+ }
+
try {
- return json_decode($this->config->getUserValue($user->getUID(), Application::APP_ID, 'enabled-themes', '[]'));
+ return $enabledThemes;
} catch (\Exception $e) {
return [];
}
}
/**
- * Set the list of enabled themes
+ * Set the list of enabled themes
* for the logged-in user
- *
+ *
* @param string[] $themes the list of enabled themes IDs
*/
private function setEnabledThemes(array $themes): void {
$user = $this->userSession->getUser();
- $this->config->setUserValue($user->getUID(), Application::APP_ID, 'enabled-themes', json_encode(array_unique(array_values($themes))));
+ $this->config->setUserValue($user->getUID(), Application::APP_ID, 'enabled-themes', json_encode(array_values(array_unique($themes))));
}
}