1<?php 2namespace TYPO3\CMS\Core\Imaging; 3 4/* 5 * This file is part of the TYPO3 CMS project. 6 * 7 * It is free software; you can redistribute it and/or modify it under 8 * the terms of the GNU General Public License, either version 2 9 * of the License, or any later version. 10 * 11 * For the full copyright and license information, please read the 12 * LICENSE.txt file that was distributed with this source code. 13 * 14 * The TYPO3 project - inspiring people to share! 15 */ 16 17use TYPO3\CMS\Core\Resource\File; 18use TYPO3\CMS\Core\Resource\FolderInterface; 19use TYPO3\CMS\Core\Resource\InaccessibleFolder; 20use TYPO3\CMS\Core\Resource\ResourceInterface; 21use TYPO3\CMS\Core\Type\Icon\IconState; 22use TYPO3\CMS\Core\Utility\GeneralUtility; 23use TYPO3\CMS\Core\Versioning\VersionState; 24use TYPO3\CMS\Extbase\SignalSlot\Dispatcher; 25 26/** 27 * The main factory class, which acts as the entrypoint for generating an Icon object which 28 * is responsible for rendering an icon. Checks for the correct icon provider through the IconRegistry. 29 */ 30class IconFactory 31{ 32 /** 33 * @var IconRegistry 34 */ 35 protected $iconRegistry; 36 37 /** 38 * Mapping of record status to overlays. 39 * $GLOBALS['TYPO3_CONF_VARS']['SYS']['IconFactory']['recordStatusMapping'] 40 * 41 * @var string[] 42 */ 43 protected $recordStatusMapping = []; 44 45 /** 46 * Order of priorities for overlays. 47 * $GLOBALS['TYPO3_CONF_VARS']['SYS']['IconFactory']['overlayPriorities'] 48 * 49 * @var string[] 50 */ 51 protected $overlayPriorities = []; 52 53 /** 54 * Runtime icon cache 55 * 56 * @var array 57 */ 58 protected static $iconCache = []; 59 60 /** 61 * @param IconRegistry $iconRegistry 62 */ 63 public function __construct(IconRegistry $iconRegistry = null) 64 { 65 $this->iconRegistry = $iconRegistry ? $iconRegistry : GeneralUtility::makeInstance(IconRegistry::class); 66 $this->recordStatusMapping = $GLOBALS['TYPO3_CONF_VARS']['SYS']['IconFactory']['recordStatusMapping']; 67 $this->overlayPriorities = $GLOBALS['TYPO3_CONF_VARS']['SYS']['IconFactory']['overlayPriorities']; 68 } 69 70 /** 71 * @param string $identifier 72 * @param string $size "large", "small" or "default", see the constants of the Icon class 73 * @param string $overlayIdentifier 74 * @param IconState $state 75 * @return Icon 76 */ 77 public function getIcon($identifier, $size = Icon::SIZE_DEFAULT, $overlayIdentifier = null, IconState $state = null) 78 { 79 $cacheIdentifier = md5($identifier . $size . $overlayIdentifier . (string)$state); 80 if (!empty(static::$iconCache[$cacheIdentifier])) { 81 return static::$iconCache[$cacheIdentifier]; 82 } 83 84 if ( 85 !$this->iconRegistry->isDeprecated($identifier) 86 && !$this->iconRegistry->isRegistered($identifier) 87 ) { 88 // in case icon identifier is neither deprecated nor registered 89 $identifier = $this->iconRegistry->getDefaultIconIdentifier(); 90 } 91 92 $iconConfiguration = $this->iconRegistry->getIconConfigurationByIdentifier($identifier); 93 $iconConfiguration['state'] = $state; 94 $icon = $this->createIcon($identifier, $size, $overlayIdentifier, $iconConfiguration); 95 96 /** @var IconProviderInterface $iconProvider */ 97 $iconProvider = GeneralUtility::makeInstance($iconConfiguration['provider']); 98 $iconProvider->prepareIconMarkup($icon, $iconConfiguration['options']); 99 100 static::$iconCache[$cacheIdentifier] = $icon; 101 102 return $icon; 103 } 104 105 /** 106 * This method is used throughout the TYPO3 Backend to show icons for a DB record 107 * 108 * @param string $table The TCA table name 109 * @param array $row The DB record of the TCA table 110 * @param string $size "large" "small" or "default", see the constants of the Icon class 111 * @return Icon 112 */ 113 public function getIconForRecord($table, array $row, $size = Icon::SIZE_DEFAULT) 114 { 115 $iconIdentifier = $this->mapRecordTypeToIconIdentifier($table, $row); 116 $overlayIdentifier = $this->mapRecordTypeToOverlayIdentifier($table, $row); 117 return $this->getIcon($iconIdentifier, $size, $overlayIdentifier); 118 } 119 120 /** 121 * This helper functions looks up the column that is used for the type of the chosen TCA table and then fetches the 122 * corresponding iconName based on the chosen icon class in this TCA. 123 * The TCA looks up 124 * - [ctrl][typeicon_column] 125 * - 126 * This method solely takes care of the type of this record, not any statuses used for overlays. 127 * 128 * see EXT:core/Configuration/TCA/pages.php for an example with the TCA table "pages" 129 * 130 * @param string $table The TCA table 131 * @param array $row The selected record 132 * @internal 133 * @TODO: make this method protected, after FormEngine doesn't need it anymore. 134 * @return string The icon identifier string for the icon of that DB record 135 */ 136 public function mapRecordTypeToIconIdentifier($table, array $row) 137 { 138 $recordType = []; 139 $ref = null; 140 141 if (isset($GLOBALS['TCA'][$table]['ctrl']['typeicon_column'])) { 142 $column = $GLOBALS['TCA'][$table]['ctrl']['typeicon_column']; 143 if (isset($row[$column])) { 144 // even if not properly documented the value of the typeicon_column in a record could be 145 // an array (multiselect) in typeicon_classes a key could consist of a comma-separated string "foo,bar" 146 // but mostly it should be only one entry in that array 147 if (is_array($row[$column])) { 148 $recordType[1] = implode(',', $row[$column]); 149 } else { 150 $recordType[1] = $row[$column]; 151 } 152 } else { 153 $recordType[1] = 'default'; 154 } 155 // Workaround to give nav_hide pages a complete different icon 156 // Although it's not a separate doctype 157 // and to give root-pages an own icon 158 if ($table === 'pages') { 159 if ((int)$row['nav_hide'] > 0) { 160 $recordType[2] = $recordType[1] . '-hideinmenu'; 161 } 162 if ((int)$row['is_siteroot'] > 0) { 163 $recordType[3] = $recordType[1] . '-root'; 164 } 165 if (!empty($row['module'])) { 166 $recordType[4] = 'contains-' . $row['module']; 167 } 168 if ((int)$row['content_from_pid'] > 0) { 169 if ($row['is_siteroot']) { 170 $recordType[4] = 'page-contentFromPid-root'; 171 } else { 172 $recordType[4] = (int)$row['nav_hide'] === 0 173 ? 'page-contentFromPid' : 'page-contentFromPid-hideinmenu'; 174 } 175 } 176 } 177 if (isset($GLOBALS['TCA'][$table]['ctrl']['typeicon_classes']) 178 && is_array($GLOBALS['TCA'][$table]['ctrl']['typeicon_classes']) 179 ) { 180 foreach ($recordType as $key => $type) { 181 if (isset($GLOBALS['TCA'][$table]['ctrl']['typeicon_classes'][$type])) { 182 $recordType[$key] = $GLOBALS['TCA'][$table]['ctrl']['typeicon_classes'][$type]; 183 } else { 184 unset($recordType[$key]); 185 } 186 } 187 $recordType[0] = $GLOBALS['TCA'][$table]['ctrl']['typeicon_classes']['default']; 188 if (isset($GLOBALS['TCA'][$table]['ctrl']['typeicon_classes']['mask'])) { 189 $recordType[5] = str_replace( 190 '###TYPE###', 191 $row[$column], 192 $GLOBALS['TCA'][$table]['ctrl']['typeicon_classes']['mask'] 193 ); 194 } 195 if (isset($GLOBALS['TCA'][$table]['ctrl']['typeicon_classes']['userFunc'])) { 196 $parameters = ['row' => $row]; 197 $recordType[6] = GeneralUtility::callUserFunction( 198 $GLOBALS['TCA'][$table]['ctrl']['typeicon_classes']['userFunc'], 199 $parameters, 200 $ref 201 ); 202 } 203 } else { 204 foreach ($recordType as &$type) { 205 $type = 'tcarecords-' . $table . '-' . $type; 206 } 207 unset($type); 208 $recordType[0] = 'tcarecords-' . $table . '-default'; 209 } 210 } elseif (isset($GLOBALS['TCA'][$table]['ctrl']['typeicon_classes']) 211 && is_array($GLOBALS['TCA'][$table]['ctrl']['typeicon_classes']) 212 ) { 213 $recordType[0] = $GLOBALS['TCA'][$table]['ctrl']['typeicon_classes']['default']; 214 } else { 215 $recordType[0] = 'tcarecords-' . $table . '-default'; 216 } 217 218 krsort($recordType); 219 foreach ($recordType as $iconName) { 220 if ($this->iconRegistry->isRegistered($iconName)) { 221 return $iconName; 222 } 223 } 224 225 return $this->iconRegistry->getDefaultIconIdentifier(); 226 } 227 228 /** 229 * This helper function checks if the DB record ($row) has any special status based on the TCA settings 230 * like hidden, starttime etc, and then returns a specific icon overlay identifier for the overlay of this DB record 231 * This method solely takes care of the overlay of this record, not any type 232 * 233 * @param string $table The TCA table 234 * @param array $row The selected record 235 * @return string The status with the highest priority 236 */ 237 protected function mapRecordTypeToOverlayIdentifier($table, array $row) 238 { 239 $tcaCtrl = $GLOBALS['TCA'][$table]['ctrl']; 240 // Calculate for a given record the actual visibility at the moment 241 $status = [ 242 'hidden' => false, 243 'starttime' => false, 244 'endtime' => false, 245 'futureendtime' => false, 246 'fe_group' => false, 247 'deleted' => false, 248 'protectedSection' => false, 249 'nav_hide' => !empty($row['nav_hide']), 250 ]; 251 // Icon state based on "enableFields": 252 if (isset($tcaCtrl['enablecolumns']) && is_array($tcaCtrl['enablecolumns'])) { 253 $enableColumns = $tcaCtrl['enablecolumns']; 254 // If "hidden" is enabled: 255 if (isset($enableColumns['disabled']) && !empty($row[$enableColumns['disabled']])) { 256 $status['hidden'] = true; 257 } 258 // If a "starttime" is set and higher than current time: 259 if (!empty($enableColumns['starttime']) && $GLOBALS['EXEC_TIME'] < (int)$row[$enableColumns['starttime']]) { 260 $status['starttime'] = true; 261 } 262 // If an "endtime" is set 263 if (!empty($enableColumns['endtime'])) { 264 if ((int)$row[$enableColumns['endtime']] > 0) { 265 if ((int)$row[$enableColumns['endtime']] < $GLOBALS['EXEC_TIME']) { 266 // End-timing applies at this point. 267 $status['endtime'] = true; 268 } else { 269 // End-timing WILL apply in the future for this element. 270 $status['futureendtime'] = true; 271 } 272 } 273 } 274 // If a user-group field is set 275 if (!empty($enableColumns['fe_group']) && $row[$enableColumns['fe_group']]) { 276 $status['fe_group'] = true; 277 } 278 } 279 // If "deleted" flag is set (only when listing records which are also deleted!) 280 if (isset($tcaCtrl['delete']) && !empty($row[$tcaCtrl['delete']])) { 281 $status['deleted'] = true; 282 } 283 // Detecting extendToSubpages (for pages only) 284 if ($table === 'pages' && (int)$row['extendToSubpages'] > 0) { 285 $status['protectedSection'] = true; 286 } 287 if (isset($row['t3ver_state']) 288 && VersionState::cast($row['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) { 289 $status['deleted'] = true; 290 } 291 292 // Now only show the status with the highest priority 293 $iconName = ''; 294 foreach ($this->overlayPriorities as $priority) { 295 if ($status[$priority]) { 296 $iconName = $this->recordStatusMapping[$priority]; 297 break; 298 } 299 } 300 301 // Hook to define an alternative iconName 302 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['overrideIconOverlay'] ?? [] as $className) { 303 $hookObject = GeneralUtility::makeInstance($className); 304 if (method_exists($hookObject, 'postOverlayPriorityLookup')) { 305 $iconName = $hookObject->postOverlayPriorityLookup($table, $row, $status, $iconName); 306 } 307 } 308 309 return $iconName; 310 } 311 312 /** 313 * Get Icon for a file by its extension 314 * 315 * @param string $fileExtension 316 * @param string $size "large" "small" or "default", see the constants of the Icon class 317 * @param string $overlayIdentifier 318 * @return Icon 319 */ 320 public function getIconForFileExtension($fileExtension, $size = Icon::SIZE_DEFAULT, $overlayIdentifier = null) 321 { 322 $iconName = $this->iconRegistry->getIconIdentifierForFileExtension($fileExtension); 323 return $this->getIcon($iconName, $size, $overlayIdentifier); 324 } 325 326 /** 327 * This method is used throughout the TYPO3 Backend to show icons for files and folders 328 * 329 * The method takes care of the translation of file extension to proper icon and for folders 330 * it will return the icon depending on the role of the folder. 331 * 332 * If the given resource is a folder there are some additional options that can be used: 333 * - mount-root => TRUE (to indicate this is the root of a mount) 334 * - folder-open => TRUE (to indicate that the folder is opened in the file tree) 335 * 336 * There is a hook in place to manipulate the icon name and overlays. 337 * 338 * @param ResourceInterface $resource 339 * @param string $size "large" "small" or "default", see the constants of the Icon class 340 * @param string $overlayIdentifier 341 * @param array $options An associative array with additional options. 342 * @return Icon 343 */ 344 public function getIconForResource( 345 ResourceInterface $resource, 346 $size = Icon::SIZE_DEFAULT, 347 $overlayIdentifier = null, 348 array $options = [] 349 ) { 350 $iconIdentifier = null; 351 352 // Folder 353 if ($resource instanceof FolderInterface) { 354 // non browsable storage 355 if ($resource->getStorage()->isBrowsable() === false && !empty($options['mount-root'])) { 356 $iconIdentifier = 'apps-filetree-folder-locked'; 357 } else { 358 // storage root 359 if ($resource->getStorage()->getRootLevelFolder()->getIdentifier() === $resource->getIdentifier()) { 360 $iconIdentifier = 'apps-filetree-root'; 361 } 362 363 $role = is_callable([$resource, 'getRole']) ? $resource->getRole() : ''; 364 365 // user/group mount root 366 if (!empty($options['mount-root'])) { 367 $iconIdentifier = 'apps-filetree-mount'; 368 if ($role === FolderInterface::ROLE_READONLY_MOUNT) { 369 $overlayIdentifier = 'overlay-locked'; 370 } elseif ($role === FolderInterface::ROLE_USER_MOUNT) { 371 $overlayIdentifier = 'overlay-restricted'; 372 } 373 } 374 375 if ($iconIdentifier === null) { 376 // in folder tree view $options['folder-open'] can define an open folder icon 377 if (!empty($options['folder-open'])) { 378 $iconIdentifier = 'apps-filetree-folder-opened'; 379 } else { 380 $iconIdentifier = 'apps-filetree-folder-default'; 381 } 382 383 if ($role === FolderInterface::ROLE_TEMPORARY) { 384 $iconIdentifier = 'apps-filetree-folder-temp'; 385 } elseif ($role === FolderInterface::ROLE_RECYCLER) { 386 $iconIdentifier = 'apps-filetree-folder-recycler'; 387 } 388 } 389 390 // if locked add overlay 391 if ($resource instanceof InaccessibleFolder || 392 !$resource->getStorage()->isBrowsable() || 393 !$resource->getStorage()->checkFolderActionPermission('add', $resource) 394 ) { 395 $overlayIdentifier = 'overlay-locked'; 396 } 397 } 398 } elseif ($resource instanceof File) { 399 $mimeTypeIcon = $this->iconRegistry->getIconIdentifierForMimeType($resource->getMimeType()); 400 401 // Check if we find a exact matching mime type 402 if ($mimeTypeIcon !== null) { 403 $iconIdentifier = $mimeTypeIcon; 404 } else { 405 $fileExtensionIcon = $this->iconRegistry->getIconIdentifierForFileExtension($resource->getExtension()); 406 if ($fileExtensionIcon !== 'mimetypes-other-other') { 407 // Fallback 1: icon by file extension 408 $iconIdentifier = $fileExtensionIcon; 409 } else { 410 // Fallback 2: icon by mime type with subtype replaced by * 411 $mimeTypeParts = explode('/', $resource->getMimeType()); 412 $mimeTypeIcon = $this->iconRegistry->getIconIdentifierForMimeType($mimeTypeParts[0] . '/*'); 413 if ($mimeTypeIcon !== null) { 414 $iconIdentifier = $mimeTypeIcon; 415 } else { 416 // Fallback 3: use 'mimetypes-other-other' 417 $iconIdentifier = $fileExtensionIcon; 418 } 419 } 420 } 421 if ($resource->isMissing()) { 422 $overlayIdentifier = 'overlay-missing'; 423 } 424 } 425 426 unset($options['mount-root']); 427 unset($options['folder-open']); 428 list($iconIdentifier, $overlayIdentifier) = 429 $this->emitBuildIconForResourceSignal($resource, $size, $options, $iconIdentifier, $overlayIdentifier); 430 return $this->getIcon($iconIdentifier, $size, $overlayIdentifier); 431 } 432 433 /** 434 * Creates an icon object 435 * 436 * @param string $identifier 437 * @param string $size "large", "small" or "default", see the constants of the Icon class 438 * @param string $overlayIdentifier 439 * @param array $iconConfiguration the icon configuration array 440 * @return Icon 441 */ 442 protected function createIcon($identifier, $size, $overlayIdentifier = null, array $iconConfiguration = []) 443 { 444 $icon = GeneralUtility::makeInstance(Icon::class); 445 $icon->setIdentifier($identifier); 446 $icon->setSize($size); 447 $icon->setState($iconConfiguration['state'] ?: new IconState()); 448 if (!empty($overlayIdentifier)) { 449 $icon->setOverlayIcon($this->getIcon($overlayIdentifier, Icon::SIZE_OVERLAY)); 450 } 451 if (!empty($iconConfiguration['options']['spinning'])) { 452 $icon->setSpinning(true); 453 } 454 455 return $icon; 456 } 457 458 /** 459 * Emits a signal right after the identifiers are built. 460 * 461 * @param ResourceInterface $resource 462 * @param string $size 463 * @param array $options 464 * @param string $iconIdentifier 465 * @param string $overlayIdentifier 466 * @return mixed 467 * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotException 468 * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotReturnException 469 */ 470 protected function emitBuildIconForResourceSignal( 471 ResourceInterface $resource, 472 $size, 473 array $options, 474 $iconIdentifier, 475 $overlayIdentifier 476 ) { 477 $result = $this->getSignalSlotDispatcher()->dispatch( 478 self::class, 479 'buildIconForResourceSignal', 480 [$resource, $size, $options, $iconIdentifier, $overlayIdentifier] 481 ); 482 $iconIdentifier = $result[3]; 483 $overlayIdentifier = $result[4]; 484 return [$iconIdentifier, $overlayIdentifier]; 485 } 486 487 /** 488 * Get the SignalSlot dispatcher 489 * 490 * @return \TYPO3\CMS\Extbase\SignalSlot\Dispatcher 491 */ 492 protected function getSignalSlotDispatcher() 493 { 494 return GeneralUtility::makeInstance(Dispatcher::class); 495 } 496 497 /** 498 * clear icon cache 499 */ 500 public function clearIconCache() 501 { 502 static::$iconCache = []; 503 } 504} 505