Passed
Push — master ( b69b17...32a6f4 )
by Morris
11:33
created

UserAvatar::getAvatarImage()   B

Complexity

Conditions 8
Paths 8

Size

Total Lines 25
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 17
nc 8
nop 1
dl 0
loc 25
rs 8.4444
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
4
/**
5
 * @copyright Copyright (c) 2018, Michael Weimann <[email protected]>
6
 *
7
 * @author Michael Weimann <[email protected]>
8
 *
9
 * @license AGPL-3.0
10
 *
11
 * This code is free software: you can redistribute it and/or modify
12
 * it under the terms of the GNU Affero General Public License, version 3,
13
 * as published by the Free Software Foundation.
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, version 3,
21
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
22
 */
23
24
namespace OC\Avatar;
25
26
use OC\NotSquareException;
27
use OC\User\User;
28
use OC_Image;
29
use OCP\Files\NotFoundException;
30
use OCP\Files\NotPermittedException;
31
use OCP\Files\SimpleFS\ISimpleFile;
32
use OCP\Files\SimpleFS\ISimpleFolder;
33
use OCP\IConfig;
34
use OCP\IImage;
35
use OCP\IL10N;
36
use OCP\ILogger;
37
38
/**
39
 * This class represents a registered user's avatar.
40
 */
