Passed
Push — master ( eb1927...20f197 )
by Julius
31:49 queued 12s
created

ImageManager::hasImage()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
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
49
	/** @var IConfig */
50
	private $config;
51
	/** @var IAppData */
52
	private $appData;
53
	/** @var IURLGenerator */
54
	private $urlGenerator;
55
	/** @var array */
56
	private $supportedImageKeys = ['background', 'logo', 'logoheader', 'favicon'];
57
	/** @var ICacheFactory */
58
	private $cacheFactory;
59
	/** @var ILogger */
60
	private $logger;
61
	/** @var ITempManager */
62
	private $tempManager;
63
64
	public function __construct(IConfig $config,
65
								IAppData $appData,
66
								IURLGenerator $urlGenerator,
67
								ICacheFactory $cacheFactory,
68
								ILogger $logger,
69
								ITempManager $tempManager
70
	) {
71
		$this->config = $config;
72
		$this->appData = $appData;
73
		$this->urlGenerator = $urlGenerator;
74
		$this->cacheFactory = $cacheFactory;
75
		$this->logger = $logger;
76
		$this->tempManager = $tempManager;
77
	}
78
79
	public function getImageUrl(string $key, bool $useSvg = true): string {
0 ignored issues
show
Unused Code introduced by
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

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