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

ImageManager::getImage()   B

Complexity

Conditions 8
Paths 19

Size

Total Lines 28
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 22
nc 19
nop 2
dl 0
loc 28
rs 8.4444
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 Julius Haertl <[email protected]>
8
 * @author Julius Härtl <[email protected]>
9
 * @author Michael Weimann <[email protected]>
10
 * @author Roeland Jago Douma <[email protected]>
11
 *
12
 * @license GNU AGPL version 3 or any later version
13
 *
14
 * This program is free software: you can redistribute it and/or modify
15
 * it under the terms of the GNU Affero General Public License as
16
 * published by the Free Software Foundation, either version 3 of the
17
 * License, or (at your option) any later version.
18
 *
19
 * This program is distributed in the hope that it will be useful,
20
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22
 * GNU Affero General Public License for more details.
23
 *
24
 * You should have received a copy of the GNU Affero General Public License
25
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
26
 *
27
 */
28
29
namespace OCA\Theming;
30
31
use OCP\Files\IAppData;
32
use OCP\Files\NotFoundException;
33
use OCP\Files\NotPermittedException;
34
use OCP\Files\SimpleFS\ISimpleFile;
35
use OCP\Files\SimpleFS\ISimpleFolder;
36
use OCP\ICacheFactory;
37
use OCP\IConfig;
38
use OCP\ILogger;
39
use OCP\ITempManager;
40
use OCP\IURLGenerator;
41
42
class ImageManager {
43
44
	/** @var IConfig */
45
	private $config;
46
	/** @var IAppData */
47
	private $appData;
48
	/** @var IURLGenerator */
49
	private $urlGenerator;
50
	/** @var array */
51
	private $supportedImageKeys = ['background', 'logo', 'logoheader', 'favicon'];
52
	/** @var ICacheFactory */
53
	private $cacheFactory;
54
	/** @var ILogger */
55
	private $logger;
56
	/** @var ITempManager */
57
	private $tempManager;
58
59
	public function __construct(IConfig $config,
60
								IAppData $appData,
61
								IURLGenerator $urlGenerator,
62
								ICacheFactory $cacheFactory,
63
								ILogger $logger,
64
								ITempManager $tempManager
65
	) {
66
		$this->config = $config;
67
		$this->appData = $appData;
68
		$this->urlGenerator = $urlGenerator;
69
		$this->cacheFactory = $cacheFactory;
70
		$this->logger = $logger;
71
		$this->tempManager = $tempManager;
72
	}
73
74
	public function getImageUrl(string $key, bool $useSvg = true): string {
75
		$cacheBusterCounter = $this->config->getAppValue('theming', 'cachebuster', '0');
76
		try {
77
			$image = $this->getImage($key, $useSvg);
0 ignored issues
show
Unused Code introduced by
The assignment to $image is dead and can be removed.
Loading history...
78
			return $this->urlGenerator->linkToRoute('theming.Theming.getImage', [ 'key' => $key ]) . '?v=' . $cacheBusterCounter;
79
		} catch (NotFoundException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
80
		}
81
82
		switch ($key) {
83
			case 'logo':
84
			case 'logoheader':
85
			case 'favicon':
86
				return $this->urlGenerator->imagePath('core', 'logo/logo.png') . '?v=' . $cacheBusterCounter;
87
			case 'background':
88
				return $this->urlGenerator->imagePath('core', 'background.png') . '?v=' . $cacheBusterCounter;
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return string. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
89
		}
90
	}
91
92
	public function getImageUrlAbsolute(string $key, bool $useSvg = true): string {
93
		return $this->urlGenerator->getAbsoluteURL($this->getImageUrl($key, $useSvg));
94
	}
95
96
	/**
97
	 * @param string $key
98
	 * @param bool $useSvg
99
	 * @return ISimpleFile
100
	 * @throws NotFoundException
101
	 * @throws NotPermittedException
102
	 */
103
	public function getImage(string $key, bool $useSvg = true): ISimpleFile {
104
		$pngFile = null;
105
		$logo = $this->config->getAppValue('theming', $key . 'Mime', false);
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type string expected by parameter $default of OCP\IConfig::getAppValue(). ( Ignorable by Annotation )

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

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