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