2 namespace TYPO3\CMS\Backend\Tree\View
;
5 * This file is part of the TYPO3 CMS project.
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.
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
14 * The TYPO3 project - inspiring people to share!
17 use TYPO3\CMS\Backend\Utility\BackendUtility
;
18 use TYPO3\CMS\Core\Imaging\Icon
;
19 use TYPO3\CMS\Core\Imaging\IconFactory
;
20 use TYPO3\CMS\Core\Localization\LanguageService
;
21 use TYPO3\CMS\Core\Messaging\FlashMessage
;
22 use TYPO3\CMS\Core\Messaging\FlashMessageService
;
23 use TYPO3\CMS\Core\
Resource\Folder
;
24 use TYPO3\CMS\Core\
Resource\FolderInterface
;
25 use TYPO3\CMS\Core\
Resource\InaccessibleFolder
;
26 use TYPO3\CMS\Core\
Resource\ResourceStorage
;
27 use TYPO3\CMS\Core\Utility\GeneralUtility
;
30 * Generate a folder tree,
31 * specially made for browsing folders in the File module
33 class FolderTreeView
extends AbstractTreeView
36 * The users' file Storages
38 * @var ResourceStorage[]
40 protected $storages = null;
45 protected $storageHashNumbers;
48 * Indicates, whether the AJAX call was successful,
49 * i.e. the requested page has been found
53 protected $ajaxStatus = false;
63 protected $iconFactory;
66 * If file-drag mode is set, temp and recycler folders are filtered out.
69 public $ext_noTempRecyclerDirs = false;
72 * override to not use a title attribute
75 public $titleAttrib = '';
78 * override to use this treeName
79 * does not need to be set in __construct()
82 public $treeName = 'folder';
85 * override to use this domIdPrefix
88 public $domIdPrefix = 'folder';
91 * Constructor function of the class
93 public function __construct()
95 parent
::__construct();
97 $this->storages
= $this->BE_USER
->getFileStorages();
98 $this->iconFactory
= GeneralUtility
::makeInstance(IconFactory
::class);
102 * Generate the plus/minus icon for the browsable tree.
104 * @param Folder $folderObject Entry folder object
105 * @param int $subFolderCounter The current entry number
106 * @param int $totalSubFolders The total number of entries. If equal to $a, a "bottom" element is returned.
107 * @param int $nextCount The number of sub-elements to the current element.
108 * @param bool $isExpanded The element was expanded to render subelements if this flag is set.
110 * @return string Image tag with the plus/minus icon.
112 * @see \TYPO3\CMS\Backend\Tree\View\PageTreeView::PMicon()
114 public function PMicon($folderObject, $subFolderCounter, $totalSubFolders, $nextCount, $isExpanded)
118 $cmd = $this->generateExpandCollapseParameter($this->bank
, !$isExpanded, $folderObject);
119 $icon = $this->PMiconATagWrap($icon, $cmd, !$isExpanded);
125 * Wrap the plus/minus icon in a link
127 * @param string $icon HTML string to wrap, probably an image tag.
128 * @param string $cmd Command for 'PM' get var
129 * @param bool $isExpand Whether to be expanded
130 * @return string Link-wrapped input string
133 public function PMiconATagWrap($icon, $cmd, $isExpand = true)
135 if (empty($this->scope
)) {
137 'class' => static::class,
138 'script' => $this->thisScript
,
139 'ext_noTempRecyclerDirs' => $this->ext_noTempRecyclerDirs
143 if ($this->thisScript
) {
144 // Activates dynamic AJAX based tree
145 $scopeData = serialize($this->scope
);
146 $scopeHash = GeneralUtility
::hmac($scopeData);
147 $js = htmlspecialchars('Tree.load(' . GeneralUtility
::quoteJSvalue($cmd) . ', ' . (int)$isExpand . ', this, ' . GeneralUtility
::quoteJSvalue($scopeData) . ', ' . GeneralUtility
::quoteJSvalue($scopeHash) . ');');
148 return '<a class="list-tree-control' . (!$isExpand ?
' list-tree-control-open' : ' list-tree-control-closed') . '" onclick="' . $js . '"><i class="fa"></i></a>';
155 * @param bool $isOpen
158 protected function renderPMIconAndLink($cmd, $isOpen)
160 $link = $this->thisScript ?
' href="' . htmlspecialchars($this->getThisScript() . 'PM=' . $cmd) . '"' : '';
161 return '<a class="list-tree-control list-tree-control-' . ($isOpen ?
'open' : 'closed') . '"' . $link . '><i class="fa"></i></a>';
165 * Wrapping the folder icon
167 * @param string $icon The image tag for the icon
168 * @param Folder $folderObject The row for the current element
170 * @return string The processed icon input value.
173 public function wrapIcon($icon, $folderObject)
175 // Add title attribute to input icon tag
177 // Wrap icon in click-menu link.
178 if (!$this->ext_IconMode
) {
179 // Check storage access to wrap with click menu
180 if (!$folderObject instanceof InaccessibleFolder
) {
181 $tableName = $this->getTableNameForClickMenu($folderObject);
182 $theFolderIcon = BackendUtility
::wrapClickMenuOnIcon($icon, $tableName, $folderObject->getCombinedIdentifier(), 'tree');
184 } elseif ($this->ext_IconMode
=== 'titlelink') {
185 $aOnClick = 'return jumpTo(' . GeneralUtility
::quoteJSvalue($this->getJumpToParam($folderObject)) . ',this,' . GeneralUtility
::quoteJSvalue($this->domIdPrefix
. $this->getId($folderObject)) . ',' . $this->bank
. ');';
186 $theFolderIcon = '<a href="#" onclick="' . htmlspecialchars($aOnClick) . '">' . $icon . '</a>';
188 return $theFolderIcon;
192 * Wrapping $title in a-tags.
194 * @param string $title Title string
195 * @param Folder $folderObject the folder record
196 * @param int $bank Bank pointer (which mount point number)
201 public function wrapTitle($title, $folderObject, $bank = 0)
203 // Check storage access to wrap with click menu
204 if ($folderObject instanceof InaccessibleFolder
) {
207 $aOnClick = 'return jumpTo(' . GeneralUtility
::quoteJSvalue($this->getJumpToParam($folderObject)) . ', this, ' . GeneralUtility
::quoteJSvalue($this->domIdPrefix
. $this->getId($folderObject)) . ', ' . $bank . ');';
208 $tableName = $this->getTableNameForClickMenu($folderObject);
209 $clickMenuParts = BackendUtility
::wrapClickMenuOnIcon('', $tableName, $folderObject->getCombinedIdentifier(), 'tree', '', '', true);
211 return '<a href="#" title="' . htmlspecialchars(strip_tags($title)) . '" onclick="' . htmlspecialchars($aOnClick) . '" ' . GeneralUtility
::implodeAttributes($clickMenuParts) . '>' . $title . '</a>';
215 * Returns the id from the record - for folders, this is an md5 hash.
217 * @param Folder $folderObject The folder object
219 * @return int The "uid" field value.
221 public function getId($folderObject)
223 return GeneralUtility
::md5int($folderObject->getCombinedIdentifier());
227 * Returns jump-url parameter value.
229 * @param Folder $folderObject The folder object
231 * @return string The jump-url parameter.
233 public function getJumpToParam($folderObject)
235 return rawurlencode($folderObject->getCombinedIdentifier());
239 * Returns the title for the input record. If blank, a "no title" label (localized) will be returned.
240 * '_title' is used for setting an alternative title for folders.
242 * @param array $row The input row array (where the key "_title" is used for the title)
243 * @param int $titleLen Title length (30)
244 * @return string The title
246 public function getTitleStr($row, $titleLen = 30)
248 return $row['_title'] ?? parent
::getTitleStr($row, $titleLen);
252 * Returns the value for the image "title" attribute
254 * @param Folder $folderObject The folder to be used
256 * @return string The attribute value (is htmlspecialchared() already)
258 public function getTitleAttrib($folderObject)
260 return htmlspecialchars($folderObject->getName());
264 * Will create and return the HTML code for a browsable tree of folders.
265 * Is based on the mounts found in the internal array ->MOUNTS (set in the constructor)
267 * @return string HTML code for the browsable tree
269 public function getBrowsableTree()
271 // Get stored tree structure AND updating it if needed according to incoming PM GET var.
272 $this->initializePositionSaving();
276 foreach ($this->storages
as $storageObject) {
277 $this->getBrowseableTreeForStorage($storageObject);
279 $treeItems = array_merge($treeItems, $this->tree
);
281 return $this->printTree($treeItems);
285 * Get a tree for one storage
287 * @param ResourceStorage $storageObject
289 public function getBrowseableTreeForStorage(ResourceStorage
$storageObject)
291 // If there are filemounts, show each, otherwise just the rootlevel folder
292 $fileMounts = $storageObject->getFileMounts();
293 $rootLevelFolders = [];
294 if (!empty($fileMounts)) {
295 foreach ($fileMounts as $fileMountInfo) {
296 $rootLevelFolders[] = [
297 'folder' => $fileMountInfo['folder'],
298 'name' => $fileMountInfo['title']
301 } elseif ($this->BE_USER
->isAdmin()) {
302 $rootLevelFolders[] = [
303 'folder' => $storageObject->getRootLevelFolder(),
304 'name' => $storageObject->getName()
309 // Go through all "root level folders" of this tree (can be the rootlevel folder or any file mount points)
310 foreach ($rootLevelFolders as $rootLevelFolderInfo) {
311 /** @var $rootLevelFolder Folder */
312 $rootLevelFolder = $rootLevelFolderInfo['folder'];
313 $rootLevelFolderName = $rootLevelFolderInfo['name'];
314 $folderHashSpecUID = GeneralUtility
::md5int($rootLevelFolder->getCombinedIdentifier());
315 $this->specUIDmap
[$folderHashSpecUID] = $rootLevelFolder->getCombinedIdentifier();
317 $storageHashNumber = $this->getShortHashNumberForStorage($storageObject, $rootLevelFolder);
319 $this->bank
= $storageHashNumber;
320 $isOpen = $this->stored
[$storageHashNumber][$folderHashSpecUID] ||
$this->expandFirst
;
322 $cmd = $this->generateExpandCollapseParameter($this->bank
, !$isOpen, $rootLevelFolder);
323 // Only show and link icon if storage is browseable
324 if (!$storageObject->isBrowsable() ||
$this->getNumberOfSubfolders($rootLevelFolder) === 0) {
327 $firstHtml = $this->renderPMIconAndLink($cmd, $isOpen);
329 // Mark a storage which is not online, as offline
330 // maybe someday there will be a special icon for this
331 if ($storageObject->isOnline() === false) {
332 $rootLevelFolderName .= ' (' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_file.xlf:sys_file_storage.isOffline') . ')';
334 // Preparing rootRec for the mount
335 $icon = $this->iconFactory
->getIconForResource($rootLevelFolder, Icon
::SIZE_SMALL
, null, ['mount-root' => true]);
336 $firstHtml .= $this->wrapIcon($icon, $rootLevelFolder);
338 'uid' => $folderHashSpecUID,
339 'title' => $rootLevelFolderName,
340 'path' => $rootLevelFolder->getCombinedIdentifier(),
341 'folder' => $rootLevelFolder
343 // Add the storage root to ->tree
345 'HTML' => $firstHtml,
347 'bank' => $this->bank
,
348 // hasSub is TRUE when the root of the storage is expanded
349 'hasSub' => $isOpen && $storageObject->isBrowsable(),
350 'invertedDepth' => 1000,
352 // If the mount is expanded, go down:
353 if ($isOpen && $storageObject->isBrowsable()) {
355 $this->getFolderTree($rootLevelFolder, 999);
361 * Fetches the data for the tree
363 * @param Folder $folderObject the folderobject
364 * @param int $depth Max depth (recursivity limit)
365 * @param string $type HTML-code prefix for recursive calls.
367 * @return int The count of items on the level
368 * @see getBrowsableTree()
370 public function getFolderTree(Folder
$folderObject, $depth = 999, $type = '')
372 $depth = (int)$depth;
374 // This generates the directory tree
375 /* array of \TYPO3\CMS\Core\Resource\Folder */
376 if ($folderObject instanceof InaccessibleFolder
) {
379 $subFolders = $folderObject->getSubfolders();
380 $subFolders = \TYPO3\CMS\Core\
Resource\Utility\ListUtility
::resolveSpecialFolderNames($subFolders);
381 uksort($subFolders, 'strnatcasecmp');
384 $totalSubFolders = count($subFolders);
386 $subFolderCounter = 0;
388 /** @var Folder $subFolder */
389 foreach ($subFolders as $subFolderName => $subFolder) {
393 // Get the key for this space
395 $isLocked = $subFolder instanceof InaccessibleFolder
;
396 $treeKey = key($this->tree
);
397 $specUID = GeneralUtility
::md5int($subFolder->getCombinedIdentifier());
398 $this->specUIDmap
[$specUID] = $subFolder->getCombinedIdentifier();
401 'path' => $subFolder->getCombinedIdentifier(),
402 'title' => $subFolderName,
403 'folder' => $subFolder
405 // Make a recursive call to the next level
406 if (!$isLocked && $depth > 1 && $this->expandNext($specUID)) {
407 $nextCount = $this->getFolderTree($subFolder, $depth - 1, $type);
408 // Set "did expand" flag
411 $nextCount = $isLocked ?
0 : $this->getNumberOfSubfolders($subFolder);
412 // Clear "did expand" flag
415 // Set HTML-icons, if any:
416 if ($this->makeHTML
) {
417 $HTML = $this->PMicon($subFolder, $subFolderCounter, $totalSubFolders, $nextCount, $isOpen);
420 $role = $subFolder->getRole();
421 if ($role !== FolderInterface
::ROLE_DEFAULT
) {
422 $row['_title'] = '<strong>' . $subFolderName . '</strong>';
424 $icon = '<span title="' . htmlspecialchars($subFolderName) . '">'
425 . $this->iconFactory
->getIconForResource($subFolder, Icon
::SIZE_SMALL
, null, ['folder-open' => (bool)$isOpen])
427 $HTML .= $this->wrapIcon($icon, $subFolder);
429 // Finally, add the row/HTML content to the ->tree array in the reserved key.
430 $this->tree
[$treeKey] = [
433 'hasSub' => $nextCount && $this->expandNext($specUID),
434 'isFirst' => $subFolderCounter == 1,
436 'invertedDepth' => $depth,
437 'bank' => $this->bank
440 if ($subFolderCounter > 0) {
441 $this->tree
[$treeKey]['isLast'] = true;
443 return $totalSubFolders;
447 * Compiles the HTML code for displaying the structure found inside the ->tree array
449 * @param array|string $treeItems "tree-array" - if blank string, the internal ->tree array is used.
450 * @return string The HTML code for the tree
452 public function printTree($treeItems = '')
457 $titleLength = (int)$this->BE_USER
->uc
['titleLen'];
458 if (!is_array($treeItems)) {
459 $treeItems = $this->tree
;
462 if (empty($treeItems)) {
463 $message = GeneralUtility
::makeInstance(
465 $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:foldertreeview.noFolders.message'),
466 $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:foldertreeview.noFolders.title'),
469 /** @var $flashMessageService \TYPO3\CMS\Core\Messaging\FlashMessageService */
470 $flashMessageService = GeneralUtility
::makeInstance(FlashMessageService
::class);
471 /** @var $defaultFlashMessageQueue \TYPO3\CMS\Core\Messaging\FlashMessageQueue */
472 $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
473 $defaultFlashMessageQueue->enqueue($message);
474 return $defaultFlashMessageQueue->renderFlashMessages();
477 $expandedFolderHash = '';
478 $invertedDepthOfAjaxRequestedItem = 0;
479 $out = '<ul class="list-tree list-tree-root">';
480 // Evaluate AJAX request
481 if (TYPO3_REQUESTTYPE
& TYPO3_REQUESTTYPE_AJAX
) {
482 list(, $expandCollapseCommand, $expandedFolderHash, ) = $this->evaluateExpandCollapseParameter();
483 if ($expandCollapseCommand == 1) {
489 // We need to count the opened <ul>'s every time we dig into another level,
490 // so we know how many we have to close when all children are done rendering
492 foreach ($treeItems as $treeItem) {
493 /** @var $folderObject Folder */
494 $folderObject = $treeItem['row']['folder'];
495 $classAttr = $treeItem['row']['_CSSCLASS'] ??
'';
496 $folderIdentifier = $folderObject->getCombinedIdentifier();
497 // this is set if the AJAX request has just opened this folder (via the PM command)
498 $isExpandedFolderIdentifier = $expandedFolderHash == GeneralUtility
::md5int($folderIdentifier);
499 $idAttr = htmlspecialchars($this->domIdPrefix
. $this->getId($folderObject) . '_' . $treeItem['bank']);
501 // If this item is the start of a new level,
502 // then a new level <ul> is needed, but not in ajax mode
503 if (!empty($treeItem['isFirst']) && !$doCollapse && !($doExpand && $isExpandedFolderIdentifier)) {
504 $itemHTML = '<ul class="list-tree">';
506 // Add CSS classes to the list item
507 if (!empty($treeItem['hasSub'])) {
508 $classAttr .= ' list-tree-control-open';
511 <li id="' . $idAttr . '" ' . ($classAttr ?
' class="' . trim($classAttr) . '"' : '') . '><span class="list-tree-group">' . $treeItem['HTML'] . $this->wrapTitle($this->getTitleStr($treeItem['row'], $titleLength), $folderObject, $treeItem['bank']) . '</span>';
512 if (empty($treeItem['hasSub'])) {
513 $itemHTML .= '</li>';
515 // We have to remember if this is the last one
516 // on level X so the last child on level X+1 closes the <ul>-tag
517 if (!empty($treeItem['isLast']) && !($doExpand && $isExpandedFolderIdentifier)) {
518 $closeDepth[$treeItem['invertedDepth']] = 1;
520 // If this is the last one and does not have subitems, we need to close
521 // the tree as long as the upper levels have last items too
522 if (!empty($treeItem['isLast']) && empty($treeItem['hasSub']) && !$doCollapse && !($doExpand && $isExpandedFolderIdentifier)) {
523 for ($i = $treeItem['invertedDepth']; !empty($closeDepth[$i]); $i++
) {
525 $itemHTML .= '</ul></li>';
528 // Ajax request: collapse
529 if ($doCollapse && $isExpandedFolderIdentifier) {
530 $this->ajaxStatus
= true;
533 // Ajax request: expand
534 if ($doExpand && $isExpandedFolderIdentifier) {
535 $ajaxOutput .= $itemHTML;
536 $invertedDepthOfAjaxRequestedItem = $treeItem['invertedDepth'];
537 } elseif ($invertedDepthOfAjaxRequestedItem) {
538 if ($treeItem['invertedDepth'] && ($treeItem['invertedDepth'] < $invertedDepthOfAjaxRequestedItem)) {
539 $ajaxOutput .= $itemHTML;
541 $this->ajaxStatus
= true;
547 // If this is an AJAX request, output directly
549 $this->ajaxStatus
= true;
552 // Finally close the first ul
558 * Returns table name for click menu
560 * @param Folder $folderObject
563 protected function getTableNameForClickMenu(Folder
$folderObject)
565 if (strpos($folderObject->getRole(), FolderInterface
::ROLE_MOUNT
) !== false) {
566 $tableName = 'sys_filemounts';
567 } elseif ($folderObject->getIdentifier() === $folderObject->getStorage()->getRootLevelFolder()->getIdentifier()) {
568 $tableName = 'sys_file_storage';
570 $tableName = 'sys_file';
576 * Counts the number of directories in a file path.
578 * @param Folder $folderObject File path.
582 public function getNumberOfSubfolders(Folder
$folderObject)
584 $subFolders = $folderObject->getSubfolders();
585 return count($subFolders);
589 * Get stored tree structure AND updating it if needed according to incoming PM GET var.
593 public function initializePositionSaving()
595 // Get stored tree structure:
596 $this->stored
= unserialize($this->BE_USER
->uc
['browseTrees'][$this->treeName
]);
597 $this->getShortHashNumberForStorage();
599 // (If an plus/minus icon has been clicked,
600 // the PM GET var is sent and we must update the stored positions in the tree):
601 // 0: mount key, 1: set/clear boolean, 2: item ID (cannot contain "_"), 3: treeName
602 list($storageHashNumber, $doExpand, $numericFolderHash, $treeName) = $this->evaluateExpandCollapseParameter();
603 if ($treeName && $treeName == $this->treeName
) {
604 if (in_array($storageHashNumber, $this->storageHashNumbers
)) {
605 if ($doExpand == 1) {
607 $this->stored
[$storageHashNumber][$numericFolderHash] = 1;
610 unset($this->stored
[$storageHashNumber][$numericFolderHash]);
612 $this->savePosition();
618 * Helper method to map md5-hash to shorter number
620 * @param ResourceStorage $storageObject
621 * @param Folder $startingPointFolder
625 protected function getShortHashNumberForStorage(ResourceStorage
$storageObject = null, Folder
$startingPointFolder = null)
627 if (!$this->storageHashNumbers
) {
628 $this->storageHashNumbers
= [];
629 foreach ($this->storages
as $storageUid => $storage) {
630 $fileMounts = $storage->getFileMounts();
631 if (!empty($fileMounts)) {
632 foreach ($fileMounts as $fileMount) {
633 $nkey = hexdec(substr(GeneralUtility
::md5int($fileMount['folder']->getCombinedIdentifier()), 0, 4));
634 $this->storageHashNumbers
[$storageUid . $fileMount['folder']->getCombinedIdentifier()] = $nkey;
637 $folder = $storage->getRootLevelFolder();
638 $nkey = hexdec(substr(GeneralUtility
::md5int($folder->getCombinedIdentifier()), 0, 4));
639 $this->storageHashNumbers
[$storageUid . $folder->getCombinedIdentifier()] = $nkey;
643 if ($storageObject) {
644 if ($startingPointFolder) {
645 return $this->storageHashNumbers
[$storageObject->getUid() . $startingPointFolder->getCombinedIdentifier()];
647 return $this->storageHashNumbers
[$storageObject->getUid()];
653 * Gets the values from the Expand/Collapse Parameter (&PM)
654 * previously known as "PM" (plus/minus)
656 * (If an plus/minus icon has been clicked,
657 * the PM GET var is sent and we must update the stored positions in the tree):
658 * 0: mount key, 1: set/clear boolean, 2: item ID (cannot contain "_"), 3: treeName
660 * @param string $PM The "plus/minus" command
663 protected function evaluateExpandCollapseParameter($PM = null)
666 $PM = GeneralUtility
::_GP('PM');
667 // IE takes anchor as parameter
668 if (($PMpos = strpos($PM, '#')) !== false) {
669 $PM = substr($PM, 0, $PMpos);
672 // Take the first three parameters
673 list($mountKey, $doExpand, $folderIdentifier) = array_pad(explode('_', $PM, 3), 3, null);
674 // In case the folder identifier contains "_", we just need to get the fourth/last parameter
675 list($folderIdentifier, $treeName) = array_pad(GeneralUtility
::revExplode('_', $folderIdentifier, 2), 2, null);
685 * Generates the "PM" string to sent to expand/collapse items
687 * @param string $mountKey The mount key / storage UID
688 * @param bool $doExpand Whether to expand/collapse
689 * @param Folder $folderObject The folder object
690 * @param string $treeName The name of the tree
694 protected function generateExpandCollapseParameter($mountKey = null, $doExpand = false, Folder
$folderObject = null, $treeName = null)
697 $mountKey !== null ?
$mountKey : $this->bank
,
698 $doExpand == 1 ?
1 : 0,
699 $folderObject !== null ? GeneralUtility
::md5int($folderObject->getCombinedIdentifier()) : '',
700 $treeName !== null ?
$treeName : $this->treeName
702 return implode('_', $parts);
706 * Gets the AJAX status.
710 public function getAjaxStatus()
712 return $this->ajaxStatus
;
716 * @return LanguageService
718 protected function getLanguageService()
720 return $GLOBALS['LANG'];