Passed
Push — master ( f116c8...be892d )
by John
15:58 queued 12s
created

ThemingController   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 336
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 185
dl 0
loc 336
rs 8.8798
c 0
b 0
f 0
wmc 44

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 24 1
D updateStylesheet() 0 65 20
A isValidUrl() 0 3 3
A getThemeStylesheet() 0 31 5
B uploadImage() 0 59 7
A getImage() 0 20 3
A getManifest() 0 45 4
A undo() 0 11 1

How to fix   Complexity   

Complex Class

Complex classes like ThemingController often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ThemingController, and based on these observations, apply Extract Interface, too.

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
	 * @PublicPage
280
	 * @NoCSRFRequired
281
	 * @NoSameSiteCookieRequired
282
	 *
283
	 * @param string $key
284
	 * @param bool $useSvg
285
	 * @return FileDisplayResponse|NotFoundResponse
286
	 * @throws NotPermittedException
287
	 */
288
	public function getImage(string $key, bool $useSvg = true) {
289
		try {
290
			$file = $this->imageManager->getImage($key, $useSvg);
291
		} catch (NotFoundException $e) {
292
			return new NotFoundResponse();
293
		}
294
295
		$response = new FileDisplayResponse($file);
296
		$csp = new Http\ContentSecurityPolicy();
297
		$csp->allowInlineStyle();
298
		$response->setContentSecurityPolicy($csp);
299
		$response->cacheFor(3600);
300
		$response->addHeader('Content-Type', $this->config->getAppValue($this->appName, $key . 'Mime', ''));
301
		$response->addHeader('Content-Disposition', 'attachment; filename="' . $key . '"');
302
		if (!$useSvg) {
303
			$response->addHeader('Content-Type', 'image/png');
304
		} else {
305
			$response->addHeader('Content-Type', $this->config->getAppValue($this->appName, $key . 'Mime', ''));
306
		}
307
		return $response;
308
	}
309
310
	/**
311
	 * @NoCSRFRequired
312
	 * @PublicPage
313
	 * @NoSameSiteCookieRequired
314
	 * @NoTwoFactorRequired
315
	 *
316
	 * @return DataDisplayResponse|NotFoundResponse
317
	 */
318
	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

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