Passed
Push — 5.x ( 55f474...5d6836 )
by Jeroen
14:39 queued 12s
created

EntityIconService::generateIcon()   C

Complexity

Conditions 12
Paths 12

Size

Total Lines 72
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 31
CRAP Score 13.6402

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 12
eloc 38
c 1
b 1
f 0
nc 12
nop 5
dl 0
loc 72
ccs 31
cts 40
cp 0.775
crap 13.6402
rs 6.9666

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Elgg;
4
5
use Elgg\Database\EntityTable;
6
use Elgg\Exceptions\ExceptionInterface;
7
use Elgg\Exceptions\InvalidArgumentException;
8
use Elgg\Exceptions\UnexpectedValueException;
9
use Elgg\Filesystem\MimeTypeService;
10
use Elgg\Http\Request as HttpRequest;
11
use Elgg\Traits\Loggable;
12
use Elgg\Traits\TimeUsing;
13
14
/**
15
 * Entity icon service
16
 *
17
 * @internal
18
 * @since 2.2
19
 */
20
class EntityIconService {
21
22
	use Loggable;
23
	use TimeUsing;
24
25
	/**
26
	 * @var Config
27
	 */
28
	private $config;
29
30
	/**
31
	 * @var EventsService
32
	 */
33
	private $events;
34
35
	/**
36
	 * @var EntityTable
37
	 */
38
	private $entities;
39
40
	/**
41
	 * @var UploadService
42
	 */
43
	private $uploads;
44
45
	/**
46
	 * @var ImageService
47
	 */
48
	private $images;
49
	
50
	/**
51
	 * @var MimeTypeService
52
	 */
53
	protected $mimetype;
54
	
55
	/**
56
	 * @var HttpRequest
57
	 */
58
	protected $request;
59
60
	/**
61
	 * Constructor
62
	 *
63
	 * @param Config          $config   Config
64
	 * @param EventsService   $events   Events service
65
	 * @param EntityTable     $entities Entity table
66
	 * @param UploadService   $uploads  Upload service
67
	 * @param ImageService    $images   Image service
68
	 * @param MimeTypeService $mimetype MimeType service
69
	 * @param Request         $request  Http Request service
70
	 */
71 131
	public function __construct(
72
		Config $config,
73
		EventsService $events,
74
		EntityTable $entities,
75
		UploadService $uploads,
76
		ImageService $images,
77
		MimeTypeService $mimetype,
78
		HttpRequest $request
79
	) {
80 131
		$this->config = $config;
81 131
		$this->events = $events;
82 131
		$this->entities = $entities;
83 131
		$this->uploads = $uploads;
84 131
		$this->images = $images;
85 131
		$this->mimetype = $mimetype;
86 131
		$this->request = $request;
87
	}
88
89
	/**
90
	 * Saves icons using an uploaded file as the source.
91
	 *
92
	 * @param \ElggEntity $entity     Entity to own the icons
93
	 * @param string      $input_name Form input name
94
	 * @param string      $type       The name of the icon. e.g., 'icon', 'cover_photo'
95
	 * @param array       $coords     An array of cropping coordinates x1, y1, x2, y2
96
	 *
97
	 * @return bool
98
	 */
99 3
	public function saveIconFromUploadedFile(\ElggEntity $entity, $input_name, $type = 'icon', array $coords = []) {
100 3
		$input = $this->uploads->getFile($input_name);
101 3
		if (empty($input)) {
102 2
			return false;
103
		}
104
				
105
		// auto detect cropping coordinates
106 1
		if (empty($coords)) {
107 1
			$auto_coords = $this->detectCroppingCoordinates($input_name);
108 1
			if (!empty($auto_coords)) {
109
				$coords = $auto_coords;
110
			}
111
		}
112
113 1
		$tmp = new \ElggTempFile();
114 1
		$tmp->setFilename(uniqid() . $input->getClientOriginalName());
115 1
		$tmp->open('write');
116 1
		$tmp->close();
117
		
118 1
		copy($input->getPathname(), $tmp->getFilenameOnFilestore());
119
120 1
		$tmp->mimetype = $this->mimetype->getMimeType($tmp->getFilenameOnFilestore());
121 1
		$tmp->simpletype = $this->mimetype->getSimpleType($tmp->mimetype);
122
123 1
		$result = $this->saveIcon($entity, $tmp, $type, $coords);
124
125 1
		$tmp->delete();
126
127 1
		return $result;
128
	}
129
130
	/**
131
	 * Saves icons using a local file as the source.
132
	 *
133
	 * @param \ElggEntity $entity   Entity to own the icons
134
	 * @param string      $filename The full path to the local file
135
	 * @param string      $type     The name of the icon. e.g., 'icon', 'cover_photo'
136
	 * @param array       $coords   An array of cropping coordinates x1, y1, x2, y2
137
	 *
138
	 * @return bool
139
	 * @throws InvalidArgumentException
140
	 */
141 6
	public function saveIconFromLocalFile(\ElggEntity $entity, $filename, $type = 'icon', array $coords = []) {
142 6
		if (!file_exists($filename) || !is_readable($filename)) {
143 1
			throw new InvalidArgumentException(__METHOD__ . " expects a readable local file. {$filename} is not readable");
144
		}
145
				
146 5
		$tmp = new \ElggTempFile();
147 5
		$tmp->setFilename(uniqid() . basename($filename));
148 5
		$tmp->open('write');
149 5
		$tmp->close();
150
		
151 5
		copy($filename, $tmp->getFilenameOnFilestore());
152
153 5
		$tmp->mimetype = $this->mimetype->getMimeType($tmp->getFilenameOnFilestore());
154 5
		$tmp->simpletype = $this->mimetype->getSimpleType($tmp->mimetype);
155
156 5
		$result = $this->saveIcon($entity, $tmp, $type, $coords);
157
158 5
		$tmp->delete();
159
160 5
		return $result;
161
	}
162
163
	/**
164
	 * Saves icons using a file located in the data store as the source.
165
	 *
166
	 * @param \ElggEntity $entity Entity to own the icons
167
	 * @param \ElggFile   $file   An ElggFile instance
168
	 * @param string      $type   The name of the icon. e.g., 'icon', 'cover_photo'
169
	 * @param array       $coords An array of cropping coordinates x1, y1, x2, y2
170
	 *
171
	 * @return bool
172
	 * @throws InvalidArgumentException
173
	 */
174 40
	public function saveIconFromElggFile(\ElggEntity $entity, \ElggFile $file, $type = 'icon', array $coords = []) {
175 40
		if (!$file->exists()) {
176 1
			throw new InvalidArgumentException(__METHOD__ . ' expects an instance of ElggFile with an existing file on filestore');
177
		}
178
		
179 39
		$tmp = new \ElggTempFile();
180 39
		$tmp->setFilename(uniqid() . basename($file->getFilenameOnFilestore()));
181 39
		$tmp->open('write');
182 39
		$tmp->close();
183
		
184 39
		copy($file->getFilenameOnFilestore(), $tmp->getFilenameOnFilestore());
185
186 39
		$tmp->mimetype = $this->mimetype->getMimeType($tmp->getFilenameOnFilestore(), $file->getMimeType() ?: '');
187 39
		$tmp->simpletype = $this->mimetype->getSimpleType($tmp->mimetype);
188
189 39
		$result = $this->saveIcon($entity, $tmp, $type, $coords);
190
191 39
		$tmp->delete();
192
193 39
		return $result;
194
	}
195
196
	/**
197
	 * Saves icons using a created temporary file
198
	 *
199
	 * @param \ElggEntity $entity Temporary ElggFile instance
200
	 * @param \ElggFile   $file   Temporary ElggFile instance
201
	 * @param string      $type   The name of the icon. e.g., 'icon', 'cover_photo'
202
	 * @param array       $coords An array of cropping coordinates x1, y1, x2, y2
203
	 *
204
	 * @return bool
205
	 */
206 45
	public function saveIcon(\ElggEntity $entity, \ElggFile $file, $type = 'icon', array $coords = []) {
207
208 45
		$type = (string) $type;
209 45
		if (!strlen($type)) {
210
			$this->getLogger()->error('Icon type passed to ' . __METHOD__ . ' can not be empty');
211
			return false;
212
		}
213
		
214 45
		$entity_type = $entity->getType();
215
		
216 45
		$file = $this->events->triggerResults("entity:{$type}:prepare", $entity_type, [
217 45
			'entity' => $entity,
218 45
			'file' => $file,
219 45
		], $file);
220
		
221 45
		if (!$file instanceof \ElggFile || !$file->exists() || $file->getSimpleType() !== 'image') {
222
			$this->getLogger()->error('Source file passed to ' . __METHOD__ . ' can not be resolved to a valid image');
223
			return false;
224
		}
225
		
226 45
		$this->prepareIcon($file->getFilenameOnFilestore());
227
		
228 45
		$x1 = (int) elgg_extract('x1', $coords);
229 45
		$y1 = (int) elgg_extract('y1', $coords);
230 45
		$x2 = (int) elgg_extract('x2', $coords);
231 45
		$y2 = (int) elgg_extract('y2', $coords);
232
		
233 45
		$created = $this->events->triggerResults("entity:{$type}:save", $entity_type, [
234 45
			'entity' => $entity,
235 45
			'file' => $file,
236 45
			'x1' => $x1,
237 45
			'y1' => $y1,
238 45
			'x2' => $x2,
239 45
			'y2' => $y2,
240 45
		], false);
241
242
		// did someone else handle saving the icon?
243 45
		if ($created !== true) {
244
			// remove existing icons
245 44
			$this->deleteIcon($entity, $type, true);
246
			
247
			// save master image
248 44
			$store = $this->generateIcon($entity, $file, $type, $coords, 'master');
249
			
250 44
			if (!$store) {
251
				$this->deleteIcon($entity, $type);
252
				return false;
253
			}
254
			
255
			// validate cropping coords to prevent out-of-bounds issues
256 44
			$sizes = $this->getSizes($entity->getType(), $entity->getSubtype(), $type);
257 44
			$coords = array_merge($sizes['master'], $coords);
258
			
259 44
			$icon = $this->getIcon($entity, 'master', $type, false);
260
			
261
			try {
262 44
				$this->images->normalizeResizeParameters($icon->getFilenameOnFilestore(), $coords);
263
			} catch (ExceptionInterface $e) {
264
				// cropping coords are wrong, reset to 0
265
				$x1 = 0;
266
				$x2 = 0;
267
				$y1 = 0;
268
				$y2 = 0;
269
			}
270
		}
271
272
		// first invalidate entity metadata cache, because of a high risk of racing condition to save the coordinates
273
		// the racing condition occurs with 2 (or more) icon save calls and the time between clearing
274
		// the coordinates in deleteIcon() and the new save here
275 45
		$entity->invalidateCache();
276
		
277
		// save cropping coordinates
278 45
		if ($type == 'icon') {
279 44
			$entity->icontime = time();
280 44
			if ($x1 || $y1 || $x2 || $y2) {
281 7
				$entity->x1 = $x1;
282 7
				$entity->y1 = $y1;
283 7
				$entity->x2 = $x2;
284 44
				$entity->y2 = $y2;
285
			}
286
		} else {
287 1
			if ($x1 || $y1 || $x2 || $y2) {
288 1
				$entity->{"{$type}_coords"} = serialize([
289 1
					'x1' => $x1,
290 1
					'y1' => $y1,
291 1
					'x2' => $x2,
292 1
					'y2' => $y2,
293 1
				]);
294
			}
295
		}
296
		
297 45
		$this->events->triggerResults("entity:{$type}:saved", $entity->getType(), [
298 45
			'entity' => $entity,
299 45
			'x1' => $x1,
300 45
			'y1' => $y1,
301 45
			'x2' => $x2,
302 45
			'y2' => $y2,
303 45
		]);
304
		
305 45
		return true;
306
	}
307
	
308
	/**
309
	 * Prepares an icon
310
	 *
311
	 * @param string $filename the file to prepare
312
	 *
313
	 * @return void
314
	 */
315 45
	protected function prepareIcon($filename) {
316
		
317
		// fix orientation
318 45
		$temp_file = new \ElggTempFile();
319 45
		$temp_file->setFilename(uniqid() . basename($filename));
320
		
321 45
		copy($filename, $temp_file->getFilenameOnFilestore());
322
		
323 45
		$rotated = $this->images->fixOrientation($temp_file->getFilenameOnFilestore());
324
325 45
		if ($rotated) {
326 45
			copy($temp_file->getFilenameOnFilestore(), $filename);
327
		}
328
		
329 45
		$temp_file->delete();
330
	}
331
	
332
	/**
333
	 * Generate an icon for the given entity
334
	 *
335
	 * @param \ElggEntity $entity    Temporary ElggFile instance
336
	 * @param \ElggFile   $file      Temporary ElggFile instance
337
	 * @param string      $type      The name of the icon. e.g., 'icon', 'cover_photo'
338
	 * @param array       $coords    An array of cropping coordinates x1, y1, x2, y2
339
	 * @param string      $icon_size The icon size to generate (leave empty to generate all supported sizes)
340
	 *
341
	 * @return bool
342
	 */
343 44
	protected function generateIcon(\ElggEntity $entity, \ElggFile $file, $type = 'icon', $coords = [], $icon_size = '') {
344
		
345 44
		if (!$file->exists()) {
346
			$this->getLogger()->error('Trying to generate an icon from a non-existing file');
347
			return false;
348
		}
349
		
350 44
		$x1 = (int) elgg_extract('x1', $coords);
351 44
		$y1 = (int) elgg_extract('y1', $coords);
352 44
		$x2 = (int) elgg_extract('x2', $coords);
353 44
		$y2 = (int) elgg_extract('y2', $coords);
354
		
355 44
		$sizes = $this->getSizes($entity->getType(), $entity->getSubtype(), $type);
356
		
357 44
		if (!empty($icon_size) && !isset($sizes[$icon_size])) {
358
			$this->getLogger()->warning("The provided icon size '{$icon_size}' doesn't exist for icon type '{$type}'");
359
			return false;
360
		}
361
		
362 44
		foreach ($sizes as $size => $opts) {
363 44
			if (!empty($icon_size) && ($icon_size !== $size)) {
364
				// only generate the given icon size
365 44
				continue;
366
			}
367
			
368
			// check if the icon config allows cropping
369 44
			if (!(bool) elgg_extract('crop', $opts, true)) {
370 44
				$coords = [
371 44
					'x1' => 0,
372 44
					'y1' => 0,
373 44
					'x2' => 0,
374 44
					'y2' => 0,
375 44
				];
376
			}
377
378 44
			$icon = $this->getIcon($entity, $size, $type, false);
379
380
			// We need to make sure that file path is readable by
381
			// Imagine\Image\ImagineInterface::save(), as it fails to
382
			// build the directory structure on owner's filestore otherwise
383 44
			$icon->open('write');
384 44
			$icon->close();
385
			
386
			// Save the image without resizing or cropping if the
387
			// image size value is an empty array
388 44
			if (is_array($opts) && empty($opts)) {
389 1
				copy($file->getFilenameOnFilestore(), $icon->getFilenameOnFilestore());
390 1
				continue;
391
			}
392
393 44
			$source = $file->getFilenameOnFilestore();
394 44
			$destination = $icon->getFilenameOnFilestore();
395
396 44
			$resize_params = array_merge($opts, $coords);
397
398 44
			$image_service = _elgg_services()->imageService;
399 44
			$image_service->setLogger($this->getLogger());
400
401 44
			if (!_elgg_services()->imageService->resize($source, $destination, $resize_params)) {
402
				$this->getLogger()->error("Failed to create {$size} icon from
403
					{$file->getFilenameOnFilestore()} with coords [{$x1}, {$y1}],[{$x2}, {$y2}]");
404
				
405
				if ($size !== 'master') {
406
					// remove 0 byte icon in order to retry the resize on the next request
407
					$icon->delete();
408
				}
409
				
410
				return false;
411
			}
412
		}
413
414 44
		return true;
415
	}
416
417
	/**
418
	 * Returns entity icon as an ElggIcon object
419
	 * The icon file may or may not exist on filestore
420
	 *
421
	 * @note Returned ElggIcon object may be a placeholder. Use ElggIcon::exists() to validate if file has been written to filestore
422
	 *
423
	 * @param \ElggEntity $entity   Entity that owns the icon
424
	 * @param string      $size     Size of the icon
425
	 * @param string      $type     The name of the icon. e.g., 'icon', 'cover_photo'
426
	 * @param bool        $generate Try to generate an icon based on master if size doesn't exists
427
	 *
428
	 * @return \ElggIcon
429
	 *
430
	 * @throws UnexpectedValueException
431
	 */
432 131
	public function getIcon(\ElggEntity $entity, $size, $type = 'icon', $generate = true) {
433
434 131
		$size = elgg_strtolower($size);
435
436 131
		$params = [
437 131
			'entity' => $entity,
438 131
			'size' => $size,
439 131
			'type' => $type,
440 131
		];
441
442 131
		$entity_type = $entity->getType();
443
444 131
		$default_icon = new \ElggIcon();
445 131
		$default_icon->owner_guid = $entity->guid;
446 131
		$default_icon->setFilename("icons/{$type}/{$size}.jpg");
447
448 131
		$icon = $this->events->triggerResults("entity:{$type}:file", $entity_type, $params, $default_icon);
449 131
		if (!$icon instanceof \ElggIcon) {
450 1
			throw new UnexpectedValueException("'entity:{$type}:file', {$entity_type} event must return an instance of \ElggIcon");
451
		}
452
		
453 130
		if ($size !== 'master' && $this->hasWebPSupport()) {
454
			if (pathinfo($icon->getFilename(), PATHINFO_EXTENSION) === 'jpg') {
455
				$icon->setFilename(substr($icon->getFilename(), 0, -3) . 'webp');
456
			}
457
		}
458
		
459 130
		if ($icon->exists() || !$generate) {
460 102
			return $icon;
461
		}
462
		
463 120
		if ($size === 'master') {
464
			// don't try to generate for master
465 60
			return $icon;
466
		}
467
		
468
		// try to generate icon based on master size
469 92
		$master_icon = $this->getIcon($entity, 'master', $type, false);
470 92
		if (!$master_icon->exists()) {
471 56
			return $icon;
472
		}
473
		
474 37
		if ($type === 'icon') {
475 36
			$coords = [
476 36
				'x1' => $entity->x1,
477 36
				'y1' => $entity->y1,
478 36
				'x2' => $entity->x2,
479 36
				'y2' => $entity->y2,
480 36
			];
481
		} else {
482 1
			$coords = $entity->{"{$type}_coords"};
483 1
			$coords = empty($coords) ? [] : unserialize($coords);
484
		}
485
		
486 37
		$this->generateIcon($entity, $master_icon, $type, $coords, $size);
487
		
488 37
		return $icon;
489
	}
490
491
	/**
492
	 * Removes all icon files and metadata for the passed type of icon.
493
	 *
494
	 * @param \ElggEntity $entity        Entity that owns icons
495
	 * @param string      $type          The name of the icon. e.g., 'icon', 'cover_photo'
496
	 * @param bool        $retain_master Keep the master icon (default: false)
497
	 *
498
	 * @return bool
499
	 */
500 44
	public function deleteIcon(\ElggEntity $entity, $type = 'icon', $retain_master = false) {
501 44
		$delete = $this->events->triggerResults("entity:{$type}:delete", $entity->getType(), [
502 44
			'entity' => $entity,
503 44
		], true);
504
505 44
		if ($delete === false) {
506 1
			return false;
507
		}
508
		
509 43
		$result = true;
510 43
		$supported_extensions = [
511 43
			'jpg',
512 43
		];
513 43
		if ($this->images->hasWebPSupport()) {
514 43
			$supported_extensions[] = 'webp';
515
		}
516
517 43
		$sizes = array_keys($this->getSizes($entity->getType(), $entity->getSubtype(), $type));
518 43
		foreach ($sizes as $size) {
519 43
			if ($size === 'master' && $retain_master) {
520 43
				continue;
521
			}
522
			
523 43
			$icon = $this->getIcon($entity, $size, $type, false);
524 43
			$result &= $icon->delete();
525
			
526
			// make sure we remove all supported images (jpg and webp)
527 43
			$current_extension = pathinfo($icon->getFilename(), PATHINFO_EXTENSION);
528 43
			$extensions = $supported_extensions;
529 43
			foreach ($extensions as $extension) {
530 43
				if ($current_extension === $extension) {
531
					// already removed
532 43
					continue;
533
				}
534
				
535
				// replace the extension
536 43
				$parts = explode('.', $icon->getFilename());
537 43
				array_pop($parts);
538 43
				$parts[] = $extension;
539
				
540
				// set new filename and remove the file
541 43
				$icon->setFilename(implode('.', $parts));
542 43
				$result &= $icon->delete();
543
			}
544
		}
545
546 43
		if ($type == 'icon') {
547 42
			unset($entity->icontime);
548 42
			unset($entity->x1);
549 42
			unset($entity->y1);
550 42
			unset($entity->x2);
551 42
			unset($entity->y2);
552
		} else {
553 1
			unset($entity->{"{$type}_coords"});
554
		}
555
		
556 43
		return $result;
557
	}
558
559
	/**
560
	 * Get the URL for this entity's icon
561
	 *
562
	 * Plugins can register for the 'entity:icon:url', <type> event to customize the icon for an entity.
563
	 *
564
	 * @param \ElggEntity $entity Entity that owns the icon
565
	 * @param mixed       $params A string defining the size of the icon (e.g. tiny, small, medium, large)
566
	 *                            or an array of parameters including 'size'
567
	 *
568
	 * @return string
569
	 */
570 51
	public function getIconURL(\ElggEntity $entity, string|array $params = []): string {
571 51
		if (is_array($params)) {
572 1
			$size = elgg_extract('size', $params, 'medium');
573
		} else {
574 51
			$size = is_string($params) ? $params : 'medium';
575 51
			$params = [];
576
		}
577
578 51
		$size = elgg_strtolower($size);
579
580 51
		$params['entity'] = $entity;
581 51
		$params['size'] = $size;
582
583 51
		$type = elgg_extract('type', $params, 'icon', false);
584 51
		$entity_type = $entity->getType();
585
586 51
		$url = $this->events->triggerResults("entity:{$type}:url", $entity_type, $params, null);
587 51
		if (!isset($url)) {
588 49
			if ($this->hasIcon($entity, $size, $type)) {
589 1
				$icon = $this->getIcon($entity, $size, $type);
590 1
				$default_use_cookie = (bool) elgg_get_config('session_bound_entity_icons');
591 1
				$url = $icon->getInlineURL((bool) elgg_extract('use_cookie', $params, $default_use_cookie));
592
			} else {
593 48
				$url = $this->getFallbackIconUrl($entity, $params);
594
			}
595
		}
596
597 51
		if (!empty($url)) {
598 49
			return elgg_normalize_url($url);
599
		}
600
		
601 2
		return '';
602
	}
603
604
	/**
605
	 * Returns default/fallback icon
606
	 *
607
	 * @param \ElggEntity $entity Entity
608
	 * @param array       $params Icon params
609
	 *
610
	 * @return string
611
	 */
612 48
	public function getFallbackIconUrl(\ElggEntity $entity, array $params = []) {
613 48
		$type = elgg_extract('type', $params, 'icon', false);
614 48
		$size = elgg_extract('size', $params, 'medium', false);
615
		
616 48
		$entity_type = $entity->getType();
617 48
		$entity_subtype = $entity->getSubtype();
618
619 48
		$exts = ['svg', 'gif', 'png', 'jpg'];
620
621 48
		foreach ($exts as $ext) {
622 48
			foreach ([$entity_subtype, 'default'] as $subtype) {
623 48
				if ($ext == 'svg' && elgg_view_exists("{$type}/{$entity_type}/{$subtype}.svg", 'default')) {
624
					return elgg_get_simplecache_url("{$type}/{$entity_type}/{$subtype}.svg");
625
				}
626
				
627 48
				if (elgg_view_exists("{$type}/{$entity_type}/{$subtype}/{$size}.{$ext}", 'default')) {
628 45
					return elgg_get_simplecache_url("{$type}/{$entity_type}/{$subtype}/{$size}.{$ext}");
629
				}
630
			}
631
		}
632
633 3
		if (elgg_view_exists("{$type}/default/{$size}.png", 'default')) {
634 1
			return elgg_get_simplecache_url("{$type}/default/{$size}.png");
635
		}
636
	}
637
638
	/**
639
	 * Returns the timestamp of when the icon was changed.
640
	 *
641
	 * @param \ElggEntity $entity Entity that owns the icon
642
	 * @param string      $size   The size of the icon
643
	 * @param string      $type   The name of the icon. e.g., 'icon', 'cover_photo'
644
	 *
645
	 * @return int|null A unix timestamp of when the icon was last changed, or null if not set.
646
	 */
647 1
	public function getIconLastChange(\ElggEntity $entity, $size, $type = 'icon') {
648 1
		$icon = $this->getIcon($entity, $size, $type);
649 1
		if ($icon->exists()) {
650 1
			return $icon->getModifiedTime();
651
		}
652
	}
653
654
	/**
655
	 * Returns if the entity has an icon of the passed type.
656
	 *
657
	 * @param \ElggEntity $entity Entity that owns the icon
658
	 * @param string      $size   The size of the icon
659
	 * @param string      $type   The name of the icon. e.g., 'icon', 'cover_photo'
660
	 *
661
	 * @return bool
662
	 */
663 90
	public function hasIcon(\ElggEntity $entity, $size, $type = 'icon') {
664 90
		$icon = $this->getIcon($entity, $size, $type);
665 90
		return $icon->exists() && $icon->getSize() > 0;
666
	}
667
668
	/**
669
	 * Returns a configuration array of icon sizes
670
	 *
671
	 * @param string $entity_type    Entity type
672
	 * @param string $entity_subtype Entity subtype
673
	 * @param string $type           The name of the icon. e.g., 'icon', 'cover_photo'
674
	 *
675
	 * @return array
676
	 * @throws InvalidArgumentException
677
	 */
678 98
	public function getSizes(string $entity_type = null, string $entity_subtype = null, $type = 'icon'): array {
679 98
		$sizes = [];
680 98
		$type = $type ?: 'icon';
681 98
		if ($type == 'icon') {
682 96
			$sizes = $this->config->icon_sizes;
683
		}
684
		
685 98
		$params = [
686 98
			'type' => $type,
687 98
			'entity_type' => $entity_type,
688 98
			'entity_subtype' => $entity_subtype,
689 98
		];
690 98
		if ($entity_type) {
691 97
			$sizes = $this->events->triggerResults("entity:{$type}:sizes", $entity_type, $params, $sizes);
692
		}
693
694 98
		if (!is_array($sizes)) {
695
			$msg = "The icon size configuration for image type '{$type}'";
696
			$msg .= ' must be an associative array of image size names and their properties';
697
			throw new InvalidArgumentException($msg);
698
		}
699
700
		// lazy generation of icons requires a 'master' size
701
		// this ensures a default config for 'master' size
702 98
		$sizes['master'] = elgg_extract('master', $sizes, [
703 98
			'w' => 10240,
704 98
			'h' => 10240,
705 98
			'square' => false,
706 98
			'upscale' => false,
707 98
			'crop' => false,
708 98
		]);
709
		
710 98
		if (!isset($sizes['master']['crop'])) {
711
			$sizes['master']['crop'] = false;
712
		}
713
		
714 98
		return $sizes;
715
	}
716
	
717
	/**
718
	 * Automagicly detect cropping coordinates
719
	 *
720
	 * Based in the input names x1, x2, y1 and y2
721
	 *
722
	 * @param string $input_name the file input name which is the prefix for the cropping coordinates
723
	 *
724
	 * @return false|array
725
	 */
726 15
	protected function detectCroppingCoordinates(string $input_name) {
727
		
728 15
		$auto_coords = [
729 15
			'x1' => get_input("{$input_name}_x1", get_input('x1')), // x1 is BC fallback
730 15
			'x2' => get_input("{$input_name}_x2", get_input('x2')), // x2 is BC fallback
731 15
			'y1' => get_input("{$input_name}_y1", get_input('y1')), // y1 is BC fallback
732 15
			'y2' => get_input("{$input_name}_y2", get_input('y2')), // y2 is BC fallback
733 15
		];
734
		
735 15
		$auto_coords = array_filter($auto_coords, function($value) {
736 15
			return !elgg_is_empty($value) && is_numeric($value) && (int) $value >= 0;
737 15
		});
738
		
739 15
		if (count($auto_coords) !== 4) {
740 6
			return false;
741
		}
742
		
743
		// make ints
744 9
		array_walk($auto_coords, function (&$value) {
745 9
			$value = (int) $value;
746 9
		});
747
		
748
		// make sure coords make sense x2 > x1 && y2 > y1
749 9
		if ($auto_coords['x2'] <= $auto_coords['x1'] || $auto_coords['y2'] <= $auto_coords['y1']) {
750 4
			return false;
751
		}
752
		
753 5
		return $auto_coords;
754
	}
755
756
	/**
757
	 * Checks if browser has WebP support and if the webserver is able to generate
758
	 *
759
	 * @return bool
760
	 */
761 102
	protected function hasWebPSupport(): bool {
762 102
		return in_array('image/webp', $this->request->getAcceptableContentTypes()) && $this->images->hasWebPSupport();
763
	}
764
}
765