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