Elgg  Version master
EntityIconService.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg;
4 
13 
21 
22  use Loggable;
23  use TimeUsing;
24 
36  public function __construct(
37  protected Config $config,
38  protected EventsService $events,
39  protected EntityTable $entities,
40  protected UploadService $uploads,
41  protected ImageService $images,
42  protected MimeTypeService $mimetype,
43  protected HttpRequest $request
44  ) {
45  }
46 
57  public function saveIconFromUploadedFile(\ElggEntity $entity, string $input_name, string $type = 'icon', array $coords = []): bool {
58  $input = $this->uploads->getFile($input_name);
59  if (empty($input)) {
60  return false;
61  }
62 
63  // auto detect cropping coordinates
64  if (empty($coords)) {
65  $auto_coords = $this->detectCroppingCoordinates($input_name);
66  if (!empty($auto_coords)) {
67  $coords = $auto_coords;
68  }
69  }
70 
71  $tmp = new \ElggTempFile();
72  $tmp->setFilename(uniqid() . $input->getClientOriginalName());
73  $tmp->open('write');
74  $tmp->close();
75 
76  copy($input->getPathname(), $tmp->getFilenameOnFilestore());
77 
78  $tmp->mimetype = $this->mimetype->getMimeType($tmp->getFilenameOnFilestore());
79  $tmp->simpletype = $this->mimetype->getSimpleType($tmp->mimetype);
80 
81  $result = $this->saveIcon($entity, $tmp, $type, $coords);
82 
83  $tmp->delete();
84 
85  return $result;
86  }
87 
99  public function saveIconFromLocalFile(\ElggEntity $entity, string $filename, string $type = 'icon', array $coords = []): bool {
100  if (!file_exists($filename) || !is_readable($filename)) {
101  throw new InvalidArgumentException(__METHOD__ . " expects a readable local file. {$filename} is not readable");
102  }
103 
104  $tmp = new \ElggTempFile();
105  $tmp->setFilename(uniqid() . basename($filename));
106  $tmp->open('write');
107  $tmp->close();
108 
109  copy($filename, $tmp->getFilenameOnFilestore());
110 
111  $tmp->mimetype = $this->mimetype->getMimeType($tmp->getFilenameOnFilestore());
112  $tmp->simpletype = $this->mimetype->getSimpleType($tmp->mimetype);
113 
114  $result = $this->saveIcon($entity, $tmp, $type, $coords);
115 
116  $tmp->delete();
117 
118  return $result;
119  }
120 
132  public function saveIconFromElggFile(\ElggEntity $entity, \ElggFile $file, string $type = 'icon', array $coords = []): bool {
133  if (!$file->exists()) {
134  throw new InvalidArgumentException(__METHOD__ . ' expects an instance of ElggFile with an existing file on filestore');
135  }
136 
137  $tmp = new \ElggTempFile();
138  $tmp->setFilename(uniqid() . basename($file->getFilenameOnFilestore()));
139  $tmp->open('write');
140  $tmp->close();
141 
142  copy($file->getFilenameOnFilestore(), $tmp->getFilenameOnFilestore());
143 
144  $tmp->mimetype = $this->mimetype->getMimeType($tmp->getFilenameOnFilestore(), $file->getMimeType() ?: '');
145  $tmp->simpletype = $this->mimetype->getSimpleType($tmp->mimetype);
146 
147  $result = $this->saveIcon($entity, $tmp, $type, $coords);
148 
149  $tmp->delete();
150 
151  return $result;
152  }
153 
164  public function saveIcon(\ElggEntity $entity, \ElggFile $file, string $type = 'icon', array $coords = []): bool {
165  if (!strlen($type)) {
166  $this->getLogger()->error('Icon type passed to ' . __METHOD__ . ' can not be empty');
167  return false;
168  }
169 
170  $entity_type = $entity->getType();
171 
172  $file = $this->events->triggerResults("entity:{$type}:prepare", $entity_type, [
173  'entity' => $entity,
174  'file' => $file,
175  ], $file);
176 
177  if (!$file instanceof \ElggFile || !$file->exists() || $file->getSimpleType() !== 'image') {
178  $this->getLogger()->error('Source file passed to ' . __METHOD__ . ' can not be resolved to a valid image');
179  return false;
180  }
181 
182  $entity->lockIconThumbnailGeneration($type);
183 
184  $this->prepareIcon($file->getFilenameOnFilestore());
185 
186  $x1 = (int) elgg_extract('x1', $coords);
187  $y1 = (int) elgg_extract('y1', $coords);
188  $x2 = (int) elgg_extract('x2', $coords);
189  $y2 = (int) elgg_extract('y2', $coords);
190 
191  $created = $this->events->triggerResults("entity:{$type}:save", $entity_type, [
192  'entity' => $entity,
193  'file' => $file,
194  'x1' => $x1,
195  'y1' => $y1,
196  'x2' => $x2,
197  'y2' => $y2,
198  ], false);
199 
200  // did someone else handle saving the icon?
201  if ($created !== true) {
202  // remove existing icons
203  $this->deleteIcon($entity, $type, true);
204 
205  // save master image
206  $store = $this->generateIcon($entity, $file, $type, $coords, 'master');
207 
208  if (!$store) {
209  $this->deleteIcon($entity, $type);
210  return false;
211  }
212 
213  // validate cropping coords to prevent out-of-bounds issues
214  $sizes = $this->getSizes($entity->getType(), $entity->getSubtype(), $type);
215  $coords = array_merge($sizes['master'], $coords);
216 
217  $icon = $this->getIcon($entity, 'master', $type, false);
218 
219  try {
220  $this->images->normalizeResizeParameters($icon->getFilenameOnFilestore(), $coords);
221  } catch (ExceptionInterface $e) {
222  // cropping coords are wrong, reset to 0
223  $x1 = 0;
224  $x2 = 0;
225  $y1 = 0;
226  $y2 = 0;
227  }
228  }
229 
230  // first invalidate entity metadata cache, because of a high risk of racing condition to save the coordinates
231  // the racing condition occurs with 2 (or more) icon save calls and the time between clearing
232  // the coordinates in deleteIcon() and the new save here
233  $entity->invalidateCache();
234 
235  if ($x1 || $y1 || $x2 || $y2) {
236  $entity->saveIconCoordinates($coords);
237  }
238 
239  $this->events->triggerResults("entity:{$type}:saved", $entity->getType(), [
240  'entity' => $entity,
241  'x1' => $x1,
242  'y1' => $y1,
243  'x2' => $x2,
244  'y2' => $y2,
245  ]);
246 
247  $entity->unlockIconThumbnailGeneration($type);
248 
249  return true;
250  }
251 
259  protected function prepareIcon(string $filename): void {
260  // fix orientation
261  $temp_file = new \ElggTempFile();
262  $temp_file->setFilename(uniqid() . basename($filename));
263 
264  copy($filename, $temp_file->getFilenameOnFilestore());
265 
266  $rotated = $this->images->fixOrientation($temp_file->getFilenameOnFilestore());
267 
268  if ($rotated) {
269  copy($temp_file->getFilenameOnFilestore(), $filename);
270  }
271 
272  $temp_file->delete();
273  }
274 
286  protected function generateIcon(\ElggEntity $entity, \ElggFile $file, string $type = 'icon', array $coords = [], string $icon_size = ''): bool {
287  if (!$file->exists()) {
288  $this->getLogger()->error('Trying to generate an icon from a non-existing file');
289  return false;
290  }
291 
292  $x1 = (int) elgg_extract('x1', $coords);
293  $y1 = (int) elgg_extract('y1', $coords);
294  $x2 = (int) elgg_extract('x2', $coords);
295  $y2 = (int) elgg_extract('y2', $coords);
296 
297  $sizes = $this->getSizes($entity->getType(), $entity->getSubtype(), $type);
298 
299  if (!empty($icon_size) && !isset($sizes[$icon_size])) {
300  $this->getLogger()->warning("The provided icon size '{$icon_size}' doesn't exist for icon type '{$type}'");
301  return false;
302  }
303 
304  foreach ($sizes as $size => $opts) {
305  if (!empty($icon_size) && ($icon_size !== $size)) {
306  // only generate the given icon size
307  continue;
308  }
309 
310  // check if the icon config allows cropping
311  if (!(bool) elgg_extract('crop', $opts, true)) {
312  $coords = [
313  'x1' => 0,
314  'y1' => 0,
315  'x2' => 0,
316  'y2' => 0,
317  ];
318  }
319 
320  $icon = $this->getIcon($entity, $size, $type, false);
321 
322  // We need to make sure that file path is readable by
323  // Imagine\Image\ImagineInterface::save(), as it fails to
324  // build the directory structure on owner's filestore otherwise
325  $icon->open('write');
326  $icon->close();
327 
328  // Save the image without resizing or cropping if the
329  // image size value is an empty array
330  if (is_array($opts) && empty($opts)) {
331  copy($file->getFilenameOnFilestore(), $icon->getFilenameOnFilestore());
332  continue;
333  }
334 
335  $source = $file->getFilenameOnFilestore();
336  $destination = $icon->getFilenameOnFilestore();
337 
338  $resize_params = array_merge($opts, $coords);
339 
340  $image_service = _elgg_services()->imageService;
341  $image_service->setLogger($this->getLogger());
342 
343  if (!_elgg_services()->imageService->resize($source, $destination, $resize_params)) {
344  $this->getLogger()->error("Failed to create {$size} icon from
345  {$file->getFilenameOnFilestore()} with coords [{$x1}, {$y1}],[{$x2}, {$y2}]");
346 
347  if ($size !== 'master') {
348  // remove 0 byte icon in order to retry the resize on the next request
349  $icon->delete();
350  }
351 
352  return false;
353  }
354  }
355 
356  return true;
357  }
358 
374  public function getIcon(\ElggEntity $entity, string $size, string $type = 'icon', bool $generate = true): \ElggIcon {
375  $size = elgg_strtolower($size);
376 
377  $params = [
378  'entity' => $entity,
379  'size' => $size,
380  'type' => $type,
381  ];
382 
383  $entity_type = $entity->getType();
384 
385  $default_icon = new \ElggIcon();
386  $default_icon->owner_guid = $entity->guid;
387  $default_icon->setFilename("icons/{$type}/{$size}.jpg");
388 
389  $icon = $this->events->triggerResults("entity:{$type}:file", $entity_type, $params, $default_icon);
390  if (!$icon instanceof \ElggIcon) {
391  throw new UnexpectedValueException("'entity:{$type}:file', {$entity_type} event must return an instance of \ElggIcon");
392  }
393 
394  if ($size !== 'master' && $this->hasWebPSupport()) {
395  if (pathinfo($icon->getFilename(), PATHINFO_EXTENSION) === 'jpg') {
396  $icon->setFilename(substr($icon->getFilename(), 0, -3) . 'webp');
397  }
398  }
399 
400  if ($icon->exists() || !$generate) {
401  return $icon;
402  }
403 
404  if ($size === 'master') {
405  // don't try to generate for master
406  return $icon;
407  }
408 
409  if ($entity->isIconThumbnailGenerationLocked($type)) {
410  return $icon;
411  }
412 
413  // try to generate icon based on master size
414  $master_icon = $this->getIcon($entity, 'master', $type, false);
415  if (!$master_icon->exists()) {
416  return $icon;
417  }
418 
419  $coords = $entity->getIconCoordinates($type);
420 
421  $this->generateIcon($entity, $master_icon, $type, $coords, $size);
422 
423  return $icon;
424  }
425 
435  public function deleteIcon(\ElggEntity $entity, string $type = 'icon', bool $retain_master = false): bool {
436  $delete = $this->events->triggerResults("entity:{$type}:delete", $entity->getType(), [
437  'entity' => $entity,
438  'retain_master' => $retain_master, // just removing thumbs or everything?
439  ], true);
440 
441  if ($delete === false) {
442  return false;
443  }
444 
445  $result = true;
446  $supported_extensions = [
447  'jpg',
448  ];
449  if ($this->images->hasWebPSupport()) {
450  $supported_extensions[] = 'webp';
451  }
452 
453  $sizes = array_keys($this->getSizes($entity->getType(), $entity->getSubtype(), $type));
454  foreach ($sizes as $size) {
455  if ($size === 'master' && $retain_master) {
456  continue;
457  }
458 
459  $icon = $this->getIcon($entity, $size, $type, false);
460  $result &= $icon->delete();
461 
462  // make sure we remove all supported images (jpg and webp)
463  $current_extension = pathinfo($icon->getFilename(), PATHINFO_EXTENSION);
464  $extensions = $supported_extensions;
465  foreach ($extensions as $extension) {
466  if ($current_extension === $extension) {
467  // already removed
468  continue;
469  }
470 
471  // replace the extension
472  $parts = explode('.', $icon->getFilename());
473  array_pop($parts);
474  $parts[] = $extension;
475 
476  // set new filename and remove the file
477  $icon->setFilename(implode('.', $parts));
478  $result &= $icon->delete();
479  }
480  }
481 
482  $entity->removeIconCoordinates($type);
483 
484  return $result;
485  }
486 
498  public function getIconURL(\ElggEntity $entity, string|array $params = []): string {
499  if (is_array($params)) {
500  $size = elgg_extract('size', $params, 'medium');
501  } else {
502  $size = is_string($params) ? $params : 'medium';
503  $params = [];
504  }
505 
507 
508  $params['entity'] = $entity;
509  $params['size'] = $size;
510 
511  $type = elgg_extract('type', $params, 'icon', false);
512  $entity_type = $entity->getType();
513 
514  $url = $this->events->triggerResults("entity:{$type}:url", $entity_type, $params, null);
515  if (!isset($url)) {
516  if ($this->hasIcon($entity, $size, $type)) {
517  $icon = $this->getIcon($entity, $size, $type);
518  $default_use_cookie = (bool) elgg_get_config('session_bound_entity_icons');
519  $url = $icon->getInlineURL((bool) elgg_extract('use_cookie', $params, $default_use_cookie));
520  } else {
521  $url = $this->getFallbackIconUrl($entity, $params);
522  }
523  }
524 
525  if (!empty($url)) {
526  return elgg_normalize_url($url);
527  }
528 
529  return '';
530  }
531 
540  public function getFallbackIconUrl(\ElggEntity $entity, array $params = []): string {
541  $type = elgg_extract('type', $params, 'icon', false);
542  $size = elgg_extract('size', $params, 'medium', false);
543 
544  $entity_type = $entity->getType();
545  $entity_subtype = $entity->getSubtype();
546 
547  $exts = ['svg', 'gif', 'png', 'jpg'];
548 
549  foreach ($exts as $ext) {
550  foreach ([$entity_subtype, 'default'] as $subtype) {
551  if ($ext == 'svg' && elgg_view_exists("{$type}/{$entity_type}/{$subtype}.svg", 'default')) {
552  return elgg_get_simplecache_url("{$type}/{$entity_type}/{$subtype}.svg");
553  }
554 
555  if (elgg_view_exists("{$type}/{$entity_type}/{$subtype}/{$size}.{$ext}", 'default')) {
556  return elgg_get_simplecache_url("{$type}/{$entity_type}/{$subtype}/{$size}.{$ext}");
557  }
558  }
559  }
560 
561  if (elgg_view_exists("{$type}/default/{$size}.png", 'default')) {
562  return elgg_get_simplecache_url("{$type}/default/{$size}.png");
563  }
564 
565  return '';
566  }
567 
577  public function getIconLastChange(\ElggEntity $entity, string $size, string $type = 'icon'): ?int {
578  $icon = $this->getIcon($entity, $size, $type);
579  if ($icon->exists()) {
580  return $icon->getModifiedTime();
581  }
582 
583  return null;
584  }
585 
595  public function hasIcon(\ElggEntity $entity, string $size, string $type = 'icon'): bool {
596  $icon = $this->getIcon($entity, $size, $type);
597  return $icon->exists() && $icon->getSize() > 0;
598  }
599 
610  public function getSizes(string $entity_type = null, string $entity_subtype = null, $type = 'icon'): array {
611  $sizes = [];
612  $type = $type ?: 'icon';
613  if ($type == 'icon') {
614  $sizes = $this->config->icon_sizes;
615  }
616 
617  $params = [
618  'type' => $type,
619  'entity_type' => $entity_type,
620  'entity_subtype' => $entity_subtype,
621  ];
622  if ($entity_type) {
623  $sizes = $this->events->triggerResults("entity:{$type}:sizes", $entity_type, $params, $sizes);
624  }
625 
626  if (!is_array($sizes)) {
627  $msg = "The icon size configuration for image type '{$type}'";
628  $msg .= ' must be an associative array of image size names and their properties';
629  throw new InvalidArgumentException($msg);
630  }
631 
632  // lazy generation of icons requires a 'master' size
633  // this ensures a default config for 'master' size
634  $sizes['master'] = elgg_extract('master', $sizes, [
635  'w' => 10240,
636  'h' => 10240,
637  'square' => false,
638  'upscale' => false,
639  'crop' => false,
640  ]);
641 
642  if (!isset($sizes['master']['crop'])) {
643  $sizes['master']['crop'] = false;
644  }
645 
646  return $sizes;
647  }
648 
658  protected function detectCroppingCoordinates(string $input_name): ?array {
659  $auto_coords = [
660  'x1' => get_input("{$input_name}_x1", get_input('x1')), // x1 is BC fallback
661  'x2' => get_input("{$input_name}_x2", get_input('x2')), // x2 is BC fallback
662  'y1' => get_input("{$input_name}_y1", get_input('y1')), // y1 is BC fallback
663  'y2' => get_input("{$input_name}_y2", get_input('y2')), // y2 is BC fallback
664  ];
665 
666  $auto_coords = array_filter($auto_coords, function($value) {
667  return !elgg_is_empty($value) && is_numeric($value) && (int) $value >= 0;
668  });
669 
670  if (count($auto_coords) !== 4) {
671  return null;
672  }
673 
674  // make ints
675  array_walk($auto_coords, function (&$value) {
676  $value = (int) $value;
677  });
678 
679  // make sure coords make sense x2 > x1 && y2 > y1
680  if ($auto_coords['x2'] <= $auto_coords['x1'] || $auto_coords['y2'] <= $auto_coords['y1']) {
681  return null;
682  }
683 
684  return $auto_coords;
685  }
686 
692  protected function hasWebPSupport(): bool {
693  return in_array('image/webp', $this->request->getAcceptableContentTypes()) && $this->images->hasWebPSupport();
694  }
695 }
getSubtype()
Get the entity subtype.
$source
Exception thrown if an argument is not of the expected type.
$input_name
Definition: crop.php:24
$params
Saves global plugin settings.
Definition: save.php:13
elgg_get_config(string $name, $default=null)
Get an Elgg configuration value.
getIcon(\ElggEntity $entity, string $size, string $type= 'icon', bool $generate=true)
Returns entity icon as an ElggIcon object The icon file may or may not exist on filestore.
invalidateCache()
Invalidate cache for entity.
$request
Definition: livesearch.php:12
if($icon===false) if($icon!== '') $icon_size
Definition: icon.php:22
c Accompany it with the information you received as to the offer to distribute corresponding source complete source code means all the source code for all modules it plus any associated interface definition plus the scripts used to control compilation and installation of the executable as a special the source code distributed need not include anything that is normally and so on of the operating system on which the executable unless that component itself accompanies the executable If distribution of executable or object code is made by offering access to copy from a designated then offering equivalent access to copy the source code from the same place counts as distribution of the source even though third parties are not compelled to copy the source along with the object code You may not or distribute the Program except as expressly provided under this License Any attempt otherwise to sublicense or distribute the Program is void
Definition: LICENSE.txt:215
Events service.
exists()
Returns if the file exists.
Definition: ElggFile.php:321
getSizes(string $entity_type=null, string $entity_subtype=null, $type= 'icon')
Returns a configuration array of icon sizes.
getSimpleType()
Get the simple type of the file.
Definition: ElggFile.php:153
getFallbackIconUrl(\ElggEntity $entity, array $params=[])
Returns default/fallback icon.
$delete
$type
Definition: delete.php:22
trait TimeUsing
Adds methods for setting the current time (for testing)
Definition: TimeUsing.php:10
elgg_strtolower()
Wrapper function for mb_strtolower().
Definition: mb_wrapper.php:161
saveIconFromLocalFile(\ElggEntity $entity, string $filename, string $type= 'icon', array $coords=[])
Saves icons using a local file as the source.
Generic interface which allows catching of all exceptions thrown in Elgg.
$value
Definition: generic.php:51
get_input(string $variable, $default=null, bool $filter_result=true)
Parameter input functions.
Definition: input.php:20
elgg_is_empty($value)
Check if a value isn&#39;t empty, but allow 0 and &#39;0&#39;.
Definition: input.php:176
elgg_extract($key, $array, $default=null, bool $strict=true)
Checks for $array[$key] and returns its value if it exists, else returns $default.
Definition: elgglib.php:254
$config
Advanced site settings, debugging section.
Definition: debugging.php:6
getFilenameOnFilestore()
Return the filename of this file as it is/will be stored on the filestore, which may be different to ...
Definition: ElggFile.php:111
saveIconFromElggFile(\ElggEntity $entity,\ElggFile $file, string $type= 'icon', array $coords=[])
Saves icons using a file located in the data store as the source.
trait Loggable
Enables adding a logger.
Definition: Loggable.php:14
if(function_exists('apache_get_version')) $icon
Definition: generic.php:49
$entity
Definition: reset.php:8
Entity icon service.
Exception thrown if a value does not match with a set of values.
saveIconFromUploadedFile(\ElggEntity $entity, string $input_name, string $type= 'icon', array $coords=[])
Saves icons using an uploaded file as the source.
generateIcon(\ElggEntity $entity,\ElggFile $file, string $type= 'icon', array $coords=[], string $icon_size= '')
Generate an icon for the given entity.
getMimeType()
Get the mime type of the file.
Definition: ElggFile.php:121
saveIcon(\ElggEntity $entity,\ElggFile $file, string $type= 'icon', array $coords=[])
Saves icons using a created temporary file.
getIconURL(\ElggEntity $entity, string|array $params=[])
Get the URL for this entity&#39;s icon.
hasWebPSupport()
Checks if browser has WebP support and if the webserver is able to generate.
Image manipulation service.
deleteIcon(\ElggEntity $entity, string $type= 'icon', bool $retain_master=false)
Removes all icon files and metadata for the passed type of icon.
$size
Definition: thumb.php:23
$extensions
prepareIcon(string $filename)
Prepares an icon.
__construct(protected Config $config, protected EventsService $events, protected EntityTable $entities, protected UploadService $uploads, protected ImageService $images, protected MimeTypeService $mimetype, protected HttpRequest $request)
Constructor.
getLogger()
Returns logger.
Definition: Loggable.php:37
and give any other recipients of the Program a copy of this License along with the Program You may charge a fee for the physical act of transferring a copy
Definition: LICENSE.txt:140
getType()
Returns the entity type.
$subtype
Definition: delete.php:23
$input
Form field view.
Definition: field.php:13
foreach($plugin_guids as $guid) if(empty($deactivated_plugins)) $url
Definition: deactivate.php:39
Entity icon class.
Definition: ElggIcon.php:8
_elgg_services()
Get the global service provider.
Definition: elgglib.php:346
detectCroppingCoordinates(string $input_name)
Automagicly detect cropping coordinates.
elgg_get_simplecache_url(string $view)
Get the URL for the cached view.
Definition: cache.php:130
elgg_normalize_url(string $url)
Definition: output.php:163
hasIcon(\ElggEntity $entity, string $size, string $type= 'icon')
Returns if the entity has an icon of the passed type.
Public service related to MIME type detection.
elgg_view_exists(string $view, string $viewtype= '', bool $recurse=true)
Returns whether the specified view exists.
Definition: views.php:131
Entity table database service.
Definition: EntityTable.php:26
getIconLastChange(\ElggEntity $entity, string $size, string $type= 'icon')
Returns the timestamp of when the icon was changed.
$extension
Definition: default.php:25
File upload handling service.