Passed
Push — master ( a884f3...785682 )
by
unknown
14:57 queued 14s
created

apps/theming/lib/ImageManager.php (1 issue)

Severity
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016 Julius Härtl <[email protected]>
4
 *
5
 * @author Christoph Wurst <[email protected]>
6
 * @author Daniel Kesselberg <[email protected]>
7
 * @author Gary Kim <[email protected]>
8
 * @author Jacob Neplokh <[email protected]>
9
 * @author John Molakvoæ <[email protected]>
10
 * @author Julien Veyssier <[email protected]>
11
 * @author Julius Haertl <[email protected]>
12
 * @author Julius Härtl <[email protected]>
13
 * @author Michael Weimann <[email protected]>
14
 * @author Morris Jobke <[email protected]>
15
 * @author Roeland Jago Douma <[email protected]>
16
 * @author ste101 <[email protected]>
17
 *
18
 * @license GNU AGPL version 3 or any later version
19
 *
20
 * This program is free software: you can redistribute it and/or modify
21
 * it under the terms of the GNU Affero General Public License as
22
 * published by the Free Software Foundation, either version 3 of the
23
 * License, or (at your option) any later version.
24
 *
25
 * This program is distributed in the hope that it will be useful,
26
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
27
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
28
 * GNU Affero General Public License for more details.
29
 *
30
 * You should have received a copy of the GNU Affero General Public License
31
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
32
 *
33
 */
