value . $overlayIdentifier . ($state->value ?? '')); $icon = $this->runtimeCache->get($cacheIdentifier); if ($icon instanceof Icon) { return $icon; } if (!$this->iconRegistry->isDeprecated($identifier) && !$this->iconRegistry->isRegistered($identifier)) { // If icon identifier is neither deprecated nor registered $identifier = $this->iconRegistry->getDefaultIconIdentifier(); } $iconConfiguration = $this->iconRegistry->getIconConfigurationByIdentifier($identifier); $iconConfiguration['state'] = $state; $icon = $this->createIcon($identifier, $size, $overlayIdentifier, $iconConfiguration); /** @var IconProviderInterface $iconProvider */ $iconProvider = $this->container->has($iconConfiguration['provider']) ? $this->container->get($iconConfiguration['provider']) : GeneralUtility::makeInstance($iconConfiguration['provider']); $iconProvider->prepareIconMarkup($icon, $iconConfiguration['options']); $this->runtimeCache->set($cacheIdentifier, $icon); return $icon; } /** * This method is used throughout the TYPO3 Backend to show icons for a DB record */ public function getIconForRecord(string $table, array $row, IconSize $size = IconSize::MEDIUM, ?TcaSchema $schema = null): Icon { if ($schema === null) { $tcaSchemaFactory = GeneralUtility::makeInstance(TcaSchemaFactory::class); $schema = $tcaSchemaFactory->get($table); } $iconIdentifier = $this->mapRecordTypeToIconIdentifier($table, $row, $schema); $overlayIdentifier = $this->mapRecordTypeToOverlayIdentifier($table, $row, $schema); return $this->getIcon($iconIdentifier, $size, $overlayIdentifier); } /** * This helper functions looks up the column that is used for the type of the chosen TCA table and then fetches the * corresponding iconName based on the chosen icon class in this TCA. * The TCA looks up * - [ctrl][typeicon_column] * - * This method solely takes care of the type of this record, not any statuses used for overlays. * * see EXT:core/Configuration/TCA/pages.php for an example with the TCA table "pages" * * @param string $table The TCA table * @param array $row The selected record * @internal * @todo: Protect method when FormEngine doesn't need it anymore. * @return string The icon identifier string for the icon of that DB record */ public function mapRecordTypeToIconIdentifier(string $table, array $row, TcaSchema $schema): string { $recordType = []; $ref = null; if (isset($schema->getRawConfiguration()['typeicon_column'])) { $column = $schema->getRawConfiguration()['typeicon_column']; if (isset($row[$column])) { // even if not properly documented the value of the typeicon_column in a record could be // an array (multiselect) in typeicon_classes a key could consist of a comma-separated string "foo,bar" // but mostly it should be only one entry in that array if (is_array($row[$column])) { $recordType[1] = implode(',', $row[$column]); } else { $recordType[1] = $row[$column]; } } else { $recordType[1] = 'default'; } // Workaround to give nav_hide pages a complete different icon // Although it's not a separate doctype // and to give root-pages an own icon if ($table === 'pages') { if (($row['nav_hide'] ?? 0) > 0) { $recordType[2] = $this->getRecordTypeForPageType( $recordType[1], 'hideinmenu', $schema ); } if (($row['is_siteroot'] ?? 0) > 0) { $recordType[3] = $this->getRecordTypeForPageType( $recordType[1], 'root', $schema ); } if (!empty($row['module'])) { if (is_array($row['module'])) { // field 'module' is configured as type 'select' in the TCA, // so the value may have already been converted to an array $moduleSuffix = reset($row['module']); } else { $moduleSuffix = $row['module']; } $recordType[4] = 'contains-' . $moduleSuffix; } $contentFromPid = is_array($row['content_from_pid'] ?? 0) ? ($row['content_from_pid'][0]['uid'] ?? 0) : $row['content_from_pid'] ?? 0; if ($contentFromPid > 0) { if ($row['is_siteroot'] ?? false) { $recordType[4] = $this->getRecordTypeForPageType( $recordType[1], 'contentFromPid-root', $schema ); } else { $suffix = (int)$row['nav_hide'] === 0 ? 'contentFromPid' : 'contentFromPid-hideinmenu'; $recordType[4] = $this->getRecordTypeForPageType($recordType[1], $suffix, $schema, 'page'); } } } if (isset($schema->getRawConfiguration()['typeicon_classes']) && is_array($schema->getRawConfiguration()['typeicon_classes']) ) { foreach ($recordType as $key => $type) { if (isset($schema->getRawConfiguration()['typeicon_classes'][$type])) { $recordType[$key] = $schema->getRawConfiguration()['typeicon_classes'][$type]; } else { unset($recordType[$key]); } } $recordType[0] = $schema->getRawConfiguration()['typeicon_classes']['default'] ?? ''; if (isset($schema->getRawConfiguration()['typeicon_classes']['mask']) && isset($row[$column]) && is_string($row[$column]) ) { $recordType[5] = str_replace( '###TYPE###', $row[$column] ?? '', $schema->getRawConfiguration()['typeicon_classes']['mask'] ); } if (isset($schema->getRawConfiguration()['typeicon_classes']['userFunc'])) { $parameters = ['row' => $row]; $recordType[6] = GeneralUtility::callUserFunction( $schema->getRawConfiguration()['typeicon_classes']['userFunc'], $parameters, $ref ); } } else { foreach ($recordType as &$type) { $type = 'tcarecords-' . $table . '-' . $type; } unset($type); $recordType[0] = 'tcarecords-' . $table . '-default'; } } elseif (isset($schema->getRawConfiguration()['typeicon_classes']) && is_array($schema->getRawConfiguration()['typeicon_classes']) ) { $recordType[0] = $schema->getRawConfiguration()['typeicon_classes']['default']; } else { $recordType[0] = 'tcarecords-' . $table . '-default'; } $recordType = array_filter($recordType); krsort($recordType); foreach ($recordType as $iconName) { if ($this->iconRegistry->isRegistered($iconName)) { return $iconName; } } return $this->iconRegistry->getDefaultIconIdentifier(); } /** * Returns recordType for icon based on a typeName and a suffix. * Fallback to page as typeName if resulting type is not configured. */ protected function getRecordTypeForPageType(string $typeName, string $suffix, TcaSchema $schema, string $fallbackTypeName = '1'): string { $recordType = $typeName . '-' . $suffix; // Check if typeicon class exists. If not fallback to page as typeName if (!isset($schema->getRawConfiguration()['typeicon_classes'][$recordType])) { $recordType = $fallbackTypeName . '-' . $suffix; } return $recordType; } /** * This helper function checks if the DB record ($row) has any special status based on the TCA settings * like hidden, starttime etc, and then returns a specific icon overlay identifier for the overlay of this DB record * This method solely takes care of the overlay of this record, not any type * * @param string $table The TCA table * @param array $row The selected record * @return string The status with the highest priority */ protected function mapRecordTypeToOverlayIdentifier(string $table, array $row, TcaSchema $schema): string { $tcaCtrl = $schema->getRawConfiguration(); // Calculate for a given record the actual visibility at the moment $status = [ 'hidden' => false, 'starttime' => false, 'endtime' => false, 'futureendtime' => false, 'fe_group' => false, 'deleted' => false, 'protectedSection' => false, 'nav_hide' => !empty($row['nav_hide']), ]; // Icon state based on "enableFields": if (isset($tcaCtrl['enablecolumns']) && is_array($tcaCtrl['enablecolumns'])) { $enableColumns = $tcaCtrl['enablecolumns']; // If "hidden" is enabled: if (isset($enableColumns['disabled']) && !empty($row[$enableColumns['disabled']])) { $status['hidden'] = true; } if (isset($enableColumns['starttime'])) { $starttime = $row[$enableColumns['starttime']] ?? null; if ($starttime !== null) { if ($starttime instanceof \DateTimeInterface) { $starttime = $starttime->getTimestamp(); } // If a "starttime" is set and higher than current time if ($starttime > $GLOBALS['EXEC_TIME']) { $status['starttime'] = true; } } } if (isset($enableColumns['endtime'])) { $endtime = $row[$enableColumns['endtime']] ?? null; if ($endtime !== null && $endtime !== 0) { if ($endtime instanceof \DateTimeInterface) { $endtime = $endtime->getTimestamp(); } if ($endtime < $GLOBALS['EXEC_TIME']) { // End-timing applies at this point. $status['endtime'] = true; } else { // End-timing WILL apply in the future for this element. $status['futureendtime'] = true; } } } // If a user-group field is set if (!empty($enableColumns['fe_group']) && !empty($row[$enableColumns['fe_group']])) { $status['fe_group'] = true; } } // If "deleted" flag is set (only when listing records which are also deleted!) if (isset($tcaCtrl['delete']) && !empty($row[$tcaCtrl['delete']])) { $status['deleted'] = true; } // Detecting extendToSubpages (for pages only) if ($table === 'pages' && (int)($row['extendToSubpages'] ?? 0) > 0) { $status['protectedSection'] = true; } if (VersionState::tryFrom($row['t3ver_state'] ?? 0) === VersionState::DELETE_PLACEHOLDER) { $status['deleted'] = true; } // Now only show the status with the highest priority $iconName = ''; foreach ($GLOBALS['TYPO3_CONF_VARS']['SYS']['IconFactory']['overlayPriorities'] ?? [] as $priority) { if ($status[$priority]) { if (!$GLOBALS['TYPO3_CONF_VARS']['SYS']['IconFactory']['recordStatusMapping'][$priority]) { throw new \LogicException('Priority ' . $priority . ' is not configured', 1719756056); } $iconName = $GLOBALS['TYPO3_CONF_VARS']['SYS']['IconFactory']['recordStatusMapping'][$priority]; break; } } return $this->eventDispatcher->dispatch( new ModifyRecordOverlayIconIdentifierEvent($iconName, $table, $row, $status) )->getOverlayIconIdentifier(); } /** * Get Icon for a file by its extension */ public function getIconForFileExtension(string $fileExtension, IconSize $size = IconSize::MEDIUM, ?string $overlayIdentifier = null): Icon { $iconName = $this->iconRegistry->getIconIdentifierForFileExtension($fileExtension); return $this->getIcon($iconName, $size, $overlayIdentifier); } /** * This method is used throughout the TYPO3 Backend to show icons for files and folders * * The method takes care of the translation of file extension to proper icon and for folders * it will return the icon depending on the role of the folder. * * If the given resource is a folder there are some additional options that can be used: * - mount-root => TRUE (to indicate this is the root of a mount) * - folder-open => TRUE (to indicate that the folder is opened in the file tree) * * There is a hook in place to manipulate the icon name and overlays. * * @param array $options An associative array with additional options. */ public function getIconForResource( ResourceInterface $resource, IconSize $size = IconSize::MEDIUM, ?string $overlayIdentifier = null, array $options = [] ): Icon { $iconIdentifier = null; // Folder if ($resource instanceof FolderInterface) { // non-browsable storage if ($resource->getStorage()->isBrowsable() === false && !empty($options['mount-root'])) { $iconIdentifier = 'apps-filetree-folder-locked'; } else { // storage root if ($resource->getStorage()->getRootLevelFolder()->getIdentifier() === $resource->getIdentifier()) { $iconIdentifier = 'apps-filetree-root'; } // user/group mount root if (!empty($options['mount-root'])) { $iconIdentifier = 'apps-filetree-mount'; if ($resource->getRole() === FolderInterface::ROLE_READONLY_MOUNT) { $overlayIdentifier = 'overlay-locked'; } elseif ($resource->getRole() === FolderInterface::ROLE_USER_MOUNT) { $overlayIdentifier = 'overlay-restricted'; } } if ($iconIdentifier === null) { // in folder tree view $options['folder-open'] can define an open folder icon if (!empty($options['folder-open'])) { $iconIdentifier = 'apps-filetree-folder-opened'; } else { $iconIdentifier = 'apps-filetree-folder-default'; } if ($resource->getRole() === FolderInterface::ROLE_TEMPORARY) { $iconIdentifier = 'apps-filetree-folder-temp'; } elseif ($resource->getRole() === FolderInterface::ROLE_RECYCLER) { $iconIdentifier = 'apps-filetree-folder-recycler'; } } // if locked add overlay if ($resource instanceof InaccessibleFolder || !$resource->getStorage()->isBrowsable() || !$resource->getStorage()->checkFolderActionPermission('add', $resource) ) { $overlayIdentifier = 'overlay-locked'; } } } elseif ($resource instanceof File) { $mimeTypeIcon = $this->iconRegistry->getIconIdentifierForMimeType($resource->getMimeType()); // Check if we find an exact matching mime type if ($mimeTypeIcon !== null) { $iconIdentifier = $mimeTypeIcon; } else { $fileExtensionIcon = $this->iconRegistry->getIconIdentifierForFileExtension($resource->getExtension()); if ($fileExtensionIcon !== 'mimetypes-other-other') { // Fallback 1: icon by file extension $iconIdentifier = $fileExtensionIcon; } else { // Fallback 2: icon by mime type with subtype replaced by * $mimeTypeParts = explode('/', $resource->getMimeType()); $mimeTypeIcon = $this->iconRegistry->getIconIdentifierForMimeType($mimeTypeParts[0] . '/*'); if ($mimeTypeIcon !== null) { $iconIdentifier = $mimeTypeIcon; } else { // Fallback 3: use 'mimetypes-other-other' $iconIdentifier = $fileExtensionIcon; } } } if ($resource->isMissing()) { $overlayIdentifier = 'overlay-missing'; } } $event = $this->eventDispatcher->dispatch( new ModifyIconForResourcePropertiesEvent( $resource, $size, $options, $iconIdentifier, $overlayIdentifier ) ); return $this->getIcon($event->getIconIdentifier(), $size, $event->getOverlayIdentifier()); } /** * Creates an icon object * * @param array $iconConfiguration the icon configuration array */ protected function createIcon(string $identifier, IconSize $size, ?string $overlayIdentifier = null, array $iconConfiguration = []): Icon { $icon = GeneralUtility::makeInstance(Icon::class); $icon->setIdentifier($identifier); $icon->setSize($size); $icon->setState($iconConfiguration['state'] ?? IconState::STATE_DEFAULT); if (!empty($overlayIdentifier)) { $icon->setOverlayIcon($this->getIcon($overlayIdentifier, IconSize::OVERLAY)); } if (!empty($iconConfiguration['options']['spinning'])) { $icon->setSpinning(true); } if (!empty($iconConfiguration['options']['bidi'])) { $icon->setBidi(true); } return $icon; } }