Elgg  Version 5.1
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 
28  private $config;
29 
33  private $events;
34 
38  private $entities;
39 
43  private $uploads;
44 
48  private $images;
49 
53  protected $mimetype;
54 
58  protected $request;
59 
71  public function __construct(
73  EventsService $events,
74  EntityTable $entities,
75  UploadService $uploads,
76  ImageService $images,
77  MimeTypeService $mimetype,
79  ) {
80  $this->config = $config;
81  $this->events = $events;
82  $this->entities = $entities;
83  $this->uploads = $uploads;
84  $this->images = $images;
85  $this->mimetype = $mimetype;
86  $this->request = $request;
87  }
88 
99  public function saveIconFromUploadedFile(\ElggEntity $entity, $input_name, $type = 'icon', array $coords = []) {
100  $input = $this->uploads->getFile($input_name);
101  if (empty($input)) {
102  return false;
103  }
104 
105  // auto detect cropping coordinates
106  if (empty($coords)) {
107  $auto_coords = $this->detectCroppingCoordinates($input_name);
108  if (!empty($auto_coords)) {
109  $coords = $auto_coords;
110  }
111  }
112 
113  $tmp = new \ElggTempFile();
114  $tmp->setFilename(uniqid() . $input->getClientOriginalName());
115  $tmp->open('write');
116  $tmp->close();
117 
118  copy($input->getPathname(), $tmp->getFilenameOnFilestore());
119 
120  $tmp->mimetype = $this->mimetype->getMimeType($tmp->getFilenameOnFilestore());
121  $tmp->simpletype = $this->mimetype->getSimpleType($tmp->mimetype);
122 
123  $result = $this->saveIcon($entity, $tmp, $type, $coords);
124 
125  $tmp->delete();
126 
127  return $result;
128  }
129 
141  public function saveIconFromLocalFile(\ElggEntity $entity, $filename, $type = 'icon', array $coords = []) {
142  if (!file_exists($filename) || !is_readable($filename)) {
143  throw new InvalidArgumentException(__METHOD__ . " expects a readable local file. {$filename} is not readable");
144  }
145 
146  $tmp = new \ElggTempFile();
147  $tmp->setFilename(uniqid() . basename($filename));
148  $tmp->open('write');
149  $tmp->close();
150 
151  copy($filename, $tmp->getFilenameOnFilestore());
152 
153  $tmp->mimetype = $this->mimetype->getMimeType($tmp->getFilenameOnFilestore());
154  $tmp->simpletype = $this->mimetype->getSimpleType($tmp->mimetype);
155 
156  $result = $this->saveIcon($entity, $tmp, $type, $coords);
157 
158  $tmp->delete();
159 
160  return $result;
161  }
162 
174  public function saveIconFromElggFile(\ElggEntity $entity, \ElggFile $file, $type = 'icon', array $coords = []) {
175  if (!$file->exists()) {
176  throw new InvalidArgumentException(__METHOD__ . ' expects an instance of ElggFile with an existing file on filestore');
177  }
178 
179  $tmp = new \ElggTempFile();
180  $tmp->setFilename(uniqid() . basename($file->getFilenameOnFilestore()));
181  $tmp->open('write');
182  $tmp->close();
183 
184  copy($file->getFilenameOnFilestore(), $tmp->getFilenameOnFilestore());
185 
186  $tmp->mimetype = $this->mimetype->getMimeType($tmp->getFilenameOnFilestore(), $file->getMimeType() ?: '');
187  $tmp->simpletype = $this->mimetype->getSimpleType($tmp->mimetype);
188 
189  $result = $this->saveIcon($entity, $tmp, $type, $coords);
190 
191  $tmp->delete();
192 
193  return $result;
194  }
195 
206  public function saveIcon(\ElggEntity $entity, \ElggFile $file, $type = 'icon', array $coords = []) {
207 
208  $type = (string) $type;
209  if (!strlen($type)) {
210  $this->getLogger()->error('Icon type passed to ' . __METHOD__ . ' can not be empty');
211  return false;
212  }
213 
214  $entity_type = $entity->getType();
215 
216  $file = $this->events->triggerResults("entity:{$type}:prepare", $entity_type, [
217  'entity' => $entity,
218  'file' => $file,
219  ], $file);
220 
221  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  $this->prepareIcon($file->getFilenameOnFilestore());
227 
228  $x1 = (int) elgg_extract('x1', $coords);
229  $y1 = (int) elgg_extract('y1', $coords);
230  $x2 = (int) elgg_extract('x2', $coords);
231  $y2 = (int) elgg_extract('y2', $coords);
232 
233  $created = $this->events->triggerResults("entity:{$type}:save", $entity_type, [
234  'entity' => $entity,
235  'file' => $file,
236  'x1' => $x1,
237  'y1' => $y1,
238  'x2' => $x2,
239  'y2' => $y2,
240  ], false);
241 
242  // did someone else handle saving the icon?
243  if ($created !== true) {
244  // remove existing icons
245  $this->deleteIcon($entity, $type, true);
246 
247  // save master image
248  $store = $this->generateIcon($entity, $file, $type, $coords, 'master');
249 
250  if (!$store) {
251  $this->deleteIcon($entity, $type);
252  return false;
253  }
254 
255  // validate cropping coords to prevent out-of-bounds issues
256  $sizes = $this->getSizes($entity->getType(), $entity->getSubtype(), $type);
257  $coords = array_merge($sizes['master'], $coords);
258 
259  $icon = $this->getIcon($entity, 'master', $type, false);
260 
261  try {
262  $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  $entity->invalidateCache();
276 
277  // save cropping coordinates
278  if ($type == 'icon') {
279  $entity->icontime = time();
280  if ($x1 || $y1 || $x2 || $y2) {
281  $entity->x1 = $x1;
282  $entity->y1 = $y1;
283  $entity->x2 = $x2;
284  $entity->y2 = $y2;
285  }
286  } else {
287  if ($x1 || $y1 || $x2 || $y2) {
288  $entity->{"{$type}_coords"} = serialize([
289  'x1' => $x1,
290  'y1' => $y1,
291  'x2' => $x2,
292  'y2' => $y2,
293  ]);
294  }
295  }
296 
297  $this->events->triggerResults("entity:{$type}:saved", $entity->getType(), [
298  'entity' => $entity,
299  'x1' => $x1,
300  'y1' => $y1,
301  'x2' => $x2,
302  'y2' => $y2,
303  ]);
304 
305  return true;
306  }
307 
315  protected function prepareIcon($filename) {
316 
317  // fix orientation
318  $temp_file = new \ElggTempFile();
319  $temp_file->setFilename(uniqid() . basename($filename));
320 
321  copy($filename, $temp_file->getFilenameOnFilestore());
322 
323  $rotated = $this->images->fixOrientation($temp_file->getFilenameOnFilestore());
324 
325  if ($rotated) {
326  copy($temp_file->getFilenameOnFilestore(), $filename);
327  }
328 
329  $temp_file->delete();
330  }
331 
343  protected function generateIcon(\ElggEntity $entity, \ElggFile $file, $type = 'icon', $coords = [], $icon_size = '') {
344 
345  if (!$file->exists()) {
346  $this->getLogger()->error('Trying to generate an icon from a non-existing file');
347  return false;
348  }
349 
350  $x1 = (int) elgg_extract('x1', $coords);
351  $y1 = (int) elgg_extract('y1', $coords);
352  $x2 = (int) elgg_extract('x2', $coords);
353  $y2 = (int) elgg_extract('y2', $coords);
354 
355  $sizes = $this->getSizes($entity->getType(), $entity->getSubtype(), $type);
356 
357  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  foreach ($sizes as $size => $opts) {
363  if (!empty($icon_size) && ($icon_size !== $size)) {
364  // only generate the given icon size
365  continue;
366  }
367 
368  // check if the icon config allows cropping
369  if (!(bool) elgg_extract('crop', $opts, true)) {
370  $coords = [
371  'x1' => 0,
372  'y1' => 0,
373  'x2' => 0,
374  'y2' => 0,
375  ];
376  }
377 
378  $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  $icon->open('write');
384  $icon->close();
385 
386  // Save the image without resizing or cropping if the
387  // image size value is an empty array
388  if (is_array($opts) && empty($opts)) {
389  copy($file->getFilenameOnFilestore(), $icon->getFilenameOnFilestore());
390  continue;
391  }
392 
393  $source = $file->getFilenameOnFilestore();
394  $destination = $icon->getFilenameOnFilestore();
395 
396  $resize_params = array_merge($opts, $coords);
397 
398  $image_service = _elgg_services()->imageService;
399  $image_service->setLogger($this->getLogger());
400 
401  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  return true;
415  }
416 
432  public function getIcon(\ElggEntity $entity, $size, $type = 'icon', $generate = true) {
433 
435 
436  $params = [
437  'entity' => $entity,
438  'size' => $size,
439  'type' => $type,
440  ];
441 
442  $entity_type = $entity->getType();
443 
444  $default_icon = new \ElggIcon();
445  $default_icon->owner_guid = $entity->guid;
446  $default_icon->setFilename("icons/{$type}/{$size}.jpg");
447 
448  $icon = $this->events->triggerResults("entity:{$type}:file", $entity_type, $params, $default_icon);
449  if (!$icon instanceof \ElggIcon) {
450  throw new UnexpectedValueException("'entity:{$type}:file', {$entity_type} event must return an instance of \ElggIcon");
451  }
452 
453  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  if ($icon->exists() || !$generate) {
460  return $icon;
461  }
462 
463  if ($size === 'master') {
464  // don't try to generate for master
465  return $icon;
466  }
467 
468  // try to generate icon based on master size
469  $master_icon = $this->getIcon($entity, 'master', $type, false);
470  if (!$master_icon->exists()) {
471  return $icon;
472  }
473 
474  if ($type === 'icon') {
475  $coords = [
476  'x1' => $entity->x1,
477  'y1' => $entity->y1,
478  'x2' => $entity->x2,
479  'y2' => $entity->y2,
480  ];
481  } else {
482  $coords = $entity->{"{$type}_coords"};
483  $coords = empty($coords) ? [] : unserialize($coords);
484  }
485 
486  $this->generateIcon($entity, $master_icon, $type, $coords, $size);
487 
488  return $icon;
489  }
490 
500  public function deleteIcon(\ElggEntity $entity, string $type = 'icon', bool $retain_master = false): bool {
501  $delete = $this->events->triggerResults("entity:{$type}:delete", $entity->getType(), [
502  'entity' => $entity,
503  'retain_master' => $retain_master, // just removing thumbs or everything?
504  ], true);
505 
506  if ($delete === false) {
507  return false;
508  }
509 
510  $result = true;
511  $supported_extensions = [
512  'jpg',
513  ];
514  if ($this->images->hasWebPSupport()) {
515  $supported_extensions[] = 'webp';
516  }
517 
518  $sizes = array_keys($this->getSizes($entity->getType(), $entity->getSubtype(), $type));
519  foreach ($sizes as $size) {
520  if ($size === 'master' && $retain_master) {
521  continue;
522  }
523 
524  $icon = $this->getIcon($entity, $size, $type, false);
525  $result &= $icon->delete();
526 
527  // make sure we remove all supported images (jpg and webp)
528  $current_extension = pathinfo($icon->getFilename(), PATHINFO_EXTENSION);
529  $extensions = $supported_extensions;
530  foreach ($extensions as $extension) {
531  if ($current_extension === $extension) {
532  // already removed
533  continue;
534  }
535 
536  // replace the extension
537  $parts = explode('.', $icon->getFilename());
538  array_pop($parts);
539  $parts[] = $extension;
540 
541  // set new filename and remove the file
542  $icon->setFilename(implode('.', $parts));
543  $result &= $icon->delete();
544  }
545  }
546 
547  if ($type == 'icon') {
548  unset($entity->icontime);
549  unset($entity->x1);
550  unset($entity->y1);
551  unset($entity->x2);
552  unset($entity->y2);
553  } else {
554  unset($entity->{"{$type}_coords"});
555  }
556 
557  return $result;
558  }
559 
571  public function getIconURL(\ElggEntity $entity, string|array $params = []): string {
572  if (is_array($params)) {
573  $size = elgg_extract('size', $params, 'medium');
574  } else {
575  $size = is_string($params) ? $params : 'medium';
576  $params = [];
577  }
578 
580 
581  $params['entity'] = $entity;
582  $params['size'] = $size;
583 
584  $type = elgg_extract('type', $params, 'icon', false);
585  $entity_type = $entity->getType();
586 
587  $url = $this->events->triggerResults("entity:{$type}:url", $entity_type, $params, null);
588  if (!isset($url)) {
589  if ($this->hasIcon($entity, $size, $type)) {
590  $icon = $this->getIcon($entity, $size, $type);
591  $default_use_cookie = (bool) elgg_get_config('session_bound_entity_icons');
592  $url = $icon->getInlineURL((bool) elgg_extract('use_cookie', $params, $default_use_cookie));
593  } else {
594  $url = $this->getFallbackIconUrl($entity, $params);
595  }
596  }
597 
598  if (!empty($url)) {
599  return elgg_normalize_url($url);
600  }
601 
602  return '';
603  }
604 
613  public function getFallbackIconUrl(\ElggEntity $entity, array $params = []) {
614  $type = elgg_extract('type', $params, 'icon', false);
615  $size = elgg_extract('size', $params, 'medium', false);
616 
617  $entity_type = $entity->getType();
618  $entity_subtype = $entity->getSubtype();
619 
620  $exts = ['svg', 'gif', 'png', 'jpg'];
621 
622  foreach ($exts as $ext) {
623  foreach ([$entity_subtype, 'default'] as $subtype) {
624  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  if (elgg_view_exists("{$type}/{$entity_type}/{$subtype}/{$size}.{$ext}", 'default')) {
629  return elgg_get_simplecache_url("{$type}/{$entity_type}/{$subtype}/{$size}.{$ext}");
630  }
631  }
632  }
633 
634  if (elgg_view_exists("{$type}/default/{$size}.png", 'default')) {
635  return elgg_get_simplecache_url("{$type}/default/{$size}.png");
636  }
637  }
638 
648  public function getIconLastChange(\ElggEntity $entity, $size, $type = 'icon') {
649  $icon = $this->getIcon($entity, $size, $type);
650  if ($icon->exists()) {
651  return $icon->getModifiedTime();
652  }
653  }
654 
664  public function hasIcon(\ElggEntity $entity, $size, $type = 'icon') {
665  $icon = $this->getIcon($entity, $size, $type);
666  return $icon->exists() && $icon->getSize() > 0;
667  }
668 
679  public function getSizes(string $entity_type = null, string $entity_subtype = null, $type = 'icon'): array {
680  $sizes = [];
681  $type = $type ?: 'icon';
682  if ($type == 'icon') {
683  $sizes = $this->config->icon_sizes;
684  }
685 
686  $params = [
687  'type' => $type,
688  'entity_type' => $entity_type,
689  'entity_subtype' => $entity_subtype,
690  ];
691  if ($entity_type) {
692  $sizes = $this->events->triggerResults("entity:{$type}:sizes", $entity_type, $params, $sizes);
693  }
694 
695  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  $sizes['master'] = elgg_extract('master', $sizes, [
704  'w' => 10240,
705  'h' => 10240,
706  'square' => false,
707  'upscale' => false,
708  'crop' => false,
709  ]);
710 
711  if (!isset($sizes['master']['crop'])) {
712  $sizes['master']['crop'] = false;
713  }
714 
715  return $sizes;
716  }
717 
727  protected function detectCroppingCoordinates(string $input_name) {
728 
729  $auto_coords = [
730  'x1' => get_input("{$input_name}_x1", get_input('x1')), // x1 is BC fallback
731  'x2' => get_input("{$input_name}_x2", get_input('x2')), // x2 is BC fallback
732  'y1' => get_input("{$input_name}_y1", get_input('y1')), // y1 is BC fallback
733  'y2' => get_input("{$input_name}_y2", get_input('y2')), // y2 is BC fallback
734  ];
735 
736  $auto_coords = array_filter($auto_coords, function($value) {
737  return !elgg_is_empty($value) && is_numeric($value) && (int) $value >= 0;
738  });
739 
740  if (count($auto_coords) !== 4) {
741  return false;
742  }
743 
744  // make ints
745  array_walk($auto_coords, function (&$value) {
746  $value = (int) $value;
747  });
748 
749  // make sure coords make sense x2 > x1 && y2 > y1
750  if ($auto_coords['x2'] <= $auto_coords['x1'] || $auto_coords['y2'] <= $auto_coords['y1']) {
751  return false;
752  }
753 
754  return $auto_coords;
755  }
756 
762  protected function hasWebPSupport(): bool {
763  return in_array('image/webp', $this->request->getAcceptableContentTypes()) && $this->images->hasWebPSupport();
764  }
765 }
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
saveIconFromUploadedFile(\ElggEntity $entity, $input_name, $type= 'icon', array $coords=[])
Saves icons using an uploaded file as the source.
elgg_get_config(string $name, $default=null)
Get an Elgg configuration value.
invalidateCache()
Invalidate cache for entity.
saveIconFromElggFile(\ElggEntity $entity,\ElggFile $file, $type= 'icon', array $coords=[])
Saves icons using a file located in the data store as the source.
$request
Definition: livesearch.php:12
if($icon===false) if($icon!== '') $icon_size
Definition: icon.php:22
saveIconFromLocalFile(\ElggEntity $entity, $filename, $type= 'icon', array $coords=[])
Saves icons using a local file as the source.
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
hasIcon(\ElggEntity $entity, $size, $type= 'icon')
Returns if the entity has an icon of the passed type.
prepareIcon($filename)
Prepares an icon.
$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
__construct(Config $config, EventsService $events, EntityTable $entities, UploadService $uploads, ImageService $images, MimeTypeService $mimetype, HttpRequest $request)
Constructor.
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
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.
generateIcon(\ElggEntity $entity,\ElggFile $file, $type= 'icon', $coords=[], $icon_size= '')
Generate an icon for the given entity.
getMimeType()
Get the mime type of the file.
Definition: ElggFile.php:121
getIcon(\ElggEntity $entity, $size, $type= 'icon', $generate=true)
Returns entity icon as an ElggIcon object The icon file may or may not exist on filestore.
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.
getIconLastChange(\ElggEntity $entity, $size, $type= 'icon')
Returns the timestamp of when the icon was changed.
$size
Definition: thumb.php:23
$extensions
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
saveIcon(\ElggEntity $entity,\ElggFile $file, $type= 'icon', array $coords=[])
Saves icons using a created temporary file.
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_normalize_url(string $url)
Definition: output.php:163
elgg_get_simplecache_url(string $view, string $subview= '')
Get the URL for the cached view.
Definition: cache.php:139
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:152
Entity table database service.
Definition: EntityTable.php:26
$extension
Definition: default.php:25
File upload handling service.