Completed
Push — master ( 5bc8c9...07e638 )
by Morris
52:34 queued 36:57
created

Avatar::avatarBackgroundColor()   B

Complexity

Conditions 4
Paths 8

Size

Total Lines 42
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 21
c 1
b 0
f 0
nc 8
nop 1
dl 0
loc 42
rs 8.5806
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 View Code Duplication
	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
			$generated->delete();
145
		} catch (NotFoundException $e) {
146
			//
147
		}
148
		$this->user->triggerChange('avatar', $file);
149
	}
150
151
	/**
152
	 * remove the users avatar
153
	 * @return void
154
	*/
155
	public function remove () {
156
		$avatars = $this->folder->getDirectoryListing();
157
158
		$this->config->setUserValue($this->user->getUID(), 'avatar', 'version',
159
			(int)$this->config->getUserValue($this->user->getUID(), 'avatar', 'version', 0) + 1);
160
161
		foreach ($avatars as $avatar) {
162
			$avatar->delete();
163
		}
164
		$this->user->triggerChange('avatar', '');
165
	}
166
167
	/**
168
	 * @inheritdoc
169
	 */
170
	public function getFile($size) {
171
		try {
172
			$ext = $this->getExtension();
173
		} catch (NotFoundException $e) {
174
			$data = $this->generateAvatar($this->user->getDisplayName(), 1024);
175
			$avatar = $this->folder->newFile('avatar.png');
176
			$avatar->putContent($data);
177
			$ext = 'png';
178
179
			$this->folder->newFile('generated');
180
		}
181
182
		if ($size === -1) {
183
			$path = 'avatar.' . $ext;
184
		} else {
185
			$path = 'avatar.' . $size . '.' . $ext;
186
		}
187
188
		try {
189
			$file = $this->folder->getFile($path);
190
		} catch (NotFoundException $e) {
191
			if ($size <= 0) {
192
				throw new NotFoundException;
193
			}
194
195
			if ($this->folder->fileExists('generated')) {
196
				$data = $this->generateAvatar($this->user->getDisplayName(), $size);
197
198
			} else {
199
				$avatar = new OC_Image();
200
				/** @var ISimpleFile $file */
201
				$file = $this->folder->getFile('avatar.' . $ext);
202
				$avatar->loadFromData($file->getContent());
203
				$avatar->resize($size);
204
				$data = $avatar->data();
205
			}
206
207
			try {
208
				$file = $this->folder->newFile($path);
209
				$file->putContent($data);
210
			} catch (NotPermittedException $e) {
211
				$this->logger->error('Failed to save avatar for ' . $this->user->getUID());
212
				throw new NotFoundException();
213
			}
214
215
		}
216
217
		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...
218
	}
219
220
	/**
221
	 * Get the extension of the avatar. If there is no avatar throw Exception
222
	 *
223
	 * @return string
224
	 * @throws NotFoundException
225
	 */
226
	private function getExtension() {
227
		if ($this->folder->fileExists('avatar.jpg')) {
228
			return 'jpg';
229
		} elseif ($this->folder->fileExists('avatar.png')) {
230
			return 'png';
231
		}
232
		throw new NotFoundException;
233
	}
234
235
	/**
236
	 * @param string $userDisplayName
237
	 * @param int $size
238
	 * @return string
239
	 */
240
	private function generateAvatar($userDisplayName, $size) {
241
		$text = strtoupper(substr($userDisplayName, 0, 1));
242
		$backgroundColor = $this->avatarBackgroundColor($userDisplayName);
243
244
		$im = imagecreatetruecolor($size, $size);
245
		$background = imagecolorallocate($im, $backgroundColor[0], $backgroundColor[1], $backgroundColor[2]);
246
		$white = imagecolorallocate($im, 255, 255, 255);
247
		imagefilledrectangle($im, 0, 0, $size, $size, $background);
248
249
		$font = __DIR__ . '/../../core/fonts/OpenSans-Semibold.woff';
250
251
		$fontSize = $size * 0.4;
252
		$box = imagettfbbox($fontSize, 0, $font, $text);
253
254
		$x = ($size - ($box[2] - $box[0])) / 2;
255
		$y = ($size - ($box[1] - $box[7])) / 2;
256
		$x += 1;
257
		$y -= $box[7];
258
		imagettftext($im, $fontSize, 0, $x, $y, $white, $font, $text);
259
260
		ob_start();
261
		imagepng($im);
262
		$data = ob_get_contents();
263
		ob_end_clean();
264
265
		return $data;
266
	}
