Passed
Push — master ( 8555d5...a51f0d )
by John
14:05 queued 13s
created

ThemingController::undoAll()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 0
dl 0
loc 10
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016 Bjoern Schiessle <[email protected]>
4
 * @copyright Copyright (c) 2016 Lukas Reschke <[email protected]>
5
 *
6
 * @author Arthur Schiwon <[email protected]>
7
 * @author Bjoern Schiessle <[email protected]>
8
 * @author Christoph Wurst <[email protected]>
9
 * @author Daniel Calviño Sánchez <[email protected]>
10
 * @author Jan-Christoph Borchardt <[email protected]>
11
 * @author Joas Schilling <[email protected]>
12
 * @author Julius Haertl <[email protected]>
13
 * @author Julius Härtl <[email protected]>
14
 * @author Kyle Fazzari <[email protected]>
15
 * @author Lukas Reschke <[email protected]>
16
 * @author nhirokinet <[email protected]>
17
 * @author rakekniven <[email protected]>
18
 * @author Robin Appelman <[email protected]>
19
 * @author Roeland Jago Douma <[email protected]>
20
 * @author Thomas Citharel <[email protected]>
21
 *
22
 * @license GNU AGPL version 3 or any later version
23
 *
24
 * This program is free software: you can redistribute it and/or modify
25
 * it under the terms of the GNU Affero General Public License as
26
 * published by the Free Software Foundation, either version 3 of the
27
 * License, or (at your option) any later version.
28
 *
29
 * This program is distributed in the hope that it will be useful,
30
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
31
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
32
 * GNU Affero General Public License for more details.
33
 *
34
 * You should have received a copy of the GNU Affero General Public License
35
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
36
 *
37
 */
38
namespace OCA\Theming\Controller;
39
40
use OCA\Theming\ImageManager;
41
use OCA\Theming\Service\ThemesService;
42
use OCA\Theming\ThemingDefaults;
43
use OCP\App\IAppManager;
44
use OCP\AppFramework\Controller;
45
use OCP\AppFramework\Http;
46
use OCP\AppFramework\Http\DataDisplayResponse;
47
use OCP\AppFramework\Http\DataResponse;
48
use OCP\AppFramework\Http\FileDisplayResponse;
49
use OCP\AppFramework\Http\NotFoundResponse;
50
use OCP\Files\IAppData;
51
use OCP\Files\NotFoundException;
52
use OCP\Files\NotPermittedException;
53
use OCP\IConfig;
54
use OCP\IL10N;
55
use OCP\IRequest;
56
use OCP\ITempManager;
57
use OCP\IURLGenerator;
58
use ScssPhp\ScssPhp\Compiler;
59
60
/**
61
 * Class ThemingController
62
 *
63
 * handle ajax requests to update the theme
64
 *
65
 * @package OCA\Theming\Controller
66
 */
