Completed
Pull Request — master (#7498)
by Julius
14:16
created

Avatar::get()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 8
nc 2
nop 1
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Arthur Schiwon <[email protected]>
6
 * @author Christopher Schäpers <[email protected]>
7
 * @author Lukas Reschke <[email protected]>
8
 * @author Morris Jobke <[email protected]>
9
 * @author Olivier Mehani <[email protected]>
10
 * @author Robin Appelman <[email protected]>
11
 * @author Roeland Jago Douma <[email protected]>
12
 * @author Thomas Müller <[email protected]>
13
 *
14
 * @license AGPL-3.0
15
 *
16
 * This code is free software: you can redistribute it and/or modify
17
 * it under the terms of the GNU Affero General Public License, version 3,
18
 * as published by the Free Software Foundation.
19
 *
20
 * This program is distributed in the hope that it will be useful,
21
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
22
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23
 * GNU Affero General Public License for more details.
24
 *
25
 * You should have received a copy of the GNU Affero General Public License, version 3,
26
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
27
 *
28
 */
29
30
namespace OC;
31
32
use OC\User\User;
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\IAvatar;
38
use OCP\IConfig;
39
use OCP\IImage;
40
use OCP\IL10N;
41
use OC_Image;
42
use OCP\ILogger;
43
44
/**
45
 * This class gets and sets users avatars.
46
 */
47
48
class Avatar implements IAvatar {
49
	/** @var ISimpleFolder */
50
	private $folder;
51
	/** @var IL10N */
52
	private $l;
53
	/** @var User */
54
	private $user;
55
	/** @var ILogger  */
56
	private $logger;
57
	/** @var IConfig */
58
	private $config;
59
60
	/**
61
	 * constructor
62
	 *
63
	 * @param ISimpleFolder $folder The folder where the avatars are
64
	 * @param IL10N $l
65
	 * @param User $user
66
	 * @param ILogger $logger
67
	 * @param IConfig $config
68
	 */
69
	public function __construct(ISimpleFolder $folder,
70
								IL10N $l,
71
								$user,
72
								ILogger $logger,
73
								IConfig $config) {
74
		$this->folder = $folder;
75
		$this->l = $l;
76
		$this->user = $user;
77
		$this->logger = $logger;
78
		$this->config = $config;
79
	}
80
81
	/**
82
	 * @inheritdoc
83
	 */
84
	public function get ($size = 64) {
85
		try {
86
			$file = $this->getFile($size);
87
		} catch (NotFoundException $e) {
88
			return false;
89
		}
90
91
		$avatar = new OC_Image();
92
		$avatar->loadFromData($file->getContent());
93
		return $avatar;
94
	}
95
96
	/**
97
	 * Check if an avatar exists for the user
98
	 *
99
	 * @return bool
100
	 */
101
	public function exists() {
102
103
		return $this->folder->fileExists('avatar.jpg') || $this->folder->fileExists('avatar.png');
104
	}
105
106
	/**
107
	 * sets the users avatar
108
	 * @param IImage|resource|string $data An image object, imagedata or path to set a new avatar
109
	 * @throws \Exception if the provided file is not a jpg or png image
110
	 * @throws \Exception if the provided image is not valid
111
	 * @throws NotSquareException if the image is not square
112
	 * @return void
113
	*/
114
	public function set ($data) {
115
116
		if($data instanceOf IImage) {
117
			$img = $data;
118
			$data = $img->data();
119
		} else {
120
			$img = new OC_Image($data);
121
		}
122
		$type = substr($img->mimeType(), -3);
123
		if ($type === 'peg') {
124
			$type = 'jpg';
125
		}
126
		if ($type !== 'jpg' && $type !== 'png') {
127
			throw new \Exception($this->l->t('Unknown filetype'));
128
		}
129
130
		if (!$img->valid()) {
131
			throw new \Exception($this->l->t('Invalid image'));
132
		}
133
134
		if (!($img->height() === $img->width())) {
135
			throw new NotSquareException($this->l->t('Avatar image is not square'));
136
		}
137
138
		$this->remove();
139
		$file = $this->folder->newFile('avatar.'.$type);
140
		$file->putContent($data);
0 ignored issues
show
Bug introduced by
It seems like $data defined by parameter $data on line 114 can also be of type resource; however, OCP\Files\SimpleFS\ISimpleFile::putContent() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
141
142
		try {
143
			$generated = $this->folder->getFile('generated');
144
			$this->config->setUserValue($this->user->getUID(), 'avatar', 'generated', 'false');
145
			$generated->delete();
146
		} catch (NotFoundException $e) {
147
			//
148
		}
149
		$this->user->triggerChange('avatar', $file);
150
	}
151
152
	/**
153
	 * remove the users avatar
154
	 * @return void
155
	*/
156
	public function remove () {
157
		$avatars = $this->folder->getDirectoryListing();
158
159
		$this->config->setUserValue($this->user->getUID(), 'avatar', 'version',
160
			(int)$this->config->getUserValue($this->user->getUID(), 'avatar', 'version', 0) + 1);
161
162
		foreach ($avatars as $avatar) {
163
			$avatar->delete();
164
		}
165
		$this->config->setUserValue($this->user->getUID(), 'avatar', 'generated', 'true');
166
		$this->user->triggerChange('avatar', '');
167
	}
168
169
	/**
170
	 * @inheritdoc
171
	 */
172
	public function getFile($size) {
173
		try {
174
			$ext = $this->getExtension();
175
		} catch (NotFoundException $e) {
176
			$data = $this->generateAvatar($this->user->getDisplayName(), 1024);
177
			$avatar = $this->folder->newFile('avatar.png');
178
			$avatar->putContent($data);
179
			$ext = 'png';
180
181
			$this->folder->newFile('generated');
182
			$this->config->setUserValue($this->user->getUID(), 'avatar', 'generated', 'true');
183
		}
184
185
		if ($size === -1) {
186
			$path = 'avatar.' . $ext;
187
		} else {
188
			$path = 'avatar.' . $size . '.' . $ext;
189
		}
190
191
		try {
192
			$file = $this->folder->getFile($path);
193
		} catch (NotFoundException $e) {
194
			if ($size <= 0) {
195
				throw new NotFoundException;
196
			}
197
198
			if ($this->folder->fileExists('generated')) {
199
				$data = $this->generateAvatar($this->user->getDisplayName(), $size);
200
201
			} else {
202
				$avatar = new OC_Image();
203
				/** @var ISimpleFile $file */
204
				$file = $this->folder->getFile('avatar.' . $ext);
205
				$avatar->loadFromData($file->getContent());
206
				$avatar->resize($size);
207
				$data = $avatar->data();
208
			}
209
210
			try {
211
				$file = $this->folder->newFile($path);
212
				$file->putContent($data);
213
			} catch (NotPermittedException $e) {
214
				$this->logger->error('Failed to save avatar for ' . $this->user->getUID());
215
				throw new NotFoundException();
216
			}
217
218
		}
219
220
		return $file;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $file; (OCP\Files\SimpleFS\ISimpleFile) is incompatible with the return type declared by the interface OCP\IAvatar::getFile of type OCP\Files\File.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
221
	}
222
223
	/**
224
	 * Get the extension of the avatar. If there is no avatar throw Exception
225
	 *
226
	 * @return string
227
	 * @throws NotFoundException
228
	 */
229
	private function getExtension() {
230
		if ($this->folder->fileExists('avatar.jpg')) {
231
			return 'jpg';
232
		} elseif ($this->folder->fileExists('avatar.png')) {
233
			return 'png';
234
		}
235
		throw new NotFoundException;
236
	}
237
238
	/**
239
	 * @param string $userDisplayName
240
	 * @param int $size
241
	 * @return string
242
	 */
243
	private function generateAvatar($userDisplayName, $size) {
244
		$text = strtoupper(substr($userDisplayName, 0, 1));
245
		$backgroundColor = $this->avatarBackgroundColor($userDisplayName);
246
247
		$im = imagecreatetruecolor($size, $size);
248
		$background = imagecolorallocate($im, $backgroundColor[0], $backgroundColor[1], $backgroundColor[2]);
249
		$white = imagecolorallocate($im, 255, 255, 255);
250
		imagefilledrectangle($im, 0, 0, $size, $size, $background);
251
252
		$font = __DIR__ . '/../../core/fonts/OpenSans-Semibold.woff';
253
254
		$fontSize = $size * 0.4;
255
		$box = imagettfbbox($fontSize, 0, $font, $text);
256
257
		$x = ($size - ($box[2] - $box[0])) / 2;
258
		$y = ($size - ($box[1] - $box[7])) / 2;
259
		$x += 1;
260
		$y -= $box[7];
261
		imagettftext($im, $fontSize, 0, $x, $y, $white, $font, $text);
262
263
		ob_start();
264
		imagepng($im);
265
		$data = ob_get_contents();
266
		ob_end_clean();
267
268
		return $data;
269
	}
270
271
	/**
272
	 * @param int $r
273
	 * @param int $g
274
	 * @param int $b
275
	 * @return double[] Array containing h s l in [0, 1] range
276
	 */
277
	private function rgbToHsl($r, $g, $b) {
278
		$r /= 255.0;
279
		$g /= 255.0;
280
		$b /= 255.0;
281
282
		$max = max($r, $g, $b);
283
		$min = min($r, $g, $b);
284
285
286
		$h = ($max + $min) / 2.0;
287
		$l = ($max + $min) / 2.0;
288
289
		if($max === $min) {
290
			$h = $s = 0; // Achromatic
291
		} else {
292
			$d = $max - $min;
293
			$s = $l > 0.5 ? $d / (2 - $max - $min) : $d / ($max + $min);
294
			switch($max) {
295
				case $r:
296
					$h = ($g - $b) / $d + ($g < $b ? 6 : 0);
297
					break;
298
				case $g:
299
					$h = ($b - $r) / $d + 2.0;
300
					break;
301
				case $b:
302
					$h = ($r - $g) / $d + 4.0;
303
					break;
304
			}
305
			$h /= 6.0;
306
		}
307
		return [$h, $s, $l];
308
309
	}
310
311
	/**
312
	 * @param string $text
313
	 * @return int[] Array containting r g b in the range [0, 255]
314
	 */
315
	private function avatarBackgroundColor($text) {
316
		$hash = preg_replace('/[^0-9a-f]+/', '', $text);
317
318
		$hash = md5($hash);
319
		$hashChars = str_split($hash);
320
321
322
		// Init vars
323
		$result = ['0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0'];
324
		$rgb = [0, 0, 0];
325
		$sat = 0.70;
326
		$lum = 0.68;
327
		$modulo = 16;
328
329
330
		// Splitting evenly the string
331
		foreach($hashChars as  $i => $char) {
332
			$result[$i % $modulo] .= intval($char, 16);
333
		}
334
335
		// Converting our data into a usable rgb format
336
		// Start at 1 because 16%3=1 but 15%3=0 and makes the repartition even
337
		for($count = 1; $count < $modulo; $count++) {
338
			$rgb[$count%3] += (int)$result[$count];
339
		}
340
341
		// Reduce values bigger than rgb requirements
342
		$rgb[0] %= 255;
343
		$rgb[1] %= 255;
344
		$rgb[2] %= 255;
345
346
		$hsl = $this->rgbToHsl($rgb[0], $rgb[1], $rgb[2]);
347
348
		// Classic formula to check the brightness for our eye
349
		// If too bright, lower the sat
350
		$bright = sqrt(0.299 * ($rgb[0] ** 2) + 0.587 * ($rgb[1] ** 2) + 0.114 * ($rgb[2] ** 2));
351
		if ($bright >= 200) {
352
			$sat = 0.60;
353
		}
354
355
		return $this->hslToRgb($hsl[0], $sat, $lum);
356
	}
357
358
	/**
359
	 * @param double $h Hue in range [0, 1]
360
	 * @param double $s Saturation in range [0, 1]
361
	 * @param double $l Lightness in range [0, 1]
362
	 * @return int[] Array containing r g b in the range [0, 255]
363
	 */
364
	private function hslToRgb($h, $s, $l){
365
		$hue2rgb = function ($p, $q, $t){
366
			if($t < 0) {
367
				$t += 1;
368
			}
369
			if($t > 1) {
370
				$t -= 1;
371
			}
372 View Code Duplication
			if($t < 1/6) {
373
				return $p + ($q - $p) * 6 * $t;
374
			}
375
			if($t < 1/2) {
376
				return $q;
377
			}
378 View Code Duplication
			if($t < 2/3) {
379
				return $p + ($q - $p) * (2/3 - $t) * 6;
380
			}
381
			return $p;
382
		};
383
384
		if($s === 0){
385
			$r = $l;
386
			$g = $l;
387
			$b = $l; // achromatic
388
		}else{
389
			$q = $l < 0.5 ? $l * (1 + $s) : $l + $s - $l * $s;
390
			$p = 2 * $l - $q;
391
			$r = $hue2rgb($p, $q, $h + 1/3);
392
			$g = $hue2rgb($p, $q, $h);
393
			$b = $hue2rgb($p, $q, $h - 1/3);
394
		}
395
396
		return array(round($r * 255), round($g * 255), round($b * 255));
397
	}
398
399
	public function userChanged($feature, $oldValue, $newValue) {
400
		// We only change the avatar on display name changes
401
		if ($feature !== 'displayName') {
402
			return;
403
		}
404
405
		// If the avatar is not generated (so an uploaded image) we skip this
406
		if (!$this->folder->fileExists('generated')) {
407
			return;
408
		}
409
410
		$this->remove();
411
	}
412
413
}
414