[BUGFIX] Show mountpoint path in record + link browsers if enabled in UserTS
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Tree / View / AbstractTreeView.php
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
17 use TYPO3\CMS\Backend\Routing\Router;
18 use TYPO3\CMS\Backend\Routing\UriBuilder;
19 use TYPO3\CMS\Backend\Utility\BackendUtility;
20 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
21 use TYPO3\CMS\Core\Database\ConnectionPool;
22 use TYPO3\CMS\Core\Database\Query\QueryHelper;
23 use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction;
24 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
25 use TYPO3\CMS\Core\Imaging\Icon;
26 use TYPO3\CMS\Core\Imaging\IconFactory;
27 use TYPO3\CMS\Core\Utility\GeneralUtility;
28 use TYPO3\CMS\Lang\LanguageService;
29
30 /**
31 * Base class for creating a browsable array/page/folder tree in HTML
32 */
33 abstract class AbstractTreeView
34 {
35 // EXTERNAL, static:
36 // If set, the first element in the tree is always expanded.
37 /**
38 * @var int
39 */
40 public $expandFirst = 0;
41
42 // If set, then ALL items will be expanded, regardless of stored settings.
43 /**
44 * @var int
45 */
46 public $expandAll = 0;
47
48 // Holds the current script to reload to.
49 /**
50 * @var string
51 */
52 public $thisScript = '';
53
54 // Which HTML attribute to use: alt/title. See init().
55 /**
56 * @var string
57 */
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.
62 /**
63 * @var bool
64 */
65 public $ext_IconMode = false;
66
67 // If set, the id of the mounts will be added to the internal ids array
68 /**
69 * @var int
70 */
71 public $addSelfId = 0;
72
73 // Used if the tree is made of records (not folders for ex.)
74 /**
75 * @var string
76 */
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.
82 /**
83 * @var bool
84 */
85 public $showDefaultTitleAttribute = false;
86
87 /**
88 * Needs to be initialized with $GLOBALS['BE_USER']
89 * Done by default in init()
90 *
91 * @var \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
92 */
93 public $BE_USER = '';
94
95 /**
96 * Needs to be initialized with e.g. $GLOBALS['BE_USER']->returnWebmounts()
97 * Default setting in init() is 0 => 0
98 * The keys are mount-ids (can be anything basically) and the
99 * values are the ID of the root element (COULD be zero or anything else.
100 * For pages that would be the uid of the page, zero for the pagetree root.)
101 *
102 * @var array|NULL
103 */
104 public $MOUNTS = null;
105
106 /**
107 * Database table to get the tree data from.
108 * Leave blank if data comes from an array.
109 *
110 * @var string
111 */
112 public $table = '';
113
114 /**
115 * Defines the field of $table which is the parent id field (like pid for table pages).
116 *
117 * @var string
118 */
119 public $parentField = 'pid';
120
121 /**
122 * WHERE clause used for selecting records for the tree. Is set by function init.
123 * Only makes sense when $this->table is set.
124 *
125 * @see init()
126 * @var string
127 */
128 public $clause = '';
129
130 /**
131 * Field for ORDER BY. Is set by function init.
132 * Only makes sense when $this->table is set.
133 *
134 * @see init()
135 * @var string
136 */
137 public $orderByFields = '';
138
139 /**
140 * Default set of fields selected from the tree table.
141 * Make SURE that these fields names listed herein are actually possible to select from $this->table (if that variable is set to a TCA table name)
142 *
143 * @see addField()
144 * @var array
145 */
146 public $fieldArray = ['uid', 'pid', 'title'];
147
148 /**
149 * List of other fields which are ALLOWED to set (here, based on the "pages" table!)
150 *
151 * @see addField()
152 * @var array
153 */
154 public $defaultList = 'uid,pid,tstamp,sorting,deleted,perms_userid,perms_groupid,perms_user,perms_group,perms_everybody,crdate,cruser_id';
155
156 /**
157 * Unique name for the tree.
158 * Used as key for storing the tree into the BE users settings.
159 * Used as key to pass parameters in links.
160 * MUST NOT contain underscore chars.
161 * etc.
162 *
163 * @var string
164 */
165 public $treeName = '';
166
167 /**
168 * A prefix for table cell id's which will be wrapped around an item.
169 * Can be used for highlighting by JavaScript.
170 * Needs to be unique if multiple trees are on one HTML page.
171 *
172 * @see printTree()
173 * @var string
174 */
175 public $domIdPrefix = 'row';
176
177 /**
178 * If TRUE, HTML code is also accumulated in ->tree array during rendering of the tree.
179 * If 2, then also the icon prefix code (depthData) is stored
180 *
181 * @var int
182 */
183 public $makeHTML = 1;
184
185 /**
186 * If TRUE, records as selected will be stored internally in the ->recs array
187 *
188 * @var int
189 */
190 public $setRecs = 0;
191
192 /**
193 * Sets the associative array key which identifies a new sublevel if arrays are used for trees.
194 * This value has formerly been "subLevel" and "--sublevel--"
195 *
196 * @var string
197 */
198 public $subLevelID = '_SUB_LEVEL';
199
200 // *********
201 // Internal
202 // *********
203 // For record trees:
204 // one-dim array of the uid's selected.
205 /**
206 * @var array
207 */
208 public $ids = [];
209
210 // The hierarchy of element uids
211 /**
212 * @var array
213 */
214 public $ids_hierarchy = [];
215
216 // The hierarchy of versioned element uids
217 /**
218 * @var array
219 */
220 public $orig_ids_hierarchy = [];
221
222 // Temporary, internal array
223 /**
224 * @var array
225 */
226 public $buffer_idH = [];
227
228 // For FOLDER trees:
229 // Special UIDs for folders (integer-hashes of paths)
230 /**
231 * @var array
232 */
233 public $specUIDmap = [];
234
235 // For arrays:
236 // Holds the input data array
237 /**
238 * @var bool
239 */
240 public $data = false;
241
242 // Holds an index with references to the data array.
243 /**
244 * @var bool
245 */
246 public $dataLookup = false;
247
248 // For both types
249 // Tree is accumulated in this variable
250 /**
251 * @var array
252 */
253 public $tree = [];
254
255 // Holds (session stored) information about which items in the tree are unfolded and which are not.
256 /**
257 * @var array
258 */
259 public $stored = [];
260
261 // Points to the current mountpoint key
262 /**
263 * @var int
264 */
265 public $bank = 0;
266
267 // Accumulates the displayed records.
268 /**
269 * @var array
270 */
271 public $recs = [];
272
273 /**
274 * Constructor
275 */
276 public function __construct()
277 {
278 $this->determineScriptUrl();
279 }
280
281 /**
282 * Sets the script url depending on being a module or script request
283 */
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
298 /**
299 * @return string
300 */
301 protected function getThisScript()
302 {
303 return strpos($this->thisScript, '?') === false ? $this->thisScript . '?' : $this->thisScript . '&';
304 }
305
306 /**
307 * Initialize the tree class. Needs to be overwritten
308 *
309 * @param string $clause Record WHERE clause
310 * @param string $orderByFields Record ORDER BY field
311 * @return void
312 */
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
335 /**
336 * Adds a fieldname to the internal array ->fieldArray
337 *
338 * @param string $field Field name to
339 * @param bool $noCheck If set, the fieldname will be set no matter what. Otherwise the field name must either be found as key in $GLOBALS['TCA'][$table]['columns'] or in the list ->defaultList
340 * @return void
341 */
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
349 /**
350 * Resets the tree, recs, ids, ids_hierarchy and orig_ids_hierarchy internal variables. Use it if you need it.
351 *
352 * @return void
353 */
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 *******************************************/
368 /**
369 * Will create and return the HTML code for a browsable tree
370 * Is based on the mounts found in the internal array ->MOUNTS (set in the constructor)
371 *
372 * @return string HTML code for the browsable tree
373 */
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) {
403 $title = \TYPO3\CMS\Backend\Tree\Pagetree\Commands::getMountPointPath($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
433 /**
434 * Compiles the HTML code for displaying the structure found inside the ->tree array
435 *
436 * @param array|string $treeArr "tree-array" - if blank string, the internal ->tree array is used.
437 * @return string The HTML code for the tree
438 */
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 *******************************************/
493 /**
494 * Generate the plus/minus icon for the browsable tree.
495 *
496 * @param array $row Record for the entry
497 * @param int $a The current entry number
498 * @param int $c The total number of entries. If equal to $a, a "bottom" element is returned.
499 * @param int $nextCount The number of sub-elements to the current element.
500 * @param bool $isOpen The element was expanded to render subelements if this flag is set.
501 * @return string Image tag with the plus/minus icon.
502 * @access private
503 * @see \TYPO3\CMS\Backend\Tree\View\PageTreeView::PMicon()
504 */
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
516 /**
517 * Wrap the plus/minus icon in a link
518 *
519 * @param string $icon HTML string to wrap, probably an image tag.
520 * @param string $cmd Command for 'PM' get var
521 * @param string $bMark If set, the link will have an anchor point (=$bMark) and a name attribute (=$bMark)
522 * @param bool $isOpen
523 * @return string Link-wrapped input string
524 * @access private
525 */
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
538 /**
539 * Wrapping $title in a-tags.
540 *
541 * @param string $title Title string
542 * @param string $row Item record
543 * @param int $bank Bank pointer (which mount point number)
544 * @return string
545 * @access private
546 */
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
553 /**
554 * Wrapping the image tag, $icon, for the row, $row (except for mount points)
555 *
556 * @param string $icon The image tag for the icon
557 * @param array $row The row for the current element
558 * @return string The processed icon input value.
559 * @access private
560 */
561 public function wrapIcon($icon, $row)
562 {
563 return $icon;
564 }
565
566 /**
567 * Adds attributes to image tag.
568 *
569 * @param string $icon Icon image tag
570 * @param string $attr Attributes to add, eg. ' border="0"'
571 * @return string Image tag, modified with $attr attributes added.
572 */
573 public function addTagAttributes($icon, $attr)
574 {
575 return preg_replace('/ ?\\/?>$/', '', $icon) . ' ' . $attr . ' />';
576 }
577
578 /**
579 * Adds a red "+" to the input string, $str, if the field "php_tree_stop" in the $row (pages) is set
580 *
581 * @param string $str Input string, like a page title for the tree
582 * @param array $row record row with "php_tree_stop" field
583 * @return string Modified string
584 * @access private
585 */
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 *******************************************/
599 /**
600 * Returns TRUE/FALSE if the next level for $id should be expanded - based on
601 * data in $this->stored[][] and ->expandAll flag.
602 * Extending parent function
603 *
604 * @param int $id Record id/key
605 * @return bool
606 * @access private
607 * @see \TYPO3\CMS\Backend\Tree\View\PageTreeView::expandNext()
608 */
609 public function expandNext($id)
610 {
611 return $this->stored[$this->bank][$id] || $this->expandAll ? 1 : 0;
612 }
613
614 /**
615 * Get stored tree structure AND updating it if needed according to incoming PM GET var.
616 *
617 * @return void
618 * @access private
619 */
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
643 /**
644 * Saves the content of ->stored (keeps track of expanded positions in the tree)
645 * $this->treeName will be used as key for BE_USER->uc[] to store it in
646 *
647 * @return void
648 * @access private
649 */
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 ********************************/
661 /**
662 * Returns the root icon for a tree/mountpoint (defaults to the globe)
663 *
664 * @param array $rec Record for root.
665 * @return string Icon image tag.
666 */
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
673 /**
674 * Get icon for the row.
675 *
676 * @param array|int $row Item row or uid
677 * @return string Image tag.
678 */
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
690 /**
691 * Returns the title for the input record. If blank, a "no title" label (localized) will be returned.
692 * Do NOT htmlspecialchar the string from this function - has already been done.
693 *
694 * @param array $row The input row array (where the key "title" is used for the title)
695 * @param int $titleLen Title length (30)
696 * @return string The title.
697 */
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/locallang_core.xlf:labels.no_title')) . ']</em>' : $title;
702 return $title;
703 }
704
705 /**
706 * Returns the value for the image "title" attribute
707 *
708 * @param array $row The input row array (where the key "title" is used for the title)
709 * @return string The attribute value (is htmlspecialchared() already)
710 * @see wrapIcon()
711 */
712 public function getTitleAttrib($row)
713 {
714 return htmlspecialchars($row['title']);
715 }
716
717 /**
718 * Returns the id from the record (typ. uid)
719 *
720 * @param array $row Record array
721 * @return int The "uid" field value.
722 */
723 public function getId($row)
724 {
725 return $row['uid'];
726 }
727
728 /**
729 * Returns jump-url parameter value.
730 *
731 * @param array $row The record array.
732 * @return string The jump-url parameter.
733 */
734 public function getJumpToParam($row)
735 {
736 return $this->getId($row);
737 }
738
739 /********************************
740 *
741 * tree data buidling
742 *
743 ********************************/
744 /**
745 * Fetches the data for the tree
746 *
747 * @param int $uid item id for which to select subitems (parent id)
748 * @param int $depth Max depth (recursivity limit)
749 * @param string $depthData HTML-code prefix for recursive calls.
750
751 * @return int The count of items on the level
752 */
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 ********************************/
836 /**
837 * Returns the number of records having the parent id, $uid
838 *
839 * @param int $uid Id to count subitems for
840 * @return int
841 * @access private
842 */
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 ),
862 QueryHelper::stripLogicalOperatorPrefix($this->clause)
863 )
864 ->execute()
865 ->fetchColumn();
866
867 return (int)$count;
868 }
869 }
870
871 /**
872 * Returns root record for uid (<=0)
873 *
874 * @return array Array with title/uid keys with values of $this->title/0 (zero)
875 */
876 public function getRootRecord()
877 {
878 return ['title' => $this->title, 'uid' => 0];
879 }
880
881 /**
882 * Returns the record for a uid.
883 * For tables: Looks up the record in the database.
884 * For arrays: Returns the fake record for uid id.
885 *
886 * @param int $uid UID to look up
887 * @return array The record
888 */
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
898 /**
899 * Getting the tree data: Selecting/Initializing data pointer to items for a certain parent id.
900 * For tables: This will make a database query to select all children to "parent"
901 * For arrays: This will return key to the ->dataLookup array
902 *
903 * @param int $parentId parent item id
904 *
905 * @return mixed Data handle (Tables: An sql-resource, arrays: A parentId integer. -1 is returned if there were NO subLevel.)
906 * @access private
907 */
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 ),
931 QueryHelper::stripLogicalOperatorPrefix($this->clause)
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
943 /**
944 * Getting the tree data: Counting elements in resource
945 *
946 * @param mixed $res Data handle
947 * @return int number of items
948 * @access private
949 * @see getDataInit()
950 */
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
960 /**
961 * Getting the tree data: next entry
962 *
963 * @param mixed $res Data handle
964 *
965 * @return array item data array OR FALSE if end of elements.
966 * @access private
967 * @see getDataInit()
968 */
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
989 /**
990 * Getting the tree data: frees data handle
991 *
992 * @param mixed $res Data handle
993 * @return void
994 * @access private
995 */
996 public function getDataFree(&$res)
997 {
998 if (!is_array($this->data)) {
999 $res->closeCursor();
1000 }
1001 }
1002
1003 /**
1004 * Used to initialize class with an array to browse.
1005 * The array inputted will be traversed and an internal index for lookup is created.
1006 * The keys of the input array are perceived as "uid"s of records which means that keys GLOBALLY must be unique like uids are.
1007 * "uid" and "pid" "fakefields" are also set in each record.
1008 * All other fields are optional.
1009 *
1010 * @param array $dataArr The input array, see examples below in this script.
1011 * @param bool $traverse Internal, for recursion.
1012 * @param int $pid Internal, for recursion.
1013 * @return void
1014 */
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
1034 /**
1035 * Sets the internal data arrays
1036 *
1037 * @param array $treeArr Content for $this->data
1038 * @param array $treeLookupArr Content for $this->dataLookup
1039 * @return void
1040 */
1041 public function setDataFromTreeArray(&$treeArr, &$treeLookupArr)
1042 {
1043 $this->data = &$treeArr;
1044 $this->dataLookup = &$treeLookupArr;
1045 }
1046
1047 /**
1048 * @return LanguageService
1049 */
1050 protected function getLanguageService()
1051 {
1052 return $GLOBALS['LANG'];
1053 }
1054
1055 /**
1056 * @return BackendUserAuthentication
1057 */
1058 protected function getBackendUser()
1059 {
1060 return $GLOBALS['BE_USER'];
1061 }
1062 }