Passed
Push — master ( 4d1d4d...9d67c2 )
by Roeland
12:27 queued 13s
created

ThemingController::getImage()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 20
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 16
c 0
b 0
f 0
dl 0
loc 20
rs 9.7333
cc 3
nc 3
nop 2
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 Daniel Kesselberg <[email protected]>
11
 * @author Jan-Christoph Borchardt <[email protected]>
12
 * @author Joas Schilling <[email protected]>
13
 * @author Julius Haertl <[email protected]>
14
 * @author Julius Härtl <[email protected]>
15
 * @author Kyle Fazzari <[email protected]>
16
 * @author Lukas Reschke <[email protected]>
17
 * @author Michael Weimann <[email protected]>
18
 * @author rakekniven <[email protected]>
19
 * @author Robin Appelman <[email protected]>
20
 * @author Roeland Jago Douma <[email protected]>
21
 * @author Thomas Citharel <[email protected]>
22
 *
23
 * @license GNU AGPL version 3 or any later version
24
 *
25
 * This program is free software: you can redistribute it and/or modify
26
 * it under the terms of the GNU Affero General Public License as
27
 * published by the Free Software Foundation, either version 3 of the
28
 * License, or (at your option) any later version.
29
 *
30
 * This program is distributed in the hope that it will be useful,
31
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
32
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
33
 * GNU Affero General Public License for more details.
34
 *
35
 * You should have received a copy of the GNU Affero General Public License
36
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
37
 *
38
 */
39
40
namespace OCA\Theming\Controller;
41
42
use OC\Template\SCSSCacher;
43
use OCA\Theming\ImageManager;
44
use OCA\Theming\ThemingDefaults;
45
use OCP\App\IAppManager;
46
use OCP\AppFramework\Controller;
47
use OCP\AppFramework\Http;
48
use OCP\AppFramework\Http\DataResponse;
49
use OCP\AppFramework\Http\FileDisplayResponse;
50
use OCP\AppFramework\Http\NotFoundResponse;
51
use OCP\Files\IAppData;
52
use OCP\Files\NotFoundException;
53
use OCP\Files\NotPermittedException;
54
use OCP\IConfig;
55
use OCP\IL10N;
56
use OCP\IRequest;
57
use OCP\ITempManager;
58
use OCP\IURLGenerator;
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
	/** @var ThemingDefaults */
69
	private $themingDefaults;
70
	/** @var IL10N */
71
	private $l10n;
72
	/** @var IConfig */
73
	private $config;
74
	/** @var ITempManager */
75
	private $tempManager;
76
	/** @var IAppData */
77
	private $appData;
78
	/** @var SCSSCacher */
79
	private $scssCacher;
80
	/** @var IURLGenerator */
81
	private $urlGenerator;
82
	/** @var IAppManager */
83
	private $appManager;
84
	/** @var ImageManager */
85
	private $imageManager;
86
87
	/**
88
	 * ThemingController constructor.
89
	 *
90
	 * @param string $appName
91
	 * @param IRequest $request
92
	 * @param IConfig $config
93
	 * @param ThemingDefaults $themingDefaults
94
	 * @param IL10N $l
95
	 * @param ITempManager $tempManager
96
	 * @param IAppData $appData
97
	 * @param SCSSCacher $scssCacher
98
	 * @param IURLGenerator $urlGenerator
99
	 * @param IAppManager $appManager
100
	 * @param ImageManager $imageManager
101
	 */
102
	public function __construct(
103
		$appName,
104
		IRequest $request,
105
		IConfig $config,
106
		ThemingDefaults $themingDefaults,
107
		IL10N $l,
108
		ITempManager $tempManager,
109
		IAppData $appData,
110
		SCSSCacher $scssCacher,
111
		IURLGenerator $urlGenerator,
112
		IAppManager $appManager,
113
		ImageManager $imageManager
114
	) {
115
		parent::__construct($appName, $request);
116
117
		$this->themingDefaults = $themingDefaults;
118
		$this->l10n = $l;
119
		$this->config = $config;
120
		$this->tempManager = $tempManager;
121
		$this->appData = $appData;
122
		$this->scssCacher = $scssCacher;
123
		$this->urlGenerator = $urlGenerator;
124
		$this->appManager = $appManager;
125
		$this->imageManager = $imageManager;
126
	}
127
128
	/**
129
	 * @param string $setting
130
	 * @param string $value
131
	 * @return DataResponse
132
	 * @throws NotPermittedException
133
	 */
