1<?php
2namespace TYPO3\CMS\Backend\Tree\View;
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
17use TYPO3\CMS\Backend\Utility\BackendUtility;
18use TYPO3\CMS\Core\Imaging\Icon;
19use TYPO3\CMS\Core\Imaging\IconFactory;
20use TYPO3\CMS\Core\Localization\LanguageService;
21use TYPO3\CMS\Core\Messaging\FlashMessage;
22use TYPO3\CMS\Core\Messaging\FlashMessageService;
23use TYPO3\CMS\Core\Resource\Folder;
24use TYPO3\CMS\Core\Resource\FolderInterface;
25use TYPO3\CMS\Core\Resource\InaccessibleFolder;
26use TYPO3\CMS\Core\Resource\ResourceStorage;
27use TYPO3\CMS\Core\Utility\GeneralUtility;
28
29/**
30 * Generate a folder tree,
31 * specially made for browsing folders in the File module
32 *
33 * @internal This class is a TYPO3 Backend implementation and is not considered part of the Public TYPO3 API.
34 */
35class FolderTreeView extends AbstractTreeView
36{
37    /**
38     * The users' file Storages
39     *
40     * @var ResourceStorage[]
41     */
42    protected $storages;
43
44    /**
45     * @var array
46     */
47    protected $storageHashNumbers;
48
49    /**
50     * Indicates, whether the AJAX call was successful,
51     * i.e. the requested page has been found
52     *
53     * @var bool
54     */
55    protected $ajaxStatus = false;
56
57    /**
58     * @var array
59     */
60    protected $scope;
61
62    /**
63     * @var IconFactory
64     */
65    protected $iconFactory;
66
67    /**
68     * override to not use a title attribute
69     * @var string
70     */
71    public $titleAttrib = '';
72
73    /**
74     * override to use this treeName
75     * does not need to be set in __construct()
76     * @var string
77     */
78    public $treeName = 'folder';
79
80    /**
81     * override to use this domIdPrefix
82     * @var string
83     */
84    public $domIdPrefix = 'folder';
85
86    /**
87     * Constructor function of the class
88     */
89    public function __construct()
90    {
91        parent::__construct();
92        $this->init();
93        $this->storages = $this->BE_USER->getFileStorages();
94        $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
95    }
96
97    /**
98     * Generate the plus/minus icon for the browsable tree.
99     *
100     * @param Folder $folderObject Entry folder object
101     * @param int $subFolderCounter The current entry number
102     * @param int $totalSubFolders The total number of entries. If equal to $a, a "bottom" element is returned.
103     * @param int $nextCount The number of sub-elements to the current element.
104     * @param bool $isExpanded The element was expanded to render subelements if this flag is set.
105     *
106     * @return string Image tag with the plus/minus icon.
107     * @internal
108     * @see \TYPO3\CMS\Backend\Tree\View\PageTreeView::PMicon()
109     */
110    public function PMicon($folderObject, $subFolderCounter, $totalSubFolders, $nextCount, $isExpanded)
111    {
112        $icon = '';
113        if ($nextCount) {
114            $cmd = $this->generateExpandCollapseParameter($this->bank, !$isExpanded, $folderObject);
115            $icon = $this->PMiconATagWrap($icon, $cmd, !$isExpanded);
116        }
117        return $icon;
118    }
119
120    /**
121     * Wrap the plus/minus icon in a link
122     *
123     * @param string $icon HTML string to wrap, probably an image tag.
124     * @param string $cmd Command for 'PM' get var
125     * @param bool $isExpand Whether to be expanded
126     * @return string Link-wrapped input string
127     * @internal
128     */
129    public function PMiconATagWrap($icon, $cmd, $isExpand = true)
130    {
131        if (empty($this->scope)) {
132            $this->scope = [
133                'class' => static::class,
134                'script' => $this->thisScript,
135            ];
136        }
137
138        if ($this->thisScript) {
139            // Activates dynamic AJAX based tree
140            $scopeData = json_encode($this->scope);
141            $scopeHash = GeneralUtility::hmac($scopeData);
142            $js = htmlspecialchars('Tree.load(' . GeneralUtility::quoteJSvalue($cmd) . ', ' . (int)$isExpand . ', this, ' . GeneralUtility::quoteJSvalue($scopeData) . ', ' . GeneralUtility::quoteJSvalue($scopeHash) . ');');
143            return '<a class="list-tree-control' . (!$isExpand ? ' list-tree-control-open' : ' list-tree-control-closed') . '" onclick="' . $js . '"><i class="fa"></i></a>';
144        }
145        return $icon;
146    }
147
148    /**
149     * @param string $cmd
150     * @param bool $isOpen
151     * @return string
152     */
153    protected function renderPMIconAndLink($cmd, $isOpen)
154    {
155        $link = $this->thisScript ? ' href="' . htmlspecialchars($this->getThisScript() . 'PM=' . $cmd) . '"' : '';
156        return '<a class="list-tree-control list-tree-control-' . ($isOpen ? 'open' : 'closed') . '"' . $link . '><i class="fa"></i></a>';
157    }
158
159    /**
160     * Wrapping the folder icon
161     *
162     * @param string $icon The image tag for the icon
163     * @param Folder $folderObject The row for the current element
164     *
165     * @return string The processed icon input value.
166     * @internal
167     */
168    public function wrapIcon($icon, $folderObject)
169    {
170        // Add title attribute to input icon tag
171        $theFolderIcon = '';
172        // Wrap icon in click-menu link.
173        if (!$this->ext_IconMode) {
174            // Check storage access to wrap with click menu
175            if (!$folderObject instanceof InaccessibleFolder) {
176                $tableName = $this->getTableNameForClickMenu($folderObject);
177                $theFolderIcon = BackendUtility::wrapClickMenuOnIcon($icon, $tableName, $folderObject->getCombinedIdentifier(), 'tree');
178            }
179        } elseif ($this->ext_IconMode === 'titlelink') {
180            $aOnClick = 'return jumpTo(' . GeneralUtility::quoteJSvalue($this->getJumpToParam($folderObject)) . ',this,' . GeneralUtility::quoteJSvalue($this->domIdPrefix . $this->getId($folderObject)) . ',' . $this->bank . ');';
181            $theFolderIcon = '<a href="#" onclick="' . htmlspecialchars($aOnClick) . '">' . $icon . '</a>';
182        }
183        return $theFolderIcon;
184    }
185
186    /**
187     * Wrapping $title in a-tags.
188     *
189     * @param string $title Title string
190     * @param Folder $folderObject the folder record
191     * @param int $bank Bank pointer (which mount point number)
192     *
193     * @return string
194     * @internal
195     */
196    public function wrapTitle($title, $folderObject, $bank = 0)
197    {
198        // Check storage access to wrap with click menu
199        if ($folderObject instanceof InaccessibleFolder) {
200            return $title;
201        }
202        $aOnClick = 'return jumpTo(' . GeneralUtility::quoteJSvalue($this->getJumpToParam($folderObject)) . ', this, ' . GeneralUtility::quoteJSvalue($this->domIdPrefix . $this->getId($folderObject)) . ', ' . $bank . ');';
203        $tableName = $this->getTableNameForClickMenu($folderObject);
204        $clickMenuParts = BackendUtility::wrapClickMenuOnIcon('', $tableName, $folderObject->getCombinedIdentifier(), 'tree', '', '', true);
205
206        return '<a href="#" title="' . htmlspecialchars(strip_tags($title)) . '" onclick="' . htmlspecialchars($aOnClick) . '" ' . GeneralUtility::implodeAttributes($clickMenuParts) . '>' . $title . '</a>';
207    }
208
209    /**
210     * Returns the id from the record - for folders, this is an md5 hash.
211     *
212     * @param Folder $folderObject The folder object
213     *
214     * @return int The "uid" field value.
215     */
216    public function getId($folderObject)
217    {
218        return GeneralUtility::md5int($folderObject->getCombinedIdentifier());
219    }
220
221    /**
222     * Returns jump-url parameter value.
223     *
224     * @param Folder $folderObject The folder object
225     *
226     * @return string The jump-url parameter.
227     */
228    public function getJumpToParam($folderObject)
229    {
230        return rawurlencode($folderObject->getCombinedIdentifier());
231    }
232
233    /**
234     * Returns the title for the input record. If blank, a "no title" label (localized) will be returned.
235     * '_title' is used for setting an alternative title for folders.
236     *
237     * @param array $row The input row array (where the key "_title" is used for the title)
238     * @param int $titleLen Title length (30)
239     * @return string The title
240     */
241    public function getTitleStr($row, $titleLen = 30)
242    {
243        return $row['_title'] ?? parent::getTitleStr($row, $titleLen);
244    }
245
246    /**
247     * Returns the value for the image "title" attribute
248     *
249     * @param Folder $folderObject The folder to be used
250     *
251     * @return string The attribute value (is htmlspecialchared() already)
252     */
253    public function getTitleAttrib($folderObject)
254    {
255        return htmlspecialchars($folderObject->getName());
256    }
257
258    /**
259     * Will create and return the HTML code for a browsable tree of folders.
260     * Is based on the mounts found in the internal array ->MOUNTS (set in the constructor)
261     *
262     * @return string HTML code for the browsable tree
263     */
264    public function getBrowsableTree()
265    {
266        // Get stored tree structure AND updating it if needed according to incoming PM GET var.
267        $this->initializePositionSaving();
268        // Init done:
269        $treeItems = [];
270        // Traverse mounts:
271        foreach ($this->storages as $storageObject) {
272            $this->getBrowseableTreeForStorage($storageObject);
273            // Add tree:
274            $treeItems = array_merge($treeItems, $this->tree);
275        }
276        return $this->printTree($treeItems);
277    }
278
279    /**
280     * Get a tree for one storage
281     *
282     * @param ResourceStorage $storageObject
283     */
284    public function getBrowseableTreeForStorage(ResourceStorage $storageObject)
285    {
286        // If there are filemounts, show each, otherwise just the rootlevel folder
287        $fileMounts = $storageObject->getFileMounts();
288        $rootLevelFolders = [];
289        if (!empty($fileMounts)) {
290            foreach ($fileMounts as $fileMountInfo) {
291                $rootLevelFolders[] = [
292                    'folder' => $fileMountInfo['folder'],
293                    'name' => $fileMountInfo['title']
294                ];
295            }
296        } elseif ($this->BE_USER->isAdmin()) {
297            $rootLevelFolders[] = [
298                'folder' => $storageObject->getRootLevelFolder(),
299                'name' => $storageObject->getName()
300            ];
301        }
302        // Clean the tree
303        $this->reset();
304        // Go through all "root level folders" of this tree (can be the rootlevel folder or any file mount points)
305        foreach ($rootLevelFolders as $rootLevelFolderInfo) {
306            /** @var Folder $rootLevelFolder */
307            $rootLevelFolder = $rootLevelFolderInfo['folder'];
308            $rootLevelFolderName = $rootLevelFolderInfo['name'];
309            $folderHashSpecUID = GeneralUtility::md5int($rootLevelFolder->getCombinedIdentifier());
310            $this->specUIDmap[$folderHashSpecUID] = $rootLevelFolder->getCombinedIdentifier();
311            // Hash key
312            $storageHashNumber = $this->getShortHashNumberForStorage($storageObject, $rootLevelFolder);
313            // Set first:
314            $this->bank = $storageHashNumber;
315            $isOpen = $this->stored[$storageHashNumber][$folderHashSpecUID] || $this->expandFirst;
316            // Set PM icon:
317            $cmd = $this->generateExpandCollapseParameter($this->bank, !$isOpen, $rootLevelFolder);
318            // Only show and link icon if storage is browseable
319            if (!$storageObject->isBrowsable() || $this->getNumberOfSubfolders($rootLevelFolder) === 0) {
320                $firstHtml = '';
321            } else {
322                $firstHtml = $this->renderPMIconAndLink($cmd, $isOpen);
323            }
324            // Mark a storage which is not online, as offline
325            // maybe someday there will be a special icon for this
326            if ($storageObject->isOnline() === false) {
327                $rootLevelFolderName .= ' (' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_file.xlf:sys_file_storage.isOffline') . ')';
328            }
329            // Preparing rootRec for the mount
330            $icon = $this->iconFactory->getIconForResource($rootLevelFolder, Icon::SIZE_SMALL, null, ['mount-root' => true]);
331            $firstHtml .= $this->wrapIcon($icon, $rootLevelFolder);
332            $row = [
333                'uid' => $folderHashSpecUID,
334                'title' => $rootLevelFolderName,
335                'path' => $rootLevelFolder->getCombinedIdentifier(),
336                'folder' => $rootLevelFolder
337            ];
338            // Add the storage root to ->tree
339            $this->tree[] = [
340                'HTML' => $firstHtml,
341                'row' => $row,
342                'bank' => $this->bank,
343                // hasSub is TRUE when the root of the storage is expanded
344                'hasSub' => $isOpen && $storageObject->isBrowsable(),
345                'invertedDepth' => 1000,
346            ];
347            // If the mount is expanded, go down:
348            if ($isOpen && $storageObject->isBrowsable()) {
349                // Set depth:
350                $this->getFolderTree($rootLevelFolder, 999);
351            }
352        }
353    }
354
355    /**
356     * Fetches the data for the tree
357     *
358     * @param Folder $folderObject the folderobject
359     * @param int $depth Max depth (recursivity limit)
360     * @param string $type HTML-code prefix for recursive calls.
361     *
362     * @return int The count of items on the level
363     * @see getBrowsableTree()
364     */
365    public function getFolderTree(Folder $folderObject, $depth = 999, $type = '')
366    {
367        $depth = (int)$depth;
368
369        // This generates the directory tree
370        /* array of \TYPO3\CMS\Core\Resource\Folder */
371        if ($folderObject instanceof InaccessibleFolder) {
372            $subFolders = [];
373        } else {
374            $subFolders = $folderObject->getSubfolders();
375            $subFolders = \TYPO3\CMS\Core\Resource\Utility\ListUtility::resolveSpecialFolderNames($subFolders);
376            uksort($subFolders, 'strnatcasecmp');
377        }
378
379        $totalSubFolders = count($subFolders);
380        $HTML = '';
381        $subFolderCounter = 0;
382        $treeKey = '';
383        /** @var Folder $subFolder */
384        foreach ($subFolders as $subFolderName => $subFolder) {
385            $subFolderCounter++;
386            // Reserve space.
387            $this->tree[] = [];
388            // Get the key for this space
389            end($this->tree);
390            $isLocked = $subFolder instanceof InaccessibleFolder;
391            $treeKey = key($this->tree);
392            $specUID = GeneralUtility::md5int($subFolder->getCombinedIdentifier());
393            $this->specUIDmap[$specUID] = $subFolder->getCombinedIdentifier();
394            $row = [
395                'uid' => $specUID,
396                'path' => $subFolder->getCombinedIdentifier(),
397                'title' => $subFolderName,
398                'folder' => $subFolder
399            ];
400            // Make a recursive call to the next level
401            if (!$isLocked && $depth > 1 && $this->expandNext($specUID)) {
402                $nextCount = $this->getFolderTree($subFolder, $depth - 1, $type);
403                // Set "did expand" flag
404                $isOpen = 1;
405            } else {
406                $nextCount = $isLocked ? 0 : $this->getNumberOfSubfolders($subFolder);
407                // Clear "did expand" flag
408                $isOpen = 0;
409            }
410            // Set HTML-icons, if any:
411            if ($this->makeHTML) {
412                $HTML = $this->PMicon($subFolder, $subFolderCounter, $totalSubFolders, $nextCount, $isOpen);
413                $type = '';
414
415                $role = $subFolder->getRole();
416                if ($role !== FolderInterface::ROLE_DEFAULT) {
417                    $row['_title'] = '<strong>' . $subFolderName . '</strong>';
418                }
419                $icon = '<span title="' . htmlspecialchars($subFolderName) . '">'
420                    . $this->iconFactory->getIconForResource($subFolder, Icon::SIZE_SMALL, null, ['folder-open' => (bool)$isOpen])
421                    . '</span>';
422                $HTML .= $this->wrapIcon($icon, $subFolder);
423            }
424            // Finally, add the row/HTML content to the ->tree array in the reserved key.
425            $this->tree[$treeKey] = [
426                'row' => $row,
427                'HTML' => $HTML,
428                'hasSub' => $nextCount && $this->expandNext($specUID),
429                'isFirst' => $subFolderCounter == 1,
430                'isLast' => false,
431                'invertedDepth' => $depth,
432                'bank' => $this->bank
433            ];
434        }
435        if ($subFolderCounter > 0) {
436            $this->tree[$treeKey]['isLast'] = true;
437        }
438        return $totalSubFolders;
439    }
440
441    /**
442     * Compiles the HTML code for displaying the structure found inside the ->tree array
443     *
444     * @param array|string $treeItems "tree-array" - if blank string, the internal ->tree array is used.
445     * @return string The HTML code for the tree
446     */
447    public function printTree($treeItems = '')
448    {
449        $doExpand = false;
450        $doCollapse = false;
451        $ajaxOutput = '';
452        $titleLength = (int)$this->BE_USER->uc['titleLen'];
453        if (!is_array($treeItems)) {
454            $treeItems = $this->tree;
455        }
456
457        if (empty($treeItems)) {
458            $message = GeneralUtility::makeInstance(
459                FlashMessage::class,
460                $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:foldertreeview.noFolders.message'),
461                $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:foldertreeview.noFolders.title'),
462                FlashMessage::INFO
463            );
464            /** @var \TYPO3\CMS\Core\Messaging\FlashMessageService $flashMessageService */
465            $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
466            /** @var \TYPO3\CMS\Core\Messaging\FlashMessageQueue $defaultFlashMessageQueue */
467            $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
468            $defaultFlashMessageQueue->enqueue($message);
469            return $defaultFlashMessageQueue->renderFlashMessages();
470        }
471
472        $expandedFolderHash = '';
473        $invertedDepthOfAjaxRequestedItem = 0;
474        $out = '<ul class="list-tree list-tree-root">';
475        // Evaluate AJAX request
476        if (TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_AJAX) {
477            list(, $expandCollapseCommand, $expandedFolderHash, ) = $this->evaluateExpandCollapseParameter();
478            if ($expandCollapseCommand == 1) {
479                $doExpand = true;
480            } else {
481                $doCollapse = true;
482            }
483        }
484        // We need to count the opened <ul>'s every time we dig into another level,
485        // so we know how many we have to close when all children are done rendering
486        $closeDepth = [];
487        foreach ($treeItems as $treeItem) {
488            /** @var Folder $folderObject */
489            $folderObject = $treeItem['row']['folder'];
490            $classAttr = $treeItem['row']['_CSSCLASS'] ?? '';
491            $folderIdentifier = $folderObject->getCombinedIdentifier();
492            // this is set if the AJAX request has just opened this folder (via the PM command)
493            $isExpandedFolderIdentifier = $expandedFolderHash == GeneralUtility::md5int($folderIdentifier);
494            $idAttr = htmlspecialchars($this->domIdPrefix . $this->getId($folderObject) . '_' . $treeItem['bank']);
495            $itemHTML = '';
496            // If this item is the start of a new level,
497            // then a new level <ul> is needed, but not in ajax mode
498            if (!empty($treeItem['isFirst']) && !$doCollapse && !($doExpand && $isExpandedFolderIdentifier)) {
499                $itemHTML = '<ul class="list-tree">';
500            }
501            // Add CSS classes to the list item
502            if (!empty($treeItem['hasSub'])) {
503                $classAttr .= ' list-tree-control-open';
504            }
505            $itemHTML .= '
506				<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>';
507            if (empty($treeItem['hasSub'])) {
508                $itemHTML .= '</li>';
509            }
510            // We have to remember if this is the last one
511            // on level X so the last child on level X+1 closes the <ul>-tag
512            if (!empty($treeItem['isLast']) && !($doExpand && $isExpandedFolderIdentifier)) {
513                $closeDepth[$treeItem['invertedDepth']] = 1;
514            }
515            // If this is the last one and does not have subitems, we need to close
516            // the tree as long as the upper levels have last items too
517            if (!empty($treeItem['isLast']) && empty($treeItem['hasSub']) && !$doCollapse && !($doExpand && $isExpandedFolderIdentifier)) {
518                for ($i = $treeItem['invertedDepth']; !empty($closeDepth[$i]); $i++) {
519                    $closeDepth[$i] = 0;
520                    $itemHTML .= '</ul></li>';
521                }
522            }
523            // Ajax request: collapse
524            if ($doCollapse && $isExpandedFolderIdentifier) {
525                $this->ajaxStatus = true;
526                return $itemHTML;
527            }
528            // Ajax request: expand
529            if ($doExpand && $isExpandedFolderIdentifier) {
530                $ajaxOutput .= $itemHTML;
531                $invertedDepthOfAjaxRequestedItem = $treeItem['invertedDepth'];
532            } elseif ($invertedDepthOfAjaxRequestedItem) {
533                if ($treeItem['invertedDepth'] && ($treeItem['invertedDepth'] < $invertedDepthOfAjaxRequestedItem)) {
534                    $ajaxOutput .= $itemHTML;
535                } else {
536                    $this->ajaxStatus = true;
537                    return $ajaxOutput;
538                }
539            }
540            $out .= $itemHTML;
541        }
542        // If this is an AJAX request, output directly
543        if ($ajaxOutput) {
544            $this->ajaxStatus = true;
545            return $ajaxOutput;
546        }
547        // Finally close the first ul
548        $out .= '</ul>';
549        return $out;
550    }
551
552    /**
553     * Returns table name for click menu
554     *
555     * @param Folder $folderObject
556     * @return string
557     */
558    protected function getTableNameForClickMenu(Folder $folderObject)
559    {
560        if (strpos($folderObject->getRole(), FolderInterface::ROLE_MOUNT) !== false) {
561            $tableName = 'sys_filemounts';
562        } elseif ($folderObject->getIdentifier() === $folderObject->getStorage()->getRootLevelFolder()->getIdentifier()) {
563            $tableName = 'sys_file_storage';
564        } else {
565            $tableName = 'sys_file';
566        }
567        return $tableName;
568    }
569
570    /**
571     * Counts the number of directories in a file path.
572     *
573     * @param Folder $folderObject File path.
574     *
575     * @return int
576     */
577    public function getNumberOfSubfolders(Folder $folderObject)
578    {
579        $subFolders = $folderObject->getSubfolders();
580        return count($subFolders);
581    }
582
583    /**
584     * Get stored tree structure AND updating it if needed according to incoming PM GET var.
585     *
586     * @internal
587     */
588    public function initializePositionSaving()
589    {
590        // Get stored tree structure:
591        $this->stored = unserialize($this->BE_USER->uc['browseTrees'][$this->treeName], ['allowed_classes' => false]);
592        $this->getShortHashNumberForStorage();
593        // PM action:
594        // (If an plus/minus icon has been clicked,
595        // the PM GET var is sent and we must update the stored positions in the tree):
596        // 0: mount key, 1: set/clear boolean, 2: item ID (cannot contain "_"), 3: treeName
597        list($storageHashNumber, $doExpand, $numericFolderHash, $treeName) = $this->evaluateExpandCollapseParameter();
598        if ($treeName && $treeName == $this->treeName) {
599            if (in_array($storageHashNumber, $this->storageHashNumbers)) {
600                if ($doExpand == 1) {
601                    // Set
602                    $this->stored[$storageHashNumber][$numericFolderHash] = 1;
603                } else {
604                    // Clear
605                    unset($this->stored[$storageHashNumber][$numericFolderHash]);
606                }
607                $this->savePosition();
608            }
609        }
610    }
611
612    /**
613     * Helper method to map md5-hash to shorter number
614     *
615     * @param ResourceStorage $storageObject
616     * @param Folder $startingPointFolder
617     *
618     * @return int
619     */
620    protected function getShortHashNumberForStorage(ResourceStorage $storageObject = null, Folder $startingPointFolder = null)
621    {
622        if (!$this->storageHashNumbers) {
623            $this->storageHashNumbers = [];
624            foreach ($this->storages as $storageUid => $storage) {
625                $fileMounts = $storage->getFileMounts();
626                if (!empty($fileMounts)) {
627                    foreach ($fileMounts as $fileMount) {
628                        $nkey = hexdec(substr(GeneralUtility::md5int($fileMount['folder']->getCombinedIdentifier()), 0, 4));
629                        $this->storageHashNumbers[$storageUid . $fileMount['folder']->getCombinedIdentifier()] = $nkey;
630                    }
631                } else {
632                    $folder = $storage->getRootLevelFolder();
633                    $nkey = hexdec(substr(GeneralUtility::md5int($folder->getCombinedIdentifier()), 0, 4));
634                    $this->storageHashNumbers[$storageUid . $folder->getCombinedIdentifier()] = $nkey;
635                }
636            }
637        }
638        if ($storageObject) {
639            if ($startingPointFolder) {
640                return $this->storageHashNumbers[$storageObject->getUid() . $startingPointFolder->getCombinedIdentifier()];
641            }
642            return $this->storageHashNumbers[$storageObject->getUid()];
643        }
644        return null;
645    }
646
647    /**
648     * Gets the values from the Expand/Collapse Parameter (&PM)
649     * previously known as "PM" (plus/minus)
650     * PM action:
651     * (If an plus/minus icon has been clicked,
652     * the PM GET var is sent and we must update the stored positions in the tree):
653     * 0: mount key, 1: set/clear boolean, 2: item ID (cannot contain "_"), 3: treeName
654     *
655     * @param string $PM The "plus/minus" command
656     * @return array
657     */
658    protected function evaluateExpandCollapseParameter($PM = null)
659    {
660        if ($PM === null) {
661            $PM = GeneralUtility::_GP('PM');
662            // IE takes anchor as parameter
663            if (($PMpos = strpos($PM, '#')) !== false) {
664                $PM = substr($PM, 0, $PMpos);
665            }
666        }
667        // Take the first three parameters
668        list($mountKey, $doExpand, $folderIdentifier) = array_pad(explode('_', $PM, 3), 3, null);
669        // In case the folder identifier contains "_", we just need to get the fourth/last parameter
670        list($folderIdentifier, $treeName) = array_pad(GeneralUtility::revExplode('_', $folderIdentifier, 2), 2, null);
671        return [
672            $mountKey,
673            $doExpand,
674            $folderIdentifier,
675            $treeName
676        ];
677    }
678
679    /**
680     * Generates the "PM" string to sent to expand/collapse items
681     *
682     * @param string $mountKey The mount key / storage UID
683     * @param bool $doExpand Whether to expand/collapse
684     * @param Folder $folderObject The folder object
685     * @param string $treeName The name of the tree
686     *
687     * @return string
688     */
689    protected function generateExpandCollapseParameter($mountKey = null, $doExpand = false, Folder $folderObject = null, $treeName = null)
690    {
691        $parts = [
692            $mountKey ?? $this->bank,
693            $doExpand == 1 ? 1 : 0,
694            $folderObject !== null ? GeneralUtility::md5int($folderObject->getCombinedIdentifier()) : '',
695            $treeName ?? $this->treeName
696        ];
697        return implode('_', $parts);
698    }
699
700    /**
701     * Gets the AJAX status.
702     *
703     * @return bool
704     */
705    public function getAjaxStatus()
706    {
707        return $this->ajaxStatus;
708    }
709
710    /**
711     * @return LanguageService
712     */
713    protected function getLanguageService()
714    {
715        return $GLOBALS['LANG'];
716    }
717}
718