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