134
	public function updateStylesheet($setting, $value) {
135
		$value = trim($value);
136
		$error = null;
137
		switch ($setting) {
138
			case 'name':
139
				if (strlen($value) > 250) {
140
					$error = $this->l10n->t('The given name is too long');
141
				}
142
				break;
143
			case 'url':
144
				if (strlen($value) > 500) {
145
					$error = $this->l10n->t('The given web address is too long');
146
				}
147
				if (!$this->isValidUrl($value)) {
148
					$error = $this->l10n->t('The given web address is not a valid URL');
149
				}
150
				break;
151
			case 'imprintUrl':
152
				if (strlen($value) > 500) {
153
					$error = $this->l10n->t('The given legal notice address is too long');
154
				}
155
				if (!$this->isValidUrl($value)) {
156
					$error = $this->l10n->t('The given legal notice address is not a valid URL');
157
				}
158
				break;
159
			case 'privacyUrl':
160
				if (strlen($value) > 500) {
161
					$error = $this->l10n->t('The given privacy policy address is too long');
162
				}
163
				if (!$this->isValidUrl($value)) {
164
					$error = $this->l10n->t('The given privacy policy address is not a valid URL');
165
				}
166
				break;
167
			case 'slogan':
168
				if (strlen($value) > 500) {
169
					$error = $this->l10n->t('The given slogan is too long');
170
				}
171
				break;
172
			case 'color':
173
				if (!preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $value)) {
174
					$error = $this->l10n->t('The given color is invalid');
175
				}
176
				break;
177
		}
178
		if ($error !== null) {
179
			return new DataResponse([
180
				'data' => [
181
					'message' => $error,
182
				],
183
				'status' => 'error'
184
			], Http::STATUS_BAD_REQUEST);
185
		}
186
187
		$this->themingDefaults->set($setting, $value);
188
189
		// reprocess server scss for preview
190
		$cssCached = $this->scssCacher->process(\OC::$SERVERROOT, 'core/css/css-variables.scss', 'core');
0 ignored issues
show
Unused Code introduced by
The assignment to $cssCached is dead and can be removed.
Loading history...
191
192
		return new DataResponse(
193
			[
194
				'data' =>
195
					[
196
						'message' => $this->l10n->t('Saved'),
197
						'serverCssUrl' => $this->urlGenerator->linkTo('', $this->scssCacher->getCachedSCSS('core', '/core/css/css-variables.scss'))
198
					],
199
				'status' => 'success'
200
			]
201
		);
202
	}
203
204
	/**
205
	 * Check that a string is a valid http/https url
206
	 */
207
	private function isValidUrl(string $url): bool {
208
		return ((strpos($url, 'http://') === 0 || strpos($url, 'https://') === 0) &&
209
			filter_var($url, FILTER_VALIDATE_URL) !== false);
210
	}
211
212
	/**
213
	 * @return DataResponse
214
	 * @throws NotPermittedException
215
	 */
