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