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