[BUGFIX] Use correct check to find site root pages
[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\Tree\Pagetree\Commands;
20 use TYPO3\CMS\Backend\Utility\BackendUtility;
21 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
22 use TYPO3\CMS\Core\Database\ConnectionPool;
23 use TYPO3\CMS\Core\Database\Query\QueryHelper;
24 use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction;
25 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
26 use TYPO3\CMS\Core\Imaging\Icon;
27 use TYPO3\CMS\Core\Imaging\IconFactory;
28 use TYPO3\CMS\Core\Utility\GeneralUtility;
29 use TYPO3\CMS\Lang\LanguageService;
30
31 /**
32 * Base class for creating a browsable array/page/folder tree in HTML
33 */
34 abstract class AbstractTreeView
35 {
36 // EXTERNAL, static:
37 // If set, the first element in the tree is always expanded.
38 /**
39 * @var int
40 */
41 public $expandFirst = 0;
42
43 // If set, then ALL items will be expanded, regardless of stored settings.
44 /**
45 * @var int
46 */
47 public $expandAll = 0;
48
49 // Holds the current script to reload to.
50 /**
51 * @var string
52 */
53 public $thisScript = '';
54
55 // Which HTML attribute to use: alt/title. See init().
56 /**
57 * @var string
58 */
59 public $titleAttrib = 'title';
60
61 // If TRUE, no context menu is rendered on icons. If set to "titlelink" the
62 // icon is linked as the title is.
63 /**
64 * @var bool
65 */
66 public $ext_IconMode = false;
67
68 /**
69 * @var bool
70 */
71 public $ext_showPathAboveMounts = false;
72
73 // If set, the id of the mounts will be added to the internal ids array
74 /**
75 * @var int
76 */
77 public $addSelfId = 0;
78
79 // Used if the tree is made of records (not folders for ex.)
80 /**
81 * @var string
82 */
83 public $title = 'no title';
84
85 // If TRUE, a default title attribute showing the UID of the record is shown.
86 // This cannot be enabled by default because it will destroy many applications
87 // where another title attribute is in fact applied later.
88 /**
89 * @var bool
90 */
91 public $showDefaultTitleAttribute = false;
92
93 /**
94 * Needs to be initialized with $GLOBALS['BE_USER']
95 * Done by default in init()
96 *
97 * @var \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
98 */
99 public $BE_USER = '';
100
101 /**
102 * Needs to be initialized with e.g. $GLOBALS['BE_USER']->returnWebmounts()
103 * Default setting in init() is 0 => 0
104 * The keys are mount-ids (can be anything basically) and the
105 * values are the ID of the root element (COULD be zero or anything else.
106 * For pages that would be the uid of the page, zero for the pagetree root.)
107 *
108 * @var array|NULL
109 */
110 public $MOUNTS = null;
111
112 /**
113 * Database table to get the tree data from.
114 * Leave blank if data comes from an array.
115 *
116 * @var string
117 */
118 public $table = '';
119
120 /**
121 * Defines the field of $table which is the parent id field (like pid for table pages).
122 *
123 * @var string
124 */
125 public $parentField = 'pid';
126
127 /**
128 * WHERE clause used for selecting records for the tree. Is set by function init.
129 * Only makes sense when $this->table is set.
130 *
131 * @see init()
132 * @var string
133 */
134 public $clause = '';
135
136 /**
137 * Field for ORDER BY. Is set by function init.
138 * Only makes sense when $this->table is set.
139 *
140 * @see init()
141 * @var string
142 */
143 public $orderByFields = '';
144
145 /**
146 * Default set of fields selected from the tree table.
147 * 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)
148 *
149 * @see addField()
150 * @var array
151 */
152 public $fieldArray = ['uid', 'pid', 'title', 'is_siteroot'];
153
154 /**
155 * List of other fields which are ALLOWED to set (here, based on the "pages" table!)
156 *
157 * @see addField()
158 * @var array
159 */
160 public $defaultList = 'uid,pid,tstamp,sorting,deleted,perms_userid,perms_groupid,perms_user,perms_group,perms_everybody,crdate,cruser_id';
161
162 /**
163 * Unique name for the tree.
164 * Used as key for storing the tree into the BE users settings.
165 * Used as key to pass parameters in links.
166 * MUST NOT contain underscore chars.
167 * etc.
168 *
169 * @var string
170 */
171 public $treeName = '';
172
173 /**
174 * A prefix for table cell id's which will be wrapped around an item.
175 * Can be used for highlighting by JavaScript.
176 * Needs to be unique if multiple trees are on one HTML page.
177 *
178 * @see printTree()
179 * @var string
180 */
181 public $domIdPrefix = 'row';
182
183 /**
184 * If 1, HTML code is also accumulated in ->tree array during rendering of the tree
185 *
186 * @var int
187 */
188 public $makeHTML = 1;
189
190 /**
191 * If TRUE, records as selected will be stored internally in the ->recs array
192 *
193 * @var int
194 */
195 public $setRecs = 0;
196
197 /**
198 * Sets the associative array key which identifies a new sublevel if arrays are used for trees.
199 * This value has formerly been "subLevel" and "--sublevel--"
200 *
201 * @var string
202 */
203 public $subLevelID = '_SUB_LEVEL';
204
205 // *********
206 // Internal
207 // *********
208 // For record trees:
209 // one-dim array of the uid's selected.
210 /**
211 * @var array
212 */
213 public $ids = [];
214
215 // The hierarchy of element uids
216 /**
217 * @var array
218 */
219 public $ids_hierarchy = [];
220
221 // The hierarchy of versioned element uids
222 /**
223 * @var array
224 */
225 public $orig_ids_hierarchy = [];
226
227 // Temporary, internal array
228 /**
229 * @var array
230 */
231 public $buffer_idH = [];
232
233 // For FOLDER trees:
234 // Special UIDs for folders (integer-hashes of paths)
235 /**
236 * @var array
237 */
238 public $specUIDmap = [];
239
240 // For arrays:
241 // Holds the input data array
242 /**
243 * @var bool
244 */
245 public $data = false;
246
247 // Holds an index with references to the data array.
248 /**
249 * @var bool
250 */
251 public $dataLookup = false;
252
253 // For both types
254 // Tree is accumulated in this variable
255 /**
256 * @var array
257 */
258 public $tree = [];
259
260 // Holds (session stored) information about which items in the tree are unfolded and which are not.
261 /**
262 * @var array
263 */
264 public $stored = [];
265
266 // Points to the current mountpoint key
267 /**
268 * @var int
269 */
270 public $bank = 0;
271
272 // Accumulates the displayed records.
273 /**
274 * @var array
275 */
276 public $recs = [];
277
278 /**
279 * Constructor
280 */
281 public function __construct()
282 {
283 $this->determineScriptUrl();
284 }
285
286 /**
287 * Sets the script url depending on being a module or script request
288 */
289 protected function determineScriptUrl()
290 {
291 if ($routePath = GeneralUtility::_GP('route')) {
292 $router = GeneralUtility::makeInstance(Router::class);
293 $route = $router->match($routePath);
294 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
295 $this->thisScript = (string)$uriBuilder->buildUriFromRoute($route->getOption('_identifier'));
296 } elseif ($moduleName = GeneralUtility::_GP('M')) {
297 $this->thisScript = BackendUtility::getModuleUrl($moduleName);
298 } else {
299 $this->thisScript = GeneralUtility::getIndpEnv('SCRIPT_NAME');
300 }
301 }
302
303 /**
304 * @return string
305 */
306 protected function getThisScript()
307 {
308 return strpos($this->thisScript, '?') === false ? $this->thisScript . '?' : $this->thisScript . '&';
309 }
310
311 /**
312 * Initialize the tree class. Needs to be overwritten
313 *
314 * @param string $clause Record WHERE clause
315 * @param string $orderByFields Record ORDER BY field
316 * @return void
317 */
318 public function init($clause = '', $orderByFields = '')
319 {
320 // Setting BE_USER by default
321 $this->BE_USER = $GLOBALS['BE_USER'];
322 // Setting clause
323 if ($clause) {
324 $this->clause = $clause;
325 }
326 if ($orderByFields) {
327 $this->orderByFields = $orderByFields;
328 }
329 if (!is_array($this->MOUNTS)) {
330 // Dummy
331 $this->MOUNTS = [0 => 0];
332 }
333 // Sets the tree name which is used to identify the tree, used for JavaScript and other things
334 $this->treeName = str_replace('_', '', $this->treeName ?: $this->table);
335 // Setting this to FALSE disables the use of array-trees by default
336 $this->data = false;
337 $this->dataLookup = false;
338 }
339
340 /**
341 * Adds a fieldname to the internal array ->fieldArray
342 *
343 * @param string $field Field name to
344 * @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
345 * @return void
346 */
347 public function addField($field, $noCheck = false)
348 {
349 if ($noCheck || is_array($GLOBALS['TCA'][$this->table]['columns'][$field]) || GeneralUtility::inList($this->defaultList, $field)) {
350 $this->fieldArray[] = $field;
351 }
352 }
353
354 /**
355 * Resets the tree, recs, ids, ids_hierarchy and orig_ids_hierarchy internal variables. Use it if you need it.
356 *
357 * @return void
358 */
359 public function reset()
360 {
361 $this->tree = [];
362 $this->recs = [];
363 $this->ids = [];
364 $this->ids_hierarchy = [];
365 $this->orig_ids_hierarchy = [];
366 }
367
368 /*******************************************
369 *
370 * output
371 *
372 *******************************************/
373 /**
374 * Will create and return the HTML code for a browsable tree
375 * Is based on the mounts found in the internal array ->MOUNTS (set in the constructor)
376 *
377 * @return string HTML code for the browsable tree
378 */
379 public function getBrowsableTree()
380 {
381 // Get stored tree structure AND updating it if needed according to incoming PM GET var.
382 $this->initializePositionSaving();
383 // Init done:
384 $lastMountPointPid = 0;
385 $treeArr = [];
386 // Traverse mounts:
387 foreach ($this->MOUNTS as $idx => $uid) {
388 // Set first:
389 $this->bank = $idx;
390 $isOpen = $this->stored[$idx][$uid] || $this->expandFirst;
391 // Save ids while resetting everything else.
392 $curIds = $this->ids;
393 $this->reset();
394 $this->ids = $curIds;
395 // Set PM icon for root of mount:
396 $cmd = $this->bank . '_' . ($isOpen ? '0_' : '1_') . $uid . '_' . $this->treeName;
397
398 $firstHtml = $this->PM_ATagWrap('', $cmd, '', $isOpen);
399 // Preparing rootRec for the mount
400 if ($uid) {
401 $rootRec = $this->getRecord($uid);
402 if (is_array($rootRec)) {
403 $firstHtml .= $this->getIcon($rootRec);
404 }
405
406 if ($this->ext_showPathAboveMounts) {
407 $mountPointPid = $rootRec['pid'];
408 if ($lastMountPointPid !== $mountPointPid) {
409 $title = Commands::getMountPointPath($mountPointPid);
410 $this->tree[] = ['isMountPointPath' => true, 'title' => $title];
411 }
412 $lastMountPointPid = $mountPointPid;
413 }
414 } else {
415 // Artificial record for the tree root, id=0
416 $rootRec = $this->getRootRecord();
417 $firstHtml .= $this->getRootIcon($rootRec);
418 }
419 if (is_array($rootRec)) {
420 // In case it was swapped inside getRecord due to workspaces.
421 $uid = $rootRec['uid'];
422 // Add the root of the mount to ->tree
423 $this->tree[] = ['HTML' => $firstHtml, 'row' => $rootRec, 'hasSub' => $isOpen, 'bank' => $this->bank];
424 // If the mount is expanded, go down:
425 if ($isOpen) {
426 $depthData = '<span class="treeline-icon treeline-icon-clear"></span>';
427 if ($this->addSelfId) {
428 $this->ids[] = $uid;
429 }
430 $this->getTree($uid, 999, $depthData);
431 }
432 // Add tree:
433 $treeArr = array_merge($treeArr, $this->tree);
434 }
435 }
436 return $this->printTree($treeArr);
437 }
438
439 /**
440 * Compiles the HTML code for displaying the structure found inside the ->tree array
441 *
442 * @param array|string $treeArr "tree-array" - if blank string, the internal ->tree array is used.
443 * @return string The HTML code for the tree
444 */
445 public function printTree($treeArr = '')
446 {
447 $titleLen = (int)$this->BE_USER->uc['titleLen'];
448 if (!is_array($treeArr)) {
449 $treeArr = $this->tree;
450 }
451 $out = '';
452 $closeDepth = [];
453 foreach ($treeArr as $treeItem) {
454 $classAttr = '';
455 if ($treeItem['isFirst']) {
456 $out .= '<ul class="list-tree">';
457 }
458
459 // Add CSS classes to the list item
460 if ($treeItem['hasSub']) {
461 $classAttr .= ' list-tree-control-open';
462 }
463
464 $idAttr = htmlspecialchars($this->domIdPrefix . $this->getId($treeItem['row']) . '_' . $treeItem['bank']);
465 $out .= '
466 <li id="' . $idAttr . '"' . ($classAttr ? ' class="' . trim($classAttr) . '"' : '') . '>
467 <span class="list-tree-group">
468 <span class="list-tree-icon">' . $treeItem['HTML'] . '</span>
469 <span class="list-tree-title">' . $this->wrapTitle($this->getTitleStr($treeItem['row'], $titleLen), $treeItem['row'], $treeItem['bank']) . '</span>
470 </span>';
471
472 if (!$treeItem['hasSub']) {
473 $out .= '</li>';
474 }
475
476 // We have to remember if this is the last one
477 // on level X so the last child on level X+1 closes the <ul>-tag
478 if ($treeItem['isLast']) {
479 $closeDepth[$treeItem['invertedDepth']] = 1;
480 }
481 // If this is the last one and does not have subitems, we need to close
482 // the tree as long as the upper levels have last items too
483 if ($treeItem['isLast'] && !$treeItem['hasSub']) {
484 for ($i = $treeItem['invertedDepth']; $closeDepth[$i] == 1; $i++) {
485 $closeDepth[$i] = 0;
486 $out .= '</ul></li>';
487 }
488 }
489 }
490 $out = '<ul class="list-tree list-tree-root list-tree-root-clean">' . $out . '</ul>';
491 return $out;
492 }
493
494 /*******************************************
495 *
496 * rendering parts
497 *
498 *******************************************/
499 /**
500 * Generate the plus/minus icon for the browsable tree.
501 *
502 * @param array $row Record for the entry
503 * @param int $a The current entry number
504 * @param int $c The total number of entries. If equal to $a, a "bottom" element is returned.
505 * @param int $nextCount The number of sub-elements to the current element.
506 * @param bool $isOpen The element was expanded to render subelements if this flag is set.
507 * @return string Image tag with the plus/minus icon.
508 * @access private
509 * @see \TYPO3\CMS\Backend\Tree\View\PageTreeView::PMicon()
510 */
511 public function PMicon($row, $a, $c, $nextCount, $isOpen)
512 {
513 if ($nextCount) {
514 $cmd = $this->bank . '_' . ($isOpen ? '0_' : '1_') . $row['uid'] . '_' . $this->treeName;
515 $bMark = $this->bank . '_' . $row['uid'];
516 return $this->PM_ATagWrap('', $cmd, $bMark, $isOpen);
517 } else {
518 return '';
519 }
520 }
521
522 /**
523 * Wrap the plus/minus icon in a link
524 *
525 * @param string $icon HTML string to wrap, probably an image tag.
526 * @param string $cmd Command for 'PM' get var
527 * @param string $bMark If set, the link will have an anchor point (=$bMark) and a name attribute (=$bMark)
528 * @param bool $isOpen
529 * @return string Link-wrapped input string
530 * @access private
531 */
532 public function PM_ATagWrap($icon, $cmd, $bMark = '', $isOpen = false)
533 {
534 if ($this->thisScript) {
535 $anchor = $bMark ? '#' . $bMark : '';
536 $name = $bMark ? ' name="' . $bMark . '"' : '';
537 $aUrl = $this->getThisScript() . 'PM=' . $cmd . $anchor;
538 return '<a class="list-tree-control ' . ($isOpen ? 'list-tree-control-open' : 'list-tree-control-closed') . '" href="' . htmlspecialchars($aUrl) . '"' . $name . '><i class="fa"></i></a>';
539 } else {
540 return $icon;
541 }
542 }
543
544 /**
545 * Wrapping $title in a-tags.
546 *
547 * @param string $title Title string
548 * @param array $row Item record
549 * @param int $bank Bank pointer (which mount point number)
550 * @return string
551 * @access private
552 */
553 public function wrapTitle($title, $row, $bank = 0)
554 {
555 $aOnClick = 'return jumpTo(' . GeneralUtility::quoteJSvalue($this->getJumpToParam($row)) . ',this,' . GeneralUtility::quoteJSvalue($this->domIdPrefix . $this->getId($row)) . ',' . $bank . ');';
556 return '<a href="#" onclick="' . htmlspecialchars($aOnClick) . '">' . $title . '</a>';
557 }
558
559 /**
560 * Wrapping the image tag, $icon, for the row, $row (except for mount points)
561 *
562 * @param string $icon The image tag for the icon
563 * @param array $row The row for the current element
564 * @return string The processed icon input value.
565 * @access private
566 */
567 public function wrapIcon($icon, $row)
568 {
569 return $icon;
570 }
571
572 /**
573 * Adds attributes to image tag.
574 *
575 * @param string $icon Icon image tag
576 * @param string $attr Attributes to add, eg. ' border="0"'
577 * @return string Image tag, modified with $attr attributes added.
578 */
579 public function addTagAttributes($icon, $attr)
580 {
581 return preg_replace('/ ?\\/?>$/', '', $icon) . ' ' . $attr . ' />';
582 }
583
584 /**
585 * Adds a red "+" to the input string, $str, if the field "php_tree_stop" in the $row (pages) is set
586 *
587 * @param string $str Input string, like a page title for the tree
588 * @param array $row record row with "php_tree_stop" field
589 * @return string Modified string
590 * @access private
591 */
592 public function wrapStop($str, $row)
593 {
594 if ($row['php_tree_stop']) {
595 $str .= '<a href="' . htmlspecialchars(GeneralUtility::linkThisScript(['setTempDBmount' => $row['uid']])) . '" class="text-danger">+</a> ';
596 }
597 return $str;
598 }
599
600 /*******************************************
601 *
602 * tree handling
603 *
604 *******************************************/
605 /**
606 * Returns TRUE/FALSE if the next level for $id should be expanded - based on
607 * data in $this->stored[][] and ->expandAll flag.
608 * Extending parent function
609 *
610 * @param int $id Record id/key
611 * @return bool
612 * @access private
613 * @see \TYPO3\CMS\Backend\Tree\View\PageTreeView::expandNext()
614 */
615 public function expandNext($id)
616 {
617 return $this->stored[$this->bank][$id] || $this->expandAll ? 1 : 0;
618 }
619
620 /**
621 * Get stored tree structure AND updating it if needed according to incoming PM GET var.
622 *
623 * @return void
624 * @access private
625 */
626 public function initializePositionSaving()
627 {
628 // Get stored tree structure:
629 $this->stored = unserialize($this->BE_USER->uc['browseTrees'][$this->treeName]);
630 // PM action
631 // (If an plus/minus icon has been clicked, the PM GET var is sent and we
632 // must update the stored positions in the tree):
633 // 0: mount key, 1: set/clear boolean, 2: item ID (cannot contain "_"), 3: treeName
634 $PM = explode('_', GeneralUtility::_GP('PM'));
635 if (count($PM) === 4 && $PM[3] == $this->treeName) {
636 if (isset($this->MOUNTS[$PM[0]])) {
637 // set
638 if ($PM[1]) {
639 $this->stored[$PM[0]][$PM[2]] = 1;
640 $this->savePosition();
641 } else {
642 unset($this->stored[$PM[0]][$PM[2]]);
643 $this->savePosition();
644 }
645 }
646 }
647 }
648
649 /**
650 * Saves the content of ->stored (keeps track of expanded positions in the tree)
651 * $this->treeName will be used as key for BE_USER->uc[] to store it in
652 *
653 * @return void
654 * @access private
655 */
656 public function savePosition()
657 {
658 $this->BE_USER->uc['browseTrees'][$this->treeName] = serialize($this->stored);
659 $this->BE_USER->writeUC();
660 }
661
662 /******************************
663 *
664 * Functions that might be overwritten by extended classes
665 *
666 ********************************/
667 /**
668 * Returns the root icon for a tree/mountpoint (defaults to the globe)
669 *
670 * @param array $rec Record for root.
671 * @return string Icon image tag.
672 */
673 public function getRootIcon($rec)
674 {
675 $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
676 return $this->wrapIcon($iconFactory->getIcon('apps-pagetree-root', Icon::SIZE_SMALL)->render(), $rec);
677 }
678
679 /**
680 * Get icon for the row.
681 *
682 * @param array|int $row Item row or uid
683 * @return string Image tag.
684 */
685 public function getIcon($row)
686 {
687 if (is_int($row)) {
688 $row = BackendUtility::getRecord($this->table, $row);
689 }
690 $title = $this->showDefaultTitleAttribute ? htmlspecialchars('UID: ' . $row['uid']) : $this->getTitleAttrib($row);
691 $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
692 $icon = $row['is_siteroot'] ? $iconFactory->getIcon('apps-pagetree-folder-root', Icon::SIZE_SMALL) : $iconFactory->getIconForRecord($this->table, $row, Icon::SIZE_SMALL);
693 $icon = '<span title="' . $title . '">' . $icon->render() . '</span>';
694 return $this->wrapIcon($icon, $row);
695 }
696
697 /**
698 * Returns the title for the input record. If blank, a "no title" label (localized) will be returned.
699 * Do NOT htmlspecialchar the string from this function - has already been done.
700 *
701 * @param array $row The input row array (where the key "title" is used for the title)
702 * @param int $titleLen Title length (30)
703 * @return string The title.
704 */
705 public function getTitleStr($row, $titleLen = 30)
706 {
707 $title = htmlspecialchars(GeneralUtility::fixed_lgd_cs($row['title'], $titleLen));
708 $title = trim($row['title']) === '' ? '<em>[' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.no_title')) . ']</em>' : $title;
709 return $title;
710 }
711
712 /**
713 * Returns the value for the image "title" attribute
714 *
715 * @param array $row The input row array (where the key "title" is used for the title)
716 * @return string The attribute value (is htmlspecialchared() already)
717 * @see wrapIcon()
718 */
719 public function getTitleAttrib($row)
720 {
721 return htmlspecialchars($row['title']);
722 }
723
724 /**
725 * Returns the id from the record (typ. uid)
726 *
727 * @param array $row Record array
728 * @return int The "uid" field value.
729 */
730 public function getId($row)
731 {
732 return $row['uid'];
733 }
734
735 /**
736 * Returns jump-url parameter value.
737 *
738 * @param array $row The record array.
739 * @return string The jump-url parameter.
740 */
741 public function getJumpToParam($row)
742 {
743 return $this->getId($row);
744 }
745
746 /********************************
747 *
748 * tree data buidling
749 *
750 ********************************/
751 /**
752 * Fetches the data for the tree
753 *
754 * @param int $uid item id for which to select subitems (parent id)
755 * @param int $depth Max depth (recursivity limit)
756 * @param string $depthData HTML-code prefix for recursive calls.
757
758 * @return int The count of items on the level
759 */
760 public function getTree($uid, $depth = 999, $depthData = '')
761 {
762 // Buffer for id hierarchy is reset:
763 $this->buffer_idH = [];
764 // Init vars
765 $depth = (int)$depth;
766 $HTML = '';
767 $a = 0;
768 $res = $this->getDataInit($uid);
769 $c = $this->getDataCount($res);
770 $crazyRecursionLimiter = 999;
771 $idH = [];
772 // Traverse the records:
773 while ($crazyRecursionLimiter > 0 && ($row = $this->getDataNext($res))) {
774 $pageUid = ($this->table === 'pages') ? $row['uid'] : $row['pid'];
775 if (!$this->getBackendUser()->isInWebMount($pageUid)) {
776 // Current record is not within web mount => skip it
777 continue;
778 }
779
780 $a++;
781 $crazyRecursionLimiter--;
782 $newID = $row['uid'];
783 if ($newID == 0) {
784 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);
785 }
786 // Reserve space.
787 $this->tree[] = [];
788 end($this->tree);
789 // Get the key for this space
790 $treeKey = key($this->tree);
791 // If records should be accumulated, do so
792 if ($this->setRecs) {
793 $this->recs[$row['uid']] = $row;
794 }
795 // Accumulate the id of the element in the internal arrays
796 $this->ids[] = ($idH[$row['uid']]['uid'] = $row['uid']);
797 $this->ids_hierarchy[$depth][] = $row['uid'];
798 $this->orig_ids_hierarchy[$depth][] = $row['_ORIG_uid'] ?: $row['uid'];
799
800 // Make a recursive call to the next level
801 $nextLevelDepthData = $depthData . '<span class="treeline-icon treeline-icon-' . ($a === $c ? 'clear' : 'line') . '"></span>';
802 $hasSub = $this->expandNext($newID) && !$row['php_tree_stop'];
803 if ($depth > 1 && $hasSub) {
804 $nextCount = $this->getTree($newID, $depth - 1, $nextLevelDepthData);
805 if (!empty($this->buffer_idH)) {
806 $idH[$row['uid']]['subrow'] = $this->buffer_idH;
807 }
808 // Set "did expand" flag
809 $isOpen = 1;
810 } else {
811 $nextCount = $this->getCount($newID);
812 // Clear "did expand" flag
813 $isOpen = 0;
814 }
815 // Set HTML-icons, if any:
816 if ($this->makeHTML) {
817 $HTML = $this->PMicon($row, $a, $c, $nextCount, $isOpen) . $this->wrapStop($this->getIcon($row), $row);
818 }
819 // Finally, add the row/HTML content to the ->tree array in the reserved key.
820 $this->tree[$treeKey] = [
821 'row' => $row,
822 'HTML' => $HTML,
823 'invertedDepth' => $depth,
824 'depthData' => $depthData,
825 'bank' => $this->bank,
826 'hasSub' => $nextCount && $hasSub,
827 'isFirst' => $a === 1,
828 'isLast' => $a === $c,
829 ];
830 }
831
832 $this->getDataFree($res);
833 $this->buffer_idH = $idH;
834 return $c;
835 }
836
837 /********************************
838 *
839 * Data handling
840 * Works with records and arrays
841 *
842 ********************************/
843 /**
844 * Returns the number of records having the parent id, $uid
845 *
846 * @param int $uid Id to count subitems for
847 * @return int
848 * @access private
849 */
850 public function getCount($uid)
851 {
852 if (is_array($this->data)) {
853 $res = $this->getDataInit($uid);
854 return $this->getDataCount($res);
855 } else {
856 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->table);
857 $queryBuilder->getRestrictions()
858 ->removeAll()
859 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
860 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
861 $count = $queryBuilder
862 ->count('uid')
863 ->from($this->table)
864 ->where(
865 $queryBuilder->expr()->eq(
866 $this->parentField,
867 $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
868 ),
869 QueryHelper::stripLogicalOperatorPrefix($this->clause)
870 )
871 ->execute()
872 ->fetchColumn();
873
874 return (int)$count;
875 }
876 }
877
878 /**
879 * Returns root record for uid (<=0)
880 *
881 * @return array Array with title/uid keys with values of $this->title/0 (zero)
882 */
883 public function getRootRecord()
884 {
885 return ['title' => $this->title, 'uid' => 0];
886 }
887
888 /**
889 * Returns the record for a uid.
890 * For tables: Looks up the record in the database.
891 * For arrays: Returns the fake record for uid id.
892 *
893 * @param int $uid UID to look up
894 * @return array The record
895 */
896 public function getRecord($uid)
897 {
898 if (is_array($this->data)) {
899 return $this->dataLookup[$uid];
900 } else {
901 return BackendUtility::getRecordWSOL($this->table, $uid);
902 }
903 }
904
905 /**
906 * Getting the tree data: Selecting/Initializing data pointer to items for a certain parent id.
907 * For tables: This will make a database query to select all children to "parent"
908 * For arrays: This will return key to the ->dataLookup array
909 *
910 * @param int $parentId parent item id
911 *
912 * @return mixed Data handle (Tables: An sql-resource, arrays: A parentId integer. -1 is returned if there were NO subLevel.)
913 * @access private
914 */
915 public function getDataInit($parentId)
916 {
917 if (is_array($this->data)) {
918 if (!is_array($this->dataLookup[$parentId][$this->subLevelID])) {
919 $parentId = -1;
920 } else {
921 reset($this->dataLookup[$parentId][$this->subLevelID]);
922 }
923 return $parentId;
924 } else {
925 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->table);
926 $queryBuilder->getRestrictions()
927 ->removeAll()
928 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
929 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
930 $queryBuilder
931 ->select(...$this->fieldArray)
932 ->from($this->table)
933 ->where(
934 $queryBuilder->expr()->eq(
935 $this->parentField,
936 $queryBuilder->createNamedParameter($parentId, \PDO::PARAM_INT)
937 ),
938 QueryHelper::stripLogicalOperatorPrefix($this->clause)
939 );
940
941 foreach (QueryHelper::parseOrderBy($this->orderByFields) as $orderPair) {
942 list($fieldName, $order) = $orderPair;
943 $queryBuilder->addOrderBy($fieldName, $order);
944 }
945
946 return $queryBuilder->execute();
947 }
948 }
949
950 /**
951 * Getting the tree data: Counting elements in resource
952 *
953 * @param mixed $res Data handle
954 * @return int number of items
955 * @access private
956 * @see getDataInit()
957 */
958 public function getDataCount(&$res)
959 {
960 if (is_array($this->data)) {
961 return count($this->dataLookup[$res][$this->subLevelID]);
962 } else {
963 return $res->rowCount();
964 }
965 }
966
967 /**
968 * Getting the tree data: next entry
969 *
970 * @param mixed $res Data handle
971 *
972 * @return array item data array OR FALSE if end of elements.
973 * @access private
974 * @see getDataInit()
975 */
976 public function getDataNext(&$res)
977 {
978 if (is_array($this->data)) {
979 if ($res < 0) {
980 $row = false;
981 } else {
982 list(, $row) = each($this->dataLookup[$res][$this->subLevelID]);
983 }
984 return $row;
985 } else {
986 while ($row = $res->fetch()) {
987 BackendUtility::workspaceOL($this->table, $row, $this->BE_USER->workspace, true);
988 if (is_array($row)) {
989 break;
990 }
991 }
992 return $row;
993 }
994 }
995
996 /**
997 * Getting the tree data: frees data handle
998 *
999 * @param mixed $res Data handle
1000 * @return void
1001 * @access private
1002 */
1003 public function getDataFree(&$res)
1004 {
1005 if (!is_array($this->data)) {
1006 $res->closeCursor();
1007 }
1008 }
1009
1010 /**
1011 * Used to initialize class with an array to browse.
1012 * The array inputted will be traversed and an internal index for lookup is created.
1013 * The keys of the input array are perceived as "uid"s of records which means that keys GLOBALLY must be unique like uids are.
1014 * "uid" and "pid" "fakefields" are also set in each record.
1015 * All other fields are optional.
1016 *
1017 * @param array $dataArr The input array, see examples below in this script.
1018 * @param bool $traverse Internal, for recursion.
1019 * @param int $pid Internal, for recursion.
1020 * @return void
1021 */
1022 public function setDataFromArray(&$dataArr, $traverse = false, $pid = 0)
1023 {
1024 if (!$traverse) {
1025 $this->data = &$dataArr;
1026 $this->dataLookup = [];
1027 // Add root
1028 $this->dataLookup[0][$this->subLevelID] = &$dataArr;
1029 }
1030 foreach ($dataArr as $uid => $val) {
1031 $dataArr[$uid]['uid'] = $uid;
1032 $dataArr[$uid]['pid'] = $pid;
1033 // Gives quick access to id's
1034 $this->dataLookup[$uid] = &$dataArr[$uid];
1035 if (is_array($val[$this->subLevelID])) {
1036 $this->setDataFromArray($dataArr[$uid][$this->subLevelID], true, $uid);
1037 }
1038 }
1039 }
1040
1041 /**
1042 * Sets the internal data arrays
1043 *
1044 * @param array $treeArr Content for $this->data
1045 * @param array $treeLookupArr Content for $this->dataLookup
1046 * @return void
1047 */
1048 public function setDataFromTreeArray(&$treeArr, &$treeLookupArr)
1049 {
1050 $this->data = &$treeArr;
1051 $this->dataLookup = &$treeLookupArr;
1052 }
1053
1054 /**
1055 * @return LanguageService
1056 */
1057 protected function getLanguageService()
1058 {
1059 return $GLOBALS['LANG'];
1060 }
1061
1062 /**
1063 * @return BackendUserAuthentication
1064 */
1065 protected function getBackendUser()
1066 {
1067 return $GLOBALS['BE_USER'];
1068 }
1069 }