Passed
Push — master ( 5ef74e...846d21 )
by Roeland
10:27
created

PhotoCache::getFolder()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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