Passed
Push — master ( c613c8...97e427 )
by John
37:17 queued 19:48
created

PhotoCache::getPhoto()   A

Complexity

Conditions 2
Paths 3

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 7
c 1
b 0
f 0
nc 3
nop 1
dl 0
loc 10
rs 10
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016 Roeland Jago Douma <[email protected]>
4
 *
5
 * @author Arthur Schiwon <[email protected]>
6
 * @author Christoph Wurst <[email protected]>
7
 * @author Daniel Kesselberg <[email protected]>
8
 * @author Jacob Neplokh <[email protected]>
9
 * @author Joas Schilling <[email protected]>
10
 * @author John Molakvoæ <[email protected]>
11
 * @author Morris Jobke <[email protected]>
12
 * @author Roeland Jago Douma <[email protected]>
13
 *
14
 * @license GNU AGPL version 3 or any later version
15
 *
16
 * This program is free software: you can redistribute it and/or modify
17
 * it under the terms of the GNU Affero General Public License as
18
 * published by the Free Software Foundation, either version 3 of the
19
 * License, or (at your option) any later version.
20
 *
21
 * This program is distributed in the hope that it will be useful,
22
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
23
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24
 * GNU Affero General Public License for more details.
25
 *
26
 * You should have received a copy of the GNU Affero General Public License
27
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
28
 *
29
 */
