TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:09 +02:00
commit af8cc155b5
6818 changed files with 642608 additions and 0 deletions
+465
View File
@@ -0,0 +1,465 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Imaging;
use Psr\Container\ContainerInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Imaging\Event\ModifyIconForResourcePropertiesEvent;
use TYPO3\CMS\Core\Imaging\Event\ModifyRecordOverlayIconIdentifierEvent;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\FolderInterface;
use TYPO3\CMS\Core\Resource\InaccessibleFolder;
use TYPO3\CMS\Core\Resource\ResourceInterface;
use TYPO3\CMS\Core\Schema\TcaSchema;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Versioning\VersionState;
/**
* The main factory class, which acts as the entrypoint for generating an Icon object which
* is responsible for rendering an icon. Checks for the correct icon provider through the IconRegistry.
*/
readonly class IconFactory
{
public function __construct(
private EventDispatcherInterface $eventDispatcher,
private IconRegistry $iconRegistry,
private ContainerInterface $container,
private FrontendInterface $runtimeCache,
) {}
public function getIcon(
string $identifier,
IconSize $size = IconSize::MEDIUM,
?string $overlayIdentifier = null,
?IconState $state = null
): Icon {
$cacheIdentifier = 'icon-factory-' . hash('xxh3', $identifier . $size->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;
}
}