216
	public function uploadImage(): DataResponse {
217
		$key = $this->request->getParam('key');
218
		$image = $this->request->getUploadedFile('image');
219
		$error = null;
220
		$phpFileUploadErrors = [
221
			UPLOAD_ERR_OK => $this->l10n->t('The file was uploaded'),
222
			UPLOAD_ERR_INI_SIZE => $this->l10n->t('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
223
			UPLOAD_ERR_FORM_SIZE => $this->l10n->t('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
224
			UPLOAD_ERR_PARTIAL => $this->l10n->t('The file was only partially uploaded'),
225
			UPLOAD_ERR_NO_FILE => $this->l10n->t('No file was uploaded'),
226
			UPLOAD_ERR_NO_TMP_DIR => $this->l10n->t('Missing a temporary folder'),
227
			UPLOAD_ERR_CANT_WRITE => $this->l10n->t('Could not write file to disk'),
228
			UPLOAD_ERR_EXTENSION => $this->l10n->t('A PHP extension stopped the file upload'),
229
		];
230
		if (empty($image)) {
231
			$error = $this->l10n->t('No file uploaded');
232
		}
233
		if (!empty($image) && array_key_exists('error', $image) && $image['error'] !== UPLOAD_ERR_OK) {
234
			$error = $phpFileUploadErrors[$image['error']];
235
		}
236
237
		if ($error !== null) {
238
			return new DataResponse(
239
				[
240
					'data' => [
241
						'message' => $error
242
					],
243
					'status' => 'failure',
244
				],
245
				Http::STATUS_UNPROCESSABLE_ENTITY
246
			);
247
		}
248
249
		try {
250
			$mime = $this->imageManager->updateImage($key, $image['tmp_name']);
251
			$this->themingDefaults->set($key . 'Mime', $mime);
252
		} catch (\Exception $e) {
253
			return new DataResponse(
254
				[
255
					'data' => [
256
						'message' => $e->getMessage()
257
					],
258
					'status' => 'failure',
259
				],
260
				Http::STATUS_UNPROCESSABLE_ENTITY
261
			);
262
		}
263
264
		$name = $image['name'];
265
		$cssCached = $this->scssCacher->process(\OC::$SERVERROOT, 'core/css/css-variables.scss', 'core');
0 ignored issues
show
Unused Code introduced by
The assignment to $cssCached is dead and can be removed.
Loading history...
266
267
		return new DataResponse(
268
			[
269
				'data' =>
270
					[
271
						'name' => $name,
272
						'url' => $this->imageManager->getImageUrl($key),
273
						'message' => $this->l10n->t('Saved'),
274
						'serverCssUrl' => $this->urlGenerator->linkTo('', $this->scssCacher->getCachedSCSS('core', '/core/css/css-variables.scss'))
275
					],
276
				'status' => 'success'
277
			]
278
		);
279
	}
280
281
	/**
282
	 * Revert setting to default value
283
	 *
284
	 * @param string $setting setting which should be reverted
285
	 * @return DataResponse
286
	 * @throws NotPermittedException
287
	 */
288
	public function undo(string $setting): DataResponse {
289
		$value = $this->themingDefaults->undo($setting);
290
		// reprocess server scss for preview
291
		$cssCached = $this->scssCacher->process(\OC::$SERVERROOT, 'core/css/css-variables.scss', 'core');
0 ignored issues
show
Unused Code introduced by
The assignment to $cssCached is dead and can be removed.
Loading history...
292
293
		return new DataResponse(
294
			[
295
				'data' =>
296
					[
297
						'value' => $value,
298
						'message' => $this->l10n->t('Saved'),
299
						'serverCssUrl' => $this->urlGenerator->linkTo('', $this->scssCacher->getCachedSCSS('core', '/core/css/css-variables.scss'))
300
					],
301
				'status' => 'success'
302
			]
303
		);
304
	}
305
306
	/**
307
	 * @PublicPage
308
	 * @NoCSRFRequired
309
	 *
310
	 * @param string $key
311
	 * @param bool $useSvg
312
	 * @return FileDisplayResponse|NotFoundResponse
313
	 * @throws NotPermittedException
314
	 */
315
	public function getImage(string $key, bool $useSvg = true) {
316
		try {
317
			$file = $this->imageManager->getImage($key, $useSvg);
318
		} catch (NotFoundException $e) {
319
			return new NotFoundResponse();
320
		}
321
322
		$response = new FileDisplayResponse($file);
323
		$csp = new Http\ContentSecurityPolicy();
324
		$csp->allowInlineStyle();
325
		$response->setContentSecurityPolicy($csp);
326
		$response->cacheFor(3600);
327
		$response->addHeader('Content-Type', $this->config->getAppValue($this->appName, $key . 'Mime', ''));
328
		$response->addHeader('Content-Disposition', 'attachment; filename="' . $key . '"');
329
		if (!$useSvg) {
330
			$response->addHeader('Content-Type', 'image/png');
331
		} else {
332
			$response->addHeader('Content-Type', $this->config->getAppValue($this->appName, $key . 'Mime', ''));
333
		}
334
		return $response;
335
	}
336
337
	/**
338
	 * @NoCSRFRequired
339
	 * @PublicPage
340
	 * @NoSameSiteCookieRequired
341
	 *
342
	 * @return FileDisplayResponse|NotFoundResponse
343
	 * @throws NotPermittedException
344
	 * @throws \Exception
345
	 * @throws \OCP\App\AppPathNotFoundException
346
	 */
347
	public function getStylesheet() {
348
		$appPath = $this->appManager->getAppPath('theming');
349
350
		/* SCSSCacher is required here
351
		 * We cannot rely on automatic caching done by \OC_Util::addStyle,
352
		 * since we need to add the cacheBuster value to the url
353
		 */
354
		$cssCached = $this->scssCacher->process($appPath, 'css/theming.scss', 'theming');
355
		if (!$cssCached) {
356
			return new NotFoundResponse();
357
		}
358
359
		try {
360
			$cssFile = $this->scssCacher->getCachedCSS('theming', 'theming.css');
361
			$response = new FileDisplayResponse($cssFile, Http::STATUS_OK, ['Content-Type' => 'text/css']);
362
			$response->cacheFor(86400);
363
			return $response;
364
		} catch (NotFoundException $e) {
365
			return new NotFoundResponse();
366
		}
367
	}
368
369
	/**
370
	 * @NoCSRFRequired
371
	 * @PublicPage
372
	 *
373
	 * @return Http\JSONResponse
374
	 */
375
	public function getManifest($app) {
376
		$cacheBusterValue = $this->config->getAppValue('theming', 'cachebuster', '0');
377
		$responseJS = [
378
			'name' => $this->themingDefaults->getName(),
379
			'start_url' => $this->urlGenerator->getBaseUrl(),
380
			'icons' =>
381
				[
382
					[
383
						'src' => $this->urlGenerator->linkToRoute('theming.Icon.getTouchIcon',
384
								['app' => $app]) . '?v=' . $cacheBusterValue,
385
						'type'=> 'image/png',
386
						'sizes'=> '128x128'
387
					],
388
					[
389
						'src' => $this->urlGenerator->linkToRoute('theming.Icon.getFavicon',
390
								['app' => $app]) . '?v=' . $cacheBusterValue,
391
						'type' => 'image/svg+xml',
392
						'sizes' => '16x16'
393
					]
394
				],
395
			'display' => 'standalone'
396
		];
397
		$response = new Http\JSONResponse($responseJS);
398
		$response->cacheFor(3600);
399
		return $response;
400
	}
401
}
402