TYPO3 CMS  TYPO3_7-6
FolderTreeView.php
Go to the documentation of this file.
1 <?php
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 
28 
34 {
40  protected $storages = null;
41 
46 
53  protected $ajaxStatus = false;
54 
58  protected $scope;
59 
63  protected $iconFactory;
64 
69  public $ext_noTempRecyclerDirs = false;
70 
75  public $titleAttrib = '';
76 
82  public $treeName = 'folder';
83 
88  public $domIdPrefix = 'folder';
89 
93  public function __construct()
94  {
95  parent::__construct();
96  $this->init();
97  $this->storages = $this->BE_USER->getFileStorages();
98  $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
99  }
100 
114  public function PMicon($folderObject, $subFolderCounter, $totalSubFolders, $nextCount, $isExpanded)
115  {
116  $icon = '';
117  if ($nextCount) {
118  $cmd = $this->generateExpandCollapseParameter($this->bank, !$isExpanded, $folderObject);
119  $icon = $this->PMiconATagWrap($icon, $cmd, !$isExpanded);
120  }
121  return $icon;
122  }
123 
133  public function PMiconATagWrap($icon, $cmd, $isExpand = true)
134  {
135  if (empty($this->scope)) {
136  $this->scope = [
137  'class' => get_class($this),
138  'script' => $this->thisScript,
139  'ext_noTempRecyclerDirs' => $this->ext_noTempRecyclerDirs
140  ];
141  }
142 
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>';
149  } else {
150  return $icon;
151  }
152  }
153 
159  protected function renderPMIconAndLink($cmd, $isOpen)
160  {
161  $link = $this->thisScript ? ' href="' . htmlspecialchars($this->getThisScript() . 'PM=' . $cmd) . '"' : '';
162  return '<a class="list-tree-control list-tree-control-' . ($isOpen ? 'open' : 'closed') . '"' . $link . '><i class="fa"></i></a>';
163  }
164 
174  public function wrapIcon($icon, $folderObject)
175  {
176  // Add title attribute to input icon tag
177  $theFolderIcon = '';
178  // Wrap icon in click-menu link.
179  if (!$this->ext_IconMode) {
180  // Check storage access to wrap with click menu
181  if (!$folderObject instanceof InaccessibleFolder) {
182  $theFolderIcon = BackendUtility::wrapClickMenuOnIcon($icon, $folderObject->getCombinedIdentifier(), '', 0);
183  }
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>';
187  }
188  return $theFolderIcon;
189  }
190 
201  public function wrapTitle($title, $folderObject, $bank = 0)
202  {
203  // Check storage access to wrap with click menu
204  if ($folderObject instanceof InaccessibleFolder) {
205  return $title;
206  }
207  $aOnClick = 'return jumpTo(' . GeneralUtility::quoteJSvalue($this->getJumpToParam($folderObject)) . ', this, ' . GeneralUtility::quoteJSvalue($this->domIdPrefix . $this->getId($folderObject)) . ', ' . $bank . ');';
208  $clickMenuParts = BackendUtility::wrapClickMenuOnIcon('', $folderObject->getCombinedIdentifier(), '', 0, ('&bank=' . $this->bank), '', true);
209 
210  return '<a href="#" title="' . htmlspecialchars(strip_tags($title)) . '" onclick="' . htmlspecialchars($aOnClick) . '" ' . GeneralUtility::implodeAttributes($clickMenuParts) . '>' . $title . '</a>';
211  }
212 
220  public function getId($folderObject)
221  {
222  return GeneralUtility::md5Int($folderObject->getCombinedIdentifier());
223  }
224 
232  public function getJumpToParam($folderObject)
233  {
234  return rawurlencode($folderObject->getCombinedIdentifier());
235  }
236 
245  public function getTitleStr($row, $titleLen = 30)
246  {
247  return $row['_title'] ?: parent::getTitleStr($row, $titleLen);
248  }
249 
257  public function getTitleAttrib($folderObject)
258  {
259  return htmlspecialchars($folderObject->getName());
260  }
261 
268  public function getBrowsableTree()
269  {
270  // Get stored tree structure AND updating it if needed according to incoming PM GET var.
271  $this->initializePositionSaving();
272  // Init done:
273  $treeItems = [];
274  // Traverse mounts:
275  foreach ($this->storages as $storageObject) {
276  $this->getBrowseableTreeForStorage($storageObject);
277  // Add tree:
278  $treeItems = array_merge($treeItems, $this->tree);
279  }
280  return $this->printTree($treeItems);
281  }
282 
289  public function getBrowseableTreeForStorage(ResourceStorage $storageObject)
290  {
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']
299  ];
300  }
301  } elseif ($this->BE_USER->isAdmin()) {
302  $rootLevelFolders[] = [
303  'folder' => $storageObject->getRootLevelFolder(),
304  'name' => $storageObject->getName()
305  ];
306  }
307  // Clean the tree
308  $this->reset();
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) {
312  $rootLevelFolder = $rootLevelFolderInfo['folder'];
313  $rootLevelFolderName = $rootLevelFolderInfo['name'];
314  $folderHashSpecUID = GeneralUtility::md5int($rootLevelFolder->getCombinedIdentifier());
315  $this->specUIDmap[$folderHashSpecUID] = $rootLevelFolder->getCombinedIdentifier();
316  // Hash key
317  $storageHashNumber = $this->getShortHashNumberForStorage($storageObject, $rootLevelFolder);
318  // Set first:
319  $this->bank = $storageHashNumber;
320  $isOpen = $this->stored[$storageHashNumber][$folderHashSpecUID] || $this->expandFirst;
321  // Set PM icon:
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) {
325  $firstHtml = '';
326  } else {
327  $firstHtml = $this->renderPMIconAndLink($cmd, $isOpen);
328  }
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:lang/locallang_mod_file.xlf:sys_file_storage.isOffline') . ')';
333  }
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);
337  $row = [
338  'uid' => $folderHashSpecUID,
339  'title' => $rootLevelFolderName,
340  'path' => $rootLevelFolder->getCombinedIdentifier(),
341  'folder' => $rootLevelFolder
342  ];
343  // Add the storage root to ->tree
344  $this->tree[] = [
345  'HTML' => $firstHtml,
346  'row' => $row,
347  'bank' => $this->bank,
348  // hasSub is TRUE when the root of the storage is expanded
349  'hasSub' => $isOpen && $storageObject->isBrowsable(),
350  'invertedDepth' => 1000,
351  ];
352  // If the mount is expanded, go down:
353  if ($isOpen && $storageObject->isBrowsable()) {
354  // Set depth:
355  $this->getFolderTree($rootLevelFolder, 999);
356  }
357  }
358  }
359 
370  public function getFolderTree(Folder $folderObject, $depth = 999, $type = '')
371  {
372  $depth = (int)$depth;
373 
374  // This generates the directory tree
375  /* array of \TYPO3\CMS\Core\Resource\Folder */
376  if ($folderObject instanceof InaccessibleFolder) {
377  $subFolders = [];
378  } else {
379  $subFolders = $folderObject->getSubfolders();
380  $subFolders = \TYPO3\CMS\Core\Resource\Utility\ListUtility::resolveSpecialFolderNames($subFolders);
381  uksort($subFolders, 'strnatcasecmp');
382  }
383 
384  $totalSubFolders = count($subFolders);
385  $HTML = '';
386  $subFolderCounter = 0;
387  $treeKey = '';
389  foreach ($subFolders as $subFolderName => $subFolder) {
390  $subFolderCounter++;
391  // Reserve space.
392  $this->tree[] = [];
393  // Get the key for this space
394  end($this->tree);
395  $isLocked = $subFolder instanceof InaccessibleFolder;
396  $treeKey = key($this->tree);
397  $specUID = GeneralUtility::md5int($subFolder->getCombinedIdentifier());
398  $this->specUIDmap[$specUID] = $subFolder->getCombinedIdentifier();
399  $row = [
400  'uid' => $specUID,
401  'path' => $subFolder->getCombinedIdentifier(),
402  'title' => $subFolderName,
403  'folder' => $subFolder
404  ];
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
409  $isOpen = 1;
410  } else {
411  $nextCount = $isLocked ? 0 : $this->getNumberOfSubfolders($subFolder);
412  // Clear "did expand" flag
413  $isOpen = 0;
414  }
415  // Set HTML-icons, if any:
416  if ($this->makeHTML) {
417  $HTML = $this->PMicon($subFolder, $subFolderCounter, $totalSubFolders, $nextCount, $isOpen);
418  $type = '';
419 
420  $role = $subFolder->getRole();
421  if ($role !== FolderInterface::ROLE_DEFAULT) {
422  $row['_title'] = '<strong>' . $subFolderName . '</strong>';
423  }
424  $icon = '<span title="' . htmlspecialchars($subFolderName) . '">'
425  . $this->iconFactory->getIconForResource($subFolder, Icon::SIZE_SMALL, null, ['folder-open' => (bool)$isOpen])
426  . '</span>';
427  $HTML .= $this->wrapIcon($icon, $subFolder);
428  }
429  // Finally, add the row/HTML content to the ->tree array in the reserved key.
430  $this->tree[$treeKey] = [
431  'row' => $row,
432  'HTML' => $HTML,
433  'hasSub' => $nextCount && $this->expandNext($specUID),
434  'isFirst' => $subFolderCounter == 1,
435  'isLast' => false,
436  'invertedDepth' => $depth,
437  'bank' => $this->bank
438  ];
439  }
440  if ($subFolderCounter > 0) {
441  $this->tree[$treeKey]['isLast'] = true;
442  }
443  return $totalSubFolders;
444  }
445 
452  public function printTree($treeItems = '')
453  {
454  $doExpand = false;
455  $doCollapse = false;
456  $ajaxOutput = '';
457  $titleLength = (int)$this->BE_USER->uc['titleLen'];
458  if (!is_array($treeItems)) {
459  $treeItems = $this->tree;
460  }
461 
462  if (empty($treeItems)) {
463  $message = GeneralUtility::makeInstance(
464  FlashMessage::class,
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'),
468  );
470  $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
472  $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
473  $defaultFlashMessageQueue->enqueue($message);
474  return $defaultFlashMessageQueue->renderFlashMessages();
475  }
476 
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) {
484  $doExpand = true;
485  } else {
486  $doCollapse = true;
487  }
488  }
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
491  $closeDepth = [];
492  foreach ($treeItems as $treeItem) {
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']);
500  $itemHTML = '';
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 ($treeItem['isFirst'] && !$doCollapse && !($doExpand && $isExpandedFolderIdentifier)) {
504  $itemHTML = '<ul class="list-tree">';
505  }
506  // Add CSS classes to the list item
507  if ($treeItem['hasSub']) {
508  $classAttr .= ' list-tree-control-open';
509  }
510  $itemHTML .= '
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 (!$treeItem['hasSub']) {
513  $itemHTML .= '</li>';
514  }
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 ($treeItem['isLast'] && !($doExpand && $isExpandedFolderIdentifier)) {
518  $closeDepth[$treeItem['invertedDepth']] = 1;
519  }
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 ($treeItem['isLast'] && !$treeItem['hasSub'] && !$doCollapse && !($doExpand && $isExpandedFolderIdentifier)) {
523  for ($i = $treeItem['invertedDepth']; $closeDepth[$i] == 1; $i++) {
524  $closeDepth[$i] = 0;
525  $itemHTML .= '</ul></li>';
526  }
527  }
528  // Ajax request: collapse
529  if ($doCollapse && $isExpandedFolderIdentifier) {
530  $this->ajaxStatus = true;
531  return $itemHTML;
532  }
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;
540  } else {
541  $this->ajaxStatus = true;
542  return $ajaxOutput;
543  }
544  }
545  $out .= $itemHTML;
546  }
547  // If this is an AJAX request, output directly
548  if ($ajaxOutput) {
549  $this->ajaxStatus = true;
550  return $ajaxOutput;
551  }
552  // Finally close the first ul
553  $out .= '</ul>';
554  return $out;
555  }
556 
564  public function getNumberOfSubfolders(Folder $folderObject)
565  {
566  $subFolders = $folderObject->getSubfolders();
567  return count($subFolders);
568  }
569 
576  public function initializePositionSaving()
577  {
578  // Get stored tree structure:
579  $this->stored = unserialize($this->BE_USER->uc['browseTrees'][$this->treeName]);
581  // PM action:
582  // (If an plus/minus icon has been clicked,
583  // the PM GET var is sent and we must update the stored positions in the tree):
584  // 0: mount key, 1: set/clear boolean, 2: item ID (cannot contain "_"), 3: treeName
585  list($storageHashNumber, $doExpand, $numericFolderHash, $treeName) = $this->evaluateExpandCollapseParameter();
586  if ($treeName && $treeName == $this->treeName) {
587  if (in_array($storageHashNumber, $this->storageHashNumbers)) {
588  if ($doExpand == 1) {
589  // Set
590  $this->stored[$storageHashNumber][$numericFolderHash] = 1;
591  } else {
592  // Clear
593  unset($this->stored[$storageHashNumber][$numericFolderHash]);
594  }
595  $this->savePosition();
596  }
597  }
598  }
599 
608  protected function getShortHashNumberForStorage(ResourceStorage $storageObject = null, Folder $startingPointFolder = null)
609  {
610  if (!$this->storageHashNumbers) {
611  $this->storageHashNumbers = [];
612  // Mapping md5-hash to shorter number:
613  $hashMap = [];
614  foreach ($this->storages as $storageUid => $storage) {
615  $fileMounts = $storage->getFileMounts();
616  if (!empty($fileMounts)) {
617  foreach ($fileMounts as $fileMount) {
618  $nkey = hexdec(substr(GeneralUtility::md5int($fileMount['folder']->getCombinedIdentifier()), 0, 4));
619  $this->storageHashNumbers[$storageUid . $fileMount['folder']->getCombinedIdentifier()] = $nkey;
620  }
621  } else {
622  $folder = $storage->getRootLevelFolder();
623  $nkey = hexdec(substr(GeneralUtility::md5int($folder->getCombinedIdentifier()), 0, 4));
624  $this->storageHashNumbers[$storageUid . $folder->getCombinedIdentifier()] = $nkey;
625  }
626  }
627  }
628  if ($storageObject) {
629  if ($startingPointFolder) {
630  return $this->storageHashNumbers[$storageObject->getUid() . $startingPointFolder->getCombinedIdentifier()];
631  } else {
632  return $this->storageHashNumbers[$storageObject->getUid()];
633  }
634  } else {
635  return null;
636  }
637  }
638 
650  protected function evaluateExpandCollapseParameter($PM = null)
651  {
652  if ($PM === null) {
653  $PM = GeneralUtility::_GP('PM');
654  // IE takes anchor as parameter
655  if (($PMpos = strpos($PM, '#')) !== false) {
656  $PM = substr($PM, 0, $PMpos);
657  }
658  }
659  // Take the first three parameters
660  list($mountKey, $doExpand, $folderIdentifier) = explode('_', $PM, 3);
661  // In case the folder identifier contains "_", we just need to get the fourth/last parameter
662  list($folderIdentifier, $treeName) = GeneralUtility::revExplode('_', $folderIdentifier, 2);
663  return [
664  $mountKey,
665  $doExpand,
666  $folderIdentifier,
667  $treeName
668  ];
669  }
670 
681  protected function generateExpandCollapseParameter($mountKey = null, $doExpand = false, Folder $folderObject = null, $treeName = null)
682  {
683  $parts = [
684  $mountKey !== null ? $mountKey : $this->bank,
685  $doExpand == 1 ? 1 : 0,
686  $folderObject !== null ? GeneralUtility::md5int($folderObject->getCombinedIdentifier()) : '',
687  $treeName !== null ? $treeName : $this->treeName
688  ];
689  return implode('_', $parts);
690  }
691 
697  public function getAjaxStatus()
698  {
699  return $this->ajaxStatus;
700  }
701 
705  protected function getLanguageService()
706  {
707  return $GLOBALS['LANG'];
708  }
709 }
PMiconATagWrap($icon, $cmd, $isExpand=true)
if(!defined("DB_ERROR")) define("DB_ERROR"
static implodeAttributes(array $arr, $xhtmlSafe=false, $dontOmitBlankAttribs=false)
PMicon($folderObject, $subFolderCounter, $totalSubFolders, $nextCount, $isExpanded)
static hmac($input, $additionalSecret='')
getRootLevelFolder($respectFileMounts=true)
getShortHashNumberForStorage(ResourceStorage $storageObject=null, Folder $startingPointFolder=null)
wrapTitle($title, $folderObject, $bank=0)
getSubfolders($start=0, $numberOfItems=0, $filterMode=self::FILTER_MODE_USE_OWN_AND_STORAGE_FILTERS, $recursive=false)
Definition: Folder.php:273
static revExplode($delimiter, $string, $count=0)
static wrapClickMenuOnIcon( $content, $table, $uid=0, $listFrame=true, $addParams='', $enDisItems='', $returnTagParameters=false)
if(TYPO3_MODE==='BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']
generateExpandCollapseParameter($mountKey=null, $doExpand=false, Folder $folderObject=null, $treeName=null)