TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user