67
class ThemingController extends Controller {
68
	private ThemingDefaults $themingDefaults;
69
	private IL10N $l10n;
70
	private IConfig $config;
71
	private ITempManager $tempManager;
72
	private IAppData $appData;
73
	private IURLGenerator $urlGenerator;
74
	private IAppManager $appManager;
75
	private ImageManager $imageManager;
76
	private ThemesService $themesService;
77
78
	public function __construct(
79
		$appName,
80
		IRequest $request,
81
		IConfig $config,
82
		ThemingDefaults $themingDefaults,
83
		IL10N $l,
84
		ITempManager $tempManager,
85
		IAppData $appData,
86
		IURLGenerator $urlGenerator,
87
		IAppManager $appManager,
88
		ImageManager $imageManager,
89
		ThemesService $themesService
90
	) {
91
		parent::__construct($appName, $request);
92
93
		$this->themingDefaults = $themingDefaults;
94
		$this->l10n = $l;
95
		$this->config = $config;
96
		$this->tempManager = $tempManager;
97
		$this->appData = $appData;
98
		$this->urlGenerator = $urlGenerator;
99
		$this->appManager = $appManager;
100
		$this->imageManager = $imageManager;
101
		$this->themesService = $themesService;
102
	}
103
104
	/**
105
	 * @AuthorizedAdminSetting(settings=OCA\Theming\Settings\Admin)
106
	 * @param string $setting
107
	 * @param string $value
108
	 * @return DataResponse
109
	 * @throws NotPermittedException
110
	 */
111
	public function updateStylesheet($setting, $value) {
112
		$value = trim($value);
113
		$error = null;
114
		switch ($setting) {
115
			case 'name':
116
				if (strlen($value) > 250) {
117
					$error = $this->l10n->t('The given name is too long');
118
				}
119
				break;
120
			case 'url':
121
				if (strlen($value) > 500) {
122
					$error = $this->l10n->t('The given web address is too long');
123
				}
124
				if (!$this->isValidUrl($value)) {
125
					$error = $this->l10n->t('The given web address is not a valid URL');
126
				}
127
				break;
128
			case 'imprintUrl':
129
				if (strlen($value) > 500) {
130
					$error = $this->l10n->t('The given legal notice address is too long');
131
				}
132
				if (!$this->isValidUrl($value)) {
133
					$error = $this->l10n->t('The given legal notice address is not a valid URL');
134
				}
135
				break;
136
			case 'privacyUrl':
137
				if (strlen($value) > 500) {
138
					$error = $this->l10n->t('The given privacy policy address is too long');
139
				}
140
				if (!$this->isValidUrl($value)) {
141
					$error = $this->l10n->t('The given privacy policy address is not a valid URL');
142
				}
143
				break;
144
			case 'slogan':
145
				if (strlen($value) > 500) {
146
					$error = $this->l10n->t('The given slogan is too long');
147
				}
148
				break;
149
			case 'color':
150
				if (!preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $value)) {
151
					$error = $this->l10n->t('The given color is invalid');
152
				}
153
				break;
154
			case 'disable-user-theming':
155
				if ($value !== "yes" && $value !== "no") {
156
					$error = $this->l10n->t('Disable-user-theming should be true or false');
157
				}
158
				break;
159
		}
160
		if ($error !== null) {
161
			return new DataResponse([
162
				'data' => [
163
					'message' => $error,
164
				],
165
				'status' => 'error'
166
			], Http::STATUS_BAD_REQUEST);
167
		}
168
169
		$this->themingDefaults->set($setting, $value);
170
171
		return new DataResponse([
172
			'data' => [
173
				'message' => $this->l10n->t('Saved'),
174
			],
175
			'status' => 'success'
176
		]);
177
	}
178
179
	/**
180
	 * Check that a string is a valid http/https url
181
	 */
182
	private function isValidUrl(string $url): bool {
183
		return ((strpos($url, 'http://') === 0 || strpos($url, 'https://') === 0) &&
184
			filter_var($url, FILTER_VALIDATE_URL) !== false);
185
	}
186
187
	/**
188
	 * @AuthorizedAdminSetting(settings=OCA\Theming\Settings\Admin)
189
	 * @return DataResponse
190
	 * @throws NotPermittedException
191
	 */
