Elgg  Version 3.0
EntityIconService.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg;
4 
8 use ElggEntity;
9 use ElggFile;
10 use ElggIcon;
15 
25 
26  use Loggable;
27  use TimeUsing;
28 
32  private $config;
33 
37  private $hooks;
38 
42  private $request;
43 
47  private $entities;
48 
52  private $uploads;
53 
57  private $images;
58 
70  public function __construct(
72  PluginHooksService $hooks,
74  LoggerInterface $logger,
75  EntityTable $entities,
76  UploadService $uploads,
77  ImageService $images
78  ) {
79  $this->config = $config;
80  $this->hooks = $hooks;
81  $this->request = $request;
82  $this->logger = $logger;
83  $this->entities = $entities;
84  $this->uploads = $uploads;
85  $this->images = $images;
86  }
87 
97  public function saveIconFromUploadedFile(ElggEntity $entity, $input_name, $type = 'icon', array $coords = []) {
98  $input = $this->uploads->getFile($input_name);
99  if (empty($input)) {
100  return false;
101  }
102 
103  // auto detect cropping coordinates
104  if (empty($coords)) {
105  $auto_coords = $this->detectCroppingCoordinates();
106  if (!empty($auto_coords)) {
107  $coords = $auto_coords;
108  }
109  }
110 
111  $tmp = new \ElggTempFile();
112  $tmp->setFilename(uniqid() . $input->getClientOriginalName());
113  $tmp->open('write');
114  $tmp->close();
115 
116  copy($input->getPathname(), $tmp->getFilenameOnFilestore());
117 
118  $tmp->mimetype = (new MimeTypeDetector())->getType($tmp->getFilenameOnFilestore(), $input->getClientMimeType());
119  $tmp->simpletype = elgg_get_file_simple_type($tmp->mimetype);
120 
121  $result = $this->saveIcon($entity, $tmp, $type, $coords);
122 
123  $tmp->delete();
124 
125  return $result;
126  }
127 
138  public function saveIconFromLocalFile(ElggEntity $entity, $filename, $type = 'icon', array $coords = []) {
139  if (!file_exists($filename) || !is_readable($filename)) {
140  throw new InvalidParameterException(__METHOD__ . " expects a readable local file. $filename is not readable");
141  }
142 
143  $tmp = new \ElggTempFile();
144  $tmp->setFilename(uniqid() . basename($filename));
145  $tmp->open('write');
146  $tmp->close();
147 
148  copy($filename, $tmp->getFilenameOnFilestore());
149 
150  $tmp->mimetype = (new MimeTypeDetector())->getType($tmp->getFilenameOnFilestore());
151  $tmp->simpletype = elgg_get_file_simple_type($tmp->mimetype);
152 
153  $result = $this->saveIcon($entity, $tmp, $type, $coords);
154 
155  $tmp->delete();
156 
157  return $result;
158  }
159 
170  public function saveIconFromElggFile(ElggEntity $entity, ElggFile $file, $type = 'icon', array $coords = []) {
171  if (!$file->exists()) {
172  throw new InvalidParameterException(__METHOD__ . ' expects an instance of ElggFile with an existing file on filestore');
173  }
174 
175  $tmp = new \ElggTempFile();
176  $tmp->setFilename(uniqid() . basename($file->getFilenameOnFilestore()));
177  $tmp->open('write');
178  $tmp->close();
179 
180  copy($file->getFilenameOnFilestore(), $tmp->getFilenameOnFilestore());
181 
182  $tmp->mimetype = (new MimeTypeDetector())->getType($tmp->getFilenameOnFilestore(), $file->getMimeType());
183  $tmp->simpletype = elgg_get_file_simple_type($tmp->mimetype);
184 
185  $result = $this->saveIcon($entity, $tmp, $type, $coords);
186 
187  $tmp->delete();
188 
189  return $result;
190  }
191 
201  public function saveIcon(ElggEntity $entity, ElggFile $file, $type = 'icon', array $coords = []) {
202 
203  $type = (string) $type;
204  if (!strlen($type)) {
205  $this->logger->error('Icon type passed to ' . __METHOD__ . ' can not be empty');
206  return false;
207  }
208 
209  $entity_type = $entity->getType();
210 
211  $file = $this->hooks->trigger("entity:$type:prepare", $entity_type, [
212  'entity' => $entity,
213  'file' => $file,
214  ], $file);
215 
216  if (!$file instanceof ElggFile || !$file->exists() || $file->getSimpleType() !== 'image') {
217  $this->logger->error('Source file passed to ' . __METHOD__ . ' can not be resolved to a valid image');
218  return false;
219  }
220 
221  $this->prepareIcon($file->getFilenameOnFilestore());
222 
223  $x1 = (int) elgg_extract('x1', $coords);
224  $y1 = (int) elgg_extract('y1', $coords);
225  $x2 = (int) elgg_extract('x2', $coords);
226  $y2 = (int) elgg_extract('y2', $coords);
227 
228  $created = $this->hooks->trigger("entity:$type:save", $entity_type, [
229  'entity' => $entity,
230  'file' => $file,
231  'x1' => $x1,
232  'y1' => $y1,
233  'x2' => $x2,
234  'y2' => $y2,
235  ], false);
236 
237  // did someone else handle saving the icon?
238  if ($created !== true) {
239  // remove existing icons
240  $this->deleteIcon($entity, $type, true);
241 
242  // save master image
243  $store = $this->generateIcon($entity, $file, $type, $coords, 'master');
244 
245  if (!$store) {
246  $this->deleteIcon($entity, $type);
247  return false;
248  }
249  }
250 
251  if ($type == 'icon') {
252  $entity->icontime = time();
253  if ($x1 || $y1 || $x2 || $y2) {
254  $entity->x1 = $x1;
255  $entity->y1 = $y1;
256  $entity->x2 = $x2;
257  $entity->y2 = $y2;
258  }
259  } else {
260  if ($x1 || $y1 || $x2 || $y2) {
261  $entity->{"{$type}_coords"} = serialize([
262  'x1' => $x1,
263  'y1' => $y1,
264  'x2' => $x2,
265  'y2' => $y2,
266  ]);
267  }
268  }
269 
270  $this->hooks->trigger("entity:$type:saved", $entity->getType(), [
271  'entity' => $entity,
272  'x1' => $x1,
273  'y1' => $y1,
274  'x2' => $x2,
275  'y2' => $y2,
276  ]);
277 
278  return true;
279  }
280 
288  protected function prepareIcon($filename) {
289 
290  // fix orientation
291  $temp_file = new \ElggTempFile();
292  $temp_file->setFilename(uniqid() . basename($filename));
293 
294  copy($filename, $temp_file->getFilenameOnFilestore());
295 
296  $rotated = $this->images->fixOrientation($temp_file->getFilenameOnFilestore());
297 
298  if ($rotated) {
299  copy($temp_file->getFilenameOnFilestore(), $filename);
300  }
301 
302  $temp_file->delete();
303  }
304 
316  protected function generateIcon(ElggEntity $entity, ElggFile $file, $type = 'icon', $coords = [], $icon_size = '') {
317 
318  if (!$file->exists()) {
319  $this->logger->error('Trying to generate an icon from a non-existing file');
320  return false;
321  }
322 
323  $x1 = (int) elgg_extract('x1', $coords);
324  $y1 = (int) elgg_extract('y1', $coords);
325  $x2 = (int) elgg_extract('x2', $coords);
326  $y2 = (int) elgg_extract('y2', $coords);
327 
328  $sizes = $this->getSizes($entity->getType(), $entity->getSubtype(), $type);
329 
330  if (!empty($icon_size) && !isset($sizes[$icon_size])) {
331  $this->logger->warning("The provided icon size '{$icon_size}' doesn't exist for icon type '{$type}'");
332  return false;
333  }
334 
335  foreach ($sizes as $size => $opts) {
336  if (!empty($icon_size) && ($icon_size !== $size)) {
337  // only generate the given icon size
338  continue;
339  }
340 
341  // check if the icon config allows cropping
342  if (!(bool) elgg_extract('crop', $opts, true)) {
343  $coords = [
344  'x1' => 0,
345  'y1' => 0,
346  'x2' => 0,
347  'y2' => 0,
348  ];
349  }
350 
351  $icon = $this->getIcon($entity, $size, $type, false);
352 
353  // We need to make sure that file path is readable by
354  // Imagine\Image\ImagineInterface::save(), as it fails to
355  // build the directory structure on owner's filestore otherwise
356  $icon->open('write');
357  $icon->close();
358 
359  // Save the image without resizing or cropping if the
360  // image size value is an empty array
361  if (is_array($opts) && empty($opts)) {
362  copy($file->getFilenameOnFilestore(), $icon->getFilenameOnFilestore());
363  continue;
364  }
365 
366  $source = $file->getFilenameOnFilestore();
367  $destination = $icon->getFilenameOnFilestore();
368 
369  $resize_params = array_merge($opts, $coords);
370 
371  $image_service = _elgg_services()->imageService;
372  $image_service->setLogger($this->logger);
373 
374  if (!_elgg_services()->imageService->resize($source, $destination, $resize_params)) {
375  $this->logger->error("Failed to create {$size} icon from
376  {$file->getFilenameOnFilestore()} with coords [{$x1}, {$y1}],[{$x2}, {$y2}]");
377  return false;
378  }
379  }
380 
381  return true;
382  }
383 
399  public function getIcon(ElggEntity $entity, $size, $type = 'icon', $generate = true) {
400 
402 
403  $params = [
404  'entity' => $entity,
405  'size' => $size,
406  'type' => $type,
407  ];
408 
409  $entity_type = $entity->getType();
410 
411  $default_icon = new ElggIcon();
412  $default_icon->owner_guid = $entity->guid;
413  $default_icon->setFilename("icons/$type/$size.jpg");
414 
415  $icon = $this->hooks->trigger("entity:$type:file", $entity_type, $params, $default_icon);
416  if (!$icon instanceof ElggIcon) {
417  throw new InvalidParameterException("'entity:$type:file', $entity_type hook must return an instance of ElggIcon");
418  }
419 
420  if ($icon->exists() || !$generate) {
421  return $icon;
422  }
423 
424  if ($size === 'master') {
425  // don't try to generate for master
426  return $icon;
427  }
428 
429  // try to generate icon based on master size
430  $master_icon = $this->getIcon($entity, 'master', $type, false);
431  if (!$master_icon->exists()) {
432  return $icon;
433  }
434 
435  if ($type === 'icon') {
436  $coords = [
437  'x1' => $entity->x1,
438  'y1' => $entity->y1,
439  'x2' => $entity->x2,
440  'y2' => $entity->y2,
441  ];
442  } else {
443  $coords = $entity->{"{$type}_coords"};
444  $coords = empty($coords) ? [] : unserialize($coords);
445  }
446 
447  $this->generateIcon($entity, $master_icon, $type, $coords, $size);
448 
449  return $icon;
450  }
451 
461  public function deleteIcon(ElggEntity $entity, $type = 'icon', $retain_master = false) {
462  $delete = $this->hooks->trigger("entity:$type:delete", $entity->getType(), [
463  'entity' => $entity,
464  ], true);
465 
466  if ($delete === false) {
467  return false;
468  }
469 
470  $result = true;
471 
472  $sizes = array_keys($this->getSizes($entity->getType(), $entity->getSubtype(), $type));
473  foreach ($sizes as $size) {
474  if ($size === 'master' && $retain_master) {
475  continue;
476  }
477 
478  $icon = $this->getIcon($entity, $size, $type, false);
479  $result &= $icon->delete();
480  }
481 
482  if ($type == 'icon') {
483  unset($entity->icontime);
484  unset($entity->x1);
485  unset($entity->y1);
486  unset($entity->x2);
487  unset($entity->y2);
488  } else {
489  unset($entity->{"{$type}_coords"});
490  }
491 
492  return $result;
493  }
494 
505  public function getIconURL(ElggEntity $entity, $params = []) {
506  if (is_array($params)) {
507  $size = elgg_extract('size', $params, 'medium');
508  } else {
509  $size = is_string($params) ? $params : 'medium';
510  $params = [];
511  }
512 
514 
515  $params['entity'] = $entity;
516  $params['size'] = $size;
517 
518  $type = elgg_extract('type', $params) ? : 'icon';
519  $entity_type = $entity->getType();
520 
521  $url = $this->hooks->trigger("entity:$type:url", $entity_type, $params, null);
522  if ($url == null) {
523  if ($this->hasIcon($entity, $size, $type)) {
524  $icon = $this->getIcon($entity, $size, $type);
525  $default_use_cookie = (bool) elgg_get_config('session_bound_entity_icons', false);
526  $url = $icon->getInlineURL((bool) elgg_extract('use_cookie', $params, $default_use_cookie));
527  } else {
528  $url = $this->getFallbackIconUrl($entity, $params);
529  }
530  }
531 
532  if ($url) {
533  return elgg_normalize_url($url);
534  }
535  }
536 
544  public function getFallbackIconUrl(ElggEntity $entity, array $params = []) {
545 
546  $type = elgg_extract('type', $params) ? : 'icon';
547  $size = elgg_extract('size', $params) ? : 'medium';
548 
549  $entity_type = $entity->getType();
550  $entity_subtype = $entity->getSubtype();
551 
552  $exts = ['svg', 'gif', 'png', 'jpg'];
553 
554  foreach ($exts as $ext) {
555  foreach ([$entity_subtype, 'default'] as $subtype) {
556  if ($ext == 'svg' && elgg_view_exists("$type/$entity_type/$subtype.svg")) {
557  return elgg_get_simplecache_url("$type/$entity_type/$subtype.svg");
558  }
559  if (elgg_view_exists("$type/$entity_type/$subtype/$size.$ext")) {
560  return elgg_get_simplecache_url("$type/$entity_type/$subtype/$size.$ext");
561  }
562  }
563  }
564 
565  if (elgg_view_exists("$type/default/$size.png")) {
566  return elgg_get_simplecache_url("$type/default/$size.png");
567  }
568  }
569 
579  public function getIconLastChange(ElggEntity $entity, $size, $type = 'icon') {
580  $icon = $this->getIcon($entity, $size, $type);
581  if ($icon->exists()) {
582  return $icon->getModifiedTime();
583  }
584  }
585 
594  public function hasIcon(\ElggEntity $entity, $size, $type = 'icon') {
595  return $this->getIcon($entity, $size, $type)->exists();
596  }
597 
607  public function getSizes($entity_type = null, $entity_subtype = null, $type = 'icon') {
608  $sizes = [];
609  if (!$type) {
610  $type = 'icon';
611  }
612  if ($type == 'icon') {
613  $sizes = $this->config->icon_sizes;
614  }
615  $params = [
616  'type' => $type,
617  'entity_type' => $entity_type,
618  'entity_subtype' => $entity_subtype,
619  ];
620  if ($entity_type) {
621  $sizes = $this->hooks->trigger("entity:$type:sizes", $entity_type, $params, $sizes);
622  }
623 
624  if (!is_array($sizes)) {
625  throw new InvalidParameterException("The icon size configuration for image type '$type' " .
626  "must be an associative array of image size names and their properties");
627  }
628 
629  // lazy generation of icons requires a 'master' size
630  // this ensures a default config for 'master' size
631  $sizes['master'] = elgg_extract('master', $sizes, [
632  'w' => 10240,
633  'h' => 10240,
634  'square' => false,
635  'upscale' => false,
636  'crop' => false,
637  ]);
638 
639  if (!isset($sizes['master']['crop'])) {
640  $sizes['master']['crop'] = false;
641  }
642 
643  return $sizes;
644  }
645 
652  public function handleServeIconRequest($allow_removing_headers = true) {
653 
654  $response = new Response();
655  $response->setExpires($this->getCurrentTime('-1 day'));
656  $response->prepare($this->request);
657 
658  if ($allow_removing_headers) {
659  // clear cache-boosting headers set by PHP session
660  header_remove('Cache-Control');
661  header_remove('Pragma');
662  header_remove('Expires');
663  }
664 
665  $path = implode('/', $this->request->getUrlSegments());
666  if (!preg_match('~serve-icon/(\d+)/(.*+)$~', $path, $m)) {
667  return $response->setStatusCode(400)->setContent('Malformatted request URL');
668  }
669 
670  list(, $guid, $size) = $m;
671 
672  $entity = $this->entities->get($guid);
673  if (!$entity instanceof \ElggEntity) {
674  return $response->setStatusCode(404)->setContent('Item does not exist');
675  }
676 
677  $thumbnail = $entity->getIcon($size);
678  if (!$thumbnail->exists()) {
679  return $response->setStatusCode(404)->setContent('Icon does not exist');
680  }
681 
682  $if_none_match = $this->request->headers->get('if_none_match');
683  if (!empty($if_none_match)) {
684  // strip mod_deflate suffixes
685  $this->request->headers->set('if_none_match', str_replace('-gzip', '', $if_none_match));
686  }
687 
688  $filenameonfilestore = $thumbnail->getFilenameOnFilestore();
689  $last_updated = filemtime($filenameonfilestore);
690  $etag = '"' . $last_updated . '"';
691 
692  $response->setPrivate()
693  ->setEtag($etag)
694  ->setExpires($this->getCurrentTime('+1 day'))
695  ->setMaxAge(86400);
696 
697  if ($response->isNotModified($this->request)) {
698  return $response;
699  }
700 
701  $headers = [
702  'Content-Type' => (new MimeTypeDetector())->getType($filenameonfilestore),
703  ];
704  $response = new BinaryFileResponse($filenameonfilestore, 200, $headers, false, 'inline');
705  $response->prepare($this->request);
706 
707  $response->setPrivate()
708  ->setEtag($etag)
709  ->setExpires($this->getCurrentTime('+1 day'))
710  ->setMaxAge(86400);
711 
712  return $response;
713  }
714 
722  protected function detectCroppingCoordinates() {
723 
724  $auto_coords = [
725  'x1' => get_input('x1'),
726  'x2' => get_input('x2'),
727  'y1' => get_input('y1'),
728  'y2' => get_input('y2'),
729  ];
730 
731  $auto_coords = array_filter($auto_coords, function($value) {
732  return !elgg_is_empty($value) && is_numeric($value);
733  });
734 
735  if (count($auto_coords) !== 4) {
736  return false;
737  }
738 
739  // make ints
740  array_walk($auto_coords, function (&$value) {
741  $value = (int) $value;
742  });
743 
744  return $auto_coords;
745  }
746 
747 }
elgg_view_exists($view, $viewtype= '', $recurse=true)
Returns whether the specified view exists.
Definition: views.php:205
getSubtype()
Get the entity subtype.
if(!array_key_exists($filename, $text_files)) $file
$source
$params
Saves global plugin settings.
Definition: save.php:13
elgg_normalize_url($url)
Definition: output.php:186
if($icon===false) if(isset($icon)&&$icon!==true) $icon_size
Definition: icon.php:23
$request
Page handler for autocomplete endpoint.
Definition: livesearch.php:9
elgg_get_simplecache_url($view, $subview= '')
Get the URL for the cached view.
Definition: cache.php:141
$size
Definition: remove.php:24
getFallbackIconUrl(ElggEntity $entity, array $params=[])
Returns default/fallback icon.
trait Loggable
Enables adding a logger.
Definition: Loggable.php:12
$path
Definition: details.php:89
exists()
Returns if the file exists.
Definition: ElggFile.php:349
$subtype
Definition: delete.php:22
getSizes($entity_type=null, $entity_subtype=null, $type= 'icon')
Returns a configuration array of icon sizes.
getSimpleType()
Get the simple type of the file.
Definition: ElggFile.php:166
$delete
getCurrentTime($modifier= '')
Get the (cloned) time.
Definition: TimeUsing.php:27
hasIcon(\ElggEntity $entity, $size, $type= 'icon')
Returns if the entity has an icon of the passed type.
$guid
Removes an admin notice.
prepareIcon($filename)
Prepares an icon.
$type
Definition: delete.php:21
elgg_strtolower()
Wrapper function for mb_strtolower().
Definition: mb_wrapper.php:174
saveIconFromElggFile(ElggEntity $entity, ElggFile $file, $type= 'icon', array $coords=[])
Saves icons using a file located in the data store as the source.
if(!$owner) $icon
Definition: default.php:16
elgg_is_empty($value)
Check if a value isn&#39;t empty, but allow 0 and &#39;0&#39;.
Definition: input.php:206
$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:77
handleServeIconRequest($allow_removing_headers=true)
Handle request to /serve-icon handler.
detectCroppingCoordinates()
Automagicly detect cropping coordinates.
Configuration exception.
if(!$entity instanceof ElggEntity) $input_name
Definition: default.php:14
$entity
Definition: reset.php:8
getIconURL(ElggEntity $entity, $params=[])
Get the URL for this entity&#39;s icon.
WARNING: API IN FLUX.
get_input($variable, $default=null, $filter_result=true)
Get some input from variables passed submitted through GET or POST.
Definition: input.php:27
deleteIcon(ElggEntity $entity, $type= 'icon', $retain_master=false)
Removes all icon files and metadata for the passed type of icon.
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.
$url
Definition: default.php:33
getMimeType()
Get the mime type of the file.
Definition: ElggFile.php:103
Image manipulation service.
elgg_extract($key, $array, $default=null, $strict=true)
Checks for $array[$key] and returns its value if it exists, else returns $default.
Definition: elgglib.php:1131
generateIcon(ElggEntity $entity, ElggFile $file, $type= 'icon', $coords=[], $icon_size= '')
Generate an icon for the given entity.
getIconLastChange(ElggEntity $entity, $size, $type= 'icon')
Returns the timestamp of when the icon was changed.
__construct(Config $config, PluginHooksService $hooks, HttpRequest $request, LoggerInterface $logger, EntityTable $entities, UploadService $uploads, ImageService $images)
Constructor.
$value
Definition: debugging.php:7
Detect the MIME type of a file.
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.
$input
Form field view.
Definition: field.php:13
$filename
_elgg_services()
Get the global service provider.
Definition: elgglib.php:1292
saveIconFromUploadedFile(ElggEntity $entity, $input_name, $type= 'icon', array $coords=[])
Saves icons using an uploaded file as the source.
saveIconFromLocalFile(ElggEntity $entity, $filename, $type= 'icon', array $coords=[])
Saves icons using a local file as the source.
trait TimeUsing
Adds methods for setting the current time (for testing)
Definition: TimeUsing.php:12
elgg ElggEntity
Definition: ElggEntity.js:15
if(!$owner||!$owner->canEdit()) if(!$owner->hasIcon('master')) $coords
Definition: crop.php:18
elgg_get_file_simple_type($mime_type)
Returns the category of a file from its MIME type.
Definition: filestore.php:123
WARNING: API IN FLUX.
Definition: EntityTable.php:38
elgg_get_config($name, $default=null)
Get an Elgg configuration value.
saveIcon(ElggEntity $entity, ElggFile $file, $type= 'icon', array $coords=[])
Saves icons using a created temporary file.
WARNING: API IN FLUX.