Completed
Push — master ( abb175...0e9009 )
by Morris
131:42 queued 103:31
created

Avatar::remove()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 8
nc 2
nop 0
dl 0
loc 12
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();
121
			if (is_resource($data) && get_resource_type($data) === "gd") {
122
				$img->setResource($data);
0 ignored issues
show
Documentation introduced by
$data is of type resource, but the function expects a object<Returns>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
123
			} elseif(is_resource($data)) {
124
				$img->loadFromFileHandle($data);
125
			} else {
126
				try {
127
					// detect if it is a path or maybe the images as string
128
					$result = @realpath($data);
129
					if ($result === false || $result === null) {
130
						$img->loadFromData($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, OC_Image::loadFromData() 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...
131
					} else {
132
						$img->loadFromFile($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, OC_Image::loadFromFile() does only seem to accept boolean|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...
133
					}
134
				} catch (\Error $e) {
0 ignored issues
show
Bug introduced by
The class Error does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

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