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