192
	public function uploadImage(): DataResponse {
193
		$key = $this->request->getParam('key');
194
		$image = $this->request->getUploadedFile('image');
195
		$error = null;
196
		$phpFileUploadErrors = [
197
			UPLOAD_ERR_OK => $this->l10n->t('The file was uploaded'),
198
			UPLOAD_ERR_INI_SIZE => $this->l10n->t('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
199
			UPLOAD_ERR_FORM_SIZE => $this->l10n->t('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
200
			UPLOAD_ERR_PARTIAL => $this->l10n->t('The file was only partially uploaded'),
201
			UPLOAD_ERR_NO_FILE => $this->l10n->t('No file was uploaded'),
202
			UPLOAD_ERR_NO_TMP_DIR => $this->l10n->t('Missing a temporary folder'),
203
			UPLOAD_ERR_CANT_WRITE => $this->l10n->t('Could not write file to disk'),
204
			UPLOAD_ERR_EXTENSION => $this->l10n->t('A PHP extension stopped the file upload'),
205
		];
206
		if (empty($image)) {
207
			$error = $this->l10n->t('No file uploaded');
208
		}
209
		if (!empty($image) && array_key_exists('error', $image) && $image['error'] !== UPLOAD_ERR_OK) {
210
			$error = $phpFileUploadErrors[$image['error']];
211
		}
212
213
		if ($error !== null) {
214
			return new DataResponse(
215
				[
216
					'data' => [
217
						'message' => $error
218
					],
219
					'status' => 'failure',
220
				],
221
				Http::STATUS_UNPROCESSABLE_ENTITY
222
			);
223
		}
224
225
		try {
226
			$mime = $this->imageManager->updateImage($key, $image['tmp_name']);
227
			$this->themingDefaults->set($key . 'Mime', $mime);
228
		} catch (\Exception $e) {
229
			return new DataResponse(
230
				[
231
					'data' => [
232
						'message' => $e->getMessage()
233
					],
234
					'status' => 'failure',
235
				],
236
				Http::STATUS_UNPROCESSABLE_ENTITY
237
			);
238
		}
239
240
		$name = $image['name'];
241
242
		return new DataResponse(
243
			[
244
				'data' =>
245
					[
246
						'name' => $name,
247
						'url' => $this->imageManager->getImageUrl($key),
248
						'message' => $this->l10n->t('Saved'),
249
					],
250
				'status' => 'success'
251
			]
252
		);
253
	}
254
255
	/**
256
	 * Revert setting to default value
257
	 * @AuthorizedAdminSetting(settings=OCA\Theming\Settings\Admin)
258
	 *
259
	 * @param string $setting setting which should be reverted
260
	 * @return DataResponse
261
	 * @throws NotPermittedException
262
	 */
263
	public function undo(string $setting): DataResponse {
264
		$value = $this->themingDefaults->undo($setting);
265
266
		return new DataResponse(
267
			[
268
				'data' =>
269
					[
270
						'value' => $value,
271
						'message' => $this->l10n->t('Saved'),
272
					],
273
				'status' => 'success'
274
			]
275
		);
276
	}
277
278
	/**
279
	 * Revert all theming settings to their default values
280
	 * @AuthorizedAdminSetting(settings=OCA\Theming\Settings\Admin)
281
	 *
282
	 * @return DataResponse
283
	 * @throws NotPermittedException
284
	 */
285
	public function undoAll(): DataResponse {
286
		$this->themingDefaults->undoAll();
287
288
		return new DataResponse(
289
			[
290
				'data' =>
291
					[
292
						'message' => $this->l10n->t('Saved'),
293
					],
294
				'status' => 'success'
295
			]
296
		);
297
	}
298
299
	/**
300
	 * @PublicPage
301
	 * @NoCSRFRequired
302
	 * @NoSameSiteCookieRequired
303
	 *
304
	 * @param string $key
305
	 * @param bool $useSvg
306
	 * @return FileDisplayResponse|NotFoundResponse
307
	 * @throws NotPermittedException
308
	 */
309
	public function getImage(string $key, bool $useSvg = true) {
310
		try {
311
			$file = $this->imageManager->getImage($key, $useSvg);
312
		} catch (NotFoundException $e) {
313
			return new NotFoundResponse();
314
		}
315
316
		$response = new FileDisplayResponse($file);
317
		$csp = new Http\ContentSecurityPolicy();
318
		$csp->allowInlineStyle();
319
		$response->setContentSecurityPolicy($csp);
320
		$response->cacheFor(3600);
321
		$response->addHeader('Content-Type', $this->config->getAppValue($this->appName, $key . 'Mime', ''));
322
		$response->addHeader('Content-Disposition', 'attachment; filename="' . $key . '"');
323
		if (!$useSvg) {
324
			$response->addHeader('Content-Type', 'image/png');
325
		} else {
326
			$response->addHeader('Content-Type', $this->config->getAppValue($this->appName, $key . 'Mime', ''));
327
		}
328
		return $response;
329
	}
330
331
	/**
332
	 * @NoCSRFRequired
333
	 * @PublicPage
334
	 * @NoSameSiteCookieRequired
335
	 * @NoTwoFactorRequired
336
	 *
337
	 * @return DataDisplayResponse|NotFoundResponse
338
	 */
339
	public function getThemeStylesheet(string $themeId, bool $plain = false, bool $withCustomCss = false) {
0 ignored issues
show
Unused Code introduced by
The parameter $withCustomCss is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

339
	public function getThemeStylesheet(string $themeId, bool $plain = false, /** @scrutinizer ignore-unused */ bool $withCustomCss = false) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
340
		$themes = $this->themesService->getThemes();
341
		if (!in_array($themeId, array_keys($themes))) {
342
			return new NotFoundResponse();
343
		}
344
345
		$theme = $themes[$themeId];
346
		$customCss  = $theme->getCustomCss();
347
348
		// Generate variables
349
		$variables = '';
350
		foreach ($theme->getCSSVariables() as $variable => $value) {
351
			$variables .= "$variable:$value; ";
352
		};
353
354
		// If plain is set, the browser decides of the css priority
355
		if ($plain) {
356
			$css = ":root { $variables } " . $customCss;
357
		} else { 
358
			// If not set, we'll rely on the body class
359
			$compiler = new Compiler();
360
			$compiledCss = $compiler->compileString("[data-theme-$themeId] { $variables $customCss }");
361
			$css = $compiledCss->getCss();;
362
		}
363
364
		try {
365
			$response = new DataDisplayResponse($css, Http::STATUS_OK, ['Content-Type' => 'text/css']);
366
			$response->cacheFor(86400);
367
			return $response;
368
		} catch (NotFoundException $e) {
369
			return new NotFoundResponse();
370
		}
371
	}
372
373
	/**
374
	 * @NoCSRFRequired
375
	 * @PublicPage
376
	 *
377
	 * @return Http\JSONResponse
378
	 */
379
	public function getManifest($app) {
380
		$cacheBusterValue = $this->config->getAppValue('theming', 'cachebuster', '0');
381
		if ($app === 'core' || $app === 'settings') {
382
			$name = $this->themingDefaults->getName();
383
			$shortName = $this->themingDefaults->getName();
384
			$startUrl = $this->urlGenerator->getBaseUrl();
385
			$description = $this->themingDefaults->getSlogan();
386
		} else {
387
			$info = $this->appManager->getAppInfo($app, false, $this->l10n->getLanguageCode());
388
			$name = $info['name'] . ' - ' . $this->themingDefaults->getName();
389
			$shortName = $info['name'];
390
			if (strpos($this->request->getRequestUri(), '/index.php/') !== false) {
391
				$startUrl = $this->urlGenerator->getBaseUrl() . '/index.php/apps/' . $app . '/';
392
			} else {
393
				$startUrl = $this->urlGenerator->getBaseUrl() . '/apps/' . $app . '/';
394
			}
395
			$description = $info['summary'] ?? '';
396
		}
397
		$responseJS = [
398
			'name' => $name,
399
			'short_name' => $shortName,
400
			'start_url' => $startUrl,
401
			'theme_color' => $this->themingDefaults->getColorPrimary(),
402
			'background_color' => $this->themingDefaults->getColorPrimary(),
403
			'description' => $description,
404
			'icons' =>
405
				[
406
					[
407
						'src' => $this->urlGenerator->linkToRoute('theming.Icon.getTouchIcon',
408
								['app' => $app]) . '?v=' . $cacheBusterValue,
409
						'type' => 'image/png',
410
						'sizes' => '512x512'
411
					],
412
					[
413
						'src' => $this->urlGenerator->linkToRoute('theming.Icon.getFavicon',
414
								['app' => $app]) . '?v=' . $cacheBusterValue,
415
						'type' => 'image/svg+xml',
416
						'sizes' => '16x16'
417
					]
418
				],
419
			'display' => 'standalone'
420
		];
421
		$response = new Http\JSONResponse($responseJS);
422
		$response->cacheFor(3600);
423
		return $response;
424
	}
425
}
426