‪TYPO3CMS  ‪main
IconFactory.php
Go to the documentation of this file.
1 <?php
2 
3 /*
4  * This file is part of the TYPO3 CMS project.
5  *
6  * It is free software; you can redistribute it and/or modify it under
7  * the terms of the GNU General Public License, either version 2
8  * of the License, or any later version.
9  *
10  * For the full copyright and license information, please read the
11  * LICENSE.txt file that was distributed with this source code.
12  *
13  * The TYPO3 project - inspiring people to share!
14  */
15 
17 
18 use Psr\Container\ContainerInterface;
19 use Psr\EventDispatcher\EventDispatcherInterface;
28 
34 {
38  protected ‪$iconRegistry;
39 
46  protected ‪$recordStatusMapping = [];
47 
54  protected ‪$overlayPriorities = [];
55 
61  protected static ‪$iconCache = [];
62 
66  protected ‪$eventDispatcher;
67 
68  protected ContainerInterface ‪$container;
69 
70  public function ‪__construct(EventDispatcherInterface ‪$eventDispatcher, ‪IconRegistry ‪$iconRegistry, ContainerInterface ‪$container)
71  {
72  $this->eventDispatcher = ‪$eventDispatcher;
73  $this->iconRegistry = ‪$iconRegistry;
74  $this->container = ‪$container;
75  $this->recordStatusMapping = ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['IconFactory']['recordStatusMapping'];
76  $this->overlayPriorities = ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['IconFactory']['overlayPriorities'];
77  }
78 
87  public function ‪getIcon(‪$identifier, string|IconSize $size = IconSize::MEDIUM, $overlayIdentifier = null, \‪TYPO3\CMS\Core\Type\‪Icon\‪IconState|‪IconState $state = null)
88  {
89  if ($state instanceof \‪TYPO3\CMS\Core\Type\‪Icon\‪IconState) {
90  trigger_error(
91  'Using the non-native enumeration TYPO3\CMS\Core\Type\Icon\IconState in IconFactory->getIcon()'
92  . ' will not work in TYPO3 v14.0 anymore. Use native TYPO3\CMS\Core\Imaging\IconState instead.',
93  E_USER_DEPRECATED
94  );
95  $stateValue = (string)$state;
96  } else {
97  $stateValue = $state?->value ?? '';
98  }
99  if (is_string($size)) {
100  $size = IconSize::from($size);
101  $size->triggerDeprecation();
102  }
103  $cacheIdentifier = md5(‪$identifier . $size->value . $overlayIdentifier . $stateValue);
104  if (!empty(static::$iconCache[$cacheIdentifier])) {
105  return static::$iconCache[$cacheIdentifier];
106  }
107 
108  if (
109  !$this->iconRegistry->isDeprecated(‪$identifier)
110  && !$this->iconRegistry->isRegistered(‪$identifier)
111  ) {
112  // in case icon identifier is neither deprecated nor registered
113  ‪$identifier = $this->iconRegistry->getDefaultIconIdentifier();
114  }
115 
116  $iconConfiguration = $this->iconRegistry->getIconConfigurationByIdentifier(‪$identifier);
117  $iconConfiguration['state'] = $stateValue;
118  $icon = $this->‪createIcon(‪$identifier, $size, $overlayIdentifier, $iconConfiguration);
119 
121  $iconProvider = $this->container->has($iconConfiguration['provider']) ?
122  $this->container->get($iconConfiguration['provider']) :
123  GeneralUtility::makeInstance($iconConfiguration['provider']);
124  $iconProvider->prepareIconMarkup($icon, $iconConfiguration['options']);
125 
126  static::$iconCache[$cacheIdentifier] = $icon;
127 
128  return $icon;
129  }
130 
141  public function ‪getIconForRecord($table, array $row, $size = IconSize::MEDIUM)
142  {
143  if (is_string($size)) {
144  $size = IconSize::from($size);
145  $size->triggerDeprecation();
146  }
147  $iconIdentifier = $this->‪mapRecordTypeToIconIdentifier($table, $row);
148  $overlayIdentifier = $this->‪mapRecordTypeToOverlayIdentifier($table, $row);
149  return $this->‪getIcon($iconIdentifier, $size, $overlayIdentifier);
150  }
151 
168  public function ‪mapRecordTypeToIconIdentifier($table, array $row)
169  {
170  $recordType = [];
171  $ref = null;
172 
173  if (isset(‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_column'])) {
174  $column = ‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_column'];
175  if (isset($row[$column])) {
176  // even if not properly documented the value of the typeicon_column in a record could be
177  // an array (multiselect) in typeicon_classes a key could consist of a comma-separated string "foo,bar"
178  // but mostly it should be only one entry in that array
179  if (is_array($row[$column])) {
180  $recordType[1] = implode(',', $row[$column]);
181  } else {
182  $recordType[1] = $row[$column];
183  }
184  } else {
185  $recordType[1] = 'default';
186  }
187  // Workaround to give nav_hide pages a complete different icon
188  // Although it's not a separate doctype
189  // and to give root-pages an own icon
190  if ($table === 'pages') {
191  if (($row['nav_hide'] ?? 0) > 0) {
192  $recordType[2] = $this->‪getRecordTypeForPageType(
193  $recordType[1],
194  'hideinmenu',
195  $table
196  );
197  }
198  if (($row['is_siteroot'] ?? 0) > 0) {
199  $recordType[3] = $this->‪getRecordTypeForPageType(
200  $recordType[1],
201  'root',
202  $table
203  );
204  }
205  if (!empty($row['module'])) {
206  if (is_array($row['module'])) {
207  // field 'module' is configured as type 'select' in the TCA,
208  // so the value may have already been converted to an array
209  $moduleSuffix = reset($row['module']);
210  } else {
211  $moduleSuffix = $row['module'];
212  }
213  $recordType[4] = 'contains-' . $moduleSuffix;
214  }
215  if (($row['content_from_pid'] ?? 0) > 0) {
216  if ($row['is_siteroot'] ?? false) {
217  $recordType[4] = $this->‪getRecordTypeForPageType(
218  $recordType[1],
219  'contentFromPid-root',
220  $table
221  );
222  } else {
223  $suffix = (int)$row['nav_hide'] === 0 ? 'contentFromPid' : 'contentFromPid-hideinmenu';
224  $recordType[4] = $this->‪getRecordTypeForPageType($recordType[1], $suffix, $table, 'page');
225  }
226  }
227  }
228  if (isset(‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes'])
229  && is_array(‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes'])
230  ) {
231  foreach ($recordType as $key => $type) {
232  if (isset(‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes'][$type])) {
233  $recordType[$key] = ‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes'][$type];
234  } else {
235  unset($recordType[$key]);
236  }
237  }
238  $recordType[0] = ‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes']['default'] ?? '';
239  if (isset(‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes']['mask'])
240  && isset($row[$column]) && is_string($row[$column])
241  ) {
242  $recordType[5] = str_replace(
243  '###TYPE###',
244  $row[$column] ?? '',
245  ‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes']['mask']
246  );
247  }
248  if (isset(‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes']['userFunc'])) {
249  $parameters = ['row' => $row];
250  $recordType[6] = GeneralUtility::callUserFunction(
251  ‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes']['userFunc'],
252  $parameters,
253  $ref
254  );
255  }
256  } else {
257  foreach ($recordType as &$type) {
258  $type = 'tcarecords-' . $table . '-' . $type;
259  }
260  unset($type);
261  $recordType[0] = 'tcarecords-' . $table . '-default';
262  }
263  } elseif (isset(‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes'])
264  && is_array(‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes'])
265  ) {
266  $recordType[0] = ‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes']['default'];
267  } else {
268  $recordType[0] = 'tcarecords-' . $table . '-default';
269  }
270 
271  if (($row['CType'] ?? '') === 'list' && ($row['list_type'] ?? '') !== '') {
272  $pluginIcon = $this->‪getIconForPlugin($row['list_type']);
273  if ($pluginIcon) {
274  $recordType[7] = $pluginIcon;
275  }
276  }
277 
278  krsort($recordType);
279  foreach ($recordType as $iconName) {
280  if ($this->iconRegistry->isRegistered($iconName)) {
281  return $iconName;
282  }
283  }
284 
285  return $this->iconRegistry->getDefaultIconIdentifier();
286  }
287 
291  protected function ‪getIconForPlugin(string $pluginName): ?string
292  {
293  $result = null;
294  $items = ‪$GLOBALS['TCA']['tt_content']['columns']['list_type']['config']['items'] ?? [];
295  foreach ($items as $item) {
296  if ($item['value'] === $pluginName) {
297  $result = $item['icon'];
298  break;
299  }
300  }
301 
302  return $result;
303  }
304 
309  protected function ‪getRecordTypeForPageType(string $typeName, string $suffix, string $table, string $fallbackTypeName = '1'): string
310  {
311  $recordType = $typeName . '-' . $suffix;
312 
313  // Check if typeicon class exists. If not fallback to page as typeName
314  if (!isset(‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes'][$recordType])) {
315  $recordType = $fallbackTypeName . '-' . $suffix;
316  }
317  return $recordType;
318  }
319 
329  protected function ‪mapRecordTypeToOverlayIdentifier($table, array $row)
330  {
331  $tcaCtrl = ‪$GLOBALS['TCA'][$table]['ctrl'] ?? [];
332  // Calculate for a given record the actual visibility at the moment
333  $status = [
334  'hidden' => false,
335  'starttime' => false,
336  'endtime' => false,
337  'futureendtime' => false,
338  'fe_group' => false,
339  'deleted' => false,
340  'protectedSection' => false,
341  'nav_hide' => !empty($row['nav_hide']),
342  ];
343  // Icon state based on "enableFields":
344  if (isset($tcaCtrl['enablecolumns']) && is_array($tcaCtrl['enablecolumns'])) {
345  $enableColumns = $tcaCtrl['enablecolumns'];
346  // If "hidden" is enabled:
347  if (isset($enableColumns['disabled']) && !empty($row[$enableColumns['disabled']])) {
348  $status['hidden'] = true;
349  }
350  // If a "starttime" is set and higher than current time:
351  if (!empty($enableColumns['starttime']) && ‪$GLOBALS['EXEC_TIME'] < (int)($row[$enableColumns['starttime']] ?? 0)) {
352  $status['starttime'] = true;
353  }
354  // If an "endtime" is set
355  if (!empty($enableColumns['endtime'])) {
356  if ((int)($row[$enableColumns['endtime']] ?? 0) > 0) {
357  if ((int)$row[$enableColumns['endtime']] < ‪$GLOBALS['EXEC_TIME']) {
358  // End-timing applies at this point.
359  $status['endtime'] = true;
360  } else {
361  // End-timing WILL apply in the future for this element.
362  $status['futureendtime'] = true;
363  }
364  }
365  }
366  // If a user-group field is set
367  if (!empty($enableColumns['fe_group']) && !empty($row[$enableColumns['fe_group']])) {
368  $status['fe_group'] = true;
369  }
370  }
371  // If "deleted" flag is set (only when listing records which are also deleted!)
372  if (isset($tcaCtrl['delete']) && !empty($row[$tcaCtrl['delete']])) {
373  $status['deleted'] = true;
374  }
375  // Detecting extendToSubpages (for pages only)
376  if ($table === 'pages' && (int)($row['extendToSubpages'] ?? 0) > 0) {
377  $status['protectedSection'] = true;
378  }
379  if (VersionState::tryFrom($row['t3ver_state'] ?? 0) === VersionState::DELETE_PLACEHOLDER) {
380  $status['deleted'] = true;
381  }
382 
383  // Now only show the status with the highest priority
384  $iconName = '';
385  foreach ($this->overlayPriorities as $priority) {
386  if ($status[$priority]) {
387  $iconName = $this->recordStatusMapping[$priority];
388  break;
389  }
390  }
391 
392  return $this->eventDispatcher->dispatch(
393  new ModifyRecordOverlayIconIdentifierEvent($iconName, $table, $row, $status)
394  )->getOverlayIconIdentifier();
395  }
396 
407  public function ‪getIconForFileExtension($fileExtension, string|IconSize $size = IconSize::MEDIUM, $overlayIdentifier = null)
408  {
409  if (is_string($size)) {
410  $size = IconSize::from($size);
411  $size->triggerDeprecation();
412  }
413  $iconName = $this->iconRegistry->getIconIdentifierForFileExtension($fileExtension);
414  return $this->‪getIcon($iconName, $size, $overlayIdentifier);
415  }
416 
437  public function ‪getIconForResource(
438  ResourceInterface $resource,
439  string|IconSize $size = IconSize::MEDIUM,
440  $overlayIdentifier = null,
441  array $options = []
442  ) {
443  $iconIdentifier = null;
444 
445  // Folder
446  if ($resource instanceof FolderInterface) {
447  // non browsable storage
448  if ($resource->getStorage()->isBrowsable() === false && !empty($options['mount-root'])) {
449  $iconIdentifier = 'apps-filetree-folder-locked';
450  } else {
451  // storage root
452  if ($resource->getStorage()->getRootLevelFolder()->getIdentifier() === $resource->getIdentifier()) {
453  $iconIdentifier = 'apps-filetree-root';
454  }
455 
456  $role = is_callable([$resource, 'getRole']) ? $resource->getRole() : '';
457 
458  // user/group mount root
459  if (!empty($options['mount-root'])) {
460  $iconIdentifier = 'apps-filetree-mount';
462  $overlayIdentifier = 'overlay-locked';
463  } elseif ($role === ‪FolderInterface::ROLE_USER_MOUNT) {
464  $overlayIdentifier = 'overlay-restricted';
465  }
466  }
467 
468  if ($iconIdentifier === null) {
469  // in folder tree view $options['folder-open'] can define an open folder icon
470  if (!empty($options['folder-open'])) {
471  $iconIdentifier = 'apps-filetree-folder-opened';
472  } else {
473  $iconIdentifier = 'apps-filetree-folder-default';
474  }
475 
476  if ($role === ‪FolderInterface::ROLE_TEMPORARY) {
477  $iconIdentifier = 'apps-filetree-folder-temp';
478  } elseif ($role === ‪FolderInterface::ROLE_RECYCLER) {
479  $iconIdentifier = 'apps-filetree-folder-recycler';
480  }
481  }
482 
483  // if locked add overlay
484  if ($resource instanceof InaccessibleFolder ||
485  !$resource->getStorage()->isBrowsable() ||
486  !$resource->getStorage()->checkFolderActionPermission('add', $resource)
487  ) {
488  $overlayIdentifier = 'overlay-locked';
489  }
490  }
491  } elseif ($resource instanceof File) {
492  $mimeTypeIcon = $this->iconRegistry->getIconIdentifierForMimeType($resource->getMimeType());
493 
494  // Check if we find an exact matching mime type
495  if ($mimeTypeIcon !== null) {
496  $iconIdentifier = $mimeTypeIcon;
497  } else {
498  $fileExtensionIcon = $this->iconRegistry->getIconIdentifierForFileExtension($resource->getExtension());
499  if ($fileExtensionIcon !== 'mimetypes-other-other') {
500  // Fallback 1: icon by file extension
501  $iconIdentifier = $fileExtensionIcon;
502  } else {
503  // Fallback 2: icon by mime type with subtype replaced by *
504  $mimeTypeParts = explode('/', $resource->getMimeType());
505  $mimeTypeIcon = $this->iconRegistry->getIconIdentifierForMimeType($mimeTypeParts[0] . '/*');
506  if ($mimeTypeIcon !== null) {
507  $iconIdentifier = $mimeTypeIcon;
508  } else {
509  // Fallback 3: use 'mimetypes-other-other'
510  $iconIdentifier = $fileExtensionIcon;
511  }
512  }
513  }
514  if ($resource->isMissing()) {
515  $overlayIdentifier = 'overlay-missing';
516  }
517  }
518 
519  if (is_string($size)) {
520  $size = IconSize::from($size);
521  $size->triggerDeprecation();
522  }
523 
524  $event = $this->eventDispatcher->dispatch(
525  new ModifyIconForResourcePropertiesEvent(
526  $resource,
527  $size,
528  $options,
529  $iconIdentifier,
530  $overlayIdentifier
531  )
532  );
533  return $this->‪getIcon($event->getIconIdentifier(), $size, $event->getOverlayIdentifier());
534  }
535 
545  protected function ‪createIcon(‪$identifier, IconSize $size, $overlayIdentifier = null, array $iconConfiguration = [])
546  {
547  $icon = GeneralUtility::makeInstance(Icon::class);
548  $icon->setIdentifier(‪$identifier);
549  $icon->setSize($size);
550  $iconState = IconState::tryFrom($iconConfiguration['state']) ?? IconState::STATE_DEFAULT;
551  $icon->setState($iconState);
552  if (!empty($overlayIdentifier)) {
553  $icon->setOverlayIcon($this->‪getIcon($overlayIdentifier, IconSize::OVERLAY));
554  }
555  if (!empty($iconConfiguration['options']['spinning'])) {
556  $icon->setSpinning(true);
557  }
558 
559  return $icon;
560  }
561 
565  public function ‪clearIconCache()
566  {
567  static::$iconCache = [];
568  }
569 }
‪TYPO3\CMS\Core\Resource\ResourceInterface\getIdentifier
‪getIdentifier()
‪TYPO3\CMS\Core\Imaging\IconFactory\$iconRegistry
‪IconRegistry $iconRegistry
Definition: IconFactory.php:37
‪TYPO3\CMS\Core\Imaging
Definition: Dimension.php:16
‪TYPO3\CMS\Core\Imaging\IconFactory\getRecordTypeForPageType
‪getRecordTypeForPageType(string $typeName, string $suffix, string $table, string $fallbackTypeName='1')
Definition: IconFactory.php:304
‪TYPO3\CMS\Core\Imaging\IconFactory\getIcon
‪Icon getIcon($identifier, string|IconSize $size=IconSize::MEDIUM, $overlayIdentifier=null, \TYPO3\CMS\Core\Type\Icon\IconState|IconState $state=null)
Definition: IconFactory.php:82
‪TYPO3\CMS\Core\Imaging\IconFactory\clearIconCache
‪clearIconCache()
Definition: IconFactory.php:560
‪TYPO3\CMS\Core\Imaging\IconFactory\getIconForResource
‪Icon getIconForResource(ResourceInterface $resource, string|IconSize $size=IconSize::MEDIUM, $overlayIdentifier=null, array $options=[])
Definition: IconFactory.php:432
‪TYPO3\CMS\Core\Imaging\Icon
Definition: Icon.php:27
‪TYPO3\CMS\Core\Versioning\VersionState
‪VersionState
Definition: VersionState.php:22
‪TYPO3
‪TYPO3\CMS\Core\Resource\ResourceInterface\getStorage
‪getStorage()
‪TYPO3\CMS\Core\Imaging\IconFactory\createIcon
‪Icon createIcon($identifier, IconSize $size, $overlayIdentifier=null, array $iconConfiguration=[])
Definition: IconFactory.php:540
‪TYPO3\CMS\Core\Imaging\Event\ModifyRecordOverlayIconIdentifierEvent
Definition: ModifyRecordOverlayIconIdentifierEvent.php:24
‪TYPO3\CMS\Core\Imaging\IconFactory\$recordStatusMapping
‪string[] $recordStatusMapping
Definition: IconFactory.php:44
‪TYPO3\CMS\Core\Imaging\IconFactory\getIconForFileExtension
‪Icon getIconForFileExtension($fileExtension, string|IconSize $size=IconSize::MEDIUM, $overlayIdentifier=null)
Definition: IconFactory.php:402
‪TYPO3\CMS\Core\Imaging\IconFactory
Definition: IconFactory.php:34
‪TYPO3\CMS\Core\Resource\FolderInterface\ROLE_TEMPORARY
‪const ROLE_TEMPORARY
Definition: FolderInterface.php:31
‪TYPO3\CMS\Core\Resource\InaccessibleFolder
Definition: InaccessibleFolder.php:30
‪TYPO3\CMS\Core\Imaging\IconFactory\getIconForRecord
‪Icon getIconForRecord($table, array $row, $size=IconSize::MEDIUM)
Definition: IconFactory.php:136
‪TYPO3\CMS\Core\Resource\File
Definition: File.php:26
‪TYPO3\CMS\Core\Imaging\IconRegistry
Definition: IconRegistry.php:32
‪TYPO3\CMS\Core\Imaging\IconFactory\$iconCache
‪static array $iconCache
Definition: IconFactory.php:57
‪TYPO3\CMS\Core\Imaging\IconFactory\$overlayPriorities
‪string[] $overlayPriorities
Definition: IconFactory.php:51
‪TYPO3\CMS\Core\Imaging\Event\ModifyIconForResourcePropertiesEvent
Definition: ModifyIconForResourcePropertiesEvent.php:28
‪TYPO3\CMS\Core\Resource\FolderInterface\ROLE_USER_MOUNT
‪const ROLE_USER_MOUNT
Definition: FolderInterface.php:35
‪TYPO3\CMS\Core\Resource\FolderInterface\ROLE_READONLY_MOUNT
‪const ROLE_READONLY_MOUNT
Definition: FolderInterface.php:34
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Resource\FolderInterface\ROLE_RECYCLER
‪const ROLE_RECYCLER
Definition: FolderInterface.php:29
‪TYPO3\CMS\Core\Resource\FolderInterface
Definition: FolderInterface.php:24
‪TYPO3\CMS\Core\Imaging\IconState
‪IconState
Definition: IconState.php:24
‪TYPO3\CMS\Core\Resource\ResourceInterface
Definition: ResourceInterface.php:21
‪TYPO3\CMS\Core\Imaging\IconFactory\getIconForPlugin
‪getIconForPlugin(string $pluginName)
Definition: IconFactory.php:286
‪TYPO3\CMS\Core\Imaging\IconFactory\mapRecordTypeToIconIdentifier
‪string mapRecordTypeToIconIdentifier($table, array $row)
Definition: IconFactory.php:163
‪TYPO3\CMS\Core\Imaging\IconFactory\$eventDispatcher
‪EventDispatcherInterface $eventDispatcher
Definition: IconFactory.php:61
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Core\Imaging\IconFactory\mapRecordTypeToOverlayIdentifier
‪string mapRecordTypeToOverlayIdentifier($table, array $row)
Definition: IconFactory.php:324
‪TYPO3\CMS\Core\Imaging\IconFactory\__construct
‪__construct(EventDispatcherInterface $eventDispatcher, IconRegistry $iconRegistry, ContainerInterface $container)
Definition: IconFactory.php:65
‪TYPO3\CMS\Webhooks\Message\$identifier
‪identifier readonly string $identifier
Definition: FileAddedMessage.php:37
‪TYPO3\CMS\Core\Imaging\IconFactory\$container
‪ContainerInterface $container
Definition: IconFactory.php:63