267
268
	/**
269
	 * @param int $r
270
	 * @param int $g
271
	 * @param int $b
272
	 * @return double[] Array containing h s l in [0, 1] range
273
	 */
274
	private function rgbToHsl($r, $g, $b) {
275
		$r /= 255.0;
276
		$g /= 255.0;
277
		$b /= 255.0;
278
279
		$max = max($r, $g, $b);
280
		$min = min($r, $g, $b);
281
282
283
		$h = ($max + $min) / 2.0;
284
		$l = ($max + $min) / 2.0;
285
286
		if($max === $min) {
287
			$h = $s = 0; // Achromatic
288
		} else {
289
			$d = $max - $min;
290
			$s = $l > 0.5 ? $d / (2 - $max - $min) : $d / ($max + $min);
291
			switch($max) {
292
				case $r:
293
					$h = ($g - $b) / $d + ($g < $b ? 6 : 0);
294
					break;
295
				case $g:
296
					$h = ($b - $r) / $d + 2.0;
297
					break;
298
				case $b:
299
					$h = ($r - $g) / $d + 4.0;
300
					break;
301
			}
302
			$h /= 6.0;
303
		}
304
		return [$h, $s, $l];
305
306
	}
307
308
	/**
309
	 * @param string $text
310
	 * @return int[] Array containting r g b in the range [0, 255]
311
	 */
312
	private function avatarBackgroundColor($text) {
313
		$hash = preg_replace('/[^0-9a-f]+/', '', $text);
314
315
		$hash = md5($hash);
316
		$hashChars = str_split($hash);
317
318
319
		// Init vars
320
		$result = ['0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0'];
321
		$rgb = [0, 0, 0];
322
		$sat = 0.70;
323
		$lum = 0.68;
324
		$modulo = 16;
325
326
327
		// Splitting evenly the string
328
		foreach($hashChars as  $i => $char) {
329
			$result[$i % $modulo] .= intval($char, 16);
330
		}
331
332
		// Converting our data into a usable rgb format
333
		// Start at 1 because 16%3=1 but 15%3=0 and makes the repartition even
334
		for($count = 1; $count < $modulo; $count++) {
335
			$rgb[$count%3] += (int)$result[$count];
336
		}
337
338
		// Reduce values bigger than rgb requirements
339
		$rgb[0] %= 255;
340
		$rgb[1] %= 255;
341
		$rgb[2] %= 255;
342
343
		$hsl = $this->rgbToHsl($rgb[0], $rgb[1], $rgb[2]);
344
345
		// Classic formula to check the brightness for our eye
346
		// If too bright, lower the sat
347
		$bright = sqrt(0.299 * ($rgb[0] ** 2) + 0.587 * ($rgb[1] ** 2) + 0.114 * ($rgb[2] ** 2));
348
		if ($bright >= 200) {
349
			$sat = 0.60;
350
		}
351
352
		return $this->hslToRgb($hsl[0], $sat, $lum);
353
	}
354
355
	/**
356
	 * @param double $h Hue in range [0, 1]
357
	 * @param double $s Saturation in range [0, 1]
358
	 * @param double $l Lightness in range [0, 1]
359
	 * @return int[] Array containing r g b in the range [0, 255]
360
	 */
361
	private function hslToRgb($h, $s, $l){
362
		$hue2rgb = function ($p, $q, $t){
363
			if($t < 0) {
364
				$t += 1;
365
			}
366
			if($t > 1) {
367
				$t -= 1;
368
			}
369 View Code Duplication
			if($t < 1/6) {
370
				return $p + ($q - $p) * 6 * $t;
371
			}
372
			if($t < 1/2) {
373
				return $q;
374
			}
375 View Code Duplication
			if($t < 2/3) {
376
				return $p + ($q - $p) * (2/3 - $t) * 6;
377
			}
378
			return $p;
379
		};
380
381
		if($s === 0){
382
			$r = $l;
383
			$g = $l;
384
			$b = $l; // achromatic
385
		}else{
386
			$q = $l < 0.5 ? $l * (1 + $s) : $l + $s - $l * $s;
387
			$p = 2 * $l - $q;
388
			$r = $hue2rgb($p, $q, $h + 1/3);
389
			$g = $hue2rgb($p, $q, $h);
390
			$b = $hue2rgb($p, $q, $h - 1/3);
391
		}
392
393
		return array(round($r * 255), round($g * 255), round($b * 255));
394
	}
395
396
}
397