2349bc319dfc80fc9cf9a3122e4ca0d5e3075d69
[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 *
42 * @var string[]
43 */
44 protected $recordStatusMapping = array(
45 'hidden' => 'overlay-hidden',
46 'fe_group' => 'overlay-restricted',
47 'starttime' => 'overlay-scheduled',
48 'endtime' => 'overlay-scheduled',
49 'futureendtime' => 'overlay-scheduled',
50 'readonly' => 'overlay-readonly',
51 'deleted' => 'overlay-deleted',
52 'missing' => 'overlay-missing',
53 'translated' => 'overlay-translated',
54 'protectedSection' => 'overlay-includes-subpages'
55 );
56
57 /**
58 * Order of priorities for overlays
59 *
60 * @var string[]
61 */
62 protected $overlayPriorities = array(
63 'hidden',
64 'starttime',
65 'endtime',
66 'futureendtime',
67 'protectedSection',
68 'fe_group'
69 );
70
71 /**
72 * @param IconRegistry $iconRegistry
73 */
74 public function __construct(IconRegistry $iconRegistry = null)
75 {
76 $this->iconRegistry = $iconRegistry ? $iconRegistry : GeneralUtility::makeInstance(IconRegistry::class);
77 }
78
79 /**
80 * @param ServerRequestInterface $request
81 * @param ResponseInterface $response
82 * @return string
83 * @internal
84 */
85 public function processAjaxRequest(ServerRequestInterface $request, ResponseInterface $response)
86 {
87 $parsedBody = $request->getParsedBody();
88 $queryParams = $request->getQueryParams();
89 $requestedIcons = json_decode(
90 isset($parsedBody['requestedIcons'])
91 ? $parsedBody['requestedIcons']
92 : $queryParams['requestedIcons'],
93 true
94 );
95
96 $icons = [];
97 for ($i = 0, $count = count($requestedIcons); $i < $count; ++$i) {
98 list($identifier, $size, $overlayIdentifier, $iconState) = $requestedIcons[$i];
99 if (empty($overlayIdentifier)) {
100 $overlayIdentifier = null;
101 }
102 $iconState = IconState::cast($iconState);
103 $icons[$identifier] = $this->getIcon($identifier, $size, $overlayIdentifier, $iconState)->render();
104 }
105 $response->getBody()->write(
106 json_encode($icons)
107 );
108 return $response;
109 }
110
111 /**
112 * @param string $identifier
113 * @param string $size "large", "small" or "default", see the constants of the Icon class
114 * @param string $overlayIdentifier
115 * @param IconState $state
116 * @return Icon
117 */
118 public function getIcon($identifier, $size = Icon::SIZE_DEFAULT, $overlayIdentifier = null, IconState $state = null)
119 {
120 if ($this->iconRegistry->isDeprecated($identifier)) {
121 $deprecationSettings = $this->iconRegistry->getDeprecationSettings($identifier);
122 GeneralUtility::deprecationLog(sprintf($deprecationSettings['message'], $identifier));
123 if (!empty($deprecationSettings['replacement'])) {
124 $identifier = $deprecationSettings['replacement'];
125 }
126 }
127 if (!$this->iconRegistry->isRegistered($identifier)) {
128 $identifier = $this->iconRegistry->getDefaultIconIdentifier();
129 }
130
131 $iconConfiguration = $this->iconRegistry->getIconConfigurationByIdentifier($identifier);
132 $iconConfiguration['state'] = $state;
133 $icon = $this->createIcon($identifier, $size, $overlayIdentifier, $iconConfiguration);
134
135 /** @var IconProviderInterface $iconProvider */
136 $iconProvider = GeneralUtility::makeInstance($iconConfiguration['provider']);
137 $iconProvider->prepareIconMarkup($icon, $iconConfiguration['options']);
138
139 return $icon;
140 }
141
142 /**
143 * This method is used throughout the TYPO3 Backend to show icons for a DB record
144 *
145 * @param string $table The TCA table name
146 * @param array $row The DB record of the TCA table
147 * @param string $size "large" "small" or "default", see the constants of the Icon class
148 * @return Icon
149 */
150 public function getIconForRecord($table, array $row, $size = Icon::SIZE_DEFAULT)
151 {
152 $iconIdentifier = $this->mapRecordTypeToIconIdentifier($table, $row);
153 $overlayIdentifier = $this->mapRecordTypeToOverlayIdentifier($table, $row);
154 if (empty($overlayIdentifier)) {
155 $overlayIdentifier = null;
156 }
157 return $this->getIcon($iconIdentifier, $size, $overlayIdentifier);
158 }
159
160 /**
161 * This helper functions looks up the column that is used for the type of the chosen TCA table and then fetches the
162 * corresponding iconName based on the chosen icon class in this TCA.
163 * The TCA looks up
164 * - [ctrl][typeicon_column]
165 * -
166 * This method solely takes care of the type of this record, not any statuses used for overlays.
167 *
168 * see EXT:core/Configuration/TCA/pages.php for an example with the TCA table "pages"
169 *
170 * @param string $table The TCA table
171 * @param array $row The selected record
172 * @internal
173 * @TODO: make this method protected, after FormEngine doesn't need it anymore.
174 * @return string The icon identifier string for the icon of that DB record
175 */
176 public function mapRecordTypeToIconIdentifier($table, array $row)
177 {
178 $recordType = array();
179 $ref = null;
180
181 if (isset($GLOBALS['TCA'][$table]['ctrl']['typeicon_column'])) {
182 $column = $GLOBALS['TCA'][$table]['ctrl']['typeicon_column'];
183 if (isset($row[$column])) {
184 // even if not properly documented the value of the typeicon_column in a record could be an array (multiselect)
185 // in typeicon_classes a key could consist of a commaseparated string "foo,bar"
186 // but mostly it should be only one entry in that array
187 if (is_array($row[$column])) {
188 $recordType[1] = implode(',', $row[$column]);
189 } else {
190 $recordType[1] = $row[$column];
191 }
192 } else {
193 $recordType[1] = 'default';
194 }
195 // Workaround to give nav_hide pages a complete different icon
196 // Although it's not a separate doctype
197 // and to give root-pages an own icon
198 if ($table === 'pages') {
199 if ((int)$row['nav_hide'] > 0) {
200 $recordType[2] = $recordType[1] . '-hideinmenu';
201 }
202 if ((int)$row['is_siteroot'] > 0) {
203 $recordType[3] = $recordType[1] . '-root';
204 }
205 if (!empty($row['module'])) {
206 $recordType[4] = 'contains-' . $row['module'];
207 }
208 if ((int)$row['content_from_pid'] > 0) {
209 $recordType[4] = (int)$row['nav_hide'] === 0 ? 'page-contentFromPid' : 'page-contentFromPid-hideinmenu';
210 }
211 }
212 if (is_array($GLOBALS['TCA'][$table]['ctrl']['typeicon_classes'])) {
213 foreach ($recordType as $key => $type) {
214 if (isset($GLOBALS['TCA'][$table]['ctrl']['typeicon_classes'][$type])) {
215 $recordType[$key] = $GLOBALS['TCA'][$table]['ctrl']['typeicon_classes'][$type];
216 } else {
217 unset($recordType[$key]);
218 }
219 }
220 $recordType[0] = $GLOBALS['TCA'][$table]['ctrl']['typeicon_classes']['default'];
221 if (isset($GLOBALS['TCA'][$table]['ctrl']['typeicon_classes']['mask'])) {
222 $recordType[5] = str_replace('###TYPE###', $row[$column], $GLOBALS['TCA'][$table]['ctrl']['typeicon_classes']['mask']);
223 }
224 if (isset($GLOBALS['TCA'][$table]['ctrl']['typeicon_classes']['userFunc'])) {
225 $parameters = array('row' => $row);
226 $recordType[6] = GeneralUtility::callUserFunction($GLOBALS['TCA'][$table]['ctrl']['typeicon_classes']['userFunc'], $parameters, $ref);
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 functions checks if the DB record ($row) has any special status based on the TCA settings like hidden,
253 * 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 = array(
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']) && VersionState::cast($row['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) {
311 $status['deleted'] = true;
312 }
313
314 // Now only show the status with the highest priority
315 $iconName = '';
316 foreach ($this->overlayPriorities as $priority) {
317 if ($status[$priority]) {
318 $iconName = $this->recordStatusMapping[$priority];
319 break;
320 }
321 }
322
323 return $iconName;
324 }
325
326 /**
327 * Get Icon for a file by its extension
328 *
329 * @param string $fileExtension
330 * @param string $size "large" "small" or "default", see the constants of the Icon class
331 * @param string $overlayIdentifier
332 * @return Icon
333 */
334 public function getIconForFileExtension($fileExtension, $size = Icon::SIZE_DEFAULT, $overlayIdentifier = null)
335 {
336 $iconName = $this->iconRegistry->getIconIdentifierForFileExtension($fileExtension);
337 return $this->getIcon($iconName, $size, $overlayIdentifier);
338 }
339
340 /**
341 * This method is used throughout the TYPO3 Backend to show icons for files and folders
342 *
343 * The method takes care of the translation of file extension to proper icon and for folders
344 * it will return the icon depending on the role of the folder.
345 *
346 * If the given resource is a folder there are some additional options that can be used:
347 * - mount-root => TRUE (to indicate this is the root of a mount)
348 * - folder-open => TRUE (to indicate that the folder is opened in the file tree)
349 *
350 * There is a hook in place to manipulate the icon name and overlays.
351 *
352 * @param ResourceInterface $resource
353 * @param string $size "large" "small" or "default", see the constants of the Icon class
354 * @param string $overlayIdentifier
355 * @param array $options An associative array with additional options.
356 * @return Icon
357 */
358 public function getIconForResource(ResourceInterface $resource, $size = Icon::SIZE_DEFAULT, $overlayIdentifier = null, array $options = array())
359 {
360 $iconIdentifier = null;
361
362 // Folder
363 if ($resource instanceof FolderInterface) {
364 // non browsable storage
365 if ($resource->getStorage()->isBrowsable() === false && !empty($options['mount-root'])) {
366 $iconIdentifier = 'apps-filetree-folder-locked';
367 } else {
368 // storage root
369 if ($resource->getStorage()->getRootLevelFolder()->getIdentifier() === $resource->getIdentifier()) {
370 $iconIdentifier = 'apps-filetree-root';
371 }
372
373 $role = is_callable([$resource, 'getRole']) ? $resource->getRole() : '';
374
375 // user/group mount root
376 if (!empty($options['mount-root'])) {
377 $iconIdentifier = 'apps-filetree-mount';
378 if ($role === FolderInterface::ROLE_READONLY_MOUNT) {
379 $overlayIdentifier = 'overlay-locked';
380 } elseif ($role === FolderInterface::ROLE_USER_MOUNT) {
381 $overlayIdentifier = 'overlay-restricted';
382 }
383 }
384
385 if ($iconIdentifier === null) {
386 // in folder tree view $options['folder-open'] can define an open folder icon
387 if (!empty($options['folder-open'])) {
388 $iconIdentifier = 'apps-filetree-folder-opened';
389 } else {
390 $iconIdentifier = 'apps-filetree-folder-default';
391 }
392
393 if ($role === FolderInterface::ROLE_TEMPORARY) {
394 $iconIdentifier = 'apps-filetree-folder-temp';
395 } elseif ($role === FolderInterface::ROLE_RECYCLER) {
396 $iconIdentifier = 'apps-filetree-folder-recycler';
397 }
398 }
399
400 // if locked add overlay
401 if ($resource instanceof InaccessibleFolder ||
402 !$resource->getStorage()->isBrowsable() ||
403 !$resource->getStorage()->checkFolderActionPermission('add', $resource)
404 ) {
405 $overlayIdentifier = 'overlay-locked';
406 }
407 }
408
409 // File
410 } elseif ($resource instanceof File) {
411 $mimeTypeIcon = $this->iconRegistry->getIconIdentifierForMimeType($resource->getMimeType());
412
413 // Check if we find a exact matching mime type
414 if ($mimeTypeIcon !== null) {
415 $iconIdentifier = $mimeTypeIcon;
416 } else {
417 $fileExtensionIcon = $this->iconRegistry->getIconIdentifierForFileExtension($resource->getExtension());
418 if ($fileExtensionIcon !== 'mimetypes-other-other') {
419 // Fallback 1: icon by file extension
420 $iconIdentifier = $fileExtensionIcon;
421 } else {
422 // Fallback 2: icon by mime type with subtype replaced by *
423 $mimeTypeParts = explode('/', $resource->getMimeType());
424 $mimeTypeIcon = $this->iconRegistry->getIconIdentifierForMimeType($mimeTypeParts[0] . '/*');
425 if ($mimeTypeIcon !== null) {
426 $iconIdentifier = $mimeTypeIcon;
427 } else {
428 // Fallback 3: use 'mimetypes-other-other'
429 $iconIdentifier = $fileExtensionIcon;
430 }
431 }
432 }
433 if ($resource->isMissing()) {
434 $overlayIdentifier = 'overlay-missing';
435 }
436 }
437
438 unset($options['mount-root']);
439 unset($options['folder-open']);
440 list($iconIdentifier, $overlayIdentifier) = $this->emitBuildIconForResourceSignal($resource, $size, $options, $iconIdentifier, $overlayIdentifier);
441 return $this->getIcon($iconIdentifier, $size, $overlayIdentifier);
442 }
443
444 /**
445 * Creates an icon object
446 *
447 * @param string $identifier
448 * @param string $size "large", "small" or "default", see the constants of the Icon class
449 * @param string $overlayIdentifier
450 * @param array $iconConfiguration the icon configuration array
451 * @return Icon
452 */
453 protected function createIcon($identifier, $size, $overlayIdentifier = null, array $iconConfiguration)
454 {
455 $icon = GeneralUtility::makeInstance(Icon::class);
456 $icon->setIdentifier($identifier);
457 $icon->setSize($size);
458 $icon->setState($iconConfiguration['state'] ?: new IconState());
459 if ($overlayIdentifier !== null) {
460 $icon->setOverlayIcon($this->getIcon($overlayIdentifier, Icon::SIZE_OVERLAY));
461 }
462 if (!empty($iconConfiguration['options']['spinning'])) {
463 $icon->setSpinning(true);
464 }
465
466 return $icon;
467 }
468
469 /**
470 * Emits a signal right after the identifiers are built.
471 *
472 * @param ResourceInterface $resource
473 * @param string $size
474 * @param array $options
475 * @param string $iconIdentifier
476 * @param string $overlayIdentifier
477 * @return mixed
478 * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotException
479 * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotReturnException
480 */
481 protected function emitBuildIconForResourceSignal(ResourceInterface $resource, $size, array $options, $iconIdentifier, $overlayIdentifier)
482 {
483 $result = $this->getSignalSlotDispatcher()->dispatch(IconFactory::class, 'buildIconForResourceSignal', array($resource, $size, $options, $iconIdentifier, $overlayIdentifier));
484 $iconIdentifier = $result[3];
485 $overlayIdentifier = $result[4];
486 return array($iconIdentifier, $overlayIdentifier);
487 }
488
489 /**
490 * Get the SignalSlot dispatcher
491 *
492 * @return \TYPO3\CMS\Extbase\SignalSlot\Dispatcher
493 */
494 protected function getSignalSlotDispatcher()
495 {
496 return GeneralUtility::makeInstance(Dispatcher::class);
497 }
498 }