TYPO3  7.6
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 
26 
30 abstract class AbstractTreeView
31 {
32  // EXTERNAL, static:
33  // If set, the first element in the tree is always expanded.
37  public $expandFirst = 0;
38 
39  // If set, then ALL items will be expanded, regardless of stored settings.
43  public $expandAll = 0;
44 
45  // Holds the current script to reload to.
49  public $thisScript = '';
50 
51  // Which HTML attribute to use: alt/title. See init().
55  public $titleAttrib = 'title';
56 
57  // If TRUE, no context menu is rendered on icons. If set to "titlelink" the
58  // icon is linked as the title is.
62  public $ext_IconMode = false;
63 
64  // If set, the id of the mounts will be added to the internal ids array
68  public $addSelfId = 0;
69 
70  // Used if the tree is made of records (not folders for ex.)
74  public $title = 'no title';
75 
76  // If TRUE, a default title attribute showing the UID of the record is shown.
77  // This cannot be enabled by default because it will destroy many applications
78  // where another title attribute is in fact applied later.
83 
90  public $BE_USER = '';
91 
101  public $MOUNTS = null;
102 
109  public $table = '';
110 
116  public $parentField = 'pid';
117 
125  public $clause = '';
126 
134  public $orderByFields = '';
135 
143  public $fieldArray = array('uid', 'pid', 'title');
144 
151  public $defaultList = 'uid,pid,tstamp,sorting,deleted,perms_userid,perms_groupid,perms_user,perms_group,perms_everybody,crdate,cruser_id';
152 
162  public $treeName = '';
163 
172  public $domIdPrefix = 'row';
173 
180  public $makeHTML = 1;
181 
187  public $setRecs = 0;
188 
195  public $subLevelID = '_SUB_LEVEL';
196 
197  // *********
198  // Internal
199  // *********
200  // For record trees:
201  // one-dim array of the uid's selected.
205  public $ids = array();
206 
207  // The hierarchy of element uids
211  public $ids_hierarchy = array();
212 
213  // The hierarchy of versioned element uids
217  public $orig_ids_hierarchy = array();
218 
219  // Temporary, internal array
223  public $buffer_idH = array();
224 
225  // For FOLDER trees:
226  // Special UIDs for folders (integer-hashes of paths)
230  public $specUIDmap = array();
231 
232  // For arrays:
233  // Holds the input data array
237  public $data = false;
238 
239  // Holds an index with references to the data array.
243  public $dataLookup = false;
244 
245  // For both types
246  // Tree is accumulated in this variable
250  public $tree = array();
251 
252  // Holds (session stored) information about which items in the tree are unfolded and which are not.
256  public $stored = array();
257 
258  // Points to the current mountpoint key
262  public $bank = 0;
263 
264  // Accumulates the displayed records.
268  public $recs = array();
269 
273  public function __construct()
274  {
275  $this->determineScriptUrl();
276  }
277 
281  protected function determineScriptUrl()
282  {
283  if ($routePath = GeneralUtility::_GP('route')) {
284  $router = GeneralUtility::makeInstance(Router::class);
285  $route = $router->match($routePath);
286  $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
287  $this->thisScript = (string)$uriBuilder->buildUriFromRoute($route->getOption('_identifier'));
288  } elseif ($moduleName = GeneralUtility::_GP('M')) {
289  $this->thisScript = BackendUtility::getModuleUrl($moduleName);
290  } else {
291  $this->thisScript = GeneralUtility::getIndpEnv('SCRIPT_NAME');
292  }
293  }
294 
298  protected function getThisScript()
299  {
300  return strpos($this->thisScript, '?') === false ? $this->thisScript . '?' : $this->thisScript . '&';
301  }
302 
310  public function init($clause = '', $orderByFields = '')
311  {
312  // Setting BE_USER by default
313  $this->BE_USER = $GLOBALS['BE_USER'];
314  // Setting clause
315  if ($clause) {
316  $this->clause = $clause;
317  }
318  if ($orderByFields) {
319  $this->orderByFields = $orderByFields;
320  }
321  if (!is_array($this->MOUNTS)) {
322  // Dummy
323  $this->MOUNTS = array(0 => 0);
324  }
325  // Sets the tree name which is used to identify the tree, used for JavaScript and other things
326  $this->treeName = str_replace('_', '', $this->treeName ?: $this->table);
327  // Setting this to FALSE disables the use of array-trees by default
328  $this->data = false;
329  $this->dataLookup = false;
330  }
331 
339  public function addField($field, $noCheck = false)
340  {
341  if ($noCheck || is_array($GLOBALS['TCA'][$this->table]['columns'][$field]) || GeneralUtility::inList($this->defaultList, $field)) {
342  $this->fieldArray[] = $field;
343  }
344  }
345 
351  public function reset()
352  {
353  $this->tree = array();
354  $this->recs = array();
355  $this->ids = array();
356  $this->ids_hierarchy = array();
357  $this->orig_ids_hierarchy = array();
358  }
359 
360  /*******************************************
361  *
362  * output
363  *
364  *******************************************/
371  public function getBrowsableTree()
372  {
373  // Get stored tree structure AND updating it if needed according to incoming PM GET var.
374  $this->initializePositionSaving();
375  // Init done:
376  $treeArr = array();
377  // Traverse mounts:
378  foreach ($this->MOUNTS as $idx => $uid) {
379  // Set first:
380  $this->bank = $idx;
381  $isOpen = $this->stored[$idx][$uid] || $this->expandFirst;
382  // Save ids while resetting everything else.
383  $curIds = $this->ids;
384  $this->reset();
385  $this->ids = $curIds;
386  // Set PM icon for root of mount:
387  $cmd = $this->bank . '_' . ($isOpen ? '0_' : '1_') . $uid . '_' . $this->treeName;
388 
389  $firstHtml = $this->PM_ATagWrap('', $cmd, '', $isOpen);
390  // Preparing rootRec for the mount
391  if ($uid) {
392  $rootRec = $this->getRecord($uid);
393  $firstHtml .= $this->getIcon($rootRec);
394  } else {
395  // Artificial record for the tree root, id=0
396  $rootRec = $this->getRootRecord();
397  $firstHtml .= $this->getRootIcon($rootRec);
398  }
399  if (is_array($rootRec)) {
400  // In case it was swapped inside getRecord due to workspaces.
401  $uid = $rootRec['uid'];
402  // Add the root of the mount to ->tree
403  $this->tree[] = array('HTML' => $firstHtml, 'row' => $rootRec, 'hasSub' => $isOpen, 'bank' => $this->bank);
404  // If the mount is expanded, go down:
405  if ($isOpen) {
406  $depthData = '<span class="treeline-icon treeline-icon-clear"></span>';
407  if ($this->addSelfId) {
408  $this->ids[] = $uid;
409  }
410  $this->getTree($uid, 999, $depthData);
411  }
412  // Add tree:
413  $treeArr = array_merge($treeArr, $this->tree);
414  }
415  }
416  return $this->printTree($treeArr);
417  }
418 
425  public function printTree($treeArr = '')
426  {
427  $titleLen = (int)$this->BE_USER->uc['titleLen'];
428  if (!is_array($treeArr)) {
429  $treeArr = $this->tree;
430  }
431  $out = '';
432  $closeDepth = array();
433  foreach ($treeArr as $treeItem) {
434  $classAttr = '';
435  if ($treeItem['isFirst']) {
436  $out .= '<ul class="list-tree">';
437  }
438 
439  // Add CSS classes to the list item
440  if ($treeItem['hasSub']) {
441  $classAttr .= ' list-tree-control-open';
442  }
443 
444  $idAttr = htmlspecialchars($this->domIdPrefix . $this->getId($treeItem['row']) . '_' . $treeItem['bank']);
445  $out .= '
446  <li id="' . $idAttr . '"' . ($classAttr ? ' class="' . trim($classAttr) . '"' : '') . '>
447  <span class="list-tree-group">
448  <span class="list-tree-icon">' . $treeItem['HTML'] . '</span>
449  <span class="list-tree-title">' . $this->wrapTitle($this->getTitleStr($treeItem['row'], $titleLen), $treeItem['row'], $treeItem['bank']) . '</span>
450  </span>';
451 
452  if (!$treeItem['hasSub']) {
453  $out .= '</li>';
454  }
455 
456  // We have to remember if this is the last one
457  // on level X so the last child on level X+1 closes the <ul>-tag
458  if ($treeItem['isLast']) {
459  $closeDepth[$treeItem['invertedDepth']] = 1;
460  }
461  // If this is the last one and does not have subitems, we need to close
462  // the tree as long as the upper levels have last items too
463  if ($treeItem['isLast'] && !$treeItem['hasSub']) {
464  for ($i = $treeItem['invertedDepth']; $closeDepth[$i] == 1; $i++) {
465  $closeDepth[$i] = 0;
466  $out .= '</ul></li>';
467  }
468  }
469  }
470  $out = '<ul class="list-tree list-tree-root list-tree-root-clean">' . $out . '</ul>';
471  return $out;
472  }
473 
474  /*******************************************
475  *
476  * rendering parts
477  *
478  *******************************************/
491  public function PMicon($row, $a, $c, $nextCount, $isOpen)
492  {
493  if ($nextCount) {
494  $cmd = $this->bank . '_' . ($isOpen ? '0_' : '1_') . $row['uid'] . '_' . $this->treeName;
495  $bMark = $this->bank . '_' . $row['uid'];
496  return $this->PM_ATagWrap('', $cmd, $bMark, $isOpen);
497  } else {
498  return '';
499  }
500  }
501 
512  public function PM_ATagWrap($icon, $cmd, $bMark = '', $isOpen = false)
513  {
514  if ($this->thisScript) {
515  $anchor = $bMark ? '#' . $bMark : '';
516  $name = $bMark ? ' name="' . $bMark . '"' : '';
517  $aUrl = $this->getThisScript() . 'PM=' . $cmd . $anchor;
518  return '<a class="list-tree-control ' . ($isOpen ? 'list-tree-control-open' : 'list-tree-control-closed') . ' href="' . htmlspecialchars($aUrl) . '"' . $name . '><i class="fa"></i></a>';
519  } else {
520  return $icon;
521  }
522  }
523 
533  public function wrapTitle($title, $row, $bank = 0)
534  {
535  $aOnClick = 'return jumpTo(' . GeneralUtility::quoteJSvalue($this->getJumpToParam($row)) . ',this,' . GeneralUtility::quoteJSvalue($this->domIdPrefix . $this->getId($row)) . ',' . $bank . ');';
536  return '<a href="#" onclick="' . htmlspecialchars($aOnClick) . '">' . $title . '</a>';
537  }
538 
547  public function wrapIcon($icon, $row)
548  {
549  return $icon;
550  }
551 
559  public function addTagAttributes($icon, $attr)
560  {
561  return preg_replace('/ ?\\/?>$/', '', $icon) . ' ' . $attr . ' />';
562  }
563 
572  public function wrapStop($str, $row)
573  {
574  if ($row['php_tree_stop']) {
575  $str .= '<a href="' . htmlspecialchars(GeneralUtility::linkThisScript(array('setTempDBmount' => $row['uid']))) . '" class="text-danger">+</a> ';
576  }
577  return $str;
578  }
579 
580  /*******************************************
581  *
582  * tree handling
583  *
584  *******************************************/
595  public function expandNext($id)
596  {
597  return $this->stored[$this->bank][$id] || $this->expandAll ? 1 : 0;
598  }
599 
606  public function initializePositionSaving()
607  {
608  // Get stored tree structure:
609  $this->stored = unserialize($this->BE_USER->uc['browseTrees'][$this->treeName]);
610  // PM action
611  // (If an plus/minus icon has been clicked, the PM GET var is sent and we
612  // must update the stored positions in the tree):
613  // 0: mount key, 1: set/clear boolean, 2: item ID (cannot contain "_"), 3: treeName
614  $PM = explode('_', GeneralUtility::_GP('PM'));
615  if (count($PM) === 4 && $PM[3] == $this->treeName) {
616  if (isset($this->MOUNTS[$PM[0]])) {
617  // set
618  if ($PM[1]) {
619  $this->stored[$PM[0]][$PM[2]] = 1;
620  $this->savePosition();
621  } else {
622  unset($this->stored[$PM[0]][$PM[2]]);
623  $this->savePosition();
624  }
625  }
626  }
627  }
628 
636  public function savePosition()
637  {
638  $this->BE_USER->uc['browseTrees'][$this->treeName] = serialize($this->stored);
639  $this->BE_USER->writeUC();
640  }
641 
642  /******************************
643  *
644  * Functions that might be overwritten by extended classes
645  *
646  ********************************/
653  public function getRootIcon($rec)
654  {
655  $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
656  return $this->wrapIcon($iconFactory->getIcon('apps-pagetree-root', Icon::SIZE_SMALL)->render(), $rec);
657  }
658 
665  public function getIcon($row)
666  {
667  if (is_int($row)) {
668  $row = BackendUtility::getRecord($this->table, $row);
669  }
670  $title = $this->showDefaultTitleAttribute ? htmlspecialchars('UID: ' . $row['uid']) : $this->getTitleAttrib($row);
671  $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
672  $icon = '<span title="' . $title . '">' . $iconFactory->getIconForRecord($this->table, $row, Icon::SIZE_SMALL)->render() . '</span>';
673  return $this->wrapIcon($icon, $row);
674  }
675 
684  public function getTitleStr($row, $titleLen = 30)
685  {
686  $title = htmlspecialchars(GeneralUtility::fixed_lgd_cs($row['title'], $titleLen));
687  $title = trim($row['title']) === '' ? '<em>[' . $this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:labels.no_title', true) . ']</em>' : $title;
688  return $title;
689  }
690 
698  public function getTitleAttrib($row)
699  {
700  return htmlspecialchars($row['title']);
701  }
702 
709  public function getId($row)
710  {
711  return $row['uid'];
712  }
713 
720  public function getJumpToParam($row)
721  {
722  return $this->getId($row);
723  }
724 
725  /********************************
726  *
727  * tree data buidling
728  *
729  ********************************/
739  public function getTree($uid, $depth = 999, $depthData = '')
740  {
741  // Buffer for id hierarchy is reset:
742  $this->buffer_idH = array();
743  // Init vars
744  $depth = (int)$depth;
745  $HTML = '';
746  $a = 0;
747  $res = $this->getDataInit($uid);
748  $c = $this->getDataCount($res);
749  $crazyRecursionLimiter = 999;
750  $idH = array();
751  // Traverse the records:
752  while ($crazyRecursionLimiter > 0 && ($row = $this->getDataNext($res))) {
753  $pageUid = ($this->table === 'pages') ? $row['uid'] : $row['pid'];
754  if (!$this->getBackendUser()->isInWebMount($pageUid)) {
755  // Current record is not within web mount => skip it
756  continue;
757  }
758 
759  $a++;
760  $crazyRecursionLimiter--;
761  $newID = $row['uid'];
762  if ($newID == 0) {
763  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);
764  }
765  // Reserve space.
766  $this->tree[] = array();
767  end($this->tree);
768  // Get the key for this space
769  $treeKey = key($this->tree);
770  // If records should be accumulated, do so
771  if ($this->setRecs) {
772  $this->recs[$row['uid']] = $row;
773  }
774  // Accumulate the id of the element in the internal arrays
775  $this->ids[] = ($idH[$row['uid']]['uid'] = $row['uid']);
776  $this->ids_hierarchy[$depth][] = $row['uid'];
777  $this->orig_ids_hierarchy[$depth][] = $row['_ORIG_uid'] ?: $row['uid'];
778 
779  // Make a recursive call to the next level
780  $nextLevelDepthData = $depthData . '<span class="treeline-icon treeline-icon-' . ($a === $c ? 'clear' : 'line') . '"></span>';
781  $hasSub = $this->expandNext($newID) && !$row['php_tree_stop'];
782  if ($depth > 1 && $hasSub) {
783  $nextCount = $this->getTree($newID, $depth - 1, $nextLevelDepthData);
784  if (!empty($this->buffer_idH)) {
785  $idH[$row['uid']]['subrow'] = $this->buffer_idH;
786  }
787  // Set "did expand" flag
788  $isOpen = 1;
789  } else {
790  $nextCount = $this->getCount($newID);
791  // Clear "did expand" flag
792  $isOpen = 0;
793  }
794  // Set HTML-icons, if any:
795  if ($this->makeHTML) {
796  $HTML = $this->PMicon($row, $a, $c, $nextCount, $isOpen) . $this->wrapStop($this->getIcon($row), $row);
797  }
798  // Finally, add the row/HTML content to the ->tree array in the reserved key.
799  $this->tree[$treeKey] = array(
800  'row' => $row,
801  'HTML' => $HTML,
802  'invertedDepth' => $depth,
803  'depthData' => $depthData,
804  'bank' => $this->bank,
805  'hasSub' => $nextCount && $hasSub,
806  'isFirst' => $a === 1,
807  'isLast' => $a === $c,
808  );
809  }
810 
811  $this->getDataFree($res);
812  $this->buffer_idH = $idH;
813  return $c;
814  }
815 
816  /********************************
817  *
818  * Data handling
819  * Works with records and arrays
820  *
821  ********************************/
829  public function getCount($uid)
830  {
831  if (is_array($this->data)) {
832  $res = $this->getDataInit($uid);
833  return $this->getDataCount($res);
834  } else {
835  $db = $this->getDatabaseConnection();
836  $where = $this->parentField . '=' . $db->fullQuoteStr($uid, $this->table) . BackendUtility::deleteClause($this->table) . BackendUtility::versioningPlaceholderClause($this->table) . $this->clause;
837  return $db->exec_SELECTcountRows('uid', $this->table, $where);
838  }
839  }
840 
846  public function getRootRecord()
847  {
848  return array('title' => $this->title, 'uid' => 0);
849  }
850 
859  public function getRecord($uid)
860  {
861  if (is_array($this->data)) {
862  return $this->dataLookup[$uid];
863  } else {
864  return BackendUtility::getRecordWSOL($this->table, $uid);
865  }
866  }
867 
878  public function getDataInit($parentId)
879  {
880  if (is_array($this->data)) {
881  if (!is_array($this->dataLookup[$parentId][$this->subLevelID])) {
882  $parentId = -1;
883  } else {
884  reset($this->dataLookup[$parentId][$this->subLevelID]);
885  }
886  return $parentId;
887  } else {
888  $db = $this->getDatabaseConnection();
889  $where = $this->parentField . '=' . $db->fullQuoteStr($parentId, $this->table) . BackendUtility::deleteClause($this->table) . BackendUtility::versioningPlaceholderClause($this->table) . $this->clause;
890  return $db->exec_SELECTquery(implode(',', $this->fieldArray), $this->table, $where, '', $this->orderByFields);
891  }
892  }
893 
902  public function getDataCount(&$res)
903  {
904  if (is_array($this->data)) {
905  return count($this->dataLookup[$res][$this->subLevelID]);
906  } else {
907  return $this->getDatabaseConnection()->sql_num_rows($res);
908  }
909  }
910 
920  public function getDataNext(&$res)
921  {
922  if (is_array($this->data)) {
923  if ($res < 0) {
924  $row = false;
925  } else {
926  list(, $row) = each($this->dataLookup[$res][$this->subLevelID]);
927  }
928  return $row;
929  } else {
930  while ($row = @$this->getDatabaseConnection()->sql_fetch_assoc($res)) {
931  BackendUtility::workspaceOL($this->table, $row, $this->BE_USER->workspace, true);
932  if (is_array($row)) {
933  break;
934  }
935  }
936  return $row;
937  }
938  }
939 
947  public function getDataFree(&$res)
948  {
949  if (!is_array($this->data)) {
950  $this->getDatabaseConnection()->sql_free_result($res);
951  }
952  }
953 
966  public function setDataFromArray(&$dataArr, $traverse = false, $pid = 0)
967  {
968  if (!$traverse) {
969  $this->data = &$dataArr;
970  $this->dataLookup = array();
971  // Add root
972  $this->dataLookup[0][$this->subLevelID] = &$dataArr;
973  }
974  foreach ($dataArr as $uid => $val) {
975  $dataArr[$uid]['uid'] = $uid;
976  $dataArr[$uid]['pid'] = $pid;
977  // Gives quick access to id's
978  $this->dataLookup[$uid] = &$dataArr[$uid];
979  if (is_array($val[$this->subLevelID])) {
980  $this->setDataFromArray($dataArr[$uid][$this->subLevelID], true, $uid);
981  }
982  }
983  }
984 
992  public function setDataFromTreeArray(&$treeArr, &$treeLookupArr)
993  {
994  $this->data = &$treeArr;
995  $this->dataLookup = &$treeLookupArr;
996  }
997 
1001  protected function getLanguageService()
1002  {
1003  return $GLOBALS['LANG'];
1004  }
1005 
1009  protected function getBackendUser()
1010  {
1011  return $GLOBALS['BE_USER'];
1012  }
1013 
1017  protected function getDatabaseConnection()
1018  {
1019  return $GLOBALS['TYPO3_DB'];
1020  }
1021 }