TYPO3 CMS  TYPO3_7-6
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 
27 
31 abstract class AbstractTreeView
32 {
33  // EXTERNAL, static:
34  // If set, the first element in the tree is always expanded.
38  public $expandFirst = 0;
39 
40  // If set, then ALL items will be expanded, regardless of stored settings.
44  public $expandAll = 0;
45 
46  // Holds the current script to reload to.
50  public $thisScript = '';
51 
52  // Which HTML attribute to use: alt/title. See init().
56  public $titleAttrib = 'title';
57 
58  // If TRUE, no context menu is rendered on icons. If set to "titlelink" the
59  // icon is linked as the title is.
63  public $ext_IconMode = false;
64 
68  public $ext_showPathAboveMounts = false;
69 
70  // If set, the id of the mounts will be added to the internal ids array
74  public $addSelfId = 0;
75 
76  // Used if the tree is made of records (not folders for ex.)
80  public $title = 'no title';
81 
82  // If TRUE, a default title attribute showing the UID of the record is shown.
83  // This cannot be enabled by default because it will destroy many applications
84  // where another title attribute is in fact applied later.
89 
96  public $BE_USER = '';
97 
107  public $MOUNTS = null;
108 
115  public $table = '';
116 
122  public $parentField = 'pid';
123 
131  public $clause = '';
132 
140  public $orderByFields = '';
141 
149  public $fieldArray = ['uid', 'pid', 'title'];
150 
157  public $defaultList = 'uid,pid,tstamp,sorting,deleted,perms_userid,perms_groupid,perms_user,perms_group,perms_everybody,crdate,cruser_id';
158 
168  public $treeName = '';
169 
178  public $domIdPrefix = 'row';
179 
186  public $makeHTML = 1;
187 
193  public $setRecs = 0;
194 
201  public $subLevelID = '_SUB_LEVEL';
202 
203  // *********
204  // Internal
205  // *********
206  // For record trees:
207  // one-dim array of the uid's selected.
211  public $ids = [];
212 
213  // The hierarchy of element uids
217  public $ids_hierarchy = [];
218 
219  // The hierarchy of versioned element uids
223  public $orig_ids_hierarchy = [];
224 
225  // Temporary, internal array
229  public $buffer_idH = [];
230 
231  // For FOLDER trees:
232  // Special UIDs for folders (integer-hashes of paths)
236  public $specUIDmap = [];
237 
238  // For arrays:
239  // Holds the input data array
243  public $data = false;
244 
245  // Holds an index with references to the data array.
249  public $dataLookup = false;
250 
251  // For both types
252  // Tree is accumulated in this variable
256  public $tree = [];
257 
258  // Holds (session stored) information about which items in the tree are unfolded and which are not.
262  public $stored = [];
263 
264  // Points to the current mountpoint key
268  public $bank = 0;
269 
270  // Accumulates the displayed records.
274  public $recs = [];
275 
279  public function __construct()
280  {
281  $this->determineScriptUrl();
282  }
283 
287  protected function determineScriptUrl()
288  {
289  if ($routePath = GeneralUtility::_GP('route')) {
290  $router = GeneralUtility::makeInstance(Router::class);
291  $route = $router->match($routePath);
292  $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
293  $this->thisScript = (string)$uriBuilder->buildUriFromRoute($route->getOption('_identifier'));
294  } elseif ($moduleName = GeneralUtility::_GP('M')) {
295  $this->thisScript = BackendUtility::getModuleUrl($moduleName);
296  } else {
297  $this->thisScript = GeneralUtility::getIndpEnv('SCRIPT_NAME');
298  }
299  }
300 
304  protected function getThisScript()
305  {
306  return strpos($this->thisScript, '?') === false ? $this->thisScript . '?' : $this->thisScript . '&';
307  }
308 
316  public function init($clause = '', $orderByFields = '')
317  {
318  // Setting BE_USER by default
319  $this->BE_USER = $GLOBALS['BE_USER'];
320  // Setting clause
321  if ($clause) {
322  $this->clause = $clause;
323  }
324  if ($orderByFields) {
325  $this->orderByFields = $orderByFields;
326  }
327  if (!is_array($this->MOUNTS)) {
328  // Dummy
329  $this->MOUNTS = [0 => 0];
330  }
331  // Sets the tree name which is used to identify the tree, used for JavaScript and other things
332  $this->treeName = str_replace('_', '', $this->treeName ?: $this->table);
333  // Setting this to FALSE disables the use of array-trees by default
334  $this->data = false;
335  $this->dataLookup = false;
336  }
337 
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 
357  public function reset()
358  {
359  $this->tree = [];
360  $this->recs = [];
361  $this->ids = [];
362  $this->ids_hierarchy = [];
363  $this->orig_ids_hierarchy = [];
364  }
365 
366  /*******************************************
367  *
368  * output
369  *
370  *******************************************/
377  public function getBrowsableTree()
378  {
379  // Get stored tree structure AND updating it if needed according to incoming PM GET var.
380  $this->initializePositionSaving();
381  // Init done:
382  $lastMountPointPid = 0;
383  $treeArr = [];
384  // Traverse mounts:
385  foreach ($this->MOUNTS as $idx => $uid) {
386  // Set first:
387  $this->bank = $idx;
388  $isOpen = $this->stored[$idx][$uid] || $this->expandFirst;
389  // Save ids while resetting everything else.
390  $curIds = $this->ids;
391  $this->reset();
392  $this->ids = $curIds;
393  // Set PM icon for root of mount:
394  $cmd = $this->bank . '_' . ($isOpen ? '0_' : '1_') . $uid . '_' . $this->treeName;
395 
396  $firstHtml = $this->PM_ATagWrap('', $cmd, '', $isOpen);
397  // Preparing rootRec for the mount
398  if ($uid) {
399  $rootRec = $this->getRecord($uid);
400  if (is_array($rootRec)) {
401  $firstHtml .= $this->getIcon($rootRec);
402  }
403 
404  if ($this->ext_showPathAboveMounts) {
405  $mountPointPid = $rootRec['pid'];
406  if ($lastMountPointPid !== $mountPointPid) {
407  $title = Commands::getMountPointPath($mountPointPid);
408  $this->tree[] = ['isMountPointPath' => true, 'title' => $title];
409  }
410  $lastMountPointPid = $mountPointPid;
411  }
412  } else {
413  // Artificial record for the tree root, id=0
414  $rootRec = $this->getRootRecord();
415  $firstHtml .= $this->getRootIcon($rootRec);
416  }
417  if (is_array($rootRec)) {
418  // In case it was swapped inside getRecord due to workspaces.
419  $uid = $rootRec['uid'];
420  // Add the root of the mount to ->tree
421  $this->tree[] = ['HTML' => $firstHtml, 'row' => $rootRec, 'hasSub' => $isOpen, 'bank' => $this->bank];
422  // If the mount is expanded, go down:
423  if ($isOpen) {
424  $depthData = '<span class="treeline-icon treeline-icon-clear"></span>';
425  if ($this->addSelfId) {
426  $this->ids[] = $uid;
427  }
428  $this->getTree($uid, 999, $depthData);
429  }
430  // Add tree:
431  $treeArr = array_merge($treeArr, $this->tree);
432  }
433  }
434  return $this->printTree($treeArr);
435  }
436 
443  public function printTree($treeArr = '')
444  {
445  $titleLen = (int)$this->BE_USER->uc['titleLen'];
446  if (!is_array($treeArr)) {
447  $treeArr = $this->tree;
448  }
449  $out = '';
450  $closeDepth = [];
451  foreach ($treeArr as $treeItem) {
452  $classAttr = '';
453  if ($treeItem['isFirst']) {
454  $out .= '<ul class="list-tree">';
455  }
456 
457  // Add CSS classes to the list item
458  if ($treeItem['hasSub']) {
459  $classAttr .= ' list-tree-control-open';
460  }
461 
462  $idAttr = htmlspecialchars($this->domIdPrefix . $this->getId($treeItem['row']) . '_' . $treeItem['bank']);
463  $out .= '
464  <li id="' . $idAttr . '"' . ($classAttr ? ' class="' . trim($classAttr) . '"' : '') . '>
465  <span class="list-tree-group">
466  <span class="list-tree-icon">' . $treeItem['HTML'] . '</span>
467  <span class="list-tree-title">' . $this->wrapTitle($this->getTitleStr($treeItem['row'], $titleLen), $treeItem['row'], $treeItem['bank']) . '</span>
468  </span>';
469 
470  if (!$treeItem['hasSub']) {
471  $out .= '</li>';
472  }
473 
474  // We have to remember if this is the last one
475  // on level X so the last child on level X+1 closes the <ul>-tag
476  if ($treeItem['isLast']) {
477  $closeDepth[$treeItem['invertedDepth']] = 1;
478  }
479  // If this is the last one and does not have subitems, we need to close
480  // the tree as long as the upper levels have last items too
481  if ($treeItem['isLast'] && !$treeItem['hasSub']) {
482  for ($i = $treeItem['invertedDepth']; $closeDepth[$i] == 1; $i++) {
483  $closeDepth[$i] = 0;
484  $out .= '</ul></li>';
485  }
486  }
487  }
488  $out = '<ul class="list-tree list-tree-root list-tree-root-clean">' . $out . '</ul>';
489  return $out;
490  }
491 
492  /*******************************************
493  *
494  * rendering parts
495  *
496  *******************************************/
509  public function PMicon($row, $a, $c, $nextCount, $isOpen)
510  {
511  if ($nextCount) {
512  $cmd = $this->bank . '_' . ($isOpen ? '0_' : '1_') . $row['uid'] . '_' . $this->treeName;
513  $bMark = $this->bank . '_' . $row['uid'];
514  return $this->PM_ATagWrap('', $cmd, $bMark, $isOpen);
515  } else {
516  return '';
517  }
518  }
519 
530  public function PM_ATagWrap($icon, $cmd, $bMark = '', $isOpen = false)
531  {
532  if ($this->thisScript) {
533  $anchor = $bMark ? '#' . $bMark : '';
534  $name = $bMark ? ' name="' . $bMark . '"' : '';
535  $aUrl = $this->getThisScript() . 'PM=' . $cmd . $anchor;
536  return '<a class="list-tree-control ' . ($isOpen ? 'list-tree-control-open' : 'list-tree-control-closed') . '" href="' . htmlspecialchars($aUrl) . '"' . $name . '><i class="fa"></i></a>';
537  } else {
538  return $icon;
539  }
540  }
541 
551  public function wrapTitle($title, $row, $bank = 0)
552  {
553  $aOnClick = 'return jumpTo(' . GeneralUtility::quoteJSvalue($this->getJumpToParam($row)) . ',this,' . GeneralUtility::quoteJSvalue($this->domIdPrefix . $this->getId($row)) . ',' . $bank . ');';
554  return '<a href="#" onclick="' . htmlspecialchars($aOnClick) . '">' . $title . '</a>';
555  }
556 
565  public function wrapIcon($icon, $row)
566  {
567  return $icon;
568  }
569 
577  public function addTagAttributes($icon, $attr)
578  {
579  return preg_replace('/ ?\\/?>$/', '', $icon) . ' ' . $attr . ' />';
580  }
581 
590  public function wrapStop($str, $row)
591  {
592  if ($row['php_tree_stop']) {
593  $str .= '<a href="' . htmlspecialchars(GeneralUtility::linkThisScript(['setTempDBmount' => $row['uid']])) . '" class="text-danger">+</a> ';
594  }
595  return $str;
596  }
597 
598  /*******************************************
599  *
600  * tree handling
601  *
602  *******************************************/
613  public function expandNext($id)
614  {
615  return $this->stored[$this->bank][$id] || $this->expandAll ? 1 : 0;
616  }
617 
624  public function initializePositionSaving()
625  {
626  // Get stored tree structure:
627  $this->stored = unserialize($this->BE_USER->uc['browseTrees'][$this->treeName]);
628  // PM action
629  // (If an plus/minus icon has been clicked, the PM GET var is sent and we
630  // must update the stored positions in the tree):
631  // 0: mount key, 1: set/clear boolean, 2: item ID (cannot contain "_"), 3: treeName
632  $PM = explode('_', GeneralUtility::_GP('PM'));
633  if (count($PM) === 4 && $PM[3] == $this->treeName) {
634  if (isset($this->MOUNTS[$PM[0]])) {
635  // set
636  if ($PM[1]) {
637  $this->stored[$PM[0]][$PM[2]] = 1;
638  $this->savePosition();
639  } else {
640  unset($this->stored[$PM[0]][$PM[2]]);
641  $this->savePosition();
642  }
643  }
644  }
645  }
646 
654  public function savePosition()
655  {
656  $this->BE_USER->uc['browseTrees'][$this->treeName] = serialize($this->stored);
657  $this->BE_USER->writeUC();
658  }
659 
660  /******************************
661  *
662  * Functions that might be overwritten by extended classes
663  *
664  ********************************/
671  public function getRootIcon($rec)
672  {
673  $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
674  return $this->wrapIcon($iconFactory->getIcon('apps-pagetree-root', Icon::SIZE_SMALL)->render(), $rec);
675  }
676 
683  public function getIcon($row)
684  {
685  if (is_int($row)) {
686  $row = BackendUtility::getRecord($this->table, $row);
687  }
688  $title = $this->showDefaultTitleAttribute ? htmlspecialchars('UID: ' . $row['uid']) : $this->getTitleAttrib($row);
689  $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
690  $icon = '<span title="' . $title . '">' . $iconFactory->getIconForRecord($this->table, $row, Icon::SIZE_SMALL)->render() . '</span>';
691  return $this->wrapIcon($icon, $row);
692  }
693 
702  public function getTitleStr($row, $titleLen = 30)
703  {
704  $title = htmlspecialchars(GeneralUtility::fixed_lgd_cs($row['title'], $titleLen));
705  $title = trim($row['title']) === '' ? '<em>[' . $this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:labels.no_title', true) . ']</em>' : $title;
706  return $title;
707  }
708 
716  public function getTitleAttrib($row)
717  {
718  return htmlspecialchars($row['title']);
719  }
720 
727  public function getId($row)
728  {
729  return $row['uid'];
730  }
731 
738  public function getJumpToParam($row)
739  {
740  return $this->getId($row);
741  }
742 
743  /********************************
744  *
745  * tree data buidling
746  *
747  ********************************/
757  public function getTree($uid, $depth = 999, $depthData = '')
758  {
759  // Buffer for id hierarchy is reset:
760  $this->buffer_idH = [];
761  // Init vars
762  $depth = (int)$depth;
763  $HTML = '';
764  $a = 0;
765  $res = $this->getDataInit($uid);
766  $c = $this->getDataCount($res);
767  $crazyRecursionLimiter = 999;
768  $idH = [];
769  // Traverse the records:
770  while ($crazyRecursionLimiter > 0 && ($row = $this->getDataNext($res))) {
771  $pageUid = ($this->table === 'pages') ? $row['uid'] : $row['pid'];
772  if (!$this->getBackendUser()->isInWebMount($pageUid)) {
773  // Current record is not within web mount => skip it
774  continue;
775  }
776 
777  $a++;
778  $crazyRecursionLimiter--;
779  $newID = $row['uid'];
780  if ($newID == 0) {
781  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);
782  }
783  // Reserve space.
784  $this->tree[] = [];
785  end($this->tree);
786  // Get the key for this space
787  $treeKey = key($this->tree);
788  // If records should be accumulated, do so
789  if ($this->setRecs) {
790  $this->recs[$row['uid']] = $row;
791  }
792  // Accumulate the id of the element in the internal arrays
793  $this->ids[] = ($idH[$row['uid']]['uid'] = $row['uid']);
794  $this->ids_hierarchy[$depth][] = $row['uid'];
795  $this->orig_ids_hierarchy[$depth][] = $row['_ORIG_uid'] ?: $row['uid'];
796 
797  // Make a recursive call to the next level
798  $nextLevelDepthData = $depthData . '<span class="treeline-icon treeline-icon-' . ($a === $c ? 'clear' : 'line') . '"></span>';
799  $hasSub = $this->expandNext($newID) && !$row['php_tree_stop'];
800  if ($depth > 1 && $hasSub) {
801  $nextCount = $this->getTree($newID, $depth - 1, $nextLevelDepthData);
802  if (!empty($this->buffer_idH)) {
803  $idH[$row['uid']]['subrow'] = $this->buffer_idH;
804  }
805  // Set "did expand" flag
806  $isOpen = 1;
807  } else {
808  $nextCount = $this->getCount($newID);
809  // Clear "did expand" flag
810  $isOpen = 0;
811  }
812  // Set HTML-icons, if any:
813  if ($this->makeHTML) {
814  $HTML = $this->PMicon($row, $a, $c, $nextCount, $isOpen) . $this->wrapStop($this->getIcon($row), $row);
815  }
816  // Finally, add the row/HTML content to the ->tree array in the reserved key.
817  $this->tree[$treeKey] = [
818  'row' => $row,
819  'HTML' => $HTML,
820  'invertedDepth' => $depth,
821  'depthData' => $depthData,
822  'bank' => $this->bank,
823  'hasSub' => $nextCount && $hasSub,
824  'isFirst' => $a === 1,
825  'isLast' => $a === $c,
826  ];
827  }
828 
829  $this->getDataFree($res);
830  $this->buffer_idH = $idH;
831  return $c;
832  }
833 
834  /********************************
835  *
836  * Data handling
837  * Works with records and arrays
838  *
839  ********************************/
847  public function getCount($uid)
848  {
849  if (is_array($this->data)) {
850  $res = $this->getDataInit($uid);
851  return $this->getDataCount($res);
852  } else {
853  $db = $this->getDatabaseConnection();
854  $where = $this->parentField . '=' . $db->fullQuoteStr($uid, $this->table) . BackendUtility::deleteClause($this->table) . BackendUtility::versioningPlaceholderClause($this->table) . $this->clause;
855  return $db->exec_SELECTcountRows('uid', $this->table, $where);
856  }
857  }
858 
864  public function getRootRecord()
865  {
866  return ['title' => $this->title, 'uid' => 0];
867  }
868 
877  public function getRecord($uid)
878  {
879  if (is_array($this->data)) {
880  return $this->dataLookup[$uid];
881  } else {
882  return BackendUtility::getRecordWSOL($this->table, $uid);
883  }
884  }
885 
896  public function getDataInit($parentId)
897  {
898  if (is_array($this->data)) {
899  if (!is_array($this->dataLookup[$parentId][$this->subLevelID])) {
900  $parentId = -1;
901  } else {
902  reset($this->dataLookup[$parentId][$this->subLevelID]);
903  }
904  return $parentId;
905  } else {
906  $db = $this->getDatabaseConnection();
907  $where = $this->parentField . '=' . $db->fullQuoteStr($parentId, $this->table) . BackendUtility::deleteClause($this->table) . BackendUtility::versioningPlaceholderClause($this->table) . $this->clause;
908  return $db->exec_SELECTquery(implode(',', $this->fieldArray), $this->table, $where, '', $this->orderByFields);
909  }
910  }
911 
920  public function getDataCount(&$res)
921  {
922  if (is_array($this->data)) {
923  return count($this->dataLookup[$res][$this->subLevelID]);
924  } else {
925  return $this->getDatabaseConnection()->sql_num_rows($res);
926  }
927  }
928 
938  public function getDataNext(&$res)
939  {
940  if (is_array($this->data)) {
941  if ($res < 0) {
942  $row = false;
943  } else {
944  $key = key($this->dataLookup[$res][$this->subLevelID]);
945  next($this->dataLookup[$res][$this->subLevelID]);
946  $row = $this->dataLookup[$res][$this->subLevelID][$key];
947  }
948  return $row;
949  } else {
950  while ($row = @$this->getDatabaseConnection()->sql_fetch_assoc($res)) {
951  BackendUtility::workspaceOL($this->table, $row, $this->BE_USER->workspace, true);
952  if (is_array($row)) {
953  break;
954  }
955  }
956  return $row;
957  }
958  }
959 
967  public function getDataFree(&$res)
968  {
969  if (!is_array($this->data)) {
970  $this->getDatabaseConnection()->sql_free_result($res);
971  }
972  }
973 
986  public function setDataFromArray(&$dataArr, $traverse = false, $pid = 0)
987  {
988  if (!$traverse) {
989  $this->data = &$dataArr;
990  $this->dataLookup = [];
991  // Add root
992  $this->dataLookup[0][$this->subLevelID] = &$dataArr;
993  }
994  foreach ($dataArr as $uid => $val) {
995  $dataArr[$uid]['uid'] = $uid;
996  $dataArr[$uid]['pid'] = $pid;
997  // Gives quick access to id's
998  $this->dataLookup[$uid] = &$dataArr[$uid];
999  if (is_array($val[$this->subLevelID])) {
1000  $this->setDataFromArray($dataArr[$uid][$this->subLevelID], true, $uid);
1001  }
1002  }
1003  }
1004 
1012  public function setDataFromTreeArray(&$treeArr, &$treeLookupArr)
1013  {
1014  $this->data = &$treeArr;
1015  $this->dataLookup = &$treeLookupArr;
1016  }
1017 
1021  protected function getLanguageService()
1022  {
1023  return $GLOBALS['LANG'];
1024  }
1025 
1029  protected function getBackendUser()
1030  {
1031  return $GLOBALS['BE_USER'];
1032  }
1033 
1037  protected function getDatabaseConnection()
1038  {
1039  return $GLOBALS['TYPO3_DB'];
1040  }
1041 }
if(!defined("DB_ERROR")) define("DB_ERROR"
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)
PMicon($row, $a, $c, $nextCount, $isOpen)
getTree($uid, $depth=999, $depthData='')
setDataFromTreeArray(&$treeArr, &$treeLookupArr)
static fixed_lgd_cs($string, $chars, $appendString='...')
$uid
Definition: server.php:38
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']
static getRecordWSOL($table, $uid, $fields=' *', $where='', $useDeleteClause=true, $unsetMovePointers=false)
static deleteClause($table, $tableAlias='')