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.php43
-rw-r--r--apps/theming/lib/Controller/ThemingController.php139
-rw-r--r--apps/theming/lib/Controller/UserThemeController.php49
3 files changed, 117 insertions, 114 deletions
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');