Passed
Push — master ( e1cb1b...9a76f0 )
by John
15:33 queued 17s
created

ThemingController::getThemeStylesheet()   A

Complexity

Conditions 5
Paths 13

Size

Total Lines 31
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 20
c 0
b 0
f 0
nc 13
nop 3
dl 0
loc 31
rs 9.2888
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
		}
155
		if ($error !== null) {
156
			return new DataResponse([
157
				'data' => [
158
					'message' => $error,
159
				],
160
				'status' => 'error'
161
			], Http::STATUS_BAD_REQUEST);
162
		}
163
164
		$this->themingDefaults->set($setting, $value);
165
166
		return new DataResponse([
167
			'data' => [
168
				'message' => $this->l10n->t('Saved'),
169
			],
170
			'status' => 'success'
171
		]);
172
	}
173
174
	/**
175
	 * Check that a string is a valid http/https url
176
	 */
177
	private function isValidUrl(string $url): bool {
178
		return ((strpos($url, 'http://') === 0 || strpos($url, 'https://') === 0) &&
179
			filter_var($url, FILTER_VALIDATE_URL) !== false);
180
	}
181
182
	/**
183
	 * @AuthorizedAdminSetting(settings=OCA\Theming\Settings\Admin)
184
	 * @return DataResponse
185
	 * @throws NotPermittedException
186
	 */
