Passed
Push — master ( ef96ef...2a8452 )
by Roeland
13:23 queued 14s
created

ThemingController::getJavascript()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 17
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 13
nc 1
nop 0
dl 0
loc 17
rs 9.8333
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 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
		// logo / background
218
		// new: favicon logo-header
219
		//
220
		$key = $this->request->getParam('key');
221
		$image = $this->request->getUploadedFile('image');
222
		$error = null;
223
		$phpFileUploadErrors = [
224
			UPLOAD_ERR_OK => $this->l10n->t('The file was uploaded'),
225
			UPLOAD_ERR_INI_SIZE => $this->l10n->t('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
226
			UPLOAD_ERR_FORM_SIZE => $this->l10n->t('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
227
			UPLOAD_ERR_PARTIAL => $this->l10n->t('The file was only partially uploaded'),
228
			UPLOAD_ERR_NO_FILE => $this->l10n->t('No file was uploaded'),
229
			UPLOAD_ERR_NO_TMP_DIR => $this->l10n->t('Missing a temporary folder'),
230
			UPLOAD_ERR_CANT_WRITE => $this->l10n->t('Could not write file to disk'),
231
			UPLOAD_ERR_EXTENSION => $this->l10n->t('A PHP extension stopped the file upload'),
232
		];
233
		if (empty($image)) {
234
			$error = $this->l10n->t('No file uploaded');
235
		}
236
		if (!empty($image) && array_key_exists('error', $image) && $image['error'] !== UPLOAD_ERR_OK) {
237
			$error = $phpFileUploadErrors[$image['error']];
238
		}
239
240
		if ($error !== null) {
241
			return new DataResponse(
242
				[
243
					'data' => [
244
						'message' => $error
245
					],
246
					'status' => 'failure',
247
				],
248
				Http::STATUS_UNPROCESSABLE_ENTITY
249
			);
250
		}
251
252
		$name = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $name is dead and can be removed.
Loading history...
253
		try {
254
			$folder = $this->appData->getFolder('images');
255
		} catch (NotFoundException $e) {
256
			$folder = $this->appData->newFolder('images');
257
		}
258
259
		$this->imageManager->delete($key);
260
261
		$target = $folder->newFile($key);
262
		$supportedFormats = $this->getSupportedUploadImageFormats($key);
263
		$detectedMimeType = mime_content_type($image['tmp_name']);
264
		if (!in_array($image['type'], $supportedFormats) || !in_array($detectedMimeType, $supportedFormats)) {
265
			return new DataResponse(
266
				[
267
					'data' => [
268
						'message' => $this->l10n->t('Unsupported image type'),
269
					],
270
					'status' => 'failure',
271
				],
272
				Http::STATUS_UNPROCESSABLE_ENTITY
273
			);
274
		}
275
276
		if ($key === 'background' && strpos($detectedMimeType, 'image/svg') === false) {
277
			// Optimize the image since some people may upload images that will be
278
			// either to big or are not progressive rendering.
279
			$newImage = @imagecreatefromstring(file_get_contents($image['tmp_name'], 'r'));
0 ignored issues
show
Bug introduced by
'r' of type string is incompatible with the type boolean expected by parameter $use_include_path of file_get_contents(). ( Ignorable by Annotation )

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

279
			$newImage = @imagecreatefromstring(file_get_contents($image['tmp_name'], /** @scrutinizer ignore-type */ 'r'));
Loading history...
280
281
			$tmpFile = $this->tempManager->getTemporaryFile();
282
			$newWidth = imagesx($newImage) < 4096 ? imagesx($newImage) : 4096;
283
			$newHeight = imagesy($newImage) / (imagesx($newImage) / $newWidth);
284
			$outputImage = imagescale($newImage, $newWidth, $newHeight);
285
286
			imageinterlace($outputImage, 1);
287
			imagejpeg($outputImage, $tmpFile, 75);
288
			imagedestroy($outputImage);
289
290
			$target->putContent(file_get_contents($tmpFile, 'r'));
291
		} else {
292
			$target->putContent(file_get_contents($image['tmp_name'], 'r'));
293
		}
294
		$name = $image['name'];
295
296
		$this->themingDefaults->set($key.'Mime', $image['type']);
297
298
		$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...
299
300
		return new DataResponse(
301
			[
302
				'data' =>
303
					[
304
						'name' => $name,
305
						'url' => $this->imageManager->getImageUrl($key),
306
						'message' => $this->l10n->t('Saved'),
307
						'serverCssUrl' => $this->urlGenerator->linkTo('', $this->scssCacher->getCachedSCSS('core', '/core/css/css-variables.scss'))
308
					],
309
				'status' => 'success'
310
			]
311
		);
312
	}
313
314
	/**
315
	 * Returns a list of supported mime types for image uploads.
316
	 * "favicon" images are only allowed to be SVG when imagemagick with SVG support is available.
317
	 *
318
	 * @param string $key The image key, e.g. "favicon"
319
	 * @return array
320
	 */
321
	private function getSupportedUploadImageFormats(string $key): array {
322
		$supportedFormats = ['image/jpeg', 'image/png', 'image/gif',];
323
324
		if ($key !== 'favicon' || $this->imageManager->shouldReplaceIcons() === true) {
325
			$supportedFormats[] = 'image/svg+xml';
326
			$supportedFormats[] = 'image/svg';
327
		}
328
329
		return $supportedFormats;
330
	}
331
332
	/**
333
	 * Revert setting to default value
334
	 *
335
	 * @param string $setting setting which should be reverted
336
	 * @return DataResponse
337
	 * @throws NotPermittedException
338
	 */
339
	public function undo(string $setting): DataResponse {
340
		$value = $this->themingDefaults->undo($setting);
341
		// reprocess server scss for preview
342
		$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...
343
344
		if (strpos($setting, 'Mime') !== -1) {
345
			$imageKey = str_replace('Mime', '', $setting);
346
			$this->imageManager->delete($imageKey);
347
		}
348
349
		return new DataResponse(
350
			[
351
				'data' =>
352
					[
353
						'value' => $value,
354
						'message' => $this->l10n->t('Saved'),
355
						'serverCssUrl' => $this->urlGenerator->linkTo('', $this->scssCacher->getCachedSCSS('core', '/core/css/css-variables.scss'))
356
					],
357
				'status' => 'success'
358
			]
359
		);
360
	}
361
362
	/**
363
	 * @PublicPage
364
	 * @NoCSRFRequired
365
	 *
366
	 * @param string $key
367
	 * @param bool $useSvg
368
	 * @return FileDisplayResponse|NotFoundResponse
369
	 * @throws NotPermittedException
370
	 */
371
	public function getImage(string $key, bool $useSvg = true) {
372
		try {
373
			$file = $this->imageManager->getImage($key, $useSvg);
374
		} catch (NotFoundException $e) {
375
			return new NotFoundResponse();
376
		}
377
378
		$response = new FileDisplayResponse($file);
379
		$csp = new Http\ContentSecurityPolicy();
380
		$csp->allowInlineStyle();
381
		$response->setContentSecurityPolicy($csp);
382
		$response->cacheFor(3600);
383
		$response->addHeader('Content-Type', $this->config->getAppValue($this->appName, $key . 'Mime', ''));
384
		$response->addHeader('Content-Disposition', 'attachment; filename="' . $key . '"');
385
		if (!$useSvg) {
386
			$response->addHeader('Content-Type', 'image/png');
387
		} else {
388
			$response->addHeader('Content-Type', $this->config->getAppValue($this->appName, $key . 'Mime', ''));
389
		}
390
		return $response;
391
	}
392
393
	/**
394
	 * @NoCSRFRequired
395
	 * @PublicPage
396
	 * @NoSameSiteCookieRequired
397
	 *
398
	 * @return FileDisplayResponse|NotFoundResponse
399
	 * @throws NotPermittedException
400
	 * @throws \Exception
401
	 * @throws \OCP\App\AppPathNotFoundException
402
	 */
403
	public function getStylesheet() {
404
		$appPath = $this->appManager->getAppPath('theming');
405
406
		/* SCSSCacher is required here
407
		 * We cannot rely on automatic caching done by \OC_Util::addStyle,
408
		 * since we need to add the cacheBuster value to the url
409
		 */
410
		$cssCached = $this->scssCacher->process($appPath, 'css/theming.scss', 'theming');
411
		if (!$cssCached) {
412
			return new NotFoundResponse();
413
		}
414
415
		try {
416
			$cssFile = $this->scssCacher->getCachedCSS('theming', 'theming.css');
417
			$response = new FileDisplayResponse($cssFile, Http::STATUS_OK, ['Content-Type' => 'text/css']);
418
			$response->cacheFor(86400);
419
			return $response;
420
		} catch (NotFoundException $e) {
421
			return new NotFoundResponse();
422
		}
423
	}
424
425
	/**
426
	 * @NoCSRFRequired
427
	 * @PublicPage
428
	 *
429
	 * @return Http\JSONResponse
430
	 */
431
	public function getManifest($app) {
432
		$cacheBusterValue = $this->config->getAppValue('theming', 'cachebuster', '0');
433
		$responseJS = [
434
			'name' => $this->themingDefaults->getName(),
435
			'start_url' => $this->urlGenerator->getBaseUrl(),
436
			'icons' =>
437
				[
438
					[
439
						'src' => $this->urlGenerator->linkToRoute('theming.Icon.getTouchIcon',
440
								['app' => $app]) . '?v=' . $cacheBusterValue,
441
						'type'=> 'image/png',
442
						'sizes'=> '128x128'
443
					],
444
					[
445
						'src' => $this->urlGenerator->linkToRoute('theming.Icon.getFavicon',
446
								['app' => $app]) . '?v=' . $cacheBusterValue,
447
						'type' => 'image/svg+xml',
448
						'sizes' => '16x16'
449
					]
450
				],
451
			'display' => 'standalone'
452
		];
453
		$response = new Http\JSONResponse($responseJS);
454
		$response->cacheFor(3600);
455
		return $response;
456
	}
457
}
458