41
class UserAvatar extends Avatar {
42
	/** @var IConfig */
43
	private $config;
44
45
	/** @var ISimpleFolder */
46
	private $folder;
47
48
	/** @var IL10N */
49
	private $l;
50
51
	/** @var User */
52
	private $user;
53
54
	/**
55
	 * UserAvatar constructor.
56
	 *
57
	 * @param IConfig $config The configuration
58
	 * @param ISimpleFolder $folder The avatar files folder
59
	 * @param IL10N $l The localization helper
60
	 * @param User $user The user this class manages the avatar for
61
	 * @param ILogger $logger The logger
62
	 */
63
	public function __construct(
64
		ISimpleFolder $folder,
65
		IL10N $l,
66
		$user,
67
		ILogger $logger,
68
		IConfig $config) {
69
		parent::__construct($logger);
70
		$this->folder = $folder;
71
		$this->l = $l;
72
		$this->user = $user;
73
		$this->config = $config;
74
	}
75
76
	/**
77
	 * Check if an avatar exists for the user
78
	 *
79
	 * @return bool
80
	 */
81
	public function exists() {
82
		return $this->folder->fileExists('avatar.jpg') || $this->folder->fileExists('avatar.png');
83
	}
84
85
	/**
86
	 * Sets the users avatar.
87
	 *
88
	 * @param IImage|resource|string $data An image object, imagedata or path to set a new avatar
89
	 * @throws \Exception if the provided file is not a jpg or png image
90
	 * @throws \Exception if the provided image is not valid
91
	 * @throws NotSquareException if the image is not square
92
	 * @return void
93
	 */
94
	public function set($data) {
95
		$img = $this->getAvatarImage($data);
96
		$data = $img->data();
97
98
		$this->validateAvatar($img);
99
100
		$this->remove();
101
		$type = $this->getAvatarImageType($img);
102
		$file = $this->folder->newFile('avatar.' . $type);
103
		$file->putContent($data);
104
105
		try {
106
			$generated = $this->folder->getFile('generated');
107
			$this->config->setUserValue($this->user->getUID(), 'avatar', 'generated', 'false');
108
			$generated->delete();
109
		} catch (NotFoundException $e) {
110
			//
111
		}
112
113
		$this->user->triggerChange('avatar', $file);
114
	}
115
116
	/**
117
	 * Returns an image from several sources.
118
	 *
119
	 * @param IImage|resource|string $data An image object, imagedata or path to the avatar
120
	 * @return IImage
121
	 */
122
	private function getAvatarImage($data) {
123
		if ($data instanceof IImage) {
124
			return $data;
125
		}
126
127
		$img = new OC_Image();
128
		if (is_resource($data) && get_resource_type($data) === 'gd') {
129
			$img->setResource($data);
0 ignored issues
show
Bug introduced by
$data of type resource is incompatible with the type Returns expected by parameter $resource of OC_Image::setResource(). ( Ignorable by Annotation )

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

129
			$img->setResource(/** @scrutinizer ignore-type */ $data);
Loading history...
130
		} elseif (is_resource($data)) {
131
			$img->loadFromFileHandle($data);
132
		} else {
133
			try {
134
				// detect if it is a path or maybe the images as string
135
				$result = @realpath($data);
136
				if ($result === false || $result === null) {
137
					$img->loadFromData($data);
138
				} else {
139
					$img->loadFromFile($data);
140
				}
141
			} catch (\Error $e) {
142
				$img->loadFromData($data);
143
			}
144
		}
145
146
		return $img;
147
	}
148
149
	/**
150
	 * Returns the avatar image type.
151
	 *
152
	 * @param IImage $avatar
153
	 * @return string
154
	 */
155
	private function getAvatarImageType(IImage $avatar) {
156
		$type = substr($avatar->mimeType(), -3);
157
		if ($type === 'peg') {
158
			$type = 'jpg';
159
		}
160
		return $type;
161
	}
162
163
	/**
164
	 * Validates an avatar image:
165
	 * - must be "png" or "jpg"
166
	 * - must be "valid"
167
	 * - must be in square format
168
	 *
169
	 * @param IImage $avatar The avatar to validate
170
	 * @throws \Exception if the provided file is not a jpg or png image
171
	 * @throws \Exception if the provided image is not valid
172
	 * @throws NotSquareException if the image is not square
173
	 */
174
	private function validateAvatar(IImage $avatar) {
175
		$type = $this->getAvatarImageType($avatar);
176
177
		if ($type !== 'jpg' && $type !== 'png') {
178
			throw new \Exception($this->l->t('Unknown filetype'));
179
		}
180
181
		if (!$avatar->valid()) {
182
			throw new \Exception($this->l->t('Invalid image'));
183
		}
184
185
		if (!($avatar->height() === $avatar->width())) {
186
			throw new NotSquareException($this->l->t('Avatar image is not square'));
187
		}
188
	}
189
190
	/**
191
	 * Removes the users avatar.
192
	 * @return void
193
	 * @throws \OCP\Files\NotPermittedException
194
	 * @throws \OCP\PreConditionNotMetException
195
	 */
196
	public function remove() {
197
		$avatars = $this->folder->getDirectoryListing();
198
199
		$this->config->setUserValue($this->user->getUID(), 'avatar', 'version',
200
			(int) $this->config->getUserValue($this->user->getUID(), 'avatar', 'version', 0) + 1);
201
202
		foreach ($avatars as $avatar) {
203
			$avatar->delete();
204
		}
205
		$this->config->setUserValue($this->user->getUID(), 'avatar', 'generated', 'true');
206
		$this->user->triggerChange('avatar', '');
207
	}
208
209
	/**
210
	 * Get the extension of the avatar. If there is no avatar throw Exception
211
	 *
212
	 * @return string
213
	 * @throws NotFoundException
214
	 */
215
	private function getExtension() {
216
		if ($this->folder->fileExists('avatar.jpg')) {
217
			return 'jpg';
218
		} elseif ($this->folder->fileExists('avatar.png')) {
219
			return 'png';
220
		}
221
		throw new NotFoundException;
222
	}
223
224
	/**
225
	 * Returns the avatar for an user.
226
	 *
227
	 * If there is no avatar file yet, one is generated.
228
	 *
229
	 * @param int $size
230
	 * @return ISimpleFile
231
	 * @throws NotFoundException
232
	 * @throws \OCP\Files\NotPermittedException
233
	 * @throws \OCP\PreConditionNotMetException
234
	 */
235
	public function getFile($size) {
236
		$size = (int) $size;
237
238
		try {
239
			$ext = $this->getExtension();
240
		} catch (NotFoundException $e) {
241
			if (!$data = $this->generateAvatarFromSvg(1024)) {
242
				$data = $this->generateAvatar($this->getDisplayName(), 1024);
243
			}
244
			$avatar = $this->folder->newFile('avatar.png');
245
			$avatar->putContent($data);
246
			$ext = 'png';
247
248
			$this->folder->newFile('generated');
249
			$this->config->setUserValue($this->user->getUID(), 'avatar', 'generated', 'true');
250
		}
251
252
		if ($size === -1) {
253
			$path = 'avatar.' . $ext;
254
		} else {
255
			$path = 'avatar.' . $size . '.' . $ext;
256
		}
257
258
		try {
259
			$file = $this->folder->getFile($path);
260
		} catch (NotFoundException $e) {
261
			if ($size <= 0) {
262
				throw new NotFoundException;
263
			}
264
265
			if ($this->folder->fileExists('generated')) {
266
				if (!$data = $this->generateAvatarFromSvg($size)) {
267
					$data = $this->generateAvatar($this->getDisplayName(), $size);
268
				}
269
270
			} else {
271
				$avatar = new OC_Image();
272
				$file = $this->folder->getFile('avatar.' . $ext);
273
				$avatar->loadFromData($file->getContent());
274
				$avatar->resize($size);
275
				$data = $avatar->data();
276
			}
277
278
			try {
279
				$file = $this->folder->newFile($path);
280
				$file->putContent($data);
281
			} catch (NotPermittedException $e) {
282
				$this->logger->error('Failed to save avatar for ' . $this->user->getUID());
283
				throw new NotFoundException();
284
			}
285
286
		}
287
288
		if ($this->config->getUserValue($this->user->getUID(), 'avatar', 'generated', null) === null) {
0 ignored issues
show
introduced by
The condition $this->config->getUserVa...erated', null) === null is always false.
Loading history...
289
			$generated = $this->folder->fileExists('generated') ? 'true' : 'false';
290
			$this->config->setUserValue($this->user->getUID(), 'avatar', 'generated', $generated);
291
		}
292
293
		return $file;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $file returns the type OCP\Files\SimpleFS\ISimpleFile which is incompatible with the return type mandated by OCP\IAvatar::getFile() of OCP\Files\File.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
294
	}
295
296
	/**
297
	 * Returns the user display name.
298
	 *
299
	 * @return string
300
	 */
301
	public function getDisplayName(): string {
302
		return $this->user->getDisplayName();
303
	}
304
305
	/**
306
	 * Handles user changes.
307
	 *
308
	 * @param string $feature The changed feature
309
	 * @param mixed $oldValue The previous value
310
	 * @param mixed $newValue The new value
311
	 * @throws NotPermittedException
312
	 * @throws \OCP\PreConditionNotMetException
313
	 */
314
	public function userChanged($feature, $oldValue, $newValue) {
315
		// We only change the avatar on display name changes
316
		if ($feature !== 'displayName') {
317
			return;
318
		}
319
320
		// If the avatar is not generated (so an uploaded image) we skip this
321
		if (!$this->folder->fileExists('generated')) {
322
			return;
323
		}
324
325
		$this->remove();
326
	}
327
328
	/**
329
	 * Check if the avatar of a user is a custom uploaded one
330
	 *
331
	 * @return bool
332
	 */
333
	public function isCustomAvatar(): bool {
334
		return $this->config->getUserValue($this->user->getUID(), 'avatar', 'generated', 'false') !== 'true';
335
	}
336
}
337