‪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  $contentFromPid = is_array($row['content_from_pid'] ?? 0) ? ($row['content_from_pid'][0]['uid'] ?? 0) : $row['content_from_pid'] ?? 0;
216  if ($contentFromPid > 0) {
217  if ($row['is_siteroot'] ?? false) {
218  $recordType[4] = $this->‪getRecordTypeForPageType(
219  $recordType[1],
220  'contentFromPid-root',
221  $table
222  );
223  } else {
224  $suffix = (int)$row['nav_hide'] === 0 ? 'contentFromPid' : 'contentFromPid-hideinmenu';
225  $recordType[4] = $this->‪getRecordTypeForPageType($recordType[1], $suffix, $table, 'page');
226  }
227  }
228  }
229  if (isset(‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes'])
230  && is_array(‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes'])
231  ) {
232  foreach ($recordType as $key => $type) {
233  if (isset(‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes'][$type])) {
234  $recordType[$key] = ‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes'][$type];
235  } else {
236  unset($recordType[$key]);
237  }
238  }
239  $recordType[0] = ‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes']['default'] ?? '';
240  if (isset(‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes']['mask'])
241  && isset($row[$column]) && is_string($row[$column])
242  ) {
243  $recordType[5] = str_replace(
244  '###TYPE###',
245  $row[$column] ?? '',
246  ‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes']['mask']
247  );
248  }
249  if (isset(‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes']['userFunc'])) {
250  $parameters = ['row' => $row];
251  $recordType[6] = GeneralUtility::callUserFunction(
252  ‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes']['userFunc'],
253  $parameters,
254  $ref
255  );
256  }
257  } else {
258  foreach ($recordType as &$type) {
259  $type = 'tcarecords-' . $table . '-' . $type;
260  }
261  unset($type);
262  $recordType[0] = 'tcarecords-' . $table . '-default';
263  }
264  } elseif (isset(‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes'])
265  && is_array(‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes'])
266  ) {
267  $recordType[0] = ‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes']['default'];
268  } else {
269  $recordType[0] = 'tcarecords-' . $table . '-default';
270  }
271 
272  if (($row['CType'] ?? '') === 'list' && ($row['list_type'] ?? '') !== '') {
273  $pluginIcon = $this->‪getIconForPlugin($row['list_type']);
274  if ($pluginIcon) {
275  $recordType[7] = $pluginIcon;
276  }
277  }
278 
279  krsort($recordType);
280  foreach ($recordType as $iconName) {
281  if ($this->iconRegistry->isRegistered($iconName)) {
282  return $iconName;
283  }
284  }
285 
286  return $this->iconRegistry->getDefaultIconIdentifier();
287  }
288 
292  protected function ‪getIconForPlugin(string $pluginName): ?string
293  {
294  $result = null;
295  $items = ‪$GLOBALS['TCA']['tt_content']['columns']['list_type']['config']['items'] ?? [];
296  foreach ($items as $item) {
297  if ($item['value'] === $pluginName) {
298  $result = $item['icon'];
299  break;
300  }
301  }
302 
303  return $result;
304  }
305 
310  protected function ‪getRecordTypeForPageType(string $typeName, string $suffix, string $table, string $fallbackTypeName = '1'): string
311  {
312  $recordType = $typeName . '-' . $suffix;
313 
314  // Check if typeicon class exists. If not fallback to page as typeName
315  if (!isset(‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes'][$recordType])) {
316  $recordType = $fallbackTypeName . '-' . $suffix;
317  }
318  return $recordType;
319  }
320 
330  protected function ‪mapRecordTypeToOverlayIdentifier($table, array $row)
331  {
332  $tcaCtrl = ‪$GLOBALS['TCA'][$table]['ctrl'] ?? [];
333  // Calculate for a given record the actual visibility at the moment
334  $status = [
335  'hidden' => false,
336  'starttime' => false,
337  'endtime' => false,
338  'futureendtime' => false,
339  'fe_group' => false,
340  'deleted' => false,
341  'protectedSection' => false,
342  'nav_hide' => !empty($row['nav_hide']),
343  ];
344  // Icon state based on "enableFields":
345  if (isset($tcaCtrl['enablecolumns']) && is_array($tcaCtrl['enablecolumns'])) {
346  $enableColumns = $tcaCtrl['enablecolumns'];
347  // If "hidden" is enabled:
348  if (isset($enableColumns['disabled']) && !empty($row[$enableColumns['disabled']])) {
349  $status['hidden'] = true;
350  }
351  // If a "starttime" is set and higher than current time:
352  if (!empty($enableColumns['starttime']) && ‪$GLOBALS['EXEC_TIME'] < (int)($row[$enableColumns['starttime']] ?? 0)) {
353  $status['starttime'] = true;
354  }
355  // If an "endtime" is set
356  if (!empty($enableColumns['endtime'])) {
357  if ((int)($row[$enableColumns['endtime']] ?? 0) > 0) {
358  if ((int)$row[$enableColumns['endtime']] < ‪$GLOBALS['EXEC_TIME']) {
359  // End-timing applies at this point.
360  $status['endtime'] = true;
361  } else {
362  // End-timing WILL apply in the future for this element.
363  $status['futureendtime'] = true;
364  }
365  }
366  }
367  // If a user-group field is set
368  if (!empty($enableColumns['fe_group']) && !empty($row[$enableColumns['fe_group']])) {
369  $status['fe_group'] = true;
370  }
371  }
372  // If "deleted" flag is set (only when listing records which are also deleted!)
373  if (isset($tcaCtrl['delete']) && !empty($row[$tcaCtrl['delete']])) {
374  $status['deleted'] = true;
375  }
376  // Detecting extendToSubpages (for pages only)
377  if ($table === 'pages' && (int)($row['extendToSubpages'] ?? 0) > 0) {
378  $status['protectedSection'] = true;
379  }
380  if (VersionState::tryFrom($row['t3ver_state'] ?? 0) === VersionState::DELETE_PLACEHOLDER) {
381  $status['deleted'] = true;
382  }
383 
384  // Now only show the status with the highest priority
385  $iconName = '';
386  foreach ($this->overlayPriorities as $priority) {
387  if ($status[$priority]) {
388  $iconName = $this->recordStatusMapping[$priority];
389  break;
390  }
391  }
392 
393  return $this->eventDispatcher->dispatch(
394  new ModifyRecordOverlayIconIdentifierEvent($iconName, $table, $row, $status)
395  )->getOverlayIconIdentifier();
396  }
397 
408  public function ‪getIconForFileExtension($fileExtension, string|IconSize $size = IconSize::MEDIUM, $overlayIdentifier = null)
409  {
410  if (is_string($size)) {
411  $size = IconSize::from($size);
412  $size->triggerDeprecation();
413  }
414  $iconName = $this->iconRegistry->getIconIdentifierForFileExtension($fileExtension);
415  return $this->‪getIcon($iconName, $size, $overlayIdentifier);
416  }
417 
438  public function ‪getIconForResource(
439  ResourceInterface $resource,
440  string|IconSize $size = IconSize::MEDIUM,
441  $overlayIdentifier = null,
442  array $options = []
443  ) {
444  $iconIdentifier = null;
445 
446  // Folder
447  if ($resource instanceof FolderInterface) {
448  // non browsable storage
449  if ($resource->getStorage()->isBrowsable() === false && !empty($options['mount-root'])) {
450  $iconIdentifier = 'apps-filetree-folder-locked';
451  } else {
452  // storage root
453  if ($resource->getStorage()->getRootLevelFolder()->getIdentifier() === $resource->getIdentifier()) {
454  $iconIdentifier = 'apps-filetree-root';
455  }
456 
457  $role = is_callable([$resource, 'getRole']) ? $resource->getRole() : '';
458 
459  // user/group mount root
460  if (!empty($options['mount-root'])) {
461  $iconIdentifier = 'apps-filetree-mount';
463  $overlayIdentifier = 'overlay-locked';
464  } elseif ($role === ‪FolderInterface::ROLE_USER_MOUNT) {
465  $overlayIdentifier = 'overlay-restricted';
466  }
467  }
468 
469  if ($iconIdentifier === null) {
470  // in folder tree view $options['folder-open'] can define an open folder icon
471  if (!empty($options['folder-open'])) {
472  $iconIdentifier = 'apps-filetree-folder-opened';
473  } else {
474  $iconIdentifier = 'apps-filetree-folder-default';
475  }
476 
477  if ($role === ‪FolderInterface::ROLE_TEMPORARY) {
478  $iconIdentifier = 'apps-filetree-folder-temp';
479  } elseif ($role === ‪FolderInterface::ROLE_RECYCLER) {
480  $iconIdentifier = 'apps-filetree-folder-recycler';
481  }
482  }
483 
484  // if locked add overlay
485  if ($resource instanceof InaccessibleFolder ||
486  !$resource->getStorage()->isBrowsable() ||
487  !$resource->getStorage()->checkFolderActionPermission('add', $resource)
488  ) {
489  $overlayIdentifier = 'overlay-locked';
490  }
491  }
492  } elseif ($resource instanceof File) {
493  $mimeTypeIcon = $this->iconRegistry->getIconIdentifierForMimeType($resource->getMimeType());
494 
495  // Check if we find an exact matching mime type
496  if ($mimeTypeIcon !== null) {
497  $iconIdentifier = $mimeTypeIcon;
498  } else {
499  $fileExtensionIcon = $this->iconRegistry->getIconIdentifierForFileExtension($resource->getExtension());
500  if ($fileExtensionIcon !== 'mimetypes-other-other') {
501  // Fallback 1: icon by file extension
502  $iconIdentifier = $fileExtensionIcon;
503  } else {
504  // Fallback 2: icon by mime type with subtype replaced by *
505  $mimeTypeParts = explode('/', $resource->getMimeType());
506  $mimeTypeIcon = $this->iconRegistry->getIconIdentifierForMimeType($mimeTypeParts[0] . '/*');
507  if ($mimeTypeIcon !== null) {
508  $iconIdentifier = $mimeTypeIcon;
509  } else {
510  // Fallback 3: use 'mimetypes-other-other'
511  $iconIdentifier = $fileExtensionIcon;
512  }
513  }
514  }
515  if ($resource->isMissing()) {
516  $overlayIdentifier = 'overlay-missing';
517  }
518  }
519 
520  if (is_string($size)) {
521  $size = IconSize::from($size);
522  $size->triggerDeprecation();
523  }
524 
525  $event = $this->eventDispatcher->dispatch(
526  new ModifyIconForResourcePropertiesEvent(
527  $resource,
528  $size,
529  $options,
530  $iconIdentifier,
531  $overlayIdentifier
532  )
533  );
534  return $this->‪getIcon($event->getIconIdentifier(), $size, $event->getOverlayIdentifier());
535  }
536 
546  protected function ‪createIcon(‪$identifier, IconSize $size, $overlayIdentifier = null, array $iconConfiguration = [])
547  {
548  $icon = GeneralUtility::makeInstance(Icon::class);
549  $icon->setIdentifier(‪$identifier);
550  $icon->setSize($size);
551  $iconState = IconState::tryFrom($iconConfiguration['state']) ?? IconState::STATE_DEFAULT;
552  $icon->setState($iconState);
553  if (!empty($overlayIdentifier)) {
554  $icon->setOverlayIcon($this->‪getIcon($overlayIdentifier, IconSize::OVERLAY));
555  }
556  if (!empty($iconConfiguration['options']['spinning'])) {
557  $icon->setSpinning(true);
558  }
559 
560  return $icon;
561  }
562 
566  public function ‪clearIconCache()
567  {
568  static::$iconCache = [];
569  }
570 }
‪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:305
‪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:561
‪TYPO3\CMS\Core\Imaging\IconFactory\getIconForResource
‪Icon getIconForResource(ResourceInterface $resource, string|IconSize $size=IconSize::MEDIUM, $overlayIdentifier=null, array $options=[])
Definition: IconFactory.php:433
‪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:541
‪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:403
‪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:287
‪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:325
‪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