34
namespace OCA\Theming;
35
36
use OCP\Files\IAppData;
37
use OCP\Files\NotFoundException;
38
use OCP\Files\NotPermittedException;
39
use OCP\Files\SimpleFS\ISimpleFile;
40
use OCP\Files\SimpleFS\ISimpleFolder;
41
use OCP\ICacheFactory;
42
use OCP\IConfig;
43
use OCP\ILogger;
44
use OCP\ITempManager;
45
use OCP\IURLGenerator;
46
47
class ImageManager {
48
	public const SupportedImageKeys = ['background', 'logo', 'logoheader', 'favicon'];
49
50
	/** @var IConfig */
51
	private $config;
52
	/** @var IAppData */
53
	private $appData;
54
	/** @var IURLGenerator */
55
	private $urlGenerator;
56
	/** @var ICacheFactory */
57
	private $cacheFactory;
58
	/** @var ILogger */
59
	private $logger;
60
	/** @var ITempManager */
61
	private $tempManager;
62
63
	public function __construct(IConfig $config,
64
								IAppData $appData,
65
								IURLGenerator $urlGenerator,
66
								ICacheFactory $cacheFactory,
67
								ILogger $logger,
68
								ITempManager $tempManager) {
69
		$this->config = $config;
70
		$this->urlGenerator = $urlGenerator;
71
		$this->cacheFactory = $cacheFactory;
72
		$this->logger = $logger;
73
		$this->tempManager = $tempManager;
74
		$this->appData = $appData;
75
	}
76
77
	public function getImageUrl(string $key, bool $useSvg = true): string {
0 ignored issues
show
The parameter $useSvg 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

77
	public function getImageUrl(string $key, /** @scrutinizer ignore-unused */ bool $useSvg = true): string {

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...
78
		$cacheBusterCounter = $this->config->getAppValue('theming', 'cachebuster', '0');
79
		if ($this->hasImage($key)) {
80
			return $this->urlGenerator->linkToRoute('theming.Theming.getImage', [ 'key' => $key ]) . '?v=' . $cacheBusterCounter;
81
		}
82
83
		switch ($key) {
84
			case 'logo':
85
			case 'logoheader':
86
			case 'favicon':
87
				return $this->urlGenerator->imagePath('core', 'logo/logo.png') . '?v=' . $cacheBusterCounter;
88
			case 'background':
89
				return $this->urlGenerator->imagePath('core', 'background.png') . '?v=' . $cacheBusterCounter;
90
		}
91
		return '';
92
	}
93
94
	public function getImageUrlAbsolute(string $key, bool $useSvg = true): string {
95
		return $this->urlGenerator->getAbsoluteURL($this->getImageUrl($key, $useSvg));
96
	}
97
98
	/**
99
	 * @param string $key
100
	 * @param bool $useSvg
101
	 * @return ISimpleFile
102
	 * @throws NotFoundException
103
	 * @throws NotPermittedException
104
	 */
105
	public function getImage(string $key, bool $useSvg = true): ISimpleFile {
106
		$logo = $this->config->getAppValue('theming', $key . 'Mime', '');
107
		$folder = $this->getRootFolder()->getFolder('images');
108
109
		if ($logo === '' || !$folder->fileExists($key)) {
110
			throw new NotFoundException();
111
		}
112
113
		if (!$useSvg && $this->shouldReplaceIcons()) {
114
			if (!$folder->fileExists($key . '.png')) {
115
				try {
116
					$finalIconFile = new \Imagick();
117
					$finalIconFile->setBackgroundColor('none');
118
					$finalIconFile->readImageBlob($folder->getFile($key)->getContent());
119
					$finalIconFile->setImageFormat('png32');
120
					$pngFile = $folder->newFile($key . '.png');
121
					$pngFile->putContent($finalIconFile->getImageBlob());
122
					return $pngFile;
123
				} catch (\ImagickException $e) {
124
					$this->logger->info('The image was requested to be no SVG file, but converting it to PNG failed: ' . $e->getMessage());
125
				}
126
			} else {
127
				return $folder->getFile($key . '.png');
128
			}
129
		}
130
131
		return $folder->getFile($key);
132
	}
133
134
	public function hasImage(string $key): bool {
135
		$mimeSetting = $this->config->getAppValue('theming', $key . 'Mime', '');
136
		return $mimeSetting !== '';
137
	}
138
139
	/**
140
	 * Get folder for current theming files
141
	 *
142
	 * @return ISimpleFolder
143
	 * @throws NotPermittedException
144
	 */
145
	public function getCacheFolder(): ISimpleFolder {
146
		$cacheBusterValue = $this->config->getAppValue('theming', 'cachebuster', '0');
147
		try {
148
			$folder = $this->getRootFolder()->getFolder($cacheBusterValue);
149
		} catch (NotFoundException $e) {
150
			$folder = $this->getRootFolder()->newFolder($cacheBusterValue);
151
			$this->cleanup();
152
		}
153
		return $folder;
154
	}
155
156
	/**
157
	 * Get a file from AppData
158
	 *
159
	 * @param string $filename
160
	 * @throws NotFoundException
161
	 * @return \OCP\Files\SimpleFS\ISimpleFile
162
	 * @throws NotPermittedException
163
	 */
164
	public function getCachedImage(string $filename): ISimpleFile {
165
		$currentFolder = $this->getCacheFolder();
166
		return $currentFolder->getFile($filename);
167
	}
168
169
	/**
170
	 * Store a file for theming in AppData
171
	 *
172
	 * @param string $filename
173
	 * @param string $data
174
	 * @return \OCP\Files\SimpleFS\ISimpleFile
175
	 * @throws NotFoundException
176
	 * @throws NotPermittedException
177
	 */
178
	public function setCachedImage(string $filename, string $data): ISimpleFile {
179
		$currentFolder = $this->getCacheFolder();
180
		if ($currentFolder->fileExists($filename)) {
181
			$file = $currentFolder->getFile($filename);
182
		} else {
183
			$file = $currentFolder->newFile($filename);
184
		}
185
		$file->putContent($data);
186
		return $file;
187
	}
188
189
	public function delete(string $key): void {
190
		/* ignore exceptions, since we don't want to fail hard if something goes wrong during cleanup */
191
		try {
192
			$file = $this->getRootFolder()->getFolder('images')->getFile($key);
193
			$file->delete();
194
		} catch (NotFoundException $e) {
195
		} catch (NotPermittedException $e) {
196
		}
197
		try {
198
			$file = $this->getRootFolder()->getFolder('images')->getFile($key . '.png');
199
			$file->delete();
200
		} catch (NotFoundException $e) {
201
		} catch (NotPermittedException $e) {
202
		}
203
	}
204
205
	public function updateImage(string $key, string $tmpFile): string {
206
		$this->delete($key);
207
208
		try {
209
			$folder = $this->getRootFolder()->getFolder('images');
210
		} catch (NotFoundException $e) {
211
			$folder = $this->getRootFolder()->newFolder('images');
212
		}
213
214
		$target = $folder->newFile($key);
215
		$supportedFormats = $this->getSupportedUploadImageFormats($key);
216
		$detectedMimeType = mime_content_type($tmpFile);
217
		if (!in_array($detectedMimeType, $supportedFormats, true)) {
218
			throw new \Exception('Unsupported image type');
219
		}
220
221
		if ($key === 'background' && strpos($detectedMimeType, 'image/svg') === false && strpos($detectedMimeType, 'image/gif') === false) {
222
			// Optimize the image since some people may upload images that will be
223
			// either to big or are not progressive rendering.
224
			$newImage = @imagecreatefromstring(file_get_contents($tmpFile));
225
226
			// Preserve transparency
227
			imagesavealpha($newImage, true);
228
			imagealphablending($newImage, true);
229
230
			$tmpFile = $this->tempManager->getTemporaryFile();
231
			$newWidth = (int)(imagesx($newImage) < 4096 ? imagesx($newImage) : 4096);
232
			$newHeight = (int)(imagesy($newImage) / (imagesx($newImage) / $newWidth));
233
			$outputImage = imagescale($newImage, $newWidth, $newHeight);
234
235
			imageinterlace($outputImage, 1);
236
			imagepng($outputImage, $tmpFile, 8);
237
			imagedestroy($outputImage);
238
239
			$target->putContent(file_get_contents($tmpFile));
240
		} else {
241
			$target->putContent(file_get_contents($tmpFile));
242
		}
243
244
		return $detectedMimeType;
245
	}
246
247
	/**
248
	 * Returns a list of supported mime types for image uploads.
249
	 * "favicon" images are only allowed to be SVG when imagemagick with SVG support is available.
250
	 *
251
	 * @param string $key The image key, e.g. "favicon"
252
	 * @return string[]
253
	 */
254
	private function getSupportedUploadImageFormats(string $key): array {
255
		$supportedFormats = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
256
257
		if ($key !== 'favicon' || $this->shouldReplaceIcons() === true) {
258
			$supportedFormats[] = 'image/svg+xml';
259
			$supportedFormats[] = 'image/svg';
260
		}
261
262
		if ($key === 'favicon') {
263
			$supportedFormats[] = 'image/x-icon';
264
			$supportedFormats[] = 'image/vnd.microsoft.icon';
265
		}
266
267
		return $supportedFormats;
268
	}
269
270
	/**
271
	 * remove cached files that are not required any longer
272
	 *
273
	 * @throws NotPermittedException
274
	 * @throws NotFoundException
275
	 */
276
	public function cleanup() {
277
		$currentFolder = $this->getCacheFolder();
278
		$folders = $this->getRootFolder()->getDirectoryListing();
279
		foreach ($folders as $folder) {
280
			if ($folder->getName() !== 'images' && $folder->getName() !== $currentFolder->getName()) {
281
				$folder->delete();
282
			}
283
		}
284
	}
285
286
	/**
287
	 * Check if Imagemagick is enabled and if SVG is supported
288
	 * otherwise we can't render custom icons
289
	 *
290
	 * @return bool
291
	 */
292
	public function shouldReplaceIcons() {
293
		$cache = $this->cacheFactory->createDistributed('theming-' . $this->urlGenerator->getBaseUrl());
294
		if ($value = $cache->get('shouldReplaceIcons')) {
295
			return (bool)$value;
296
		}
297
		$value = false;
298
		if (extension_loaded('imagick')) {
299
			if (count(\Imagick::queryFormats('SVG')) >= 1) {
300
				$value = true;
301
			}
302
		}
303
		$cache->set('shouldReplaceIcons', $value);
304
		return $value;
305
	}
306
307
	private function getRootFolder(): ISimpleFolder {
308
		try {
309
			return $this->appData->getFolder('global');
310
		} catch (NotFoundException $e) {
311
			return $this->appData->newFolder('global');
312
		}
313
	}
314
}
315