[TASK] Extract request processing from IconFactory
[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 TYPO3\CMS\Core\Resource\File;
18 use TYPO3\CMS\Core\Resource\FolderInterface;
19 use TYPO3\CMS\Core\Resource\InaccessibleFolder;
20 use TYPO3\CMS\Core\Resource\ResourceInterface;
21 use TYPO3\CMS\Core\Type\Icon\IconState;
22 use TYPO3\CMS\Core\Utility\GeneralUtility;
23 use TYPO3\CMS\Core\Versioning\VersionState;
24 use 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 */
30 class 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 }