aboutsummaryrefslogtreecommitdiffstats
path: root/apps/theming/lib/Controller/ThemingController.php
diff options
context:
space:
mode:
Diffstat (limited to 'apps/theming/lib/Controller/ThemingController.php')
-rw-r--r--apps/theming/lib/Controller/ThemingController.php139
1 files changed, 79 insertions, 60 deletions
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);