TYPO3  7.6
FolderTreeView.php
Go to the documentation of this file.
1 <?php
2 namespace 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 
27 
33 {
39  protected $storages = null;
40 
45 
52  protected $ajaxStatus = false;
53 
57  protected $scope;
58 
62  protected $iconFactory;
63 
68  public $ext_noTempRecyclerDirs = false;
69 
74  public $titleAttrib = '';
75 
81  public $treeName = 'folder';
82 
87  public $domIdPrefix = 'folder';
88 
92  public function __construct()
93  {
94  parent::__construct();
95  $this->init();
96  $this->storages = $this->BE_USER->getFileStorages();
97  $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
98  }
99 
113  public function PMicon($folderObject, $subFolderCounter, $totalSubFolders, $nextCount, $isExpanded)
114  {
115  $icon = '';
116  if ($nextCount) {
117  $cmd = $this->generateExpandCollapseParameter($this->bank, !$isExpanded, $folderObject);
118  $icon = $this->PMiconATagWrap($icon, $cmd, !$isExpanded);
119  }
120  return $icon;
121  }
122 
132  public function PMiconATagWrap($icon, $cmd, $isExpand = true)
133  {
134  if (empty($this->scope)) {
135  $this->scope = array(
136  'class' => get_class($this),
137  'script' => $this->thisScript,
138  'ext_noTempRecyclerDirs' => $this->ext_noTempRecyclerDirs
139  );
140  }
141 
142  if ($this->thisScript) {
143  // Activates dynamic AJAX based tree
144  $scopeData = serialize($this->scope);
145  $scopeHash = GeneralUtility::hmac($scopeData);
146  $js = htmlspecialchars('Tree.load(' . GeneralUtility::quoteJSvalue($cmd) . ', ' . (int)$isExpand . ', this, ' . GeneralUtility::quoteJSvalue($scopeData) . ', ' . GeneralUtility::quoteJSvalue($scopeHash) . ');');
147  return '<a class="list-tree-control' . (!$isExpand ? ' list-tree-control-open' : ' list-tree-control-closed') . '" onclick="' . $js . '"><i class="fa"></i></a>';
148  } else {
149  return $icon;
150  }
151  }
152 
158  protected function renderPMIconAndLink($cmd, $isOpen)
159  {
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>';
162  }
163 
173  public function wrapIcon($icon, $folderObject)
174  {
175  // Add title attribute to input icon tag
176  $theFolderIcon = '';
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  $theFolderIcon = BackendUtility::wrapClickMenuOnIcon($icon, $folderObject->getCombinedIdentifier(), '', 0);
182  }
183  } elseif ($this->ext_IconMode === 'titlelink') {
184  $aOnClick = 'return jumpTo(' . GeneralUtility::quoteJSvalue($this->getJumpToParam($folderObject)) . ',this,' . GeneralUtility::quoteJSvalue($this->domIdPrefix . $this->getId($folderObject)) . ',' . $this->bank . ');';
185  $theFolderIcon = '<a href="#" onclick="' . htmlspecialchars($aOnClick) . '">' . $icon . '</a>';
186  }
187  return $theFolderIcon;
188  }
189 
200  public function wrapTitle($title, $folderObject, $bank = 0)
201  {
202  // Check storage access to wrap with click menu
203  if ($folderObject instanceof InaccessibleFolder) {
204  return $title;
205  }
206  $aOnClick = 'return jumpTo(' . GeneralUtility::quoteJSvalue($this->getJumpToParam($folderObject)) . ', this, ' . GeneralUtility::quoteJSvalue($this->domIdPrefix . $this->getId($folderObject)) . ', ' . $bank . ');';
207  $clickMenuParts = BackendUtility::wrapClickMenuOnIcon('', $folderObject->getCombinedIdentifier(), '', 0, ('&bank=' . $this->bank), '', true);
208 
209  return '<a href="#" title="' . htmlspecialchars(strip_tags($title)) . '" onclick="' . htmlspecialchars($aOnClick) . '" ' . GeneralUtility::implodeAttributes($clickMenuParts) . '>' . $title . '</a>';
210  }
211 
219  public function getId($folderObject)
220  {
221  return GeneralUtility::md5Int($folderObject->getCombinedIdentifier());
222  }
223 
231  public function getJumpToParam($folderObject)
232  {
233  return rawurlencode($folderObject->getCombinedIdentifier());
234  }
235 
244  public function getTitleStr($row, $titleLen = 30)
245  {
246  return $row['_title'] ?: parent::getTitleStr($row, $titleLen);
247  }
248 
256  public function getTitleAttrib($folderObject)
257  {
258  return htmlspecialchars($folderObject->getName());
259  }
260 
267  public function getBrowsableTree()
268  {
269  // Get stored tree structure AND updating it if needed according to incoming PM GET var.
270  $this->initializePositionSaving();
271  // Init done:
272  $treeItems = array();
273  // Traverse mounts:
274  foreach ($this->storages as $storageObject) {
275  $this->getBrowseableTreeForStorage($storageObject);
276  // Add tree:
277  $treeItems = array_merge($treeItems, $this->tree);
278  }
279  return $this->printTree($treeItems);
280  }
281 
288  public function getBrowseableTreeForStorage(ResourceStorage $storageObject)
289  {
290  // If there are filemounts, show each, otherwise just the rootlevel folder
291  $fileMounts = $storageObject->getFileMounts();
292  $rootLevelFolders = array();
293  if (!empty($fileMounts)) {
294  foreach ($fileMounts as $fileMountInfo) {
295  $rootLevelFolders[] = array(
296  'folder' => $fileMountInfo['folder'],
297  'name' => $fileMountInfo['title']
298  );
299  }
300  } elseif ($this->BE_USER->isAdmin()) {
301  $rootLevelFolders[] = array(
302  'folder' => $storageObject->getRootLevelFolder(),
303  'name' => $storageObject->getName()
304  );
305  }
306  // Clean the tree
307  $this->reset();
308  // Go through all "root level folders" of this tree (can be the rootlevel folder or any file mount points)
309  foreach ($rootLevelFolders as $rootLevelFolderInfo) {
311  $rootLevelFolder = $rootLevelFolderInfo['folder'];
312  $rootLevelFolderName = $rootLevelFolderInfo['name'];
313  $folderHashSpecUID = GeneralUtility::md5int($rootLevelFolder->getCombinedIdentifier());
314  $this->specUIDmap[$folderHashSpecUID] = $rootLevelFolder->getCombinedIdentifier();
315  // Hash key
316  $storageHashNumber = $this->getShortHashNumberForStorage($storageObject, $rootLevelFolder);
317  // Set first:
318  $this->bank = $storageHashNumber;
319  $isOpen = $this->stored[$storageHashNumber][$folderHashSpecUID] || $this->expandFirst;
320  // Set PM icon:
321  $cmd = $this->generateExpandCollapseParameter($this->bank, !$isOpen, $rootLevelFolder);
322  // Only show and link icon if storage is browseable
323  if (!$storageObject->isBrowsable() || $this->getNumberOfSubfolders($rootLevelFolder) === 0) {
324  $firstHtml = '';
325  } else {
326  $firstHtml = $this->renderPMIconAndLink($cmd, $isOpen);
327  }
328  // Mark a storage which is not online, as offline
329  // maybe someday there will be a special icon for this
330  if ($storageObject->isOnline() === false) {
331  $rootLevelFolderName .= ' (' . $this->getLanguageService()->sL('LLL:EXT:lang/locallang_mod_file.xlf:sys_file_storage.isOffline') . ')';
332  }
333  // Preparing rootRec for the mount
334  $icon = $this->iconFactory->getIconForResource($rootLevelFolder, Icon::SIZE_SMALL, null, array('mount-root' => true));
335  $firstHtml .= $this->wrapIcon($icon, $rootLevelFolder);
336  $row = array(
337  'uid' => $folderHashSpecUID,
338  'title' => $rootLevelFolderName,
339  'path' => $rootLevelFolder->getCombinedIdentifier(),
340  'folder' => $rootLevelFolder
341  );
342  // Add the storage root to ->tree
343  $this->tree[] = array(
344  'HTML' => $firstHtml,
345  'row' => $row,
346  'bank' => $this->bank,
347  // hasSub is TRUE when the root of the storage is expanded
348  'hasSub' => $isOpen && $storageObject->isBrowsable(),
349  'invertedDepth' => 1000,
350  );
351  // If the mount is expanded, go down:
352  if ($isOpen && $storageObject->isBrowsable()) {
353  // Set depth:
354  $this->getFolderTree($rootLevelFolder, 999);
355  }
356  }
357  }
358 
369  public function getFolderTree(Folder $folderObject, $depth = 999, $type = '')
370  {
371  $depth = (int)$depth;
372 
373  // This generates the directory tree
374  /* array of \TYPO3\CMS\Core\Resource\Folder */
375  if ($folderObject instanceof InaccessibleFolder) {
376  $subFolders = array();
377  } else {
378  $subFolders = $folderObject->getSubfolders();
379  $subFolders = \TYPO3\CMS\Core\Resource\Utility\ListUtility::resolveSpecialFolderNames($subFolders);
380  uksort($subFolders, 'strnatcasecmp');
381  }
382 
383  $totalSubFolders = count($subFolders);
384  $HTML = '';
385  $subFolderCounter = 0;
386  $treeKey = '';
388  foreach ($subFolders as $subFolderName => $subFolder) {
389  $subFolderCounter++;
390  // Reserve space.
391  $this->tree[] = array();
392  // Get the key for this space
393  end($this->tree);
394  $isLocked = $subFolder instanceof InaccessibleFolder;
395  $treeKey = key($this->tree);
396  $specUID = GeneralUtility::md5int($subFolder->getCombinedIdentifier());
397  $this->specUIDmap[$specUID] = $subFolder->getCombinedIdentifier();
398  $row = array(
399  'uid' => $specUID,
400  'path' => $subFolder->getCombinedIdentifier(),
401  'title' => $subFolderName,
402  'folder' => $subFolder
403  );
404  // Make a recursive call to the next level
405  if (!$isLocked && $depth > 1 && $this->expandNext($specUID)) {
406  $nextCount = $this->getFolderTree($subFolder, $depth - 1, $type);
407  // Set "did expand" flag
408  $isOpen = 1;
409  } else {
410  $nextCount = $isLocked ? 0 : $this->getNumberOfSubfolders($subFolder);
411  // Clear "did expand" flag
412  $isOpen = 0;
413  }
414  // Set HTML-icons, if any:
415  if ($this->makeHTML) {
416  $HTML = $this->PMicon($subFolder, $subFolderCounter, $totalSubFolders, $nextCount, $isOpen);
417  $type = '';
418 
419  $role = $subFolder->getRole();
420  if ($role !== FolderInterface::ROLE_DEFAULT) {
421  $row['_title'] = '<strong>' . $subFolderName . '</strong>';
422  }
423  $icon = '<span title="' . htmlspecialchars($subFolderName) . '">'
424  . $this->iconFactory->getIconForResource($subFolder, Icon::SIZE_SMALL, null, array('folder-open' => (bool)$isOpen))
425  . '</span>';
426  $HTML .= $this->wrapIcon($icon, $subFolder);
427  }
428  // Finally, add the row/HTML content to the ->tree array in the reserved key.
429  $this->tree[$treeKey] = array(
430  'row' => $row,
431  'HTML' => $HTML,
432  'hasSub' => $nextCount && $this->expandNext($specUID),
433  'isFirst' => $subFolderCounter == 1,
434  'isLast' => false,
435  'invertedDepth' => $depth,
436  'bank' => $this->bank
437  );
438  }
439  if ($subFolderCounter > 0) {
440  $this->tree[$treeKey]['isLast'] = true;
441  }
442  return $totalSubFolders;
443  }
444 
451  public function printTree($treeItems = '')
452  {
453  $doExpand = false;
454  $doCollapse = false;
455  $ajaxOutput = '';
456  $titleLength = (int)$this->BE_USER->uc['titleLen'];
457  if (!is_array($treeItems)) {
458  $treeItems = $this->tree;
459  }
460 
461  if (empty($treeItems)) {
462  $message = GeneralUtility::makeInstance(
463  FlashMessage::class,
464  $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:foldertreeview.noFolders.message'),
465  $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:foldertreeview.noFolders.title'),
467  );
468  return $message->render();
469  }
470 
471  $expandedFolderHash = '';
472  $invertedDepthOfAjaxRequestedItem = 0;
473  $out = '<ul class="list-tree list-tree-root">';
474  // Evaluate AJAX request
475  if (TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_AJAX) {
476  list(, $expandCollapseCommand, $expandedFolderHash, ) = $this->evaluateExpandCollapseParameter();
477  if ($expandCollapseCommand == 1) {
478  $doExpand = true;
479  } else {
480  $doCollapse = true;
481  }
482  }
483  // We need to count the opened <ul>'s every time we dig into another level,
484  // so we know how many we have to close when all children are done rendering
485  $closeDepth = array();
486  foreach ($treeItems as $treeItem) {
488  $folderObject = $treeItem['row']['folder'];
489  $classAttr = $treeItem['row']['_CSSCLASS'];
490  $folderIdentifier = $folderObject->getCombinedIdentifier();
491  // this is set if the AJAX request has just opened this folder (via the PM command)
492  $isExpandedFolderIdentifier = $expandedFolderHash == GeneralUtility::md5int($folderIdentifier);
493  $idAttr = htmlspecialchars($this->domIdPrefix . $this->getId($folderObject) . '_' . $treeItem['bank']);
494  $itemHTML = '';
495  // If this item is the start of a new level,
496  // then a new level <ul> is needed, but not in ajax mode
497  if ($treeItem['isFirst'] && !$doCollapse && !($doExpand && $isExpandedFolderIdentifier)) {
498  $itemHTML = '<ul class="list-tree">';
499  }
500  // Add CSS classes to the list item
501  if ($treeItem['hasSub']) {
502  $classAttr .= ' list-tree-control-open';
503  }
504  $itemHTML .= '
505  <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>';
506  if (!$treeItem['hasSub']) {
507  $itemHTML .= '</li>';
508  }
509  // We have to remember if this is the last one
510  // on level X so the last child on level X+1 closes the <ul>-tag
511  if ($treeItem['isLast'] && !($doExpand && $isExpandedFolderIdentifier)) {
512  $closeDepth[$treeItem['invertedDepth']] = 1;
513  }
514  // If this is the last one and does not have subitems, we need to close
515  // the tree as long as the upper levels have last items too
516  if ($treeItem['isLast'] && !$treeItem['hasSub'] && !$doCollapse && !($doExpand && $isExpandedFolderIdentifier)) {
517  for ($i = $treeItem['invertedDepth']; $closeDepth[$i] == 1; $i++) {
518  $closeDepth[$i] = 0;
519  $itemHTML .= '</ul></li>';
520  }
521  }
522  // Ajax request: collapse
523  if ($doCollapse && $isExpandedFolderIdentifier) {
524  $this->ajaxStatus = true;
525  return $itemHTML;
526  }
527  // Ajax request: expand
528  if ($doExpand && $isExpandedFolderIdentifier) {
529  $ajaxOutput .= $itemHTML;
530  $invertedDepthOfAjaxRequestedItem = $treeItem['invertedDepth'];
531  } elseif ($invertedDepthOfAjaxRequestedItem) {
532  if ($treeItem['invertedDepth'] && ($treeItem['invertedDepth'] < $invertedDepthOfAjaxRequestedItem)) {
533  $ajaxOutput .= $itemHTML;
534  } else {
535  $this->ajaxStatus = true;
536  return $ajaxOutput;
537  }
538  }
539  $out .= $itemHTML;
540  }
541  // If this is an AJAX request, output directly
542  if ($ajaxOutput) {
543  $this->ajaxStatus = true;
544  return $ajaxOutput;
545  }
546  // Finally close the first ul
547  $out .= '</ul>';
548  return $out;
549  }
550 
558  public function getNumberOfSubfolders(Folder $folderObject)
559  {
560  $subFolders = $folderObject->getSubfolders();
561  return count($subFolders);
562  }
563 
570  public function initializePositionSaving()
571  {
572  // Get stored tree structure:
573  $this->stored = unserialize($this->BE_USER->uc['browseTrees'][$this->treeName]);
575  // PM action:
576  // (If an plus/minus icon has been clicked,
577  // the PM GET var is sent and we must update the stored positions in the tree):
578  // 0: mount key, 1: set/clear boolean, 2: item ID (cannot contain "_"), 3: treeName
579  list($storageHashNumber, $doExpand, $numericFolderHash, $treeName) = $this->evaluateExpandCollapseParameter();
580  if ($treeName && $treeName == $this->treeName) {
581  if (in_array($storageHashNumber, $this->storageHashNumbers)) {
582  if ($doExpand == 1) {
583  // Set
584  $this->stored[$storageHashNumber][$numericFolderHash] = 1;
585  } else {
586  // Clear
587  unset($this->stored[$storageHashNumber][$numericFolderHash]);
588  }
589  $this->savePosition();
590  }
591  }
592  }
593 
602  protected function getShortHashNumberForStorage(ResourceStorage $storageObject = null, Folder $startingPointFolder = null)
603  {
604  if (!$this->storageHashNumbers) {
605  $this->storageHashNumbers = array();
606  // Mapping md5-hash to shorter number:
607  $hashMap = array();
608  foreach ($this->storages as $storageUid => $storage) {
609  $fileMounts = $storage->getFileMounts();
610  if (!empty($fileMounts)) {
611  foreach ($fileMounts as $fileMount) {
612  $nkey = hexdec(substr(GeneralUtility::md5int($fileMount['folder']->getCombinedIdentifier()), 0, 4));
613  $this->storageHashNumbers[$storageUid . $fileMount['folder']->getCombinedIdentifier()] = $nkey;
614  }
615  } else {
616  $folder = $storage->getRootLevelFolder();
617  $nkey = hexdec(substr(GeneralUtility::md5int($folder->getCombinedIdentifier()), 0, 4));
618  $this->storageHashNumbers[$storageUid . $folder->getCombinedIdentifier()] = $nkey;
619  }
620  }
621  }
622  if ($storageObject) {
623  if ($startingPointFolder) {
624  return $this->storageHashNumbers[$storageObject->getUid() . $startingPointFolder->getCombinedIdentifier()];
625  } else {
626  return $this->storageHashNumbers[$storageObject->getUid()];
627  }
628  } else {
629  return null;
630  }
631  }
632 
644  protected function evaluateExpandCollapseParameter($PM = null)
645  {
646  if ($PM === null) {
647  $PM = GeneralUtility::_GP('PM');
648  // IE takes anchor as parameter
649  if (($PMpos = strpos($PM, '#')) !== false) {
650  $PM = substr($PM, 0, $PMpos);
651  }
652  }
653  // Take the first three parameters
654  list($mountKey, $doExpand, $folderIdentifier) = explode('_', $PM, 3);
655  // In case the folder identifier contains "_", we just need to get the fourth/last parameter
656  list($folderIdentifier, $treeName) = GeneralUtility::revExplode('_', $folderIdentifier, 2);
657  return array(
658  $mountKey,
659  $doExpand,
660  $folderIdentifier,
661  $treeName
662  );
663  }
664 
675  protected function generateExpandCollapseParameter($mountKey = null, $doExpand = false, Folder $folderObject = null, $treeName = null)
676  {
677  $parts = array(
678  $mountKey !== null ? $mountKey : $this->bank,
679  $doExpand == 1 ? 1 : 0,
680  $folderObject !== null ? GeneralUtility::md5int($folderObject->getCombinedIdentifier()) : '',
681  $treeName !== null ? $treeName : $this->treeName
682  );
683  return implode('_', $parts);
684  }
685 
691  public function getAjaxStatus()
692  {
693  return $this->ajaxStatus;
694  }
695 
699  protected function getLanguageService()
700  {
701  return $GLOBALS['LANG'];
702  }
703 }