187
	public function uploadImage(): DataResponse {
188
		$key = $this->request->getParam('key');
189
		$image = $this->request->getUploadedFile('image');
190
		$error = null;
191
		$phpFileUploadErrors = [
192
			UPLOAD_ERR_OK => $this->l10n->t('The file was uploaded'),
193
			UPLOAD_ERR_INI_SIZE => $this->l10n->t('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
194
			UPLOAD_ERR_FORM_SIZE => $this->l10n->t('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
195
			UPLOAD_ERR_PARTIAL => $this->l10n->t('The file was only partially uploaded'),
196
			UPLOAD_ERR_NO_FILE => $this->l10n->t('No file was uploaded'),
197
			UPLOAD_ERR_NO_TMP_DIR => $this->l10n->t('Missing a temporary folder'),
198
			UPLOAD_ERR_CANT_WRITE => $this->l10n->t('Could not write file to disk'),
199
			UPLOAD_ERR_EXTENSION => $this->l10n->t('A PHP extension stopped the file upload'),
200
		];
201
		if (empty($image)) {
202
			$error = $this->l10n->t('No file uploaded');
203
		}
204
		if (!empty($image) && array_key_exists('error', $image) && $image['error'] !== UPLOAD_ERR_OK) {
205
			$error = $phpFileUploadErrors[$image['error']];
206
		}
207
208
		if ($error !== null) {
209
			return new DataResponse(
210
				[
211
					'data' => [
212
						'message' => $error
213
					],
214
					'status' => 'failure',
215
				],
216
				Http::STATUS_UNPROCESSABLE_ENTITY
217
			);
218
		}
219
220
		try {
221
			$mime = $this->imageManager->updateImage($key, $image['tmp_name']);
222
			$this->themingDefaults->set($key . 'Mime', $mime);
223
		} catch (\Exception $e) {
224
			return new DataResponse(
225
				[
226
					'data' => [
227
						'message' => $e->getMessage()
228
					],
229
					'status' => 'failure',
230
				],
231
				Http::STATUS_UNPROCESSABLE_ENTITY
232
			);
233
		}
234
235
		$name = $image['name'];
236
237
		return new DataResponse(
238
			[
239
				'data' =>
240
					[
241
						'name' => $name,
242
						'url' => $this->imageManager->getImageUrl($key),
243
						'message' => $this->l10n->t('Saved'),
244
					],
245
				'status' => 'success'
246
			]
247
		);
248
	}
249
250
	/**
251
	 * Revert setting to default value
252
	 * @AuthorizedAdminSetting(settings=OCA\Theming\Settings\Admin)
253
	 *
254
	 * @param string $setting setting which should be reverted
255
	 * @return DataResponse
256
	 * @throws NotPermittedException
257
	 */
258
	public function undo(string $setting): DataResponse {
259
		$value = $this->themingDefaults->undo($setting);
260
261
		return new DataResponse(
262
			[
263
				'data' =>
264
					[
265
						'value' => $value,
266
						'message' => $this->l10n->t('Saved'),
267
					],
268
				'status' => 'success'
269
			]
270
		);
271
	}
272
273
	/**
274
	 * @PublicPage
275
	 * @NoCSRFRequired
276
	 * @NoSameSiteCookieRequired
277
	 *
278
	 * @param string $key
279
	 * @param bool $useSvg
280
	 * @return FileDisplayResponse|NotFoundResponse
281
	 * @throws NotPermittedException
282
	 */
283
	public function getImage(string $key, bool $useSvg = true) {
284
		try {
285
			$file = $this->imageManager->getImage($key, $useSvg);
286
		} catch (NotFoundException $e) {
287
			return new NotFoundResponse();
288
		}
289
290
		$response = new FileDisplayResponse($file);
291
		$csp = new Http\ContentSecurityPolicy();
292
		$csp->allowInlineStyle();
293
		$response->setContentSecurityPolicy($csp);
294
		$response->cacheFor(3600);
295
		$response->addHeader('Content-Type', $this->config->getAppValue($this->appName, $key . 'Mime', ''));
296
		$response->addHeader('Content-Disposition', 'attachment; filename="' . $key . '"');
297
		if (!$useSvg) {
298
			$response->addHeader('Content-Type', 'image/png');
299
		} else {
300
			$response->addHeader('Content-Type', $this->config->getAppValue($this->appName, $key . 'Mime', ''));
301
		}
302
		return $response;
303
	}
304
305
	/**
306
	 * @NoCSRFRequired
307
	 * @PublicPage
308
	 * @NoSameSiteCookieRequired
309
	 * @NoTwoFactorRequired
310
	 *
311
	 * @return DataDisplayResponse|NotFoundResponse
312
	 */
313
	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

313
	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...
314
		$themes = $this->themesService->getThemes();
315
		if (!in_array($themeId, array_keys($themes))) {
316
			return new NotFoundResponse();
317
		}
318
319
		$theme = $themes[$themeId];
320
		$customCss  = $theme->getCustomCss();
321
322
		// Generate variables
323
		$variables = '';
324
		foreach ($theme->getCSSVariables() as $variable => $value) {
325
			$variables .= "$variable:$value; ";
326
		};
327
328
		// If plain is set, the browser decides of the css priority
329
		if ($plain) {
330
			$css = ":root { $variables } " . $customCss;
331
		} else { 
332
			// If not set, we'll rely on the body class
333
			$compiler = new Compiler();
334
			$compiledCss = $compiler->compileString("body[data-theme-$themeId] { $variables $customCss }");
335
			$css = $compiledCss->getCss();;
336
		}
337
338
		try {
339
			$response = new DataDisplayResponse($css, Http::STATUS_OK, ['Content-Type' => 'text/css']);
340
			$response->cacheFor(86400);
341
			return $response;
342
		} catch (NotFoundException $e) {
343
			return new NotFoundResponse();
344
		}
345
	}
346
347
	/**
348
	 * @NoCSRFRequired
349
	 * @PublicPage
350
	 *
351
	 * @return Http\JSONResponse
352
	 */
353
	public function getManifest($app) {
354
		$cacheBusterValue = $this->config->getAppValue('theming', 'cachebuster', '0');
355
		if ($app === 'core' || $app === 'settings') {
356
			$name = $this->themingDefaults->getName();
357
			$shortName = $this->themingDefaults->getName();
358
			$startUrl = $this->urlGenerator->getBaseUrl();
359
			$description = $this->themingDefaults->getSlogan();
360
		} else {
361
			$info = $this->appManager->getAppInfo($app, false, $this->l10n->getLanguageCode());
362
			$name = $info['name'] . ' - ' . $this->themingDefaults->getName();
363
			$shortName = $info['name'];
364
			if (strpos($this->request->getRequestUri(), '/index.php/') !== false) {
365
				$startUrl = $this->urlGenerator->getBaseUrl() . '/index.php/apps/' . $app . '/';
366
			} else {
367
				$startUrl = $this->urlGenerator->getBaseUrl() . '/apps/' . $app . '/';
368
			}
369
			$description = $info['summary'] ?? '';
370
		}
371
		$responseJS = [
372
			'name' => $name,
373
			'short_name' => $shortName,
374
			'start_url' => $startUrl,
375
			'theme_color' => $this->themingDefaults->getColorPrimary(),
376
			'background_color' => $this->themingDefaults->getColorPrimary(),
377
			'description' => $description,
378
			'icons' =>
379
				[
380
					[
381
						'src' => $this->urlGenerator->linkToRoute('theming.Icon.getTouchIcon',
382
								['app' => $app]) . '?v=' . $cacheBusterValue,
383
						'type' => 'image/png',
384
						'sizes' => '512x512'
385
					],
386
					[
387
						'src' => $this->urlGenerator->linkToRoute('theming.Icon.getFavicon',
388
								['app' => $app]) . '?v=' . $cacheBusterValue,
389
						'type' => 'image/svg+xml',
390
						'sizes' => '16x16'
391
					]
392
				],
393
			'display' => 'standalone'
394
		];
395
		$response = new Http\JSONResponse($responseJS);
396
		$response->cacheFor(3600);
397
		return $response;
398
	}
399
}
400