‪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;
29 
34 readonly class ‪IconFactory
35 {
36  public function ‪__construct(
37  private EventDispatcherInterface $eventDispatcher,
38  private ‪IconRegistry $iconRegistry,
39  private ContainerInterface $container,
40  private ‪FrontendInterface $runtimeCache,
41  ) {}
42 
46  public function ‪getIcon(
47  string ‪$identifier,
48  string|IconSize $size = IconSize::MEDIUM,
49  ?string $overlayIdentifier = null,
50  \‪TYPO3\CMS\Core\Type\‪Icon\‪IconState|‪IconState|null $state = null
51  ): ‪Icon {
52  if ($state instanceof \‪TYPO3\CMS\Core\Type\‪Icon\‪IconState) {
53  trigger_error(
54  'Using the non-native enumeration TYPO3\CMS\Core\Type\Icon\IconState in IconFactory->getIcon()'
55  . ' will not work in TYPO3 v14.0 anymore. Use native TYPO3\CMS\Core\Imaging\IconState instead.',
56  E_USER_DEPRECATED
57  );
58  $stateValue = (string)$state;
59  } else {
60  $stateValue = $state?->value ?? '';
61  }
62  if (is_string($size)) {
63  $size = IconSize::from($size);
64  $size->triggerDeprecation();
65  }
66  $cacheIdentifier = 'icon-factory-' . hash('xxh3', ‪$identifier . $size->value . $overlayIdentifier . $stateValue);
67  $icon = $this->runtimeCache->get($cacheIdentifier);
68  if ($icon instanceof ‪Icon) {
69  return $icon;
70  }
71 
72  if (!$this->iconRegistry->isDeprecated(‪$identifier) && !$this->iconRegistry->isRegistered(‪$identifier)) {
73  // If icon identifier is neither deprecated nor registered
74  ‪$identifier = $this->iconRegistry->getDefaultIconIdentifier();
75  }
76 
77  $iconConfiguration = $this->iconRegistry->getIconConfigurationByIdentifier(‪$identifier);
78  $iconConfiguration['state'] = $stateValue;
79  $icon = $this->‪createIcon($identifier, $size, $overlayIdentifier, $iconConfiguration);
80 
82  $iconProvider = $this->container->has($iconConfiguration['provider']) ?
83  $this->container->get($iconConfiguration['provider']) :
84  GeneralUtility::makeInstance($iconConfiguration['provider']);
85  $iconProvider->prepareIconMarkup($icon, $iconConfiguration['options']);
86 
87  $this->runtimeCache->set($cacheIdentifier, $icon);
88 
89  return $icon;
90  }
91 
99  public function ‪getIconForRecord(string $table, array $row, string|IconSize|null $size = IconSize::MEDIUM): ‪Icon
100  {
101  if (is_string($size)) {
102  $size = IconSize::from($size);
103  $size->triggerDeprecation();
104  }
105  $iconIdentifier = $this->‪mapRecordTypeToIconIdentifier($table, $row);
106  $overlayIdentifier = $this->‪mapRecordTypeToOverlayIdentifier($table, $row);
107  return $this->‪getIcon($iconIdentifier, $size, $overlayIdentifier);
108  }
109 
126  public function ‪mapRecordTypeToIconIdentifier(string $table, array $row): string
127  {
128  $recordType = [];
129  $ref = null;
130 
131  if (isset(‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_column'])) {
132  $column = ‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_column'];
133  if (isset($row[$column])) {
134  // even if not properly documented the value of the typeicon_column in a record could be
135  // an array (multiselect) in typeicon_classes a key could consist of a comma-separated string "foo,bar"
136  // but mostly it should be only one entry in that array
137  if (is_array($row[$column])) {
138  $recordType[1] = implode(',', $row[$column]);
139  } else {
140  $recordType[1] = $row[$column];
141  }
142  } else {
143  $recordType[1] = 'default';
144  }
145  // Workaround to give nav_hide pages a complete different icon
146  // Although it's not a separate doctype
147  // and to give root-pages an own icon
148  if ($table === 'pages') {
149  if (($row['nav_hide'] ?? 0) > 0) {
150  $recordType[2] = $this->‪getRecordTypeForPageType(
151  $recordType[1],
152  'hideinmenu',
153  $table
154  );
155  }
156  if (($row['is_siteroot'] ?? 0) > 0) {
157  $recordType[3] = $this->‪getRecordTypeForPageType(
158  $recordType[1],
159  'root',
160  $table
161  );
162  }
163  if (!empty($row['module'])) {
164  if (is_array($row['module'])) {
165  // field 'module' is configured as type 'select' in the TCA,
166  // so the value may have already been converted to an array
167  $moduleSuffix = reset($row['module']);
168  } else {
169  $moduleSuffix = $row['module'];
170  }
171  $recordType[4] = 'contains-' . $moduleSuffix;
172  }
173  $contentFromPid = is_array($row['content_from_pid'] ?? 0) ? ($row['content_from_pid'][0]['uid'] ?? 0) : $row['content_from_pid'] ?? 0;
174  if ($contentFromPid > 0) {
175  if ($row['is_siteroot'] ?? false) {
176  $recordType[4] = $this->‪getRecordTypeForPageType(
177  $recordType[1],
178  'contentFromPid-root',
179  $table
180  );
181  } else {
182  $suffix = (int)$row['nav_hide'] === 0 ? 'contentFromPid' : 'contentFromPid-hideinmenu';
183  $recordType[4] = $this->‪getRecordTypeForPageType($recordType[1], $suffix, $table, 'page');
184  }
185  }
186  }
187  if (isset(‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes'])
188  && is_array(‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes'])
189  ) {
190  foreach ($recordType as $key => $type) {
191  if (isset(‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes'][$type])) {
192  $recordType[$key] = ‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes'][$type];
193  } else {
194  unset($recordType[$key]);
195  }
196  }
197  $recordType[0] = ‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes']['default'] ?? '';
198  if (isset(‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes']['mask'])
199  && isset($row[$column]) && is_string($row[$column])
200  ) {
201  $recordType[5] = str_replace(
202  '###TYPE###',
203  $row[$column] ?? '',
204  ‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes']['mask']
205  );
206  }
207  if (isset(‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes']['userFunc'])) {
208  $parameters = ['row' => $row];
209  $recordType[6] = GeneralUtility::callUserFunction(
210  ‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes']['userFunc'],
211  $parameters,
212  $ref
213  );
214  }
215  } else {
216  foreach ($recordType as &$type) {
217  $type = 'tcarecords-' . $table . '-' . $type;
218  }
219  unset($type);
220  $recordType[0] = 'tcarecords-' . $table . '-default';
221  }
222  } elseif (isset(‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes'])
223  && is_array(‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes'])
224  ) {
225  $recordType[0] = ‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes']['default'];
226  } else {
227  $recordType[0] = 'tcarecords-' . $table . '-default';
228  }
229 
230  if (($row['CType'] ?? '') === 'list' && ($row['list_type'] ?? '') !== '') {
231  $pluginIcon = $this->‪getIconForPlugin($row['list_type']);
232  if ($pluginIcon) {
233  $recordType[7] = $pluginIcon;
234  }
235  }
236 
237  krsort($recordType);
238  foreach ($recordType as $iconName) {
239  if ($this->iconRegistry->isRegistered($iconName)) {
240  return $iconName;
241  }
242  }
243 
244  return $this->iconRegistry->getDefaultIconIdentifier();
245  }
246 
250  protected function ‪getIconForPlugin(string $pluginName): ?string
251  {
252  $result = null;
253  $items = ‪$GLOBALS['TCA']['tt_content']['columns']['list_type']['config']['items'] ?? [];
254  foreach ($items as $item) {
255  if ($item['value'] === $pluginName) {
256  $result = $item['icon'];
257  break;
258  }
259  }
260 
261  return $result;
262  }
263 
268  protected function ‪getRecordTypeForPageType(string $typeName, string $suffix, string $table, string $fallbackTypeName = '1'): string
269  {
270  $recordType = $typeName . '-' . $suffix;
271 
272  // Check if typeicon class exists. If not fallback to page as typeName
273  if (!isset(‪$GLOBALS['TCA'][$table]['ctrl']['typeicon_classes'][$recordType])) {
274  $recordType = $fallbackTypeName . '-' . $suffix;
275  }
276  return $recordType;
277  }
278 
288  protected function ‪mapRecordTypeToOverlayIdentifier(string $table, array $row): string
289  {
290  $tcaCtrl = ‪$GLOBALS['TCA'][$table]['ctrl'] ?? [];
291  // Calculate for a given record the actual visibility at the moment
292  $status = [
293  'hidden' => false,
294  'starttime' => false,
295  'endtime' => false,
296  'futureendtime' => false,
297  'fe_group' => false,
298  'deleted' => false,
299  'protectedSection' => false,
300  'nav_hide' => !empty($row['nav_hide']),
301  ];
302  // Icon state based on "enableFields":
303  if (isset($tcaCtrl['enablecolumns']) && is_array($tcaCtrl['enablecolumns'])) {
304  $enableColumns = $tcaCtrl['enablecolumns'];
305  // If "hidden" is enabled:
306  if (isset($enableColumns['disabled']) && !empty($row[$enableColumns['disabled']])) {
307  $status['hidden'] = true;
308  }
309  // If a "starttime" is set and higher than current time:
310  if (!empty($enableColumns['starttime']) && ‪$GLOBALS['EXEC_TIME'] < (int)($row[$enableColumns['starttime']] ?? 0)) {
311  $status['starttime'] = true;
312  }
313  // If an "endtime" is set
314  if (!empty($enableColumns['endtime'])) {
315  if ((int)($row[$enableColumns['endtime']] ?? 0) > 0) {
316  if ((int)$row[$enableColumns['endtime']] < ‪$GLOBALS['EXEC_TIME']) {
317  // End-timing applies at this point.
318  $status['endtime'] = true;
319  } else {
320  // End-timing WILL apply in the future for this element.
321  $status['futureendtime'] = true;
322  }
323  }
324  }
325  // If a user-group field is set
326  if (!empty($enableColumns['fe_group']) && !empty($row[$enableColumns['fe_group']])) {
327  $status['fe_group'] = true;
328  }
329  }
330  // If "deleted" flag is set (only when listing records which are also deleted!)
331  if (isset($tcaCtrl['delete']) && !empty($row[$tcaCtrl['delete']])) {
332  $status['deleted'] = true;
333  }
334  // Detecting extendToSubpages (for pages only)
335  if ($table === 'pages' && (int)($row['extendToSubpages'] ?? 0) > 0) {
336  $status['protectedSection'] = true;
337  }
338  if (‪VersionState::tryFrom($row['t3ver_state'] ?? 0) === VersionState::DELETE_PLACEHOLDER) {
339  $status['deleted'] = true;
340  }
341 
342  // Now only show the status with the highest priority
343  $iconName = '';
344  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['IconFactory']['overlayPriorities'] ?? [] as $priority) {
345  if ($status[$priority]) {
346  if (!‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['IconFactory']['recordStatusMapping'][$priority]) {
347  throw new \LogicException('Priority ' . $priority . ' is not configured', 1719756056);
348  }
349  $iconName = ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['IconFactory']['recordStatusMapping'][$priority];
350  break;
351  }
352  }
353 
354  return $this->eventDispatcher->dispatch(
355  new ‪ModifyRecordOverlayIconIdentifierEvent($iconName, $table, $row, $status)
356  )->getOverlayIconIdentifier();
357  }
358 
364  public function ‪getIconForFileExtension(string $fileExtension, string|IconSize $size = IconSize::MEDIUM, ?string $overlayIdentifier = null): ‪Icon
365  {
366  if (is_string($size)) {
367  $size = IconSize::from($size);
368  $size->triggerDeprecation();
369  }
370  $iconName = $this->iconRegistry->getIconIdentifierForFileExtension($fileExtension);
371  return $this->‪getIcon($iconName, $size, $overlayIdentifier);
372  }
373 
390  public function ‪getIconForResource(
391  ‪ResourceInterface $resource,
392  string|IconSize $size = IconSize::MEDIUM,
393  ?string $overlayIdentifier = null,
394  array $options = []
395  ): ‪Icon {
396  $iconIdentifier = null;
397 
398  // Folder
399  if ($resource instanceof ‪FolderInterface) {
400  // non browsable storage
401  if ($resource->‪getStorage()->isBrowsable() === false && !empty($options['mount-root'])) {
402  $iconIdentifier = 'apps-filetree-folder-locked';
403  } else {
404  // storage root
405  if ($resource->‪getStorage()->getRootLevelFolder()->getIdentifier() === $resource->‪getIdentifier()) {
406  $iconIdentifier = 'apps-filetree-root';
407  }
408 
409  $role = is_callable([$resource, 'getRole']) ? $resource->getRole() : '';
410 
411  // user/group mount root
412  if (!empty($options['mount-root'])) {
413  $iconIdentifier = 'apps-filetree-mount';
415  $overlayIdentifier = 'overlay-locked';
416  } elseif ($role === ‪FolderInterface::ROLE_USER_MOUNT) {
417  $overlayIdentifier = 'overlay-restricted';
418  }
419  }
420 
421  if ($iconIdentifier === null) {
422  // in folder tree view $options['folder-open'] can define an open folder icon
423  if (!empty($options['folder-open'])) {
424  $iconIdentifier = 'apps-filetree-folder-opened';
425  } else {
426  $iconIdentifier = 'apps-filetree-folder-default';
427  }
428 
429  if ($role === ‪FolderInterface::ROLE_TEMPORARY) {
430  $iconIdentifier = 'apps-filetree-folder-temp';
431  } elseif ($role === ‪FolderInterface::ROLE_RECYCLER) {
432  $iconIdentifier = 'apps-filetree-folder-recycler';
433  }
434  }
435 
436  // if locked add overlay
437  if ($resource instanceof ‪InaccessibleFolder ||
438  !$resource->‪getStorage()->isBrowsable() ||
439  !$resource->‪getStorage()->checkFolderActionPermission('add', $resource)
440  ) {
441  $overlayIdentifier = 'overlay-locked';
442  }
443  }
444  } elseif ($resource instanceof ‪File) {
445  $mimeTypeIcon = $this->iconRegistry->getIconIdentifierForMimeType($resource->getMimeType());
446 
447  // Check if we find an exact matching mime type
448  if ($mimeTypeIcon !== null) {
449  $iconIdentifier = $mimeTypeIcon;
450  } else {
451  $fileExtensionIcon = $this->iconRegistry->getIconIdentifierForFileExtension($resource->getExtension());
452  if ($fileExtensionIcon !== 'mimetypes-other-other') {
453  // Fallback 1: icon by file extension
454  $iconIdentifier = $fileExtensionIcon;
455  } else {
456  // Fallback 2: icon by mime type with subtype replaced by *
457  $mimeTypeParts = explode('/', $resource->getMimeType());
458  $mimeTypeIcon = $this->iconRegistry->getIconIdentifierForMimeType($mimeTypeParts[0] . '/*');
459  if ($mimeTypeIcon !== null) {
460  $iconIdentifier = $mimeTypeIcon;
461  } else {
462  // Fallback 3: use 'mimetypes-other-other'
463  $iconIdentifier = $fileExtensionIcon;
464  }
465  }
466  }
467  if ($resource->isMissing()) {
468  $overlayIdentifier = 'overlay-missing';
469  }
470  }
471 
472  if (is_string($size)) {
473  $size = IconSize::from($size);
474  $size->triggerDeprecation();
475  }
476 
477  $event = $this->eventDispatcher->dispatch(
479  $resource,
480  $size,
481  $options,
482  $iconIdentifier,
483  $overlayIdentifier
484  )
485  );
486  return $this->‪getIcon($event->getIconIdentifier(), $size, $event->getOverlayIdentifier());
487  }
488 
494  protected function ‪createIcon(string ‪$identifier, IconSize $size, ?string $overlayIdentifier = null, array $iconConfiguration = []): ‪Icon
495  {
496  $icon = GeneralUtility::makeInstance(Icon::class);
497  $icon->setIdentifier(‪$identifier);
498  $icon->setSize($size);
499  $iconState = ‪IconState::tryFrom($iconConfiguration['state']) ?? IconState::STATE_DEFAULT;
500  $icon->setState($iconState);
501  if (!empty($overlayIdentifier)) {
502  $icon->setOverlayIcon($this->‪getIcon($overlayIdentifier, IconSize::OVERLAY));
503  }
504  if (!empty($iconConfiguration['options']['spinning'])) {
505  $icon->setSpinning(true);
506  }
507  return $icon;
508  }
509 
513  public function ‪clearIconCache(): void {}
514 }
‪TYPO3\CMS\Core\Resource\ResourceInterface\getIdentifier
‪getIdentifier()
‪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:268
‪TYPO3\CMS\Core\Imaging\IconFactory\clearIconCache
‪clearIconCache()
Definition: IconFactory.php:513
‪TYPO3\CMS\Core\Imaging\Icon
Definition: Icon.php:27
‪TYPO3\CMS\Core\Imaging\IconFactory\getIconForFileExtension
‪getIconForFileExtension(string $fileExtension, string|IconSize $size=IconSize::MEDIUM, ?string $overlayIdentifier=null)
Definition: IconFactory.php:364
‪TYPO3\CMS\Core\Versioning\VersionState
‪VersionState
Definition: VersionState.php:22
‪TYPO3
‪TYPO3\CMS\Core\Resource\ResourceInterface\getStorage
‪getStorage()
‪TYPO3\CMS\Core\Imaging\Event\ModifyRecordOverlayIconIdentifierEvent
Definition: ModifyRecordOverlayIconIdentifierEvent.php:24
‪TYPO3\CMS\Core\Imaging\IconFactory
Definition: IconFactory.php:35
‪TYPO3\CMS\Core\Imaging\IconFactory\getIconForRecord
‪getIconForRecord(string $table, array $row, string|IconSize|null $size=IconSize::MEDIUM)
Definition: IconFactory.php:99
‪TYPO3\CMS\Frontend\Content\tryFrom
‪@ tryFrom
Definition: ContentSlideMode.php:27
‪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\createIcon
‪createIcon(string $identifier, IconSize $size, ?string $overlayIdentifier=null, array $iconConfiguration=[])
Definition: IconFactory.php:494
‪TYPO3\CMS\Core\Resource\File
Definition: File.php:26
‪TYPO3\CMS\Core\Imaging\IconRegistry
Definition: IconRegistry.php:32
‪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\Imaging\IconFactory\__construct
‪__construct(private EventDispatcherInterface $eventDispatcher, private IconRegistry $iconRegistry, private ContainerInterface $container, private FrontendInterface $runtimeCache,)
Definition: IconFactory.php:36
‪TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
Definition: FrontendInterface.php:22
‪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\IconFactory\mapRecordTypeToOverlayIdentifier
‪string mapRecordTypeToOverlayIdentifier(string $table, array $row)
Definition: IconFactory.php:288
‪TYPO3\CMS\Core\Imaging\IconState
‪IconState
Definition: IconState.php:24
‪TYPO3\CMS\Core\Imaging\IconFactory\getIcon
‪getIcon(string $identifier, string|IconSize $size=IconSize::MEDIUM, ?string $overlayIdentifier=null, \TYPO3\CMS\Core\Type\Icon\IconState|IconState|null $state=null)
Definition: IconFactory.php:46
‪TYPO3\CMS\Core\Resource\ResourceInterface
Definition: ResourceInterface.php:21
‪TYPO3\CMS\Core\Imaging\IconFactory\getIconForPlugin
‪getIconForPlugin(string $pluginName)
Definition: IconFactory.php:250
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Webhooks\Message\$identifier
‪identifier readonly string $identifier
Definition: FileAddedMessage.php:37
‪TYPO3\CMS\Core\Imaging\IconFactory\getIconForResource
‪getIconForResource(ResourceInterface $resource, string|IconSize $size=IconSize::MEDIUM, ?string $overlayIdentifier=null, array $options=[])
Definition: IconFactory.php:390
‪TYPO3\CMS\Core\Imaging\IconFactory\mapRecordTypeToIconIdentifier
‪string mapRecordTypeToIconIdentifier(string $table, array $row)
Definition: IconFactory.php:126