30
namespace OCA\DAV\CardDAV;
31
32
use OCP\Files\IAppData;
33
use OCP\Files\NotFoundException;
34
use OCP\Files\NotPermittedException;
35
use OCP\Files\SimpleFS\ISimpleFile;
36
use OCP\Files\SimpleFS\ISimpleFolder;
37
use OCP\ILogger;
38
use Sabre\CardDAV\Card;
39
use Sabre\VObject\Document;
40
use Sabre\VObject\Parameter;
41
use Sabre\VObject\Property\Binary;
42
use Sabre\VObject\Reader;
43
44
class PhotoCache {
45
46
	/** @var array  */
47
	public const ALLOWED_CONTENT_TYPES = [
48
		'image/png' => 'png',
49
		'image/jpeg' => 'jpg',
50
		'image/gif' => 'gif',
51
		'image/vnd.microsoft.icon' => 'ico',
52
	];
53
54
	/** @var IAppData */
55
	protected $appData;
56
57
	/** @var ILogger */
58
	protected $logger;
59
60
	/**
61
	 * PhotoCache constructor.
62
	 *
63
	 * @param IAppData $appData
64
	 * @param ILogger $logger
65
	 */
66
	public function __construct(IAppData $appData, ILogger $logger) {
67
		$this->appData = $appData;
68
		$this->logger = $logger;
69
	}
70
71
	/**
72
	 * @param int $addressBookId
73
	 * @param string $cardUri
74
	 * @param int $size
75
	 * @param Card $card
76
	 *
77
	 * @return ISimpleFile
78
	 * @throws NotFoundException
79
	 */
80
	public function get($addressBookId, $cardUri, $size, Card $card) {
81
		$folder = $this->getFolder($addressBookId, $cardUri);
82
83
		if ($this->isEmpty($folder)) {
84
			$this->init($folder, $card);
85
		}
86
87
		if (!$this->hasPhoto($folder)) {
88
			throw new NotFoundException();
89
		}
90
91
		if ($size !== -1) {
92
			$size = 2 ** ceil(log($size) / log(2));
93
		}
94
95
		return $this->getFile($folder, $size);
96
	}
97
98
	/**
99
	 * @param ISimpleFolder $folder
100
	 * @return bool
101
	 */
102
	private function isEmpty(ISimpleFolder $folder) {
103
		return $folder->getDirectoryListing() === [];
104
	}
105
106
	/**
107
	 * @param ISimpleFolder $folder
108
	 * @param Card $card
109
	 * @throws NotPermittedException
110
	 */
111
	private function init(ISimpleFolder $folder, Card $card): void {
112
		$data = $this->getPhoto($card);
113
114
		if ($data === false || !isset($data['Content-Type'])) {
115
			$folder->newFile('nophoto', '');
116
			return;
117
		}
118
119
		$contentType = $data['Content-Type'];
120
		$extension = self::ALLOWED_CONTENT_TYPES[$contentType] ?? null;
121
122
		if ($extension === null) {
123
			$folder->newFile('nophoto', '');
124
			return;
125
		}
126
127
		$file = $folder->newFile('photo.' . $extension);
128
		$file->putContent($data['body']);
129
	}
130
131
	private function hasPhoto(ISimpleFolder $folder) {
132
		return !$folder->fileExists('nophoto');
133
	}
134
135
	private function getFile(ISimpleFolder $folder, $size) {
136
		$ext = $this->getExtension($folder);
137
138
		if ($size === -1) {
139
			$path = 'photo.' . $ext;
140
		} else {
141
			$path = 'photo.' . $size . '.' . $ext;
142
		}
143
144
		try {
145
			$file = $folder->getFile($path);
146
		} catch (NotFoundException $e) {
147
			if ($size <= 0) {
148
				throw new NotFoundException;
149
			}
150
151
			$photo = new \OC_Image();
152
			/** @var ISimpleFile $file */
153
			$file = $folder->getFile('photo.' . $ext);
154
			$photo->loadFromData($file->getContent());
155
156
			$ratio = $photo->width() / $photo->height();
157
			if ($ratio < 1) {
158
				$ratio = 1 / $ratio;
159
			}
160
161
			$size = (int) ($size * $ratio);
162
			if ($size !== -1) {
163
				$photo->resize($size);
164
			}
165
166
			try {
167
				$file = $folder->newFile($path);
168
				$file->putContent($photo->data());
169
			} catch (NotPermittedException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
170
			}
171
		}
172
173
		return $file;
174
	}
175
176
	/**
177
	 * @throws NotFoundException
178
	 * @throws NotPermittedException
179
	 */
180
	private function getFolder(int $addressBookId, string $cardUri, bool $createIfNotExists = true): ISimpleFolder {
181
		$hash = md5($addressBookId . ' ' . $cardUri);
182
		try {
183
			return $this->appData->getFolder($hash);
184
		} catch (NotFoundException $e) {
185
			if ($createIfNotExists) {
186
				return $this->appData->newFolder($hash);
187
			} else {
188
				throw $e;
189
			}
190
		}
191
	}
192
193
	/**
194
	 * Get the extension of the avatar. If there is no avatar throw Exception
195
	 *
196
	 * @param ISimpleFolder $folder
197
	 * @return string
198
	 * @throws NotFoundException
199
	 */
200
	private function getExtension(ISimpleFolder $folder): string {
201
		foreach (self::ALLOWED_CONTENT_TYPES as $extension) {
202
			if ($folder->fileExists('photo.' . $extension)) {
203
				return $extension;
204
			}
205
		}
206
207
		throw new NotFoundException('Avatar not found');
208
	}
209
210
	/**
211
	 * @param Card $node
212
	 * @return bool|array{body: string, Content-Type: string}
213
	 */
214
	private function getPhoto(Card $node) {
215
		try {
216
			$vObject = $this->readCard($node->get());
217
			return $this->getPhotoFromVObject($vObject);
218
		} catch (\Exception $e) {
219
			$this->logger->logException($e, [
220
				'message' => 'Exception during vcard photo parsing'
221
			]);
222
		}
223
		return false;
224
	}
225
226
	/**
227
	 * @param Document $vObject
228
	 * @return bool|array{body: string, Content-Type: string}
229
	 */
230
	public function getPhotoFromVObject(Document $vObject) {
231
		try {
232
			if (!$vObject->PHOTO) {
233
				return false;
234
			}
235
236
			$photo = $vObject->PHOTO;
237
			$val = $photo->getValue();
238
239
			// handle data URI. e.g PHOTO;VALUE=URI:data:image/jpeg;base64,/9j/4AAQSkZJRgABAQE
240
			if ($photo->getValueType() === 'URI') {
241
				$parsed = \Sabre\URI\parse($val);
242
243
				// only allow data://
244
				if ($parsed['scheme'] !== 'data') {
245
					return false;
246
				}
247
				if (substr_count($parsed['path'], ';') === 1) {
248
					[$type] = explode(';', $parsed['path']);
249
				}
250
				$val = file_get_contents($val);
251
			} else {
252
				// get type if binary data
253
				$type = $this->getBinaryType($photo);
254
			}
255
256
			if (empty($type) || !isset(self::ALLOWED_CONTENT_TYPES[$type])) {
257
				$type = 'application/octet-stream';
258
			}
259
260
			return [
261
				'Content-Type' => $type,
262
				'body' => $val
263
			];
264
		} catch (\Exception $e) {
265
			$this->logger->logException($e, [
266
				'message' => 'Exception during vcard photo parsing'
267
			]);
268
		}
269
		return false;
270
	}
271
272
	/**
273
	 * @param string $cardData
274
	 * @return \Sabre\VObject\Document
275
	 */
276
	private function readCard($cardData) {
277
		return Reader::read($cardData);
278
	}
279
280
	/**
281
	 * @param Binary $photo
282
	 * @return string
283
	 */
284
	private function getBinaryType(Binary $photo) {
285
		$params = $photo->parameters();
286
		if (isset($params['TYPE']) || isset($params['MEDIATYPE'])) {
287
			/** @var Parameter $typeParam */
288
			$typeParam = isset($params['TYPE']) ? $params['TYPE'] : $params['MEDIATYPE'];
289
			$type = $typeParam->getValue();
290
291
			if (strpos($type, 'image/') === 0) {
292
				return $type;
293
			} else {
294
				return 'image/' . strtolower($type);
295
			}
296
		}
297
		return '';
298
	}
299
300
	/**
301
	 * @param int $addressBookId
302
	 * @param string $cardUri
303
	 * @throws NotPermittedException
304
	 */
305
	public function delete($addressBookId, $cardUri) {
306
		try {
307
			$folder = $this->getFolder($addressBookId, $cardUri, false);
308
			$folder->delete();
309
		} catch (NotFoundException $e) {
310
			// that's OK, nothing to do
311
		}
312
	}
313
}
314