EntityIconService   F
last analyzed

Complexity

Total Complexity 93

Size/Duplication

Total Lines 744
Duplicated Lines 0 %

Test Coverage

Coverage 91.33%

Importance

Changes 3
Bugs 2 Features 0
Metric Value
eloc 312
dl 0
loc 744
ccs 295
cts 323
cp 0.9133
rs 2
c 3
b 2
f 0
wmc 93

16 Methods

Rating   Name   Duplication   Size   Complexity  
A saveIconFromUploadedFile() 0 29 4
A __construct() 0 16 1
A saveIconFromElggFile() 0 20 3
A saveIconFromLocalFile() 0 20 3
A prepareIcon() 0 15 2
C saveIcon() 0 100 17
B getIcon() 0 57 11
C generateIcon() 0 72 12
B getFallbackIconUrl() 0 23 7
B deleteIcon() 0 58 9
A hasIcon() 0 3 2
B getIconURL() 0 32 6
A getIconLastChange() 0 4 2
B getSizes() 0 37 6
A detectCroppingCoordinates() 0 28 6
A hasWebPSupport() 0 2 2

How to fix   Complexity   

Complex Class

Complex classes like EntityIconService often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use EntityIconService, and based on these observations, apply Extract Interface, too.

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, string $type = 'icon', bool $retain_master = false): bool {
501 44
		$delete = $this->events->triggerResults("entity:{$type}:delete", $entity->getType(), [
502 44
			'entity' => $entity,
503 44
			'retain_master' => $retain_master, // just removing thumbs or everything?
504 44
		], true);
505
506 44
		if ($delete === false) {
507 1
			return false;
508
		}
509
		
510 43
		$result = true;
511 43
		$supported_extensions = [
512 43
			'jpg',
513 43
		];
514 43
		if ($this->images->hasWebPSupport()) {
515 43
			$supported_extensions[] = 'webp';
516
		}
517
518 43
		$sizes = array_keys($this->getSizes($entity->getType(), $entity->getSubtype(), $type));
519 43
		foreach ($sizes as $size) {
520 43
			if ($size === 'master' && $retain_master) {
521 43
				continue;
522
			}
523
			
524 43
			$icon = $this->getIcon($entity, $size, $type, false);
525 43
			$result &= $icon->delete();
526
			
527
			// make sure we remove all supported images (jpg and webp)
528 43
			$current_extension = pathinfo($icon->getFilename(), PATHINFO_EXTENSION);
529 43
			$extensions = $supported_extensions;
530 43
			foreach ($extensions as $extension) {
531 43
				if ($current_extension === $extension) {
532
					// already removed
533 43
					continue;
534
				}
535
				
536
				// replace the extension
537 43
				$parts = explode('.', $icon->getFilename());
538 43
				array_pop($parts);
539 43
				$parts[] = $extension;
540
				
541
				// set new filename and remove the file
542 43
				$icon->setFilename(implode('.', $parts));
543 43
				$result &= $icon->delete();
544
			}
545
		}
546
547 43
		if ($type == 'icon') {
548 42
			unset($entity->icontime);
549 42
			unset($entity->x1);
550 42
			unset($entity->y1);
551 42
			unset($entity->x2);
552 42
			unset($entity->y2);
553
		} else {
554 1
			unset($entity->{"{$type}_coords"});
555
		}
556
		
557 43
		return $result;
558
	}
559
560
	/**
561
	 * Get the URL for this entity's icon
562
	 *
563
	 * Plugins can register for the 'entity:icon:url', <type> event to customize the icon for an entity.
564
	 *
565
	 * @param \ElggEntity $entity Entity that owns the icon
566
	 * @param mixed       $params A string defining the size of the icon (e.g. tiny, small, medium, large)
567
	 *                            or an array of parameters including 'size'
568
	 *
569
	 * @return string
570
	 */
571 51
	public function getIconURL(\ElggEntity $entity, string|array $params = []): string {
572 51
		if (is_array($params)) {
573 1
			$size = elgg_extract('size', $params, 'medium');
574
		} else {
575 51
			$size = is_string($params) ? $params : 'medium';
576 51
			$params = [];
577
		}
578
579 51
		$size = elgg_strtolower($size);
580
581 51
		$params['entity'] = $entity;
582 51
		$params['size'] = $size;
583
584 51
		$type = elgg_extract('type', $params, 'icon', false);
585 51
		$entity_type = $entity->getType();
586
587 51
		$url = $this->events->triggerResults("entity:{$type}:url", $entity_type, $params, null);
588 51
		if (!isset($url)) {
589 49
			if ($this->hasIcon($entity, $size, $type)) {
590 1
				$icon = $this->getIcon($entity, $size, $type);
591 1
				$default_use_cookie = (bool) elgg_get_config('session_bound_entity_icons');
592 1
				$url = $icon->getInlineURL((bool) elgg_extract('use_cookie', $params, $default_use_cookie));
593
			} else {
594 48
				$url = $this->getFallbackIconUrl($entity, $params);
595
			}
596
		}
597
598 51
		if (!empty($url)) {
599 49
			return elgg_normalize_url($url);
600
		}
601
		
602 2
		return '';
603
	}
604
605
	/**
606
	 * Returns default/fallback icon
607
	 *
608
	 * @param \ElggEntity $entity Entity
609
	 * @param array       $params Icon params
610
	 *
611
	 * @return string
612
	 */
613 48
	public function getFallbackIconUrl(\ElggEntity $entity, array $params = []) {
614 48
		$type = elgg_extract('type', $params, 'icon', false);
615 48
		$size = elgg_extract('size', $params, 'medium', false);
616
		
617 48
		$entity_type = $entity->getType();
618 48
		$entity_subtype = $entity->getSubtype();
619
620 48
		$exts = ['svg', 'gif', 'png', 'jpg'];
621
622 48
		foreach ($exts as $ext) {
623 48
			foreach ([$entity_subtype, 'default'] as $subtype) {
624 48
				if ($ext == 'svg' && elgg_view_exists("{$type}/{$entity_type}/{$subtype}.svg", 'default')) {
625
					return elgg_get_simplecache_url("{$type}/{$entity_type}/{$subtype}.svg");
626
				}
627
				
628 48
				if (elgg_view_exists("{$type}/{$entity_type}/{$subtype}/{$size}.{$ext}", 'default')) {
629 45
					return elgg_get_simplecache_url("{$type}/{$entity_type}/{$subtype}/{$size}.{$ext}");
630
				}
631
			}
632
		}
633
634 3
		if (elgg_view_exists("{$type}/default/{$size}.png", 'default')) {
635 1
			return elgg_get_simplecache_url("{$type}/default/{$size}.png");
636
		}
637
	}
638
639
	/**
640
	 * Returns the timestamp of when the icon was changed.
641
	 *
642
	 * @param \ElggEntity $entity Entity that owns the icon
643
	 * @param string      $size   The size of the icon
644
	 * @param string      $type   The name of the icon. e.g., 'icon', 'cover_photo'
645
	 *
646
	 * @return int|null A unix timestamp of when the icon was last changed, or null if not set.
647
	 */
648 1
	public function getIconLastChange(\ElggEntity $entity, $size, $type = 'icon') {
649 1
		$icon = $this->getIcon($entity, $size, $type);
650 1
		if ($icon->exists()) {
651 1
			return $icon->getModifiedTime();
652
		}
653
	}
654
655
	/**
656
	 * Returns if the entity has an icon of the passed type.
657
	 *
658
	 * @param \ElggEntity $entity Entity that owns the icon
659
	 * @param string      $size   The size of the icon
660
	 * @param string      $type   The name of the icon. e.g., 'icon', 'cover_photo'
661
	 *
662
	 * @return bool
663
	 */
664 90
	public function hasIcon(\ElggEntity $entity, $size, $type = 'icon') {
665 90
		$icon = $this->getIcon($entity, $size, $type);
666 90
		return $icon->exists() && $icon->getSize() > 0;
667
	}
668
669
	/**
670
	 * Returns a configuration array of icon sizes
671
	 *
672
	 * @param string $entity_type    Entity type
673
	 * @param string $entity_subtype Entity subtype
674
	 * @param string $type           The name of the icon. e.g., 'icon', 'cover_photo'
675
	 *
676
	 * @return array
677
	 * @throws InvalidArgumentException
678
	 */
679 98
	public function getSizes(string $entity_type = null, string $entity_subtype = null, $type = 'icon'): array {
680 98
		$sizes = [];
681 98
		$type = $type ?: 'icon';
682 98
		if ($type == 'icon') {
683 96
			$sizes = $this->config->icon_sizes;
684
		}
685
		
686 98
		$params = [
687 98
			'type' => $type,
688 98
			'entity_type' => $entity_type,
689 98
			'entity_subtype' => $entity_subtype,
690 98
		];
691 98
		if ($entity_type) {
692 97
			$sizes = $this->events->triggerResults("entity:{$type}:sizes", $entity_type, $params, $sizes);
693
		}
694
695 98
		if (!is_array($sizes)) {
696
			$msg = "The icon size configuration for image type '{$type}'";
697
			$msg .= ' must be an associative array of image size names and their properties';
698
			throw new InvalidArgumentException($msg);
699
		}
700
701
		// lazy generation of icons requires a 'master' size
702
		// this ensures a default config for 'master' size
703 98
		$sizes['master'] = elgg_extract('master', $sizes, [
704 98
			'w' => 10240,
705 98
			'h' => 10240,
706 98
			'square' => false,
707 98
			'upscale' => false,
708 98
			'crop' => false,
709 98
		]);
710
		
711 98
		if (!isset($sizes['master']['crop'])) {
712
			$sizes['master']['crop'] = false;
713
		}
714
		
715 98
		return $sizes;
716
	}
717
	
718
	/**
719
	 * Automagicly detect cropping coordinates
720
	 *
721
	 * Based in the input names x1, x2, y1 and y2
722
	 *
723
	 * @param string $input_name the file input name which is the prefix for the cropping coordinates
724
	 *
725
	 * @return false|array
726
	 */
727 15
	protected function detectCroppingCoordinates(string $input_name) {
728
		
729 15
		$auto_coords = [
730 15
			'x1' => get_input("{$input_name}_x1", get_input('x1')), // x1 is BC fallback
731 15
			'x2' => get_input("{$input_name}_x2", get_input('x2')), // x2 is BC fallback
732 15
			'y1' => get_input("{$input_name}_y1", get_input('y1')), // y1 is BC fallback
733 15
			'y2' => get_input("{$input_name}_y2", get_input('y2')), // y2 is BC fallback
734 15
		];
735
		
736 15
		$auto_coords = array_filter($auto_coords, function($value) {
737 15
			return !elgg_is_empty($value) && is_numeric($value) && (int) $value >= 0;
738 15
		});
739
		
740 15
		if (count($auto_coords) !== 4) {
741 6
			return false;
742
		}
743
		
744
		// make ints
745 9
		array_walk($auto_coords, function (&$value) {
746 9
			$value = (int) $value;
747 9
		});
748
		
749
		// make sure coords make sense x2 > x1 && y2 > y1
750 9
		if ($auto_coords['x2'] <= $auto_coords['x1'] || $auto_coords['y2'] <= $auto_coords['y1']) {
751 4
			return false;
752
		}
753
		
754 5
		return $auto_coords;
755
	}
756
757
	/**
758
	 * Checks if browser has WebP support and if the webserver is able to generate
759
	 *
760
	 * @return bool
761
	 */
762 102
	protected function hasWebPSupport(): bool {
763 102
		return in_array('image/webp', $this->request->getAcceptableContentTypes()) && $this->images->hasWebPSupport();
764
	}
765
}
766