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