TYPO3 CMS  TYPO3_8-7
AbstractTreeView.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 
30 
34 abstract class AbstractTreeView
35 {
36  // EXTERNAL, static:
37  // If set, the first element in the tree is always expanded.
41  public $expandFirst = 0;
42 
43  // If set, then ALL items will be expanded, regardless of stored settings.
47  public $expandAll = 0;
48 
49  // Holds the current script to reload to.
53  public $thisScript = '';
54 
55  // Which HTML attribute to use: alt/title. See init().
59  public $titleAttrib = 'title';
60 
61  // If TRUE, no context menu is rendered on icons. If set to "titlelink" the
62  // icon is linked as the title is.
66  public $ext_IconMode = false;
67 
71  public $ext_showPathAboveMounts = false;
72 
73  // If set, the id of the mounts will be added to the internal ids array
77  public $addSelfId = 0;
78 
79  // Used if the tree is made of records (not folders for ex.)
83  public $title = 'no title';
84 
85  // If TRUE, a default title attribute showing the UID of the record is shown.
86  // This cannot be enabled by default because it will destroy many applications
87  // where another title attribute is in fact applied later.
92 
99  public $BE_USER = '';
100 
110  public $MOUNTS = null;
111 
118  public $table = '';
119 
125  public $parentField = 'pid';
126 
134  public $clause = '';
135 
143  public $orderByFields = '';
144 
152  public $fieldArray = ['uid', 'pid', 'title', 'is_siteroot'];
153 
160  public $defaultList = 'uid,pid,tstamp,sorting,deleted,perms_userid,perms_groupid,perms_user,perms_group,perms_everybody,crdate,cruser_id';
161 
171  public $treeName = '';
172 
181  public $domIdPrefix = 'row';
182 
188  public $makeHTML = 1;
189 
195  public $setRecs = 0;
196 
203  public $subLevelID = '_SUB_LEVEL';
204 
205  // *********
206  // Internal
207  // *********
208  // For record trees:
209  // one-dim array of the uid's selected.
213  public $ids = [];
214 
215  // The hierarchy of element uids
219  public $ids_hierarchy = [];
220 
221  // The hierarchy of versioned element uids
225  public $orig_ids_hierarchy = [];
226 
227  // Temporary, internal array
231  public $buffer_idH = [];
232 
233  // For FOLDER trees:
234  // Special UIDs for folders (integer-hashes of paths)
238  public $specUIDmap = [];
239 
240  // For arrays:
241  // Holds the input data array
245  public $data = false;
246 
247  // Holds an index with references to the data array.
251  public $dataLookup = false;
252 
253  // For both types
254  // Tree is accumulated in this variable
258  public $tree = [];
259 
260  // Holds (session stored) information about which items in the tree are unfolded and which are not.
264  public $stored = [];
265 
266  // Points to the current mountpoint key
270  public $bank = 0;
271 
272  // Accumulates the displayed records.
276  public $recs = [];
277 
281  public function __construct()
282  {
283  $this->determineScriptUrl();
284  }
285 
289  protected function determineScriptUrl()
290  {
291  if ($routePath = GeneralUtility::_GP('route')) {
292  $router = GeneralUtility::makeInstance(Router::class);
293  $route = $router->match($routePath);
294  $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
295  $this->thisScript = (string)$uriBuilder->buildUriFromRoute($route->getOption('_identifier'));
296  } elseif ($moduleName = GeneralUtility::_GP('M')) {
297  $this->thisScript = BackendUtility::getModuleUrl($moduleName);
298  } else {
299  $this->thisScript = GeneralUtility::getIndpEnv('SCRIPT_NAME');
300  }
301  }
302 
306  protected function getThisScript()
307  {
308  return strpos($this->thisScript, '?') === false ? $this->thisScript . '?' : $this->thisScript . '&';
309  }
310 
317  public function init($clause = '', $orderByFields = '')
318  {
319  // Setting BE_USER by default
320  $this->BE_USER = $GLOBALS['BE_USER'];
321  // Setting clause
322  if ($clause) {
323  $this->clause = $clause;
324  }
325  if ($orderByFields) {
326  $this->orderByFields = $orderByFields;
327  }
328  if (!is_array($this->MOUNTS)) {
329  // Dummy
330  $this->MOUNTS = [0 => 0];
331  }
332  // Sets the tree name which is used to identify the tree, used for JavaScript and other things
333  $this->treeName = str_replace('_', '', $this->treeName ?: $this->table);
334  // Setting this to FALSE disables the use of array-trees by default
335  $this->data = false;
336  $this->dataLookup = false;
337  }
338 
345  public function addField($field, $noCheck = false)
346  {
347  if ($noCheck || is_array($GLOBALS['TCA'][$this->table]['columns'][$field]) || GeneralUtility::inList($this->defaultList, $field)) {
348  $this->fieldArray[] = $field;
349  }
350  }
351 
355  public function reset()
356  {
357  $this->tree = [];
358  $this->recs = [];
359  $this->ids = [];
360  $this->ids_hierarchy = [];
361  $this->orig_ids_hierarchy = [];
362  }
363 
364  /*******************************************
365  *
366  * output
367  *
368  *******************************************/
375  public function getBrowsableTree()
376  {
377  // Get stored tree structure AND updating it if needed according to incoming PM GET var.
378  $this->initializePositionSaving();
379  // Init done:
380  $lastMountPointPid = 0;
381  $treeArr = [];
382  // Traverse mounts:
383  foreach ($this->MOUNTS as $idx => $uid) {
384  // Set first:
385  $this->bank = $idx;
386  $isOpen = $this->stored[$idx][$uid] || $this->expandFirst;
387  // Save ids while resetting everything else.
388  $curIds = $this->ids;
389  $this->reset();
390  $this->ids = $curIds;
391  // Set PM icon for root of mount:
392  $cmd = $this->bank . '_' . ($isOpen ? '0_' : '1_') . $uid . '_' . $this->treeName;
393 
394  $firstHtml = $this->PM_ATagWrap('', $cmd, '', $isOpen);
395  // Preparing rootRec for the mount
396  if ($uid) {
397  $rootRec = $this->getRecord($uid);
398  if (is_array($rootRec)) {
399  $firstHtml .= $this->getIcon($rootRec);
400  }
401 
402  if ($this->ext_showPathAboveMounts) {
403  $mountPointPid = $rootRec['pid'];
404  if ($lastMountPointPid !== $mountPointPid) {
405  $title = Commands::getMountPointPath($mountPointPid);
406  $this->tree[] = ['isMountPointPath' => true, 'title' => $title];
407  }
408  $lastMountPointPid = $mountPointPid;
409  }
410  } else {
411  // Artificial record for the tree root, id=0
412  $rootRec = $this->getRootRecord();
413  $firstHtml .= $this->getRootIcon($rootRec);
414  }
415  if (is_array($rootRec)) {
416  // In case it was swapped inside getRecord due to workspaces.
417  $uid = $rootRec['uid'];
418  // Add the root of the mount to ->tree
419  $this->tree[] = ['HTML' => $firstHtml, 'row' => $rootRec, 'hasSub' => $isOpen, 'bank' => $this->bank];
420  // If the mount is expanded, go down:
421  if ($isOpen) {
422  $depthData = '<span class="treeline-icon treeline-icon-clear"></span>';
423  if ($this->addSelfId) {
424  $this->ids[] = $uid;
425  }
426  $this->getTree($uid, 999, $depthData);
427  }
428  // Add tree:
429  $treeArr = array_merge($treeArr, $this->tree);
430  }
431  }
432  return $this->printTree($treeArr);
433  }
434 
441  public function printTree($treeArr = '')
442  {
443  $titleLen = (int)$this->BE_USER->uc['titleLen'];
444  if (!is_array($treeArr)) {
445  $treeArr = $this->tree;
446  }
447  $out = '';
448  $closeDepth = [];
449  foreach ($treeArr as $treeItem) {
450  $classAttr = '';
451  if ($treeItem['isFirst']) {
452  $out .= '<ul class="list-tree">';
453  }
454 
455  // Add CSS classes to the list item
456  if ($treeItem['hasSub']) {
457  $classAttr .= ' list-tree-control-open';
458  }
459 
460  $idAttr = htmlspecialchars($this->domIdPrefix . $this->getId($treeItem['row']) . '_' . $treeItem['bank']);
461  $out .= '
462  <li id="' . $idAttr . '"' . ($classAttr ? ' class="' . trim($classAttr) . '"' : '') . '>
463  <span class="list-tree-group">
464  <span class="list-tree-icon">' . $treeItem['HTML'] . '</span>
465  <span class="list-tree-title">' . $this->wrapTitle($this->getTitleStr($treeItem['row'], $titleLen), $treeItem['row'], $treeItem['bank']) . '</span>
466  </span>';
467 
468  if (!$treeItem['hasSub']) {
469  $out .= '</li>';
470  }
471 
472  // We have to remember if this is the last one
473  // on level X so the last child on level X+1 closes the <ul>-tag
474  if ($treeItem['isLast']) {
475  $closeDepth[$treeItem['invertedDepth']] = 1;
476  }
477  // If this is the last one and does not have subitems, we need to close
478  // the tree as long as the upper levels have last items too
479  if ($treeItem['isLast'] && !$treeItem['hasSub']) {
480  for ($i = $treeItem['invertedDepth']; $closeDepth[$i] == 1; $i++) {
481  $closeDepth[$i] = 0;
482  $out .= '</ul></li>';
483  }
484  }
485  }
486  $out = '<ul class="list-tree list-tree-root list-tree-root-clean">' . $out . '</ul>';
487  return $out;
488  }
489 
490  /*******************************************
491  *
492  * rendering parts
493  *
494  *******************************************/
507  public function PMicon($row, $a, $c, $nextCount, $isOpen)
508  {
509  if ($nextCount) {
510  $cmd = $this->bank . '_' . ($isOpen ? '0_' : '1_') . $row['uid'] . '_' . $this->treeName;
511  $bMark = $this->bank . '_' . $row['uid'];
512  return $this->PM_ATagWrap('', $cmd, $bMark, $isOpen);
513  }
514  return '';
515  }
516 
527  public function PM_ATagWrap($icon, $cmd, $bMark = '', $isOpen = false)
528  {
529  if ($this->thisScript) {
530  $anchor = $bMark ? '#' . $bMark : '';
531  $name = $bMark ? ' name="' . $bMark . '"' : '';
532  $aUrl = $this->getThisScript() . 'PM=' . $cmd . $anchor;
533  return '<a class="list-tree-control ' . ($isOpen ? 'list-tree-control-open' : 'list-tree-control-closed') . '" href="' . htmlspecialchars($aUrl) . '"' . $name . '><i class="fa"></i></a>';
534  }
535  return $icon;
536  }
537 
547  public function wrapTitle($title, $row, $bank = 0)
548  {
549  $aOnClick = 'return jumpTo(' . GeneralUtility::quoteJSvalue($this->getJumpToParam($row)) . ',this,' . GeneralUtility::quoteJSvalue($this->domIdPrefix . $this->getId($row)) . ',' . $bank . ');';
550  return '<a href="#" onclick="' . htmlspecialchars($aOnClick) . '">' . $title . '</a>';
551  }
552 
561  public function wrapIcon($icon, $row)
562  {
563  return $icon;
564  }
565 
573  public function addTagAttributes($icon, $attr)
574  {
575  return preg_replace('/ ?\\/?>$/', '', $icon) . ' ' . $attr . ' />';
576  }
577 
586  public function wrapStop($str, $row)
587  {
588  if ($row['php_tree_stop']) {
589  $str .= '<a href="' . htmlspecialchars(GeneralUtility::linkThisScript(['setTempDBmount' => $row['uid']])) . '" class="text-danger">+</a> ';
590  }
591  return $str;
592  }
593 
594  /*******************************************
595  *
596  * tree handling
597  *
598  *******************************************/
609  public function expandNext($id)
610  {
611  return $this->stored[$this->bank][$id] || $this->expandAll ? 1 : 0;
612  }
613 
619  public function initializePositionSaving()
620  {
621  // Get stored tree structure:
622  $this->stored = unserialize($this->BE_USER->uc['browseTrees'][$this->treeName]);
623  // PM action
624  // (If an plus/minus icon has been clicked, the PM GET var is sent and we
625  // must update the stored positions in the tree):
626  // 0: mount key, 1: set/clear boolean, 2: item ID (cannot contain "_"), 3: treeName
627  $PM = explode('_', GeneralUtility::_GP('PM'));
628  if (count($PM) === 4 && $PM[3] == $this->treeName) {
629  if (isset($this->MOUNTS[$PM[0]])) {
630  // set
631  if ($PM[1]) {
632  $this->stored[$PM[0]][$PM[2]] = 1;
633  $this->savePosition();
634  } else {
635  unset($this->stored[$PM[0]][$PM[2]]);
636  $this->savePosition();
637  }
638  }
639  }
640  }
641 
648  public function savePosition()
649  {
650  $this->BE_USER->uc['browseTrees'][$this->treeName] = serialize($this->stored);
651  $this->BE_USER->writeUC();
652  }
653 
654  /******************************
655  *
656  * Functions that might be overwritten by extended classes
657  *
658  ********************************/
665  public function getRootIcon($rec)
666  {
667  $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
668  return $this->wrapIcon($iconFactory->getIcon('apps-pagetree-root', Icon::SIZE_SMALL)->render(), $rec);
669  }
670 
677  public function getIcon($row)
678  {
679  if (is_int($row)) {
680  $row = BackendUtility::getRecord($this->table, $row);
681  }
682  $title = $this->showDefaultTitleAttribute ? htmlspecialchars('UID: ' . $row['uid']) : $this->getTitleAttrib($row);
683  $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
684  $icon = $row['is_siteroot'] ? $iconFactory->getIcon('apps-pagetree-folder-root', Icon::SIZE_SMALL) : $iconFactory->getIconForRecord($this->table, $row, Icon::SIZE_SMALL);
685  $icon = '<span title="' . $title . '">' . $icon->render() . '</span>';
686  return $this->wrapIcon($icon, $row);
687  }
688 
697  public function getTitleStr($row, $titleLen = 30)
698  {
699  $title = htmlspecialchars(GeneralUtility::fixed_lgd_cs($row['title'], $titleLen));
700  $title = trim($row['title']) === '' ? '<em>[' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.no_title')) . ']</em>' : $title;
701  return $title;
702  }
703 
711  public function getTitleAttrib($row)
712  {
713  return htmlspecialchars($row['title']);
714  }
715 
722  public function getId($row)
723  {
724  return $row['uid'];
725  }
726 
733  public function getJumpToParam($row)
734  {
735  return $this->getId($row);
736  }
737 
738  /********************************
739  *
740  * tree data buidling
741  *
742  ********************************/
752  public function getTree($uid, $depth = 999, $depthData = '')
753  {
754  // Buffer for id hierarchy is reset:
755  $this->buffer_idH = [];
756  // Init vars
757  $depth = (int)$depth;
758  $HTML = '';
759  $a = 0;
760  $res = $this->getDataInit($uid);
761  $c = $this->getDataCount($res);
762  $crazyRecursionLimiter = 999;
763  $idH = [];
764  // Traverse the records:
765  while ($crazyRecursionLimiter > 0 && ($row = $this->getDataNext($res))) {
766  $pageUid = ($this->table === 'pages') ? $row['uid'] : $row['pid'];
767  if (!$this->getBackendUser()->isInWebMount($pageUid)) {
768  // Current record is not within web mount => skip it
769  continue;
770  }
771 
772  $a++;
773  $crazyRecursionLimiter--;
774  $newID = $row['uid'];
775  if ($newID == 0) {
776  throw new \RuntimeException('Endless recursion detected: TYPO3 has detected an error in the database. Please fix it manually (e.g. using phpMyAdmin) and change the UID of ' . $this->table . ':0 to a new value. See http://forge.typo3.org/issues/16150 to get more information about a possible cause.', 1294586383);
777  }
778  // Reserve space.
779  $this->tree[] = [];
780  end($this->tree);
781  // Get the key for this space
782  $treeKey = key($this->tree);
783  // If records should be accumulated, do so
784  if ($this->setRecs) {
785  $this->recs[$row['uid']] = $row;
786  }
787  // Accumulate the id of the element in the internal arrays
788  $this->ids[] = ($idH[$row['uid']]['uid'] = $row['uid']);
789  $this->ids_hierarchy[$depth][] = $row['uid'];
790  $this->orig_ids_hierarchy[$depth][] = $row['_ORIG_uid'] ?: $row['uid'];
791 
792  // Make a recursive call to the next level
793  $nextLevelDepthData = $depthData . '<span class="treeline-icon treeline-icon-' . ($a === $c ? 'clear' : 'line') . '"></span>';
794  $hasSub = $this->expandNext($newID) && !$row['php_tree_stop'];
795  if ($depth > 1 && $hasSub) {
796  $nextCount = $this->getTree($newID, $depth - 1, $nextLevelDepthData);
797  if (!empty($this->buffer_idH)) {
798  $idH[$row['uid']]['subrow'] = $this->buffer_idH;
799  }
800  // Set "did expand" flag
801  $isOpen = 1;
802  } else {
803  $nextCount = $this->getCount($newID);
804  // Clear "did expand" flag
805  $isOpen = 0;
806  }
807  // Set HTML-icons, if any:
808  if ($this->makeHTML) {
809  $HTML = $this->PMicon($row, $a, $c, $nextCount, $isOpen) . $this->wrapStop($this->getIcon($row), $row);
810  }
811  // Finally, add the row/HTML content to the ->tree array in the reserved key.
812  $this->tree[$treeKey] = [
813  'row' => $row,
814  'HTML' => $HTML,
815  'invertedDepth' => $depth,
816  'depthData' => $depthData,
817  'bank' => $this->bank,
818  'hasSub' => $nextCount && $hasSub,
819  'isFirst' => $a === 1,
820  'isLast' => $a === $c,
821  ];
822  }
823 
824  $this->getDataFree($res);
825  $this->buffer_idH = $idH;
826  return $c;
827  }
828 
829  /********************************
830  *
831  * Data handling
832  * Works with records and arrays
833  *
834  ********************************/
842  public function getCount($uid)
843  {
844  if (is_array($this->data)) {
845  $res = $this->getDataInit($uid);
846  return $this->getDataCount($res);
847  }
848  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->table);
849  $queryBuilder->getRestrictions()
850  ->removeAll()
851  ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
852  ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
853  $count = $queryBuilder
854  ->count('uid')
855  ->from($this->table)
856  ->where(
857  $queryBuilder->expr()->eq(
858  $this->parentField,
859  $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
860  ),
862  )
863  ->execute()
864  ->fetchColumn();
865 
866  return (int)$count;
867  }
868 
874  public function getRootRecord()
875  {
876  return ['title' => $this->title, 'uid' => 0];
877  }
878 
887  public function getRecord($uid)
888  {
889  if (is_array($this->data)) {
890  return $this->dataLookup[$uid];
891  }
892  return BackendUtility::getRecordWSOL($this->table, $uid);
893  }
894 
905  public function getDataInit($parentId)
906  {
907  if (is_array($this->data)) {
908  if (!is_array($this->dataLookup[$parentId][$this->subLevelID])) {
909  $parentId = -1;
910  } else {
911  reset($this->dataLookup[$parentId][$this->subLevelID]);
912  }
913  return $parentId;
914  }
915  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->table);
916  $queryBuilder->getRestrictions()
917  ->removeAll()
918  ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
919  ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
920  $queryBuilder
921  ->select(...$this->fieldArray)
922  ->from($this->table)
923  ->where(
924  $queryBuilder->expr()->eq(
925  $this->parentField,
926  $queryBuilder->createNamedParameter($parentId, \PDO::PARAM_INT)
927  ),
929  );
930 
931  foreach (QueryHelper::parseOrderBy($this->orderByFields) as $orderPair) {
932  list($fieldName, $order) = $orderPair;
933  $queryBuilder->addOrderBy($fieldName, $order);
934  }
935 
936  return $queryBuilder->execute();
937  }
938 
947  public function getDataCount(&$res)
948  {
949  if (is_array($this->data)) {
950  return count($this->dataLookup[$res][$this->subLevelID]);
951  }
952  return $res->rowCount();
953  }
954 
964  public function getDataNext(&$res)
965  {
966  if (is_array($this->data)) {
967  if ($res < 0) {
968  $row = false;
969  } else {
970  $key = key($this->dataLookup[$res][$this->subLevelID]);
971  next($this->dataLookup[$res][$this->subLevelID]);
972  $row = $this->dataLookup[$res][$this->subLevelID][$key];
973  }
974  return $row;
975  }
976  while ($row = $res->fetch()) {
977  BackendUtility::workspaceOL($this->table, $row, $this->BE_USER->workspace, true);
978  if (is_array($row)) {
979  break;
980  }
981  }
982  return $row;
983  }
984 
991  public function getDataFree(&$res)
992  {
993  if (!is_array($this->data)) {
994  $res->closeCursor();
995  }
996  }
997 
1009  public function setDataFromArray(&$dataArr, $traverse = false, $pid = 0)
1010  {
1011  if (!$traverse) {
1012  $this->data = &$dataArr;
1013  $this->dataLookup = [];
1014  // Add root
1015  $this->dataLookup[0][$this->subLevelID] = &$dataArr;
1016  }
1017  foreach ($dataArr as $uid => $val) {
1018  $dataArr[$uid]['uid'] = $uid;
1019  $dataArr[$uid]['pid'] = $pid;
1020  // Gives quick access to id's
1021  $this->dataLookup[$uid] = &$dataArr[$uid];
1022  if (is_array($val[$this->subLevelID])) {
1023  $this->setDataFromArray($dataArr[$uid][$this->subLevelID], true, $uid);
1024  }
1025  }
1026  }
1027 
1034  public function setDataFromTreeArray(&$treeArr, &$treeLookupArr)
1035  {
1036  $this->data = &$treeArr;
1037  $this->dataLookup = &$treeLookupArr;
1038  }
1039 
1043  protected function getLanguageService()
1044  {
1045  return $GLOBALS['LANG'];
1046  }
1047 
1051  protected function getBackendUser()
1052  {
1053  return $GLOBALS['BE_USER'];
1054  }
1055 }
static getRecordWSOL( $table, $uid, $fields=' *', $where='', $useDeleteClause=true, $unsetMovePointers=false)
setDataFromArray(&$dataArr, $traverse=false, $pid=0)
static workspaceOL($table, &$row, $wsid=-99, $unsetMovePointers=false)
static linkThisScript(array $getParams=[])
PM_ATagWrap($icon, $cmd, $bMark='', $isOpen=false)
static makeInstance($className,... $constructorArguments)
PMicon($row, $a, $c, $nextCount, $isOpen)
getTree($uid, $depth=999, $depthData='')
setDataFromTreeArray(&$treeArr, &$treeLookupArr)
static stripLogicalOperatorPrefix(string $constraint)
static fixed_lgd_cs($string, $chars, $appendString='...')
static getRecord($table, $uid, $fields=' *', $where='', $useDeleteClause=true)
if(TYPO3_MODE==='BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']