aboutsummaryrefslogtreecommitdiffstats
path: root/apps/theming/lib/Controller
diff options
context:
space:
mode:
Diffstat (limited to 'apps/theming/lib/Controller')
-rw-r--r--apps/theming/lib/Controller/IconController.php213
-rw-r--r--apps/theming/lib/Controller/ThemingController.php646
-rw-r--r--apps/theming/lib/Controller/UserThemeController.php218
3 files changed, 634 insertions, 443 deletions
diff --git a/apps/theming/lib/Controller/IconController.php b/apps/theming/lib/Controller/IconController.php
index 7a5f76de6b6..e82faf78a79 100644
--- a/apps/theming/lib/Controller/IconController.php
+++ b/apps/theming/lib/Controller/IconController.php
@@ -1,26 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Julius Haertl <jus@bitgrid.net>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Julius Haertl <jus@bitgrid.net>
- * @author Julius Härtl <jus@bitgrid.net>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Theming\Controller;
@@ -28,166 +10,159 @@ use OC\IntegrityCheck\Helpers\FileAccessHelper;
use OCA\Theming\IconBuilder;
use OCA\Theming\ImageManager;
use OCA\Theming\ThemingDefaults;
+use OCP\App\IAppManager;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
-use OCP\AppFramework\Http\NotFoundResponse;
-use OCP\AppFramework\Http\FileDisplayResponse;
+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\Utility\ITimeFactory;
+use OCP\AppFramework\Http\FileDisplayResponse;
+use OCP\AppFramework\Http\NotFoundResponse;
+use OCP\AppFramework\Http\Response;
use OCP\Files\NotFoundException;
use OCP\IRequest;
-use OCA\Theming\Util;
-use OCP\IConfig;
class IconController extends Controller {
- /** @var ThemingDefaults */
- private $themingDefaults;
- /** @var Util */
- private $util;
- /** @var ITimeFactory */
- private $timeFactory;
- /** @var IConfig */
- private $config;
- /** @var IconBuilder */
- private $iconBuilder;
- /** @var ImageManager */
- private $imageManager;
/** @var FileAccessHelper */
private $fileAccessHelper;
- /**
- * IconController constructor.
- *
- * @param string $appName
- * @param IRequest $request
- * @param ThemingDefaults $themingDefaults
- * @param Util $util
- * @param ITimeFactory $timeFactory
- * @param IConfig $config
- * @param IconBuilder $iconBuilder
- * @param ImageManager $imageManager
- */
public function __construct(
$appName,
IRequest $request,
- ThemingDefaults $themingDefaults,
- Util $util,
- ITimeFactory $timeFactory,
- IConfig $config,
- IconBuilder $iconBuilder,
- ImageManager $imageManager,
- FileAccessHelper $fileAccessHelper
+ private ThemingDefaults $themingDefaults,
+ private IconBuilder $iconBuilder,
+ private ImageManager $imageManager,
+ FileAccessHelper $fileAccessHelper,
+ private IAppManager $appManager,
) {
parent::__construct($appName, $request);
-
- $this->themingDefaults = $themingDefaults;
- $this->util = $util;
- $this->timeFactory = $timeFactory;
- $this->config = $config;
- $this->iconBuilder = $iconBuilder;
- $this->imageManager = $imageManager;
$this->fileAccessHelper = $fileAccessHelper;
}
/**
- * @PublicPage
- * @NoCSRFRequired
+ * Get a themed icon
*
- * @param $app string app name
- * @param $image string image file name (svg required)
- * @return FileDisplayResponse|NotFoundResponse
+ * @param string $app ID of the app
+ * @param string $image image file name (svg required)
+ * @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: 'image/svg+xml'}>|NotFoundResponse<Http::STATUS_NOT_FOUND, array{}>
+ * @throws \Exception
+ *
+ * 200: Themed icon returned
+ * 404: Themed icon not found
*/
- public function getThemedIcon($app, $image) {
+ #[PublicPage]
+ #[NoCSRFRequired]
+ #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
+ public function getThemedIcon(string $app, string $image): Response {
+ if ($app !== 'core' && !$this->appManager->isEnabledForUser($app)) {
+ $app = 'core';
+ $image = 'favicon.png';
+ }
+
+ $color = $this->themingDefaults->getColorPrimary();
try {
- $iconFile = $this->imageManager->getCachedImage("icon-" . $app . '-' . str_replace("/","_",$image));
+ $iconFileName = $this->imageManager->getCachedImage('icon-' . $app . '-' . $color . str_replace('/', '_', $image));
} catch (NotFoundException $exception) {
$icon = $this->iconBuilder->colorSvg($app, $image);
- if ($icon === false || $icon === "") {
+ if ($icon === false || $icon === '') {
return new NotFoundResponse();
}
- $iconFile = $this->imageManager->setCachedImage("icon-" . $app . '-' . str_replace("/","_",$image), $icon);
- }
- if ($iconFile !== false) {
- $response = new FileDisplayResponse($iconFile, Http::STATUS_OK, ['Content-Type' => 'image/svg+xml']);
- $response->cacheFor(86400);
- $expires = new \DateTime();
- $expires->setTimestamp($this->timeFactory->getTime());
- $expires->add(new \DateInterval('PT24H'));
- $response->addHeader('Expires', $expires->format(\DateTime::RFC2822));
- $response->addHeader('Pragma', 'cache');
- return $response;
- } else {
- return new NotFoundResponse();
+ $iconFileName = $this->imageManager->setCachedImage('icon-' . $app . '-' . $color . str_replace('/', '_', $image), $icon);
}
+ $response = new FileDisplayResponse($iconFileName, Http::STATUS_OK, ['Content-Type' => 'image/svg+xml']);
+ $response->cacheFor(86400, false, true);
+ return $response;
}
/**
* 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
*
- * @param $app string app name
- * @return FileDisplayResponse|DataDisplayResponse
+ * 200: Favicon returned
+ * 404: Favicon not found
*/
- public function getFavicon($app = "core") {
+ #[PublicPage]
+ #[NoCSRFRequired]
+ #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
+ public function getFavicon(string $app = 'core'): Response {
+ if ($app !== 'core' && !$this->appManager->isEnabledForUser($app)) {
+ $app = 'core';
+ }
+
$response = null;
- if ($this->themingDefaults->shouldReplaceIcons()) {
+ $iconFile = null;
+ try {
+ $iconFile = $this->imageManager->getImage('favicon', false);
+ $response = new FileDisplayResponse($iconFile, Http::STATUS_OK, ['Content-Type' => 'image/x-icon']);
+ } catch (NotFoundException $e) {
+ }
+ if ($iconFile === null && $this->imageManager->shouldReplaceIcons()) {
+ $color = $this->themingDefaults->getColorPrimary();
try {
- $iconFile = $this->imageManager->getCachedImage('favIcon-' . $app);
+ $iconFile = $this->imageManager->getCachedImage('favIcon-' . $app . $color);
} catch (NotFoundException $exception) {
$icon = $this->iconBuilder->getFavicon($app);
- $iconFile = $this->imageManager->setCachedImage('favIcon-' . $app, $icon);
- }
- if ($iconFile !== false) {
- $response = new FileDisplayResponse($iconFile, Http::STATUS_OK, ['Content-Type' => 'image/x-icon']);
+ if ($icon === false || $icon === '') {
+ return new NotFoundResponse();
+ }
+ $iconFile = $this->imageManager->setCachedImage('favIcon-' . $app . $color, $icon);
}
+ $response = new FileDisplayResponse($iconFile, Http::STATUS_OK, ['Content-Type' => 'image/x-icon']);
}
- if($response === null) {
+ if ($response === null) {
$fallbackLogo = \OC::$SERVERROOT . '/core/img/favicon.png';
$response = new DataDisplayResponse($this->fileAccessHelper->file_get_contents($fallbackLogo), Http::STATUS_OK, ['Content-Type' => 'image/x-icon']);
}
$response->cacheFor(86400);
- $expires = new \DateTime();
- $expires->setTimestamp($this->timeFactory->getTime());
- $expires->add(new \DateInterval('PT24H'));
- $response->addHeader('Expires', $expires->format(\DateTime::RFC2822));
- $response->addHeader('Pragma', 'cache');
return $response;
}
/**
* 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
*
- * @param $app string app name
- * @return FileDisplayResponse|NotFoundResponse
+ * 200: Touch icon returned
+ * 404: Touch icon not found
*/
- public function getTouchIcon($app = "core") {
+ #[PublicPage]
+ #[NoCSRFRequired]
+ #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
+ public function getTouchIcon(string $app = 'core'): Response {
+ if ($app !== 'core' && !$this->appManager->isEnabledForUser($app)) {
+ $app = 'core';
+ }
+
$response = null;
- if ($this->themingDefaults->shouldReplaceIcons()) {
+ try {
+ $iconFile = $this->imageManager->getImage('favicon');
+ $response = new FileDisplayResponse($iconFile, Http::STATUS_OK, ['Content-Type' => 'image/x-icon']);
+ } catch (NotFoundException $e) {
+ }
+ if ($this->imageManager->shouldReplaceIcons()) {
+ $color = $this->themingDefaults->getColorPrimary();
try {
- $iconFile = $this->imageManager->getCachedImage('touchIcon-' . $app);
+ $iconFile = $this->imageManager->getCachedImage('touchIcon-' . $app . $color);
} catch (NotFoundException $exception) {
$icon = $this->iconBuilder->getTouchIcon($app);
- $iconFile = $this->imageManager->setCachedImage('touchIcon-' . $app, $icon);
- }
- if ($iconFile !== false) {
- $response = new FileDisplayResponse($iconFile, Http::STATUS_OK, ['Content-Type' => 'image/png']);
+ if ($icon === false || $icon === '') {
+ return new NotFoundResponse();
+ }
+ $iconFile = $this->imageManager->setCachedImage('touchIcon-' . $app . $color, $icon);
}
+ $response = new FileDisplayResponse($iconFile, Http::STATUS_OK, ['Content-Type' => 'image/png']);
}
- if($response === null) {
+ if ($response === null) {
$fallbackLogo = \OC::$SERVERROOT . '/core/img/favicon-touch.png';
$response = new DataDisplayResponse($this->fileAccessHelper->file_get_contents($fallbackLogo), Http::STATUS_OK, ['Content-Type' => 'image/png']);
}
$response->cacheFor(86400);
- $expires = new \DateTime();
- $expires->setTimestamp($this->timeFactory->getTime());
- $expires->add(new \DateInterval('PT24H'));
- $response->addHeader('Expires', $expires->format(\DateTime::RFC2822));
- $response->addHeader('Pragma', 'cache');
return $response;
}
}
diff --git a/apps/theming/lib/Controller/ThemingController.php b/apps/theming/lib/Controller/ThemingController.php
index 6592eb7f7ab..e5cee254fe8 100644
--- a/apps/theming/lib/Controller/ThemingController.php
+++ b/apps/theming/lib/Controller/ThemingController.php
@@ -1,57 +1,37 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Bjoern Schiessle <bjoern@schiessle.org>
- * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch>
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Bjoern Schiessle <bjoern@schiessle.org>
- * @author Daniel Calviño Sánchez <danxuliu@gmail.com>
- * @author Jan-Christoph Borchardt <hey@jancborchardt.net>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Julius Haertl <jus@bitgrid.net>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Robin Appelman <robin@icewind.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-
namespace OCA\Theming\Controller;
-use OC\Files\AppData\Factory;
-use OC\Template\SCSSCacher;
+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\DataDownloadResponse;
-use OCP\AppFramework\Http\FileDisplayResponse;
+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\Utility\ITimeFactory;
-use OCP\Files\File;
-use OCP\Files\IAppData;
+use OCP\AppFramework\Services\IAppConfig;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\IConfig;
use OCP\IL10N;
-use OCP\ILogger;
+use OCP\INavigationManager;
use OCP\IRequest;
-use OCA\Theming\Util;
-use OCP\ITempManager;
use OCP\IURLGenerator;
/**
@@ -62,175 +42,202 @@ use OCP\IURLGenerator;
* @package OCA\Theming\Controller
*/
class ThemingController extends Controller {
- /** @var ThemingDefaults */
- private $themingDefaults;
- /** @var Util */
- private $util;
- /** @var ITimeFactory */
- private $timeFactory;
- /** @var IL10N */
- private $l10n;
- /** @var IConfig */
- private $config;
- /** @var ITempManager */
- private $tempManager;
- /** @var IAppData */
- private $appData;
- /** @var SCSSCacher */
- private $scssCacher;
- /** @var IURLGenerator */
- private $urlGenerator;
+ public const VALID_UPLOAD_KEYS = ['header', 'logo', 'logoheader', 'background', 'favicon'];
- /**
- * ThemingController constructor.
- *
- * @param string $appName
- * @param IRequest $request
- * @param IConfig $config
- * @param ThemingDefaults $themingDefaults
- * @param Util $util
- * @param ITimeFactory $timeFactory
- * @param IL10N $l
- * @param ITempManager $tempManager
- * @param IAppData $appData
- * @param SCSSCacher $scssCacher
- * @param IURLGenerator $urlGenerator
- */
public function __construct(
- $appName,
+ string $appName,
IRequest $request,
- IConfig $config,
- ThemingDefaults $themingDefaults,
- Util $util,
- ITimeFactory $timeFactory,
- IL10N $l,
- ITempManager $tempManager,
- IAppData $appData,
- SCSSCacher $scssCacher,
- IURLGenerator $urlGenerator
+ 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->util = $util;
- $this->timeFactory = $timeFactory;
- $this->l10n = $l;
- $this->config = $config;
- $this->tempManager = $tempManager;
- $this->appData = $appData;
- $this->scssCacher = $scssCacher;
- $this->urlGenerator = $urlGenerator;
}
/**
* @param string $setting
* @param string $value
* @return DataResponse
- * @internal param string $color
+ * @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) {
- return new DataResponse([
- 'data' => [
- 'message' => $this->l10n->t('The given name is too long'),
- ],
- 'status' => 'error'
- ]);
+ $error = $this->l10n->t('The given name is too long');
}
break;
case 'url':
if (strlen($value) > 500) {
- return new DataResponse([
- 'data' => [
- 'message' => $this->l10n->t('The given web address is too long'),
- ],
- 'status' => 'error'
- ]);
+ $error = $this->l10n->t('The given web address is too long');
+ }
+ if (!$this->isValidUrl($value)) {
+ $error = $this->l10n->t('The given web address is not a valid URL');
+ }
+ break;
+ case 'imprintUrl':
+ if (strlen($value) > 500) {
+ $error = $this->l10n->t('The given legal notice address is too long');
+ }
+ if (!$this->isValidUrl($value)) {
+ $error = $this->l10n->t('The given legal notice address is not a valid URL');
+ }
+ break;
+ case 'privacyUrl':
+ if (strlen($value) > 500) {
+ $error = $this->l10n->t('The given privacy policy address is too long');
+ }
+ if (!$this->isValidUrl($value)) {
+ $error = $this->l10n->t('The given privacy policy address is not a valid URL');
}
break;
case 'slogan':
if (strlen($value) > 500) {
- return new DataResponse([
- 'data' => [
- 'message' => $this->l10n->t('The given slogan is too long'),
- ],
- 'status' => 'error'
- ]);
+ $error = $this->l10n->t('The given slogan is too long');
+ }
+ break;
+ 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 'color':
+ case 'background_color':
if (!preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $value)) {
- return new DataResponse([
- 'data' => [
- 'message' => $this->l10n->t('The given color is invalid'),
- ],
- 'status' => 'error'
- ]);
+ $error = $this->l10n->t('The given color is invalid');
+ } else {
+ $this->appConfig->setAppValueString('background_color', $value);
+ $saved = true;
+ }
+ break;
+ case 'disable-user-theming':
+ 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;
}
+ if ($error !== null) {
+ return new DataResponse([
+ 'data' => [
+ 'message' => $error,
+ ],
+ 'status' => 'error'
+ ], Http::STATUS_BAD_REQUEST);
+ }
- $this->themingDefaults->set($setting, $value);
+ if (!$saved) {
+ $this->themingDefaults->set($setting, $value);
+ }
- // reprocess server scss for preview
- $cssCached = $this->scssCacher->process(\OC::$SERVERROOT, 'core/css/server.scss', 'core');
+ return new DataResponse([
+ 'data' => [
+ 'message' => $this->l10n->t('Saved'),
+ ],
+ 'status' => 'success'
+ ]);
+ }
- return new DataResponse(
- [
- 'data' =>
- [
- 'message' => $this->l10n->t('Saved'),
- 'serverCssUrl' => $this->urlGenerator->linkTo('', $this->scssCacher->getCachedSCSS('core', '/core/css/server.scss'))
- ],
- 'status' => 'success'
- ]
- );
+ /**
+ * @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->navigationManager->setDefaultEntryIds($value);
+ } catch (InvalidArgumentException $e) {
+ $error = $this->l10n->t('Invalid app given');
+ }
+ } else {
+ $error = $this->l10n->t('Invalid type for setting "defaultApp" given');
+ }
+ break;
+ default:
+ $error = $this->l10n->t('Invalid setting key');
+ }
+ if ($error !== null) {
+ return new DataResponse([
+ 'data' => [
+ 'message' => $error,
+ ],
+ 'status' => 'error'
+ ], Http::STATUS_BAD_REQUEST);
+ }
+
+ return new DataResponse([
+ 'data' => [
+ 'message' => $this->l10n->t('Saved'),
+ ],
+ 'status' => 'success'
+ ]);
+ }
+
+ /**
+ * 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)
+ && !str_contains($url, '"');
}
/**
- * Update the logos and background image
- *
* @return DataResponse
+ * @throws NotPermittedException
*/
- public function updateLogo() {
- $backgroundColor = $this->request->getParam('backgroundColor', false);
- if($backgroundColor) {
- $this->themingDefaults->set('backgroundMime', 'backgroundColor');
+ #[AuthorizedAdminSetting(settings: Admin::class)]
+ public function uploadImage(): DataResponse {
+ $key = $this->request->getParam('key');
+ if (!in_array($key, self::VALID_UPLOAD_KEYS, true)) {
return new DataResponse(
[
- 'data' =>
- [
- 'name' => 'backgroundColor',
- 'message' => $this->l10n->t('Saved')
- ],
- 'status' => 'success'
- ]
+ 'data' => [
+ 'message' => 'Invalid key'
+ ],
+ 'status' => 'failure',
+ ],
+ Http::STATUS_BAD_REQUEST
);
}
- $newLogo = $this->request->getUploadedFile('uploadlogo');
- $newBackgroundLogo = $this->request->getUploadedFile('upload-login-background');
+ $image = $this->request->getUploadedFile('image');
$error = null;
$phpFileUploadErrors = [
- UPLOAD_ERR_OK => $this->l10n->t('There is no error, the file uploaded with success'),
+ UPLOAD_ERR_OK => $this->l10n->t('The file was uploaded'),
UPLOAD_ERR_INI_SIZE => $this->l10n->t('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
UPLOAD_ERR_FORM_SIZE => $this->l10n->t('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
- UPLOAD_ERR_PARTIAL => $this->l10n->t('The uploaded file was only partially uploaded'),
+ UPLOAD_ERR_PARTIAL => $this->l10n->t('The file was only partially uploaded'),
UPLOAD_ERR_NO_FILE => $this->l10n->t('No file was uploaded'),
UPLOAD_ERR_NO_TMP_DIR => $this->l10n->t('Missing a temporary folder'),
- UPLOAD_ERR_CANT_WRITE => $this->l10n->t('Failed to write file to disk.'),
- UPLOAD_ERR_EXTENSION => $this->l10n->t('A PHP extension stopped the file upload.'),
+ UPLOAD_ERR_CANT_WRITE => $this->l10n->t('Could not write file to disk'),
+ UPLOAD_ERR_EXTENSION => $this->l10n->t('A PHP extension stopped the file upload'),
];
- if (empty($newLogo) && empty($newBackgroundLogo)) {
+ if (empty($image)) {
$error = $this->l10n->t('No file uploaded');
}
- if (!empty($newLogo) && array_key_exists('error', $newLogo) && $newLogo['error'] !== UPLOAD_ERR_OK) {
- $error = $phpFileUploadErrors[$newLogo['error']];
- }
- if (!empty($newBackgroundLogo) && array_key_exists('error', $newBackgroundLogo) && $newBackgroundLogo['error'] !== UPLOAD_ERR_OK) {
- $error = $phpFileUploadErrors[$newBackgroundLogo['error']];
+ if (!empty($image) && array_key_exists('error', $image) && $image['error'] !== UPLOAD_ERR_OK) {
+ $error = $phpFileUploadErrors[$image['error']];
}
if ($error !== null) {
@@ -245,70 +252,30 @@ class ThemingController extends Controller {
);
}
- $name = '';
try {
- $folder = $this->appData->getFolder('images');
- } catch (NotFoundException $e) {
- $folder = $this->appData->newFolder('images');
- }
-
- if (!empty($newLogo)) {
- $target = $folder->newFile('logo');
- $supportedFormats = ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml', 'text/svg'];
- if (!in_array($newLogo['type'], $supportedFormats)) {
- return new DataResponse(
- [
- 'data' => [
- 'message' => $this->l10n->t('Unsupported image type'),
- ],
- 'status' => 'failure',
+ $mime = $this->imageManager->updateImage($key, $image['tmp_name']);
+ $this->themingDefaults->set($key . 'Mime', $mime);
+ } catch (\Exception $e) {
+ return new DataResponse(
+ [
+ 'data' => [
+ 'message' => $e->getMessage()
],
- Http::STATUS_UNPROCESSABLE_ENTITY
- );
- }
- $target->putContent(file_get_contents($newLogo['tmp_name'], 'r'));
- $this->themingDefaults->set('logoMime', $newLogo['type']);
- $name = $newLogo['name'];
+ 'status' => 'failure',
+ ],
+ Http::STATUS_UNPROCESSABLE_ENTITY
+ );
}
- if (!empty($newBackgroundLogo)) {
- $target = $folder->newFile('background');
- $image = @imagecreatefromstring(file_get_contents($newBackgroundLogo['tmp_name'], 'r'));
- if ($image === false) {
- return new DataResponse(
- [
- 'data' => [
- 'message' => $this->l10n->t('Unsupported image type'),
- ],
- 'status' => 'failure',
- ],
- Http::STATUS_UNPROCESSABLE_ENTITY
- );
- }
- // Optimize the image since some people may upload images that will be
- // either to big or are not progressive rendering.
- $tmpFile = $this->tempManager->getTemporaryFile();
- if (function_exists('imagescale')) {
- // FIXME: Once PHP 5.5.0 is a requirement the above check can be removed
- // Workaround for https://bugs.php.net/bug.php?id=65171
- $newHeight = imagesy($image) / (imagesx($image) / 1920);
- $image = imagescale($image, 1920, $newHeight);
- }
- imageinterlace($image, 1);
- imagejpeg($image, $tmpFile, 75);
- imagedestroy($image);
-
- $target->putContent(file_get_contents($tmpFile, 'r'));
- $this->themingDefaults->set('backgroundMime', $newBackgroundLogo['type']);
- $name = $newBackgroundLogo['name'];
- }
+ $name = $image['name'];
return new DataResponse(
[
- 'data' =>
- [
+ 'data'
+ => [
'name' => $name,
- 'message' => $this->l10n->t('Saved')
+ 'url' => $this->imageManager->getImageUrl($key),
+ 'message' => $this->l10n->t('Saved'),
],
'status' => 'success'
]
@@ -320,36 +287,18 @@ class ThemingController extends Controller {
*
* @param string $setting setting which should be reverted
* @return DataResponse
+ * @throws NotPermittedException
*/
- public function undo($setting) {
+ #[AuthorizedAdminSetting(settings: Admin::class)]
+ public function undo(string $setting): DataResponse {
$value = $this->themingDefaults->undo($setting);
- // reprocess server scss for preview
- $cssCached = $this->scssCacher->process(\OC::$SERVERROOT, 'core/css/server.scss', 'core');
-
- if($setting === 'logoMime') {
- try {
- $file = $this->appData->getFolder('images')->getFile('logo');
- $file->delete();
- } catch (NotFoundException $e) {
- } catch (NotPermittedException $e) {
- }
- }
- if($setting === 'backgroundMime') {
- try {
- $file = $this->appData->getFolder('images')->getFile('background');
- $file->delete();
- } catch (NotFoundException $e) {
- } catch (NotPermittedException $e) {
- }
- }
return new DataResponse(
[
- 'data' =>
- [
+ 'data'
+ => [
'value' => $value,
'message' => $this->l10n->t('Saved'),
- 'serverCssUrl' => $this->urlGenerator->linkTo('', $this->scssCacher->getCachedSCSS('core', '/core/css/server.scss'))
],
'status' => 'success'
]
@@ -357,81 +306,118 @@ class ThemingController extends Controller {
}
/**
- * @PublicPage
- * @NoCSRFRequired
+ * Revert all theming settings to their default values
*
- * @return FileDisplayResponse|NotFoundResponse
+ * @return DataResponse
+ * @throws NotPermittedException
*/
- public function getLogo() {
- try {
- /** @var File $file */
- $file = $this->appData->getFolder('images')->getFile('logo');
- } catch (NotFoundException $e) {
- return new NotFoundResponse();
- }
+ #[AuthorizedAdminSetting(settings: Admin::class)]
+ public function undoAll(): DataResponse {
+ $this->themingDefaults->undoAll();
+ $this->navigationManager->setDefaultEntryIds([]);
- $response = new FileDisplayResponse($file);
- $response->cacheFor(3600);
- $expires = new \DateTime();
- $expires->setTimestamp($this->timeFactory->getTime());
- $expires->add(new \DateInterval('PT24H'));
- $response->addHeader('Expires', $expires->format(\DateTime::RFC2822));
- $response->addHeader('Pragma', 'cache');
- $response->addHeader('Content-Type', $this->config->getAppValue($this->appName, 'logoMime', ''));
- return $response;
+ return new DataResponse(
+ [
+ 'data'
+ => [
+ 'message' => $this->l10n->t('Saved'),
+ ],
+ 'status' => 'success'
+ ]
+ );
}
/**
- * @PublicPage
- * @NoCSRFRequired
+ * @NoSameSiteCookieRequired
*
- * @return FileDisplayResponse|NotFoundResponse
+ * Get an image
+ *
+ * @param string $key Key of the image
+ * @param bool $useSvg Return image as SVG
+ * @return FileDisplayResponse<Http::STATUS_OK, array{}>|NotFoundResponse<Http::STATUS_NOT_FOUND, array{}>
+ * @throws NotPermittedException
+ *
+ * 200: Image returned
+ * 404: Image not found
*/
- public function getLoginBackground() {
+ #[PublicPage]
+ #[NoCSRFRequired]
+ #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
+ public function getImage(string $key, bool $useSvg = true) {
try {
- /** @var File $file */
- $file = $this->appData->getFolder('images')->getFile('background');
+ $file = $this->imageManager->getImage($key, $useSvg);
} catch (NotFoundException $e) {
return new NotFoundResponse();
}
$response = new FileDisplayResponse($file);
+ $csp = new ContentSecurityPolicy();
+ $csp->allowInlineStyle();
+ $response->setContentSecurityPolicy($csp);
$response->cacheFor(3600);
- $expires = new \DateTime();
- $expires->setTimestamp($this->timeFactory->getTime());
- $expires->add(new \DateInterval('PT24H'));
- $response->addHeader('Expires', $expires->format(\DateTime::RFC2822));
- $response->addHeader('Pragma', 'cache');
- $response->addHeader('Content-Type', $this->config->getAppValue($this->appName, 'backgroundMime', ''));
+ $response->addHeader('Content-Type', $this->config->getAppValue($this->appName, $key . 'Mime', ''));
+ $response->addHeader('Content-Disposition', 'attachment; filename="' . $key . '"');
+ if (!$useSvg) {
+ $response->addHeader('Content-Type', 'image/png');
+ } else {
+ $response->addHeader('Content-Type', $this->config->getAppValue($this->appName, $key . 'Mime', ''));
+ }
return $response;
}
/**
- * @NoCSRFRequired
- * @PublicPage
+ * @NoSameSiteCookieRequired
+ * @NoTwoFactorRequired
+ *
+ * Get the CSS stylesheet for a theme
*
- * @return FileDisplayResponse|NotFoundResponse
+ * @param string $themeId ID of the theme
+ * @param bool $plain Let the browser decide the CSS priority
+ * @param bool $withCustomCss Include custom CSS
+ * @return DataDisplayResponse<Http::STATUS_OK, array{Content-Type: 'text/css'}>|NotFoundResponse<Http::STATUS_NOT_FOUND, array{}>
+ *
+ * 200: Stylesheet returned
+ * 404: Theme not found
*/
- public function getStylesheet() {
- $appPath = substr(\OC::$server->getAppManager()->getAppPath('theming'), strlen(\OC::$SERVERROOT) + 1);
- /* SCSSCacher is required here
- * We cannot rely on automatic caching done by \OC_Util::addStyle,
- * since we need to add the cacheBuster value to the url
- */
- $cssCached = $this->scssCacher->process(\OC::$SERVERROOT, $appPath . '/css/theming.scss', 'theming');
- if(!$cssCached) {
+ #[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))) {
return new NotFoundResponse();
}
+ $theme = $themes[$themeId];
+ $customCss = $theme->getCustomCss();
+
+ // Generate variables
+ $variables = '';
+ foreach ($theme->getCSSVariables() as $variable => $value) {
+ $variables .= "$variable:$value; ";
+ };
+
+ // If plain is set, the browser decides of the css priority
+ if ($plain) {
+ $css = ":root { $variables } " . $customCss;
+ } else {
+ // If not set, we'll rely on the body class
+ // 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 {
- $cssFile = $this->scssCacher->getCachedCSS('theming', 'theming.css');
- $response = new FileDisplayResponse($cssFile, Http::STATUS_OK, ['Content-Type' => 'text/css']);
+ $response = new DataDisplayResponse($css, Http::STATUS_OK, ['Content-Type' => 'text/css']);
$response->cacheFor(86400);
- $expires = new \DateTime();
- $expires->setTimestamp($this->timeFactory->getTime());
- $expires->add(new \DateInterval('PT24H'));
- $response->addHeader('Expires', $expires->format(\DateTime::RFC1123));
- $response->addHeader('Pragma', 'cache');
return $response;
} catch (NotFoundException $e) {
return new NotFoundResponse();
@@ -439,61 +425,73 @@ class ThemingController extends Controller {
}
/**
- * @NoCSRFRequired
- * @PublicPage
+ * Get the manifest for an app
*
- * @return DataDownloadResponse
- */
- public function getJavascript() {
- $cacheBusterValue = $this->config->getAppValue('theming', 'cachebuster', '0');
- $responseJS = '(function() {
- OCA.Theming = {
- name: ' . json_encode($this->themingDefaults->getName()) . ',
- url: ' . json_encode($this->themingDefaults->getBaseUrl()) . ',
- slogan: ' . json_encode($this->themingDefaults->getSlogan()) . ',
- color: ' . json_encode($this->themingDefaults->getColorPrimary()) . ',
- inverted: ' . json_encode($this->util->invertTextColor($this->themingDefaults->getColorPrimary())) . ',
- cacheBuster: ' . json_encode($cacheBusterValue) . '
- };
-})();';
- $response = new DataDownloadResponse($responseJS, 'javascript', 'text/javascript');
- $response->addHeader('Expires', date(\DateTime::RFC2822, $this->timeFactory->getTime()));
- $response->addHeader('Pragma', 'cache');
- $response->cacheFor(3600);
- return $response;
- }
-
- /**
- * @NoCSRFRequired
- * @PublicPage
+ * @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: list<array{src: non-empty-string, type: string, sizes: string}>, display_override: list<string>, display: string}, array{}>|JSONResponse<Http::STATUS_NOT_FOUND, array{}, array{}>
*
- * @return Http\JSONResponse
+ * 200: Manifest returned
+ * 404: App not found
*/
- public function getManifest($app) {
+ #[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') {
+ $name = $this->themingDefaults->getName();
+ $shortName = $this->themingDefaults->getName();
+ $startUrl = $this->urlGenerator->getBaseUrl();
+ $description = $this->themingDefaults->getSlogan();
+ } else {
+ if (!$this->appManager->isEnabledForUser($app)) {
+ $response = new JSONResponse([], Http::STATUS_NOT_FOUND);
+ $response->throttle(['action' => 'manifest', 'app' => $app]);
+ return $response;
+ }
+
+ $info = $this->appManager->getAppInfo($app, false, $this->l10n->getLanguageCode());
+ $name = $info['name'] . ' - ' . $this->themingDefaults->getName();
+ $shortName = $info['name'];
+ if (str_contains($this->request->getRequestUri(), '/index.php/')) {
+ $startUrl = $this->urlGenerator->getBaseUrl() . '/index.php/apps/' . $app . '/';
+ } else {
+ $startUrl = $this->urlGenerator->getBaseUrl() . '/apps/' . $app . '/';
+ }
+ $description = $info['summary'] ?? '';
+ }
+ /**
+ * @var string $description
+ * @var string $shortName
+ */
$responseJS = [
- 'name' => $this->themingDefaults->getName(),
- 'start_url' => $this->urlGenerator->getBaseUrl(),
- 'icons' =>
- [
+ 'name' => $name,
+ 'short_name' => $shortName,
+ 'start_url' => $startUrl,
+ 'theme_color' => $this->themingDefaults->getColorPrimary(),
+ 'background_color' => $this->themingDefaults->getColorPrimary(),
+ 'description' => $description,
+ 'icons'
+ => [
[
'src' => $this->urlGenerator->linkToRoute('theming.Icon.getTouchIcon',
- ['app' => $app]) . '?v=' . $cacheBusterValue,
- 'type'=> 'image/png',
- 'sizes'=> '128x128'
+ ['app' => $app]) . '?v=' . $cacheBusterValue,
+ 'type' => 'image/png',
+ 'sizes' => '512x512'
],
[
'src' => $this->urlGenerator->linkToRoute('theming.Icon.getFavicon',
- ['app' => $app]) . '?v=' . $cacheBusterValue,
+ ['app' => $app]) . '?v=' . $cacheBusterValue,
'type' => 'image/svg+xml',
'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 Http\JSONResponse($responseJS);
- $response->addHeader('Expires', date(\DateTime::RFC2822, $this->timeFactory->getTime()));
- $response->addHeader('Pragma', 'cache');
+ $response = new JSONResponse($responseJS);
$response->cacheFor(3600);
return $response;
}
diff --git a/apps/theming/lib/Controller/UserThemeController.php b/apps/theming/lib/Controller/UserThemeController.php
new file mode 100644
index 00000000000..770f2ca922f
--- /dev/null
+++ b/apps/theming/lib/Controller/UserThemeController.php
@@ -0,0 +1,218 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Theming\Controller;
+
+use OCA\Theming\AppInfo\Application;
+use OCA\Theming\ITheme;
+use OCA\Theming\ResponseDefinitions;
+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;
+use OCP\IConfig;
+use OCP\IRequest;
+use OCP\IUserSession;
+use OCP\PreConditionNotMetException;
+
+/**
+ * @psalm-import-type ThemingBackground from ResponseDefinitions
+ */
+class UserThemeController extends OCSController {
+
+ protected ?string $userId = null;
+
+ public function __construct(
+ string $appName,
+ IRequest $request,
+ private IConfig $config,
+ IUserSession $userSession,
+ private ThemesService $themesService,
+ private ThemingDefaults $themingDefaults,
+ private BackgroundService $backgroundService,
+ ) {
+ parent::__construct($appName, $request);
+
+ $user = $userSession->getUser();
+ if ($user !== null) {
+ $this->userId = $user->getUID();
+ }
+ }
+
+ /**
+ * Enable theme
+ *
+ * @param string $themeId the theme ID
+ * @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);
+
+ // Enable selected theme
+ $this->themesService->enableTheme($theme);
+ return new DataResponse();
+ }
+
+ /**
+ * Disable theme
+ *
+ * @param string $themeId the theme ID
+ * @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);
+
+ // Enable selected theme
+ $this->themesService->disableTheme($theme);
+ return new DataResponse();
+ }
+
+ /**
+ * Validate and return the matching ITheme
+ *
+ * Disable theme
+ *
+ * @param string $themeId the theme ID
+ * @return ITheme
+ * @throws OCSBadRequestException
+ * @throws PreConditionNotMetException
+ */
+ private function validateTheme(string $themeId): ITheme {
+ if ($themeId === '' || !$themeId) {
+ throw new OCSBadRequestException('Invalid theme id: ' . $themeId);
+ }
+
+ $themes = $this->themesService->getThemes();
+ if (!isset($themes[$themeId])) {
+ throw new OCSBadRequestException('Invalid theme id: ' . $themeId);
+ }
+
+ // If trying to toggle another theme but this is enforced
+ if ($this->config->getSystemValueString('enforce_theme', '') !== ''
+ && $themes[$themeId]->getType() === ITheme::TYPE_THEME) {
+ throw new OCSForbiddenException('Theme switching is disabled');
+ }
+
+ return $themes[$themeId];
+ }
+
+ /**
+ * 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
+ */
+ #[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()]);
+ $response->cacheFor(24 * 60 * 60, false, true);
+ return $response;
+ }
+ return new NotFoundResponse();
+ }
+
+ /**
+ * Delete the background
+ *
+ * @return JSONResponse<Http::STATUS_OK, ThemingBackground, array{}>
+ *
+ * 200: Background deleted successfully
+ */
+ #[NoAdminRequired]
+ public function deleteBackground(): JSONResponse {
+ $currentVersion = (int)$this->config->getUserValue($this->userId, Application::APP_ID, 'userCacheBuster', '0');
+ $this->backgroundService->deleteBackgroundImage();
+ return new JSONResponse([
+ 'backgroundImage' => null,
+ 'backgroundColor' => $this->themingDefaults->getColorBackground(),
+ 'primaryColor' => $this->themingDefaults->getColorPrimary(),
+ 'version' => $currentVersion,
+ ]);
+ }
+
+ /**
+ * Set the background
+ *
+ * @param string $type Type of background
+ * @param string $value Path of the background image
+ * @param string|null $color Color for the background
+ * @return JSONResponse<Http::STATUS_OK, ThemingBackground, array{}>|JSONResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_INTERNAL_SERVER_ERROR, array{error: string}, array{}>
+ *
+ * 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');
+
+ // Set color if provided
+ if ($color) {
+ $this->backgroundService->setColorBackground($color);
+ }
+
+ // Set background image if provided
+ try {
+ switch ($type) {
+ case BackgroundService::BACKGROUND_SHIPPED:
+ $this->backgroundService->setShippedBackground($value);
+ break;
+ case BackgroundService::BACKGROUND_CUSTOM:
+ $this->backgroundService->setFileBackground($value);
+ break;
+ case BackgroundService::BACKGROUND_DEFAULT:
+ // Delete both background and color keys
+ $this->backgroundService->setDefaultBackground();
+ break;
+ default:
+ if (!$color) {
+ return new JSONResponse(['error' => 'Invalid type provided'], Http::STATUS_BAD_REQUEST);
+ }
+ }
+ } catch (\InvalidArgumentException $e) {
+ return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
+ } catch (\Throwable $e) {
+ return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR);
+ }
+
+ $currentVersion++;
+ $this->config->setUserValue($this->userId, Application::APP_ID, 'userCacheBuster', (string)$currentVersion);
+
+ return new JSONResponse([
+ 'backgroundImage' => $this->config->getUserValue($this->userId, Application::APP_ID, 'background_image', BackgroundService::BACKGROUND_DEFAULT),
+ 'backgroundColor' => $this->themingDefaults->getColorBackground(),
+ 'primaryColor' => $this->themingDefaults->getColorPrimary(),
+ 'version' => $currentVersion,
+ ]);
+ }
+}