1<?php
2
3/*
4 * This file is part of the TYPO3 CMS project.
5 *
6 * It is free software; you can redistribute it and/or modify it under
7 * the terms of the GNU General Public License, either version 2
8 * of the License, or any later version.
9 *
10 * For the full copyright and license information, please read the
11 * LICENSE.txt file that was distributed with this source code.
12 *
13 * The TYPO3 project - inspiring people to share!
14 */
15
16namespace TYPO3\CMS\Backend\View;
17
18use TYPO3\CMS\Backend\Tree\View\BrowseTreeView;
19use TYPO3\CMS\Backend\Utility\BackendUtility;
20use TYPO3\CMS\Core\Imaging\Icon;
21use TYPO3\CMS\Core\Imaging\IconFactory;
22use TYPO3\CMS\Core\Utility\GeneralUtility;
23
24/**
25 * Browse pages in Web module
26 * @internal This class is a TYPO3 Backend implementation and is not considered part of the Public TYPO3 API.
27 */
28class PageTreeView extends BrowseTreeView
29{
30    /**
31     * @var bool
32     */
33    public $ext_showPageId = false;
34
35    /**
36     * Indicates, whether the ajax call was successful, i.e. the requested page has been found
37     *
38     * @var bool
39     */
40    public $ajaxStatus = false;
41
42    /**
43     * Calls init functions
44     */
45    public function __construct()
46    {
47        parent::__construct();
48        $this->init();
49    }
50
51    /**
52     * Wrapping icon in browse tree
53     *
54     * @param string $thePageIcon Icon IMG code
55     * @param array $row Data row for element.
56     * @return string Page icon
57     */
58    public function wrapIcon($thePageIcon, $row)
59    {
60        /** @var IconFactory $iconFactory */
61        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
62        // If the record is locked, present a warning sign.
63        if ($lockInfo = BackendUtility::isRecordLocked('pages', $row['uid'])) {
64            $aOnClick = 'alert(' . GeneralUtility::quoteJSvalue($lockInfo['msg']) . ');return false;';
65            $lockIcon = '<a href="#" onclick="' . htmlspecialchars($aOnClick) . '">'
66                . '<span title="' . htmlspecialchars($lockInfo['msg']) . '">' . $iconFactory->getIcon('warning-in-use', Icon::SIZE_SMALL)->render() . '</span></a>';
67        } else {
68            $lockIcon = '';
69        }
70        // Wrap icon in click-menu link.
71        if (!$this->ext_IconMode) {
72            $thePageIcon = BackendUtility::wrapClickMenuOnIcon($thePageIcon, 'pages', $row['uid'], 'tree');
73        } elseif ($this->ext_IconMode === 'titlelink') {
74            $aOnClick = 'return jumpTo(' . GeneralUtility::quoteJSvalue($this->getJumpToParam($row)) . ',this,' . GeneralUtility::quoteJSvalue($this->treeName) . ');';
75            $thePageIcon = '<a href="#" onclick="' . htmlspecialchars($aOnClick) . '">' . $thePageIcon . '</a>';
76        }
77        // Wrap icon in a drag/drop span.
78        $dragDropIcon = '<span class="list-tree-icon dragIcon" id="dragIconID_' . $row['uid'] . '">' . $thePageIcon . '</span> ';
79        // Add Page ID:
80        $pageIdStr = '';
81        if ($this->ext_showPageId) {
82            $pageIdStr = '<span class="dragId">[' . $row['uid'] . ']</span> ';
83        }
84        // Call stats information hook
85        $stat = '';
86        $_params = ['pages', $row['uid']];
87        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['GLOBAL']['recStatInfoHooks'] ?? [] as $_funcRef) {
88            $stat .= GeneralUtility::callUserFunction($_funcRef, $_params, $this);
89        }
90        return $dragDropIcon . $lockIcon . $pageIdStr . $stat;
91    }
92
93    /**
94     * Wrapping $title in a-tags.
95     *
96     * @param string $title Title string
97     * @param string $row Item record
98     * @param int $bank Bank pointer (which mount point number)
99     * @return string
100     * @internal
101     */
102    public function wrapTitle($title, $row, $bank = 0)
103    {
104        // Hook for overriding the page title
105
106        $_params = ['title' => &$title, 'row' => &$row];
107        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/class.webpagetree.php']['pageTitleOverlay'] ?? [] as $_funcRef) {
108            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
109        }
110
111        $aOnClick = 'return jumpTo(' . GeneralUtility::quoteJSvalue($this->getJumpToParam($row)) . ',this,' . GeneralUtility::quoteJSvalue($this->domIdPrefix . $this->getId($row)) . ',' . $bank . ');';
112        /** @var array $clickMenuParts */
113        $clickMenuParts = BackendUtility::wrapClickMenuOnIcon('', 'pages', $row['uid'], 'tree', '', '', true);
114
115        $thePageTitle = '<a href="#" onclick="' . htmlspecialchars($aOnClick) . '" ' . GeneralUtility::implodeAttributes($clickMenuParts) . '>' . $title . '</a>';
116        // Wrap title in a drag/drop span.
117        return '<span class="list-tree-title dragTitle" id="dragTitleID_' . $row['uid'] . '">' . $thePageTitle . '</span>';
118    }
119
120    /**
121     * Compiles the HTML code for displaying the structure found inside the ->tree array
122     *
123     * @param array|string $treeArr "tree-array" - if blank string, the internal ->tree array is used.
124     * @return string The HTML code for the tree
125     */
126    public function printTree($treeArr = '')
127    {
128        $titleLen = (int)$this->BE_USER->uc['titleLen'];
129        if (!is_array($treeArr)) {
130            $treeArr = $this->tree;
131        }
132        $out = '<ul class="list-tree list-tree-root">';
133        // -- evaluate AJAX request
134        // IE takes anchor as parameter
135        $PM = GeneralUtility::_GP('PM');
136        if (($PMpos = strpos($PM, '#')) !== false) {
137            $PM = substr($PM, 0, $PMpos);
138        }
139        $PM = explode('_', $PM);
140
141        $doCollapse = false;
142        $doExpand = false;
143        $expandedPageUid = null;
144        $collapsedPageUid = null;
145        if (TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_AJAX && is_array($PM) && count($PM) === 4 && $PM[2] != 0) {
146            if ($PM[1]) {
147                $expandedPageUid = $PM[2];
148                $doExpand = true;
149            } else {
150                $collapsedPageUid = $PM[2];
151                $doCollapse = true;
152            }
153        }
154        // We need to count the opened <ul>'s every time we dig into another level,
155        // so we know how many we have to close when all children are done rendering
156        $closeDepth = [];
157        $ajaxOutput = '';
158        $invertedDepthOfAjaxRequestedItem = 0;
159        foreach ($treeArr as $k => $treeItem) {
160            $classAttr = $treeItem['row']['_CSSCLASS'];
161            $uid = $treeItem['row']['uid'];
162            $idAttr = htmlspecialchars($this->domIdPrefix . $this->getId($treeItem['row']) . '_' . $treeItem['bank']);
163            $itemHTML = '';
164            // If this item is the start of a new level,
165            // then a new level <ul> is needed, but not in ajax mode
166            if ($treeItem['isFirst'] && !$doCollapse && (!$doExpand || (int)$expandedPageUid !== (int)$uid)) {
167                $itemHTML = '<ul class="list-tree">';
168            }
169
170            // Add CSS classes to the list item
171            if ($treeItem['hasSub']) {
172                $classAttr .= ' list-tree-control-open';
173            }
174            $itemHTML .= '<li id="' . $idAttr . '" ' . ($classAttr ? ' class="' . trim($classAttr) . '"' : '')
175                . '><span class="list-tree-group">' . $treeItem['HTML']
176                . $this->wrapTitle($this->getTitleStr($treeItem['row'], $titleLen), $treeItem['row'], $treeItem['bank']) . '</span>';
177            if (!$treeItem['hasSub']) {
178                $itemHTML .= '</li>';
179            }
180
181            // We have to remember if this is the last one
182            // on level X so the last child on level X+1 closes the <ul>-tag
183            if ($treeItem['isLast'] && !($doExpand && $expandedPageUid == $uid)) {
184                $closeDepth[$treeItem['invertedDepth']] = 1;
185            }
186            // If this is the last one and does not have subitems, we need to close
187            // the tree as long as the upper levels have last items too
188            if ($treeItem['isLast'] && !$treeItem['hasSub'] && !$doCollapse && !($doExpand && $expandedPageUid == $uid)) {
189                for ($i = $treeItem['invertedDepth']; $closeDepth[$i] == 1; $i++) {
190                    $closeDepth[$i] = 0;
191                    $itemHTML .= '</ul></li>';
192                }
193            }
194            // Ajax request: collapse
195            if ($doCollapse && (int)$collapsedPageUid === (int)$uid) {
196                $this->ajaxStatus = true;
197                return $itemHTML;
198            }
199            // ajax request: expand
200            if ($doExpand && (int)$expandedPageUid === (int)$uid) {
201                $ajaxOutput .= $itemHTML;
202                $invertedDepthOfAjaxRequestedItem = $treeItem['invertedDepth'];
203            } elseif ($invertedDepthOfAjaxRequestedItem) {
204                if ($treeItem['invertedDepth'] < $invertedDepthOfAjaxRequestedItem) {
205                    $ajaxOutput .= $itemHTML;
206                } else {
207                    $this->ajaxStatus = true;
208                    return $ajaxOutput;
209                }
210            }
211            $out .= $itemHTML;
212        }
213        if ($ajaxOutput) {
214            $this->ajaxStatus = true;
215            return $ajaxOutput;
216        }
217        // Finally close the first ul
218        $out .= '</ul>';
219        return $out;
220    }
221
222    /**
223     * Generate the plus/minus icon for the browsable tree.
224     *
225     * @param array $row Record for the entry
226     * @param int $a The current entry number
227     * @param int $c The total number of entries. If equal to $a, a "bottom" element is returned.
228     * @param int $nextCount The number of sub-elements to the current element.
229     * @param bool $exp The element was expanded to render subelements if this flag is set.
230     * @return string Image tag with the plus/minus icon.
231     * @internal
232     * @see \TYPO3\CMS\Backend\Tree\View\PageTreeView::PMicon()
233     */
234    public function PMicon($row, $a, $c, $nextCount, $exp)
235    {
236        $icon = '';
237        if ($nextCount) {
238            $cmd = $this->bank . '_' . ($exp ? '0_' : '1_') . $row['uid'] . '_' . $this->treeName;
239            $icon = $this->PMiconATagWrap($icon, $cmd, !$exp);
240        }
241        return $icon;
242    }
243
244    /**
245     * Wrap the plus/minus icon in a link
246     *
247     * @param string $icon HTML string to wrap, probably an image tag.
248     * @param string $cmd Command for 'PM' get var
249     * @param bool $isExpand Link-wrapped input string
250     * @return string
251     * @internal
252     */
253    public function PMiconATagWrap($icon, $cmd, $isExpand = true)
254    {
255        if ($this->thisScript) {
256            // Activate dynamic ajax-based tree
257            $js = htmlspecialchars('Tree.load(' . GeneralUtility::quoteJSvalue($cmd) . ', ' . (int)$isExpand . ', this);');
258            return '<a class="list-tree-control' . (!$isExpand ? ' list-tree-control-open' : ' list-tree-control-closed') . '" onclick="' . $js . '"><i class="fa"></i></a>';
259        }
260        return $icon;
261    }
262
263    /**
264     * Will create and return the HTML code for a browsable tree
265     * Is based on the mounts found in the internal array ->MOUNTS (set in the constructor)
266     *
267     * @return string HTML code for the browsable tree
268     */
269    public function getBrowsableTree()
270    {
271        // Get stored tree structure AND updating it if needed according to incoming PM GET var.
272        $this->initializePositionSaving();
273        // Init done:
274        $treeArr = [];
275        // Traverse mounts:
276        $firstHtml = '';
277        foreach ($this->MOUNTS as $idx => $uid) {
278            // Set first:
279            $this->bank = $idx;
280            $isOpen = $this->stored[$idx][$uid] || $this->expandFirst || $uid === '0';
281            // Save ids while resetting everything else.
282            $curIds = $this->ids;
283            $this->reset();
284            $this->ids = $curIds;
285            // Only, if not for uid 0
286            if ($uid) {
287                // Set PM icon for root of mount:
288                $cmd = $this->bank . '_' . ($isOpen ? '0_' : '1_') . $uid . '_' . $this->treeName;
289                $firstHtml = '<a class="list-tree-control list-tree-control-' . ($isOpen ? 'open' : 'closed')
290                    . '" href="' . htmlspecialchars($this->getThisScript() . 'PM=' . $cmd) . '"><i class="fa"></i></a>';
291            }
292            // Preparing rootRec for the mount
293            if ($uid) {
294                $rootRec = $this->getRecord($uid);
295                $firstHtml .= $this->getIcon($rootRec);
296            } else {
297                // Artificial record for the tree root, id=0
298                $rootRec = $this->getRootRecord();
299                $firstHtml .= $this->getRootIcon($rootRec);
300            }
301            if (is_array($rootRec)) {
302                // In case it was swapped inside getRecord due to workspaces.
303                $uid = $rootRec['uid'];
304                // Add the root of the mount to ->tree
305                $this->tree[] = ['HTML' => $firstHtml, 'row' => $rootRec, 'bank' => $this->bank, 'hasSub' => true, 'invertedDepth' => 1000];
306                // If the mount is expanded, go down:
307                if ($isOpen) {
308                    // Set depth:
309                    if ($this->addSelfId) {
310                        $this->ids[] = $uid;
311                    }
312                    $this->getTree($uid);
313                }
314                // Add tree:
315                $treeArr = array_merge($treeArr, $this->tree);
316            }
317        }
318        return $this->printTree($treeArr);
319    }
320}
321