TYPO3CMS  8
 All Classes Namespaces Files Functions Variables Pages
AbstractTreeView.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 
29 
33 abstract class AbstractTreeView
34 {
35  // EXTERNAL, static:
36  // If set, the first element in the tree is always expanded.
40  public $expandFirst = 0;
41 
42  // If set, then ALL items will be expanded, regardless of stored settings.
46  public $expandAll = 0;
47 
48  // Holds the current script to reload to.
52  public $thisScript = '';
53 
54  // Which HTML attribute to use: alt/title. See init().
58  public $titleAttrib = 'title';
59 
60  // If TRUE, no context menu is rendered on icons. If set to "titlelink" the
61  // icon is linked as the title is.
65  public $ext_IconMode = false;
66 
67  // If set, the id of the mounts will be added to the internal ids array
71  public $addSelfId = 0;
72 
73  // Used if the tree is made of records (not folders for ex.)
77  public $title = 'no title';
78 
79  // If TRUE, a default title attribute showing the UID of the record is shown.
80  // This cannot be enabled by default because it will destroy many applications
81  // where another title attribute is in fact applied later.
86 
93  public $BE_USER = '';
94 
104  public $MOUNTS = null;
105 
112  public $table = '';
113 
119  public $parentField = 'pid';
120 
128  public $clause = '';
129 
137  public $orderByFields = '';
138 
146  public $fieldArray = ['uid', 'pid', 'title'];
147 
154  public $defaultList = 'uid,pid,tstamp,sorting,deleted,perms_userid,perms_groupid,perms_user,perms_group,perms_everybody,crdate,cruser_id';
155 
165  public $treeName = '';
166 
175  public $domIdPrefix = 'row';
176 
183  public $makeHTML = 1;
184 
190  public $setRecs = 0;
191 
198  public $subLevelID = '_SUB_LEVEL';
199 
200  // *********
201  // Internal
202  // *********
203  // For record trees:
204  // one-dim array of the uid's selected.
208  public $ids = [];
209 
210  // The hierarchy of element uids
214  public $ids_hierarchy = [];
215 
216  // The hierarchy of versioned element uids
220  public $orig_ids_hierarchy = [];
221 
222  // Temporary, internal array
226  public $buffer_idH = [];
227 
228  // For FOLDER trees:
229  // Special UIDs for folders (integer-hashes of paths)
233  public $specUIDmap = [];
234 
235  // For arrays:
236  // Holds the input data array
240  public $data = false;
241 
242  // Holds an index with references to the data array.
246  public $dataLookup = false;
247 
248  // For both types
249  // Tree is accumulated in this variable
253  public $tree = [];
254 
255  // Holds (session stored) information about which items in the tree are unfolded and which are not.
259  public $stored = [];
260 
261  // Points to the current mountpoint key
265  public $bank = 0;
266 
267  // Accumulates the displayed records.
271  public $recs = [];
272 
276  public function __construct()
277  {
278  $this->determineScriptUrl();
279  }
280 
284  protected function determineScriptUrl()
285  {
286  if ($routePath = GeneralUtility::_GP('route')) {
287  $router = GeneralUtility::makeInstance(Router::class);
288  $route = $router->match($routePath);
289  $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
290  $this->thisScript = (string)$uriBuilder->buildUriFromRoute($route->getOption('_identifier'));
291  } elseif ($moduleName = GeneralUtility::_GP('M')) {
292  $this->thisScript = BackendUtility::getModuleUrl($moduleName);
293  } else {
294  $this->thisScript = GeneralUtility::getIndpEnv('SCRIPT_NAME');
295  }
296  }
297 
301  protected function getThisScript()
302  {
303  return strpos($this->thisScript, '?') === false ? $this->thisScript . '?' : $this->thisScript . '&';
304  }
305 
313  public function init($clause = '', $orderByFields = '')
314  {
315  // Setting BE_USER by default
316  $this->BE_USER = $GLOBALS['BE_USER'];
317  // Setting clause
318  if ($clause) {
319  $this->clause = $clause;
320  }
321  if ($orderByFields) {
322  $this->orderByFields = $orderByFields;
323  }
324  if (!is_array($this->MOUNTS)) {
325  // Dummy
326  $this->MOUNTS = [0 => 0];
327  }
328  // Sets the tree name which is used to identify the tree, used for JavaScript and other things
329  $this->treeName = str_replace('_', '', $this->treeName ?: $this->table);
330  // Setting this to FALSE disables the use of array-trees by default
331  $this->data = false;
332  $this->dataLookup = false;
333  }
334 
342  public function addField($field, $noCheck = false)
343  {
344  if ($noCheck || is_array($GLOBALS['TCA'][$this->table]['columns'][$field]) || GeneralUtility::inList($this->defaultList, $field)) {
345  $this->fieldArray[] = $field;
346  }
347  }
348 
354  public function reset()
355  {
356  $this->tree = [];
357  $this->recs = [];
358  $this->ids = [];
359  $this->ids_hierarchy = [];
360  $this->orig_ids_hierarchy = [];
361  }
362 
363  /*******************************************
364  *
365  * output
366  *
367  *******************************************/
374  public function getBrowsableTree()
375  {
376  // Get stored tree structure AND updating it if needed according to incoming PM GET var.
377  $this->initializePositionSaving();
378  // Init done:
379  $treeArr = [];
380  // Traverse mounts:
381  foreach ($this->MOUNTS as $idx => $uid) {
382  // Set first:
383  $this->bank = $idx;
384  $isOpen = $this->stored[$idx][$uid] || $this->expandFirst;
385  // Save ids while resetting everything else.
386  $curIds = $this->ids;
387  $this->reset();
388  $this->ids = $curIds;
389  // Set PM icon for root of mount:
390  $cmd = $this->bank . '_' . ($isOpen ? '0_' : '1_') . $uid . '_' . $this->treeName;
391 
392  $firstHtml = $this->PM_ATagWrap('', $cmd, '', $isOpen);
393  // Preparing rootRec for the mount
394  if ($uid) {
395  $rootRec = $this->getRecord($uid);
396  if (is_array($rootRec)) {
397  $firstHtml .= $this->getIcon($rootRec);
398  }
399 
400  if ($this->ext_showPathAboveMounts) {
401  $mountPointPid = $rootRec['pid'];
402  if ($lastMountPointPid !== $mountPointPid) {
404  $this->tree[] = ['isMountPointPath' => true, 'title' => $title];
405  }
406  $lastMountPointPid = $mountPointPid;
407  }
408  } else {
409  // Artificial record for the tree root, id=0
410  $rootRec = $this->getRootRecord();
411  $firstHtml .= $this->getRootIcon($rootRec);
412  }
413  if (is_array($rootRec)) {
414  // In case it was swapped inside getRecord due to workspaces.
415  $uid = $rootRec['uid'];
416  // Add the root of the mount to ->tree
417  $this->tree[] = ['HTML' => $firstHtml, 'row' => $rootRec, 'hasSub' => $isOpen, 'bank' => $this->bank];
418  // If the mount is expanded, go down:
419  if ($isOpen) {
420  $depthData = '<span class="treeline-icon treeline-icon-clear"></span>';
421  if ($this->addSelfId) {
422  $this->ids[] = $uid;
423  }
424  $this->getTree($uid, 999, $depthData);
425  }
426  // Add tree:
427  $treeArr = array_merge($treeArr, $this->tree);
428  }
429  }
430  return $this->printTree($treeArr);
431  }
432 
439  public function printTree($treeArr = '')
440  {
441  $titleLen = (int)$this->BE_USER->uc['titleLen'];
442  if (!is_array($treeArr)) {
443  $treeArr = $this->tree;
444  }
445  $out = '';
446  $closeDepth = [];
447  foreach ($treeArr as $treeItem) {
448  $classAttr = '';
449  if ($treeItem['isFirst']) {
450  $out .= '<ul class="list-tree">';
451  }
452 
453  // Add CSS classes to the list item
454  if ($treeItem['hasSub']) {
455  $classAttr .= ' list-tree-control-open';
456  }
457 
458  $idAttr = htmlspecialchars($this->domIdPrefix . $this->getId($treeItem['row']) . '_' . $treeItem['bank']);
459  $out .= '
460  <li id="' . $idAttr . '"' . ($classAttr ? ' class="' . trim($classAttr) . '"' : '') . '>
461  <span class="list-tree-group">
462  <span class="list-tree-icon">' . $treeItem['HTML'] . '</span>
463  <span class="list-tree-title">' . $this->wrapTitle($this->getTitleStr($treeItem['row'], $titleLen), $treeItem['row'], $treeItem['bank']) . '</span>
464  </span>';
465 
466  if (!$treeItem['hasSub']) {
467  $out .= '</li>';
468  }
469 
470  // We have to remember if this is the last one
471  // on level X so the last child on level X+1 closes the <ul>-tag
472  if ($treeItem['isLast']) {
473  $closeDepth[$treeItem['invertedDepth']] = 1;
474  }
475  // If this is the last one and does not have subitems, we need to close
476  // the tree as long as the upper levels have last items too
477  if ($treeItem['isLast'] && !$treeItem['hasSub']) {
478  for ($i = $treeItem['invertedDepth']; $closeDepth[$i] == 1; $i++) {
479  $closeDepth[$i] = 0;
480  $out .= '</ul></li>';
481  }
482  }
483  }
484  $out = '<ul class="list-tree list-tree-root list-tree-root-clean">' . $out . '</ul>';
485  return $out;
486  }
487 
488  /*******************************************
489  *
490  * rendering parts
491  *
492  *******************************************/
505  public function PMicon($row, $a, $c, $nextCount, $isOpen)
506  {
507  if ($nextCount) {
508  $cmd = $this->bank . '_' . ($isOpen ? '0_' : '1_') . $row['uid'] . '_' . $this->treeName;
509  $bMark = $this->bank . '_' . $row['uid'];
510  return $this->PM_ATagWrap('', $cmd, $bMark, $isOpen);
511  } else {
512  return '';
513  }
514  }
515 
526  public function PM_ATagWrap($icon, $cmd, $bMark = '', $isOpen = false)
527  {
528  if ($this->thisScript) {
529  $anchor = $bMark ? '#' . $bMark : '';
530  $name = $bMark ? ' name="' . $bMark . '"' : '';
531  $aUrl = $this->getThisScript() . 'PM=' . $cmd . $anchor;
532  return '<a class="list-tree-control ' . ($isOpen ? 'list-tree-control-open' : 'list-tree-control-closed') . '" href="' . htmlspecialchars($aUrl) . '"' . $name . '><i class="fa"></i></a>';
533  } else {
534  return $icon;
535  }
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 
620  public function initializePositionSaving()
621  {
622  // Get stored tree structure:
623  $this->stored = unserialize($this->BE_USER->uc['browseTrees'][$this->treeName]);
624  // PM action
625  // (If an plus/minus icon has been clicked, the PM GET var is sent and we
626  // must update the stored positions in the tree):
627  // 0: mount key, 1: set/clear boolean, 2: item ID (cannot contain "_"), 3: treeName
628  $PM = explode('_', GeneralUtility::_GP('PM'));
629  if (count($PM) === 4 && $PM[3] == $this->treeName) {
630  if (isset($this->MOUNTS[$PM[0]])) {
631  // set
632  if ($PM[1]) {
633  $this->stored[$PM[0]][$PM[2]] = 1;
634  $this->savePosition();
635  } else {
636  unset($this->stored[$PM[0]][$PM[2]]);
637  $this->savePosition();
638  }
639  }
640  }
641  }
642 
650  public function savePosition()
651  {
652  $this->BE_USER->uc['browseTrees'][$this->treeName] = serialize($this->stored);
653  $this->BE_USER->writeUC();
654  }
655 
656  /******************************
657  *
658  * Functions that might be overwritten by extended classes
659  *
660  ********************************/
667  public function getRootIcon($rec)
668  {
669  $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
670  return $this->wrapIcon($iconFactory->getIcon('apps-pagetree-root', Icon::SIZE_SMALL)->render(), $rec);
671  }
672 
679  public function getIcon($row)
680  {
681  if (is_int($row)) {
682  $row = BackendUtility::getRecord($this->table, $row);
683  }
684  $title = $this->showDefaultTitleAttribute ? htmlspecialchars('UID: ' . $row['uid']) : $this->getTitleAttrib($row);
685  $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
686  $icon = '<span title="' . $title . '">' . $iconFactory->getIconForRecord($this->table, $row, Icon::SIZE_SMALL)->render() . '</span>';
687  return $this->wrapIcon($icon, $row);
688  }
689 
698  public function getTitleStr($row, $titleLen = 30)
699  {
700  $title = htmlspecialchars(GeneralUtility::fixed_lgd_cs($row['title'], $titleLen));
701  $title = trim($row['title']) === '' ? '<em>[' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.no_title')) . ']</em>' : $title;
702  return $title;
703  }
704 
712  public function getTitleAttrib($row)
713  {
714  return htmlspecialchars($row['title']);
715  }
716 
723  public function getId($row)
724  {
725  return $row['uid'];
726  }
727 
734  public function getJumpToParam($row)
735  {
736  return $this->getId($row);
737  }
738 
739  /********************************
740  *
741  * tree data buidling
742  *
743  ********************************/
753  public function getTree($uid, $depth = 999, $depthData = '')
754  {
755  // Buffer for id hierarchy is reset:
756  $this->buffer_idH = [];
757  // Init vars
758  $depth = (int)$depth;
759  $HTML = '';
760  $a = 0;
761  $res = $this->getDataInit($uid);
762  $c = $this->getDataCount($res);
763  $crazyRecursionLimiter = 999;
764  $idH = [];
765  // Traverse the records:
766  while ($crazyRecursionLimiter > 0 && ($row = $this->getDataNext($res))) {
767  $pageUid = ($this->table === 'pages') ? $row['uid'] : $row['pid'];
768  if (!$this->getBackendUser()->isInWebMount($pageUid)) {
769  // Current record is not within web mount => skip it
770  continue;
771  }
772 
773  $a++;
774  $crazyRecursionLimiter--;
775  $newID = $row['uid'];
776  if ($newID == 0) {
777  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);
778  }
779  // Reserve space.
780  $this->tree[] = [];
781  end($this->tree);
782  // Get the key for this space
783  $treeKey = key($this->tree);
784  // If records should be accumulated, do so
785  if ($this->setRecs) {
786  $this->recs[$row['uid']] = $row;
787  }
788  // Accumulate the id of the element in the internal arrays
789  $this->ids[] = ($idH[$row['uid']]['uid'] = $row['uid']);
790  $this->ids_hierarchy[$depth][] = $row['uid'];
791  $this->orig_ids_hierarchy[$depth][] = $row['_ORIG_uid'] ?: $row['uid'];
792 
793  // Make a recursive call to the next level
794  $nextLevelDepthData = $depthData . '<span class="treeline-icon treeline-icon-' . ($a === $c ? 'clear' : 'line') . '"></span>';
795  $hasSub = $this->expandNext($newID) && !$row['php_tree_stop'];
796  if ($depth > 1 && $hasSub) {
797  $nextCount = $this->getTree($newID, $depth - 1, $nextLevelDepthData);
798  if (!empty($this->buffer_idH)) {
799  $idH[$row['uid']]['subrow'] = $this->buffer_idH;
800  }
801  // Set "did expand" flag
802  $isOpen = 1;
803  } else {
804  $nextCount = $this->getCount($newID);
805  // Clear "did expand" flag
806  $isOpen = 0;
807  }
808  // Set HTML-icons, if any:
809  if ($this->makeHTML) {
810  $HTML = $this->PMicon($row, $a, $c, $nextCount, $isOpen) . $this->wrapStop($this->getIcon($row), $row);
811  }
812  // Finally, add the row/HTML content to the ->tree array in the reserved key.
813  $this->tree[$treeKey] = [
814  'row' => $row,
815  'HTML' => $HTML,
816  'invertedDepth' => $depth,
817  'depthData' => $depthData,
818  'bank' => $this->bank,
819  'hasSub' => $nextCount && $hasSub,
820  'isFirst' => $a === 1,
821  'isLast' => $a === $c,
822  ];
823  }
824 
825  $this->getDataFree($res);
826  $this->buffer_idH = $idH;
827  return $c;
828  }
829 
830  /********************************
831  *
832  * Data handling
833  * Works with records and arrays
834  *
835  ********************************/
843  public function getCount($uid)
844  {
845  if (is_array($this->data)) {
846  $res = $this->getDataInit($uid);
847  return $this->getDataCount($res);
848  } else {
849  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->table);
850  $queryBuilder->getRestrictions()
851  ->removeAll()
852  ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
853  ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
854  $count = $queryBuilder
855  ->count('uid')
856  ->from($this->table)
857  ->where(
858  $queryBuilder->expr()->eq(
859  $this->parentField,
860  $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
861  ),
863  )
864  ->execute()
865  ->fetchColumn();
866 
867  return (int)$count;
868  }
869  }
870 
876  public function getRootRecord()
877  {
878  return ['title' => $this->title, 'uid' => 0];
879  }
880 
889  public function getRecord($uid)
890  {
891  if (is_array($this->data)) {
892  return $this->dataLookup[$uid];
893  } else {
894  return BackendUtility::getRecordWSOL($this->table, $uid);
895  }
896  }
897 
908  public function getDataInit($parentId)
909  {
910  if (is_array($this->data)) {
911  if (!is_array($this->dataLookup[$parentId][$this->subLevelID])) {
912  $parentId = -1;
913  } else {
914  reset($this->dataLookup[$parentId][$this->subLevelID]);
915  }
916  return $parentId;
917  } else {
918  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->table);
919  $queryBuilder->getRestrictions()
920  ->removeAll()
921  ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
922  ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
923  $queryBuilder
924  ->select(...$this->fieldArray)
925  ->from($this->table)
926  ->where(
927  $queryBuilder->expr()->eq(
928  $this->parentField,
929  $queryBuilder->createNamedParameter($parentId, \PDO::PARAM_INT)
930  ),
932  );
933 
934  foreach (QueryHelper::parseOrderBy($this->orderByFields) as $orderPair) {
935  list($fieldName, $order) = $orderPair;
936  $queryBuilder->addOrderBy($fieldName, $order);
937  }
938 
939  return $queryBuilder->execute();
940  }
941  }
942 
951  public function getDataCount(&$res)
952  {
953  if (is_array($this->data)) {
954  return count($this->dataLookup[$res][$this->subLevelID]);
955  } else {
956  return $res->rowCount();
957  }
958  }
959 
969  public function getDataNext(&$res)
970  {
971  if (is_array($this->data)) {
972  if ($res < 0) {
973  $row = false;
974  } else {
975  list(, $row) = each($this->dataLookup[$res][$this->subLevelID]);
976  }
977  return $row;
978  } else {
979  while ($row = $res->fetch()) {
980  BackendUtility::workspaceOL($this->table, $row, $this->BE_USER->workspace, true);
981  if (is_array($row)) {
982  break;
983  }
984  }
985  return $row;
986  }
987  }
988 
996  public function getDataFree(&$res)
997  {
998  if (!is_array($this->data)) {
999  $res->closeCursor();
1000  }
1001  }
1002 
1015  public function setDataFromArray(&$dataArr, $traverse = false, $pid = 0)
1016  {
1017  if (!$traverse) {
1018  $this->data = &$dataArr;
1019  $this->dataLookup = [];
1020  // Add root
1021  $this->dataLookup[0][$this->subLevelID] = &$dataArr;
1022  }
1023  foreach ($dataArr as $uid => $val) {
1024  $dataArr[$uid]['uid'] = $uid;
1025  $dataArr[$uid]['pid'] = $pid;
1026  // Gives quick access to id's
1027  $this->dataLookup[$uid] = &$dataArr[$uid];
1028  if (is_array($val[$this->subLevelID])) {
1029  $this->setDataFromArray($dataArr[$uid][$this->subLevelID], true, $uid);
1030  }
1031  }
1032  }
1033 
1041  public function setDataFromTreeArray(&$treeArr, &$treeLookupArr)
1042  {
1043  $this->data = &$treeArr;
1044  $this->dataLookup = &$treeLookupArr;
1045  }
1046 
1050  protected function getLanguageService()
1051  {
1052  return $GLOBALS['LANG'];
1053  }
1054 
1058  protected function getBackendUser()
1059  {
1060  return $GLOBALS['BE_USER'];
1061  }
1062 }
PMicon($row, $a, $c, $nextCount, $isOpen)
static getRecordWSOL($table, $uid, $fields= '*', $where= '', $useDeleteClause=true, $unsetMovePointers=false)
setDataFromTreeArray(&$treeArr, &$treeLookupArr)
PM_ATagWrap($icon, $cmd, $bMark= '', $isOpen=false)
init($clause= '', $orderByFields= '')
static workspaceOL($table, &$row, $wsid=-99, $unsetMovePointers=false)
static getRecord($table, $uid, $fields= '*', $where= '', $useDeleteClause=true)
static linkThisScript(array $getParams=[])
getTree($uid, $depth=999, $depthData= '')
if(TYPO3_MODE=== 'BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']
static makeInstance($className,...$constructorArguments)
static stripLogicalOperatorPrefix(string $constraint)
setDataFromArray(&$dataArr, $traverse=false, $pid=0)