Elgg  Version master
EntityIconService.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg;
4 
14 
22 
23  use Loggable;
24  use TimeUsing;
25 
29  private $config;
30 
34  private $events;
35 
39  private $entities;
40 
44  private $uploads;
45 
49  private $images;
50 
54  protected $mimetype;
55 
59  protected $request;
60 
72  public function __construct(
74  EventsService $events,
75  EntityTable $entities,
76  UploadService $uploads,
77  ImageService $images,
78  MimeTypeService $mimetype,
80  ) {
81  $this->config = $config;
82  $this->events = $events;
83  $this->entities = $entities;
84  $this->uploads = $uploads;
85  $this->images = $images;
86  $this->mimetype = $mimetype;
87  $this->request = $request;
88  }
89 
100  public function saveIconFromUploadedFile(\ElggEntity $entity, $input_name, $type = 'icon', array $coords = []) {
101  $input = $this->uploads->getFile($input_name);
102  if (empty($input)) {
103  return false;
104  }
105 
106  // auto detect cropping coordinates
107  if (empty($coords)) {
108  $auto_coords = $this->detectCroppingCoordinates($input_name);
109  if (!empty($auto_coords)) {
110  $coords = $auto_coords;
111  }
112  }
113 
114  $tmp = new \ElggTempFile();
115  $tmp->setFilename(uniqid() . $input->getClientOriginalName());
116  $tmp->open('write');
117  $tmp->close();
118 
119  copy($input->getPathname(), $tmp->getFilenameOnFilestore());
120 
121  $tmp->mimetype = $this->mimetype->getMimeType($tmp->getFilenameOnFilestore());
122  $tmp->simpletype = $this->mimetype->getSimpleType($tmp->mimetype);
123 
124  $result = $this->saveIcon($entity, $tmp, $type, $coords);
125 
126  $tmp->delete();
127 
128  return $result;
129  }
130 
142  public function saveIconFromLocalFile(\ElggEntity $entity, $filename, $type = 'icon', array $coords = []) {
143  if (!file_exists($filename) || !is_readable($filename)) {
144  throw new InvalidArgumentException(__METHOD__ . " expects a readable local file. {$filename} is not readable");
145  }
146 
147  $tmp = new \ElggTempFile();
148  $tmp->setFilename(uniqid() . basename($filename));
149  $tmp->open('write');
150  $tmp->close();
151 
152  copy($filename, $tmp->getFilenameOnFilestore());
153 
154  $tmp->mimetype = $this->mimetype->getMimeType($tmp->getFilenameOnFilestore());
155  $tmp->simpletype = $this->mimetype->getSimpleType($tmp->mimetype);
156 
157  $result = $this->saveIcon($entity, $tmp, $type, $coords);
158 
159  $tmp->delete();
160 
161  return $result;
162  }
163 
175  public function saveIconFromElggFile(\ElggEntity $entity, \ElggFile $file, $type = 'icon', array $coords = []) {
176  if (!$file->exists()) {
177  throw new InvalidArgumentException(__METHOD__ . ' expects an instance of ElggFile with an existing file on filestore');
178  }
179 
180  $tmp = new \ElggTempFile();
181  $tmp->setFilename(uniqid() . basename($file->getFilenameOnFilestore()));
182  $tmp->open('write');
183  $tmp->close();
184 
185  copy($file->getFilenameOnFilestore(), $tmp->getFilenameOnFilestore());
186 
187  $tmp->mimetype = $this->mimetype->getMimeType($tmp->getFilenameOnFilestore(), $file->getMimeType() ?: '');
188  $tmp->simpletype = $this->mimetype->getSimpleType($tmp->mimetype);
189 
190  $result = $this->saveIcon($entity, $tmp, $type, $coords);
191 
192  $tmp->delete();
193 
194  return $result;
195  }
196 
207  public function saveIcon(\ElggEntity $entity, \ElggFile $file, $type = 'icon', array $coords = []) {
208 
209  $type = (string) $type;
210  if (!strlen($type)) {
211  $this->getLogger()->error('Icon type passed to ' . __METHOD__ . ' can not be empty');
212  return false;
213  }
214 
215  $entity_type = $entity->getType();
216 
217  $file = $this->events->triggerResults("entity:{$type}:prepare", $entity_type, [
218  'entity' => $entity,
219  'file' => $file,
220  ], $file);
221 
222  if (!$file instanceof \ElggFile || !$file->exists() || $file->getSimpleType() !== 'image') {
223  $this->getLogger()->error('Source file passed to ' . __METHOD__ . ' can not be resolved to a valid image');
224  return false;
225  }
226 
227  $this->prepareIcon($file->getFilenameOnFilestore());
228 
229  $x1 = (int) elgg_extract('x1', $coords);
230  $y1 = (int) elgg_extract('y1', $coords);
231  $x2 = (int) elgg_extract('x2', $coords);
232  $y2 = (int) elgg_extract('y2', $coords);
233 
234  $created = $this->events->triggerResults("entity:{$type}:save", $entity_type, [
235  'entity' => $entity,
236  'file' => $file,
237  'x1' => $x1,
238  'y1' => $y1,
239  'x2' => $x2,
240  'y2' => $y2,
241  ], false);
242 
243  // did someone else handle saving the icon?
244  if ($created !== true) {
245  // remove existing icons
246  $this->deleteIcon($entity, $type, true);
247 
248  // save master image
249  $store = $this->generateIcon($entity, $file, $type, $coords, 'master');
250 
251  if (!$store) {
252  $this->deleteIcon($entity, $type);
253  return false;
254  }
255 
256  // validate cropping coords to prevent out-of-bounds issues
257  try {
258  $sizes = $this->getSizes($entity->getType(), $entity->getSubtype(), $type);
259  $coords = array_merge($sizes['master'], $coords);
260 
261  $icon = $this->getIcon($entity, 'master', $type, false);
262 
263  $this->images->normalizeResizeParameters($icon->getFilenameOnFilestore(), $coords);
264  } catch (LogicException $e) {
265  // cropping coords are wrong, reset to 0
266  $x1 = 0;
267  $x2 = 0;
268  $y1 = 0;
269  $y2 = 0;
270  }
271  }
272 
273  if ($type == 'icon') {
274  $entity->icontime = time();
275  if ($x1 || $y1 || $x2 || $y2) {
276  $entity->x1 = $x1;
277  $entity->y1 = $y1;
278  $entity->x2 = $x2;
279  $entity->y2 = $y2;
280  }
281  } else {
282  if ($x1 || $y1 || $x2 || $y2) {
283  $entity->{"{$type}_coords"} = serialize([
284  'x1' => $x1,
285  'y1' => $y1,
286  'x2' => $x2,
287  'y2' => $y2,
288  ]);
289  }
290  }
291 
292  $this->events->triggerResults("entity:{$type}:saved", $entity->getType(), [
293  'entity' => $entity,
294  'x1' => $x1,
295  'y1' => $y1,
296  'x2' => $x2,
297  'y2' => $y2,
298  ]);
299 
300  return true;
301  }
302 
310  protected function prepareIcon($filename) {
311 
312  // fix orientation
313  $temp_file = new \ElggTempFile();
314  $temp_file->setFilename(uniqid() . basename($filename));
315 
316  copy($filename, $temp_file->getFilenameOnFilestore());
317 
318  $rotated = $this->images->fixOrientation($temp_file->getFilenameOnFilestore());
319 
320  if ($rotated) {
321  copy($temp_file->getFilenameOnFilestore(), $filename);
322  }
323 
324  $temp_file->delete();
325  }
326 
338  protected function generateIcon(\ElggEntity $entity, \ElggFile $file, $type = 'icon', $coords = [], $icon_size = '') {
339 
340  if (!$file->exists()) {
341  $this->getLogger()->error('Trying to generate an icon from a non-existing file');
342  return false;
343  }
344 
345  $x1 = (int) elgg_extract('x1', $coords);
346  $y1 = (int) elgg_extract('y1', $coords);
347  $x2 = (int) elgg_extract('x2', $coords);
348  $y2 = (int) elgg_extract('y2', $coords);
349 
350  $sizes = $this->getSizes($entity->getType(), $entity->getSubtype(), $type);
351 
352  if (!empty($icon_size) && !isset($sizes[$icon_size])) {
353  $this->getLogger()->warning("The provided icon size '{$icon_size}' doesn't exist for icon type '{$type}'");
354  return false;
355  }
356 
357  foreach ($sizes as $size => $opts) {
358  if (!empty($icon_size) && ($icon_size !== $size)) {
359  // only generate the given icon size
360  continue;
361  }
362 
363  // check if the icon config allows cropping
364  if (!(bool) elgg_extract('crop', $opts, true)) {
365  $coords = [
366  'x1' => 0,
367  'y1' => 0,
368  'x2' => 0,
369  'y2' => 0,
370  ];
371  }
372 
373  $icon = $this->getIcon($entity, $size, $type, false);
374 
375  // We need to make sure that file path is readable by
376  // Imagine\Image\ImagineInterface::save(), as it fails to
377  // build the directory structure on owner's filestore otherwise
378  $icon->open('write');
379  $icon->close();
380 
381  // Save the image without resizing or cropping if the
382  // image size value is an empty array
383  if (is_array($opts) && empty($opts)) {
384  copy($file->getFilenameOnFilestore(), $icon->getFilenameOnFilestore());
385  continue;
386  }
387 
388  $source = $file->getFilenameOnFilestore();
389  $destination = $icon->getFilenameOnFilestore();
390 
391  $resize_params = array_merge($opts, $coords);
392 
393  $image_service = _elgg_services()->imageService;
394  $image_service->setLogger($this->getLogger());
395 
396  if (!_elgg_services()->imageService->resize($source, $destination, $resize_params)) {
397  $this->getLogger()->error("Failed to create {$size} icon from
398  {$file->getFilenameOnFilestore()} with coords [{$x1}, {$y1}],[{$x2}, {$y2}]");
399  return false;
400  }
401  }
402 
403  return true;
404  }
405 
421  public function getIcon(\ElggEntity $entity, $size, $type = 'icon', $generate = true) {
422 
424 
425  $params = [
426  'entity' => $entity,
427  'size' => $size,
428  'type' => $type,
429  ];
430 
431  $entity_type = $entity->getType();
432 
433  $default_icon = new \ElggIcon();
434  $default_icon->owner_guid = $entity->guid;
435  $default_icon->setFilename("icons/{$type}/{$size}.jpg");
436 
437  $icon = $this->events->triggerResults("entity:{$type}:file", $entity_type, $params, $default_icon);
438  if (!$icon instanceof \ElggIcon) {
439  throw new UnexpectedValueException("'entity:{$type}:file', {$entity_type} event must return an instance of \ElggIcon");
440  }
441 
442  if ($size !== 'master' && $this->hasWebPSupport()) {
443  if (pathinfo($icon->getFilename(), PATHINFO_EXTENSION) === 'jpg') {
444  $icon->setFilename(substr($icon->getFilename(), 0, -3) . 'webp');
445  }
446  }
447 
448  if ($icon->exists() || !$generate) {
449  return $icon;
450  }
451 
452  if ($size === 'master') {
453  // don't try to generate for master
454  return $icon;
455  }
456 
457  // try to generate icon based on master size
458  $master_icon = $this->getIcon($entity, 'master', $type, false);
459  if (!$master_icon->exists()) {
460  return $icon;
461  }
462 
463  if ($type === 'icon') {
464  $coords = [
465  'x1' => $entity->x1,
466  'y1' => $entity->y1,
467  'x2' => $entity->x2,
468  'y2' => $entity->y2,
469  ];
470  } else {
471  $coords = $entity->{"{$type}_coords"};
472  $coords = empty($coords) ? [] : unserialize($coords);
473  }
474 
475  $this->generateIcon($entity, $master_icon, $type, $coords, $size);
476 
477  return $icon;
478  }
479 
489  public function deleteIcon(\ElggEntity $entity, $type = 'icon', $retain_master = false) {
490  $delete = $this->events->triggerResults("entity:{$type}:delete", $entity->getType(), [
491  'entity' => $entity,
492  ], true);
493 
494  if ($delete === false) {
495  return false;
496  }
497 
498  $result = true;
499  $supported_extensions = [
500  'jpg',
501  ];
502  if ($this->images->hasWebPSupport()) {
503  $supported_extensions[] = 'webp';
504  }
505 
506  $sizes = array_keys($this->getSizes($entity->getType(), $entity->getSubtype(), $type));
507  foreach ($sizes as $size) {
508  if ($size === 'master' && $retain_master) {
509  continue;
510  }
511 
512  $icon = $this->getIcon($entity, $size, $type, false);
513  $result &= $icon->delete();
514 
515  // make sure we remove all supported images (jpg and webp)
516  $current_extension = pathinfo($icon->getFilename(), PATHINFO_EXTENSION);
517  $extensions = $supported_extensions;
518  foreach ($extensions as $extension) {
519  if ($current_extension === $extension) {
520  // already removed
521  continue;
522  }
523 
524  // replace the extension
525  $parts = explode('.', $icon->getFilename());
526  array_pop($parts);
527  $parts[] = $extension;
528 
529  // set new filename and remove the file
530  $icon->setFilename(implode('.', $parts));
531  $result &= $icon->delete();
532  }
533  }
534 
535  if ($type == 'icon') {
536  unset($entity->icontime);
537  unset($entity->x1);
538  unset($entity->y1);
539  unset($entity->x2);
540  unset($entity->y2);
541  } else {
542  unset($entity->{"{$type}_coords"});
543  }
544 
545  return $result;
546  }
547 
559  public function getIconURL(\ElggEntity $entity, string|array $params = []): string {
560  if (is_array($params)) {
561  $size = elgg_extract('size', $params, 'medium');
562  } else {
563  $size = is_string($params) ? $params : 'medium';
564  $params = [];
565  }
566 
568 
569  $params['entity'] = $entity;
570  $params['size'] = $size;
571 
572  $type = elgg_extract('type', $params, 'icon', false);
573  $entity_type = $entity->getType();
574 
575  $url = $this->events->triggerResults("entity:{$type}:url", $entity_type, $params, null);
576  if (!isset($url)) {
577  if ($this->hasIcon($entity, $size, $type)) {
578  $icon = $this->getIcon($entity, $size, $type);
579  $default_use_cookie = (bool) elgg_get_config('session_bound_entity_icons');
580  $url = $icon->getInlineURL((bool) elgg_extract('use_cookie', $params, $default_use_cookie));
581  } else {
582  $url = $this->getFallbackIconUrl($entity, $params);
583  }
584  }
585 
586  if (!empty($url)) {
587  return elgg_normalize_url($url);
588  }
589 
590  return '';
591  }
592 
601  public function getFallbackIconUrl(\ElggEntity $entity, array $params = []) {
602  $type = elgg_extract('type', $params, 'icon', false);
603  $size = elgg_extract('size', $params, 'medium', false);
604 
605  $entity_type = $entity->getType();
606  $entity_subtype = $entity->getSubtype();
607 
608  $exts = ['svg', 'gif', 'png', 'jpg'];
609 
610  foreach ($exts as $ext) {
611  foreach ([$entity_subtype, 'default'] as $subtype) {
612  if ($ext == 'svg' && elgg_view_exists("{$type}/{$entity_type}/{$subtype}.svg", 'default')) {
613  return elgg_get_simplecache_url("{$type}/{$entity_type}/{$subtype}.svg");
614  }
615 
616  if (elgg_view_exists("{$type}/{$entity_type}/{$subtype}/{$size}.{$ext}", 'default')) {
617  return elgg_get_simplecache_url("{$type}/{$entity_type}/{$subtype}/{$size}.{$ext}");
618  }
619  }
620  }
621 
622  if (elgg_view_exists("{$type}/default/{$size}.png", 'default')) {
623  return elgg_get_simplecache_url("{$type}/default/{$size}.png");
624  }
625  }
626 
636  public function getIconLastChange(\ElggEntity $entity, $size, $type = 'icon') {
637  $icon = $this->getIcon($entity, $size, $type);
638  if ($icon->exists()) {
639  return $icon->getModifiedTime();
640  }
641  }
642 
652  public function hasIcon(\ElggEntity $entity, $size, $type = 'icon') {
653  $icon = $this->getIcon($entity, $size, $type);
654  return $icon->exists() && $icon->getSize() > 0;
655  }
656 
667  public function getSizes(string $entity_type = null, string $entity_subtype = null, $type = 'icon'): array {
668  $sizes = [];
669  $type = $type ?: 'icon';
670  if ($type == 'icon') {
671  $sizes = $this->config->icon_sizes;
672  }
673 
674  $params = [
675  'type' => $type,
676  'entity_type' => $entity_type,
677  'entity_subtype' => $entity_subtype,
678  ];
679  if ($entity_type) {
680  $sizes = $this->events->triggerResults("entity:{$type}:sizes", $entity_type, $params, $sizes);
681  }
682 
683  if (!is_array($sizes)) {
684  $msg = "The icon size configuration for image type '{$type}'";
685  $msg .= ' must be an associative array of image size names and their properties';
686  throw new InvalidArgumentException($msg);
687  }
688 
689  // lazy generation of icons requires a 'master' size
690  // this ensures a default config for 'master' size
691  $sizes['master'] = elgg_extract('master', $sizes, [
692  'w' => 10240,
693  'h' => 10240,
694  'square' => false,
695  'upscale' => false,
696  'crop' => false,
697  ]);
698 
699  if (!isset($sizes['master']['crop'])) {
700  $sizes['master']['crop'] = false;
701  }
702 
703  return $sizes;
704  }
705 
715  protected function detectCroppingCoordinates(string $input_name) {
716 
717  $auto_coords = [
718  'x1' => get_input("{$input_name}_x1", get_input('x1')), // x1 is BC fallback
719  'x2' => get_input("{$input_name}_x2", get_input('x2')), // x2 is BC fallback
720  'y1' => get_input("{$input_name}_y1", get_input('y1')), // y1 is BC fallback
721  'y2' => get_input("{$input_name}_y2", get_input('y2')), // y2 is BC fallback
722  ];
723 
724  $auto_coords = array_filter($auto_coords, function($value) {
725  return !elgg_is_empty($value) && is_numeric($value) && (int) $value >= 0;
726  });
727 
728  if (count($auto_coords) !== 4) {
729  return false;
730  }
731 
732  // make ints
733  array_walk($auto_coords, function (&$value) {
734  $value = (int) $value;
735  });
736 
737  // make sure coords make sense x2 > x1 && y2 > y1
738  if ($auto_coords['x2'] <= $auto_coords['x1'] || $auto_coords['y2'] <= $auto_coords['y1']) {
739  return false;
740  }
741 
742  return $auto_coords;
743  }
744 
750  protected function hasWebPSupport(): bool {
751  return in_array('image/webp', $this->request->getAcceptableContentTypes()) && $this->images->hasWebPSupport();
752  }
753 }
getSubtype()
Get the entity subtype.
deleteIcon(\ElggEntity $entity, $type= 'icon', $retain_master=false)
Removes all icon files and metadata for the passed type of icon.
$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.
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.
$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
Exception that represents error in the program logic.
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.
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:160
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.