[TASK] Clean up tree code
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / View / PageTreeView.php
1 <?php
2 namespace TYPO3\CMS\Backend\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\Utility\IconUtility;
18 use TYPO3\CMS\Core\Utility\GeneralUtility;
19
20 /**
21 * Browse pages in Web module
22 */
23 class PageTreeView extends \TYPO3\CMS\Backend\Tree\View\BrowseTreeView {
24
25 /**
26 * @var bool
27 */
28 public $ext_showPageId;
29
30 /**
31 * @var string
32 */
33 public $ext_IconMode;
34
35 /**
36 * @var string
37 */
38 public $ext_separateNotinmenuPages;
39
40 /**
41 * @var string
42 */
43 public $ext_alphasortNotinmenuPages;
44
45 /**
46 * Indicates, whether the ajax call was successful, i.e. the requested page has been found
47 *
48 * @var bool
49 */
50 public $ajaxStatus = FALSE;
51
52 /**
53 * Calls init functions
54 */
55 public function __construct() {
56 $this->init();
57 }
58
59 /**
60 * Wrapping icon in browse tree
61 *
62 * @param string $thePageIcon Icon IMG code
63 * @param array $row Data row for element.
64 * @return string Page icon
65 */
66 public function wrapIcon($thePageIcon, &$row) {
67 // If the record is locked, present a warning sign.
68 if ($lockInfo = \TYPO3\CMS\Backend\Utility\BackendUtility::isRecordLocked('pages', $row['uid'])) {
69 $aOnClick = 'alert(' . GeneralUtility::quoteJSvalue($lockInfo['msg']) . ');return false;';
70 $lockIcon = '<a href="#" onclick="' . htmlspecialchars($aOnClick) . '">' . IconUtility::getSpriteIcon('status-warning-in-use', array('title' => $lockInfo['msg'])) . '</a>';
71 } else {
72 $lockIcon = '';
73 }
74 // Wrap icon in click-menu link.
75 if (!$this->ext_IconMode) {
76 $thePageIcon = $GLOBALS['TBE_TEMPLATE']->wrapClickMenuOnIcon($thePageIcon, 'pages', $row['uid'], 0, '&bank=' . $this->bank);
77 } elseif ($this->ext_IconMode === 'titlelink') {
78 $aOnClick = 'return jumpTo(' . GeneralUtility::quoteJSvalue($this->getJumpToParam($row)) . ',this,' . GeneralUtility::quoteJSvalue($this->treeName) . ');';
79 $thePageIcon = '<a href="#" onclick="' . htmlspecialchars($aOnClick) . '">' . $thePageIcon . '</a>';
80 }
81 // Wrap icon in a drag/drop span.
82 $dragDropIcon = '<span class="dragIcon" id="dragIconID_' . $row['uid'] . '">' . $thePageIcon . '</span>';
83 // Add Page ID:
84 $pageIdStr = '';
85 if ($this->ext_showPageId) {
86 $pageIdStr = '<span class="dragId">[' . $row['uid'] . ']</span> ';
87 }
88 // Call stats information hook
89 $stat = '';
90 if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['GLOBAL']['recStatInfoHooks'])) {
91 $_params = array('pages', $row['uid']);
92 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['GLOBAL']['recStatInfoHooks'] as $_funcRef) {
93 $stat .= GeneralUtility::callUserFunction($_funcRef, $_params, $this);
94 }
95 }
96 return $dragDropIcon . $lockIcon . $pageIdStr . $stat;
97 }
98
99 /**
100 * Wrapping $title in a-tags.
101 *
102 * @param string $title Title string
103 * @param string $row Item record
104 * @param int $bank Bank pointer (which mount point number)
105 * @return string
106 * @access private
107 */
108 public function wrapTitle($title, $row, $bank = 0) {
109 // Hook for overriding the page title
110 if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/class.webpagetree.php']['pageTitleOverlay'])) {
111 $_params = array('title' => &$title, 'row' => &$row);
112 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/class.webpagetree.php']['pageTitleOverlay'] as $_funcRef) {
113 GeneralUtility::callUserFunction($_funcRef, $_params, $this);
114 }
115 unset($_params);
116 }
117 $aOnClick = 'return jumpTo(' . GeneralUtility::quoteJSvalue($this->getJumpToParam($row)) . ',this,' . GeneralUtility::quoteJSvalue($this->domIdPrefix . $this->getId($row)) . ',' . $bank . ');';
118 $clickMenuParts = $GLOBALS['TBE_TEMPLATE']->wrapClickMenuOnIcon('', 'pages', $row['uid'], 0, ('&bank=' . $this->bank), '', TRUE);
119
120 $thePageTitle = '<a href="#" onclick="' . htmlspecialchars($aOnClick) . '"' . GeneralUtility::implodeAttributes($clickMenuParts) . '>' . $title . '</a>';
121 // Wrap title in a drag/drop span.
122 return '<span class="dragTitle" id="dragTitleID_' . $row['uid'] . '">' . $thePageTitle . '</span>';
123 }
124
125 /**
126 * Compiles the HTML code for displaying the structure found inside the ->tree array
127 *
128 * @param array $treeArr "tree-array" - if blank string, the internal ->tree array is used.
129 * @return string The HTML code for the tree
130 */
131 public function printTree($treeArr = '') {
132 $titleLen = (int)$this->BE_USER->uc['titleLen'];
133 if (!is_array($treeArr)) {
134 $treeArr = $this->tree;
135 }
136 $out = '<ul class="tree list-tree-root">';
137 // -- evaluate AJAX request
138 // IE takes anchor as parameter
139 $PM = GeneralUtility::_GP('PM');
140 if (($PMpos = strpos($PM, '#')) !== FALSE) {
141 $PM = substr($PM, 0, $PMpos);
142 }
143 $PM = explode('_', $PM);
144 if (TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_AJAX && is_array($PM) && count($PM) === 4 && $PM[2] != 0) {
145 if ($PM[1]) {
146 $expandedPageUid = $PM[2];
147 $ajaxOutput = '';
148 // We don't know yet. Will be set later.
149 $invertedDepthOfAjaxRequestedItem = 0;
150 $doExpand = TRUE;
151 } else {
152 $collapsedPageUid = $PM[2];
153 $doCollapse = TRUE;
154 }
155 }
156 // We need to count the opened <ul>'s every time we dig into another level,
157 // so we know how many we have to close when all children are done rendering
158 $closeDepth = array();
159 foreach ($treeArr as $k => $v) {
160 $classAttr = $v['row']['_CSSCLASS'];
161 $uid = $v['row']['uid'];
162 $idAttr = htmlspecialchars($this->domIdPrefix . $this->getId($v['row']) . '_' . $v['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 ($v['isFirst'] && !$doCollapse && !($doExpand && $expandedPageUid == $uid)) {
167 $itemHTML = '<ul>';
168 }
169 // Add CSS classes to the list item
170 if ($v['hasSub']) {
171 $classAttr .= $classAttr ? ' expanded' : 'expanded';
172 }
173 if ($v['isLast']) {
174 $classAttr .= $classAttr ? ' last' : 'last';
175 }
176 $itemHTML .= '
177 <li id="' . $idAttr . '"' . ($classAttr ? ' class="' . $classAttr . '"' : '') . '><div class="treeLinkItem">' . $v['HTML'] . $this->wrapTitle($this->getTitleStr($v['row'], $titleLen), $v['row'], $v['bank']) . '</div>
178 ';
179 if (!$v['hasSub']) {
180 $itemHTML .= '</li>';
181 }
182 // We have to remember if this is the last one
183 // on level X so the last child on level X+1 closes the <ul>-tag
184 if ($v['isLast'] && !($doExpand && $expandedPageUid == $uid)) {
185 $closeDepth[$v['invertedDepth']] = 1;
186 }
187 // If this is the last one and does not have subitems, we need to close
188 // the tree as long as the upper levels have last items too
189 if ($v['isLast'] && !$v['hasSub'] && !$doCollapse && !($doExpand && $expandedPageUid == $uid)) {
190 for ($i = $v['invertedDepth']; $closeDepth[$i] == 1; $i++) {
191 $closeDepth[$i] = 0;
192 $itemHTML .= '</ul></li>';
193 }
194 }
195 // Ajax request: collapse
196 if ($doCollapse && $collapsedPageUid == $uid) {
197 $this->ajaxStatus = TRUE;
198 return $itemHTML;
199 }
200 // ajax request: expand
201 if ($doExpand && $expandedPageUid == $uid) {
202 $ajaxOutput .= $itemHTML;
203 $invertedDepthOfAjaxRequestedItem = $v['invertedDepth'];
204 } elseif ($invertedDepthOfAjaxRequestedItem) {
205 if ($v['invertedDepth'] < $invertedDepthOfAjaxRequestedItem) {
206 $ajaxOutput .= $itemHTML;
207 } else {
208 $this->ajaxStatus = TRUE;
209 return $ajaxOutput;
210 }
211 }
212 $out .= $itemHTML;
213 }
214 if ($ajaxOutput) {
215 $this->ajaxStatus = TRUE;
216 return $ajaxOutput;
217 }
218 // Finally close the first ul
219 $out .= '</ul>';
220 return $out;
221 }
222
223 /**
224 * Generate the plus/minus icon for the browsable tree.
225 *
226 * @param array $row Record for the entry
227 * @param int $a The current entry number
228 * @param int $c The total number of entries. If equal to $a, a "bottom" element is returned.
229 * @param int $nextCount The number of sub-elements to the current element.
230 * @param bool $exp The element was expanded to render subelements if this flag is set.
231 * @return string Image tag with the plus/minus icon.
232 * @access private
233 * @see \TYPO3\CMS\Backend\Tree\View\PageTreeView::PMicon()
234 */
235 public function PMicon($row, $a, $c, $nextCount, $exp) {
236 $PM = $nextCount ? ($exp ? 'minus' : 'plus') : 'join';
237 $BTM = $a == $c ? 'bottom' : '';
238 $icon = '<img' . IconUtility::skinImg($this->backPath, ('gfx/ol/' . $PM . $BTM . '.gif'), 'width="18" height="16"') . ' alt="" />';
239 if ($nextCount) {
240 $cmd = $this->bank . '_' . ($exp ? '0_' : '1_') . $row['uid'] . '_' . $this->treeName;
241 $icon = $this->PMiconATagWrap($icon, $cmd, !$exp);
242 }
243 return $icon;
244 }
245
246 /**
247 * Wrap the plus/minus icon in a link
248 *
249 * @param string $icon HTML string to wrap, probably an image tag.
250 * @param string $cmd Command for 'PM' get var
251 * @return bool $isExpand Link-wrapped input string
252 * @access private
253 */
254 public function PMiconATagWrap($icon, $cmd, $isExpand = TRUE) {
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="pm" onclick="' . $js . '">' . $icon . '</a>';
259 } else {
260 return $icon;
261 }
262 }
263
264 /**
265 * Will create and return the HTML code for a browsable tree
266 * Is based on the mounts found in the internal array ->MOUNTS (set in the constructor)
267 *
268 * @return string HTML code for the browsable tree
269 */
270 public function getBrowsableTree() {
271 // Get stored tree structure AND updating it if needed according to incoming PM GET var.
272 $this->initializePositionSaving();
273 // Init done:
274 $titleLen = (int)$this->BE_USER->uc['titleLen'];
275 $treeArr = array();
276 // Traverse mounts:
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 // Set PM icon for root of mount:
286 $cmd = $this->bank . '_' . ($isOpen ? '0_' : '1_') . $uid . '_' . $this->treeName;
287 // Only, if not for uid 0
288 if ($uid) {
289 $icon = '<img' . IconUtility::skinImg($this->backPath, ('gfx/ol/' . ($isOpen ? 'minus' : 'plus') . 'only.gif')) . ' alt="" />';
290 $firstHtml = $this->PMiconATagWrap($icon, $cmd, !$isOpen);
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($uid);
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[] = array('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 /**
322 * Fetches the data for the tree
323 *
324 * @param int $uid Item id for which to select subitems (parent id)
325 * @param int $depth Max depth (recursivity limit)
326 * @param string $blankLineCode ? (internal)
327 * @param string $subCSSclass
328 * @return int The count of items on the level
329 */
330 public function getTree($uid, $depth = 999, $blankLineCode = '', $subCSSclass = '') {
331 // Buffer for id hierarchy is reset:
332 $this->buffer_idH = array();
333 // Init vars
334 $depth = (int)$depth;
335 $HTML = '';
336 $a = 0;
337 $res = $this->getDataInit($uid);
338 $c = $this->getDataCount($res);
339 $crazyRecursionLimiter = 999;
340 $inMenuPages = array();
341 $outOfMenuPages = array();
342 $outOfMenuPagesTextIndex = array();
343 while ($crazyRecursionLimiter > 0 && ($row = $this->getDataNext($res))) {
344 $crazyRecursionLimiter--;
345 // Not in menu:
346 if ($this->ext_separateNotinmenuPages && ($row['doktype'] == \TYPO3\CMS\Frontend\Page\PageRepository::DOKTYPE_BE_USER_SECTION || $row['doktype'] >= 200 || $row['nav_hide'])) {
347 $outOfMenuPages[] = $row;
348 $outOfMenuPagesTextIndex[] = ($row['doktype'] >= 200 ? 'zzz' . $row['doktype'] . '_' : '') . $row['title'];
349 } else {
350 $inMenuPages[] = $row;
351 }
352 }
353 $label_shownAlphabetically = '';
354 if (!empty($outOfMenuPages)) {
355 // Sort out-of-menu pages:
356 $outOfMenuPages_alphabetic = array();
357 if ($this->ext_alphasortNotinmenuPages) {
358 asort($outOfMenuPagesTextIndex);
359 $label_shownAlphabetically = ' (alphabetic)';
360 }
361 foreach ($outOfMenuPagesTextIndex as $idx => $txt) {
362 $outOfMenuPages_alphabetic[] = $outOfMenuPages[$idx];
363 }
364 // Merge:
365 $outOfMenuPages_alphabetic[0]['_FIRST_NOT_IN_MENU'] = TRUE;
366 $allRows = array_merge($inMenuPages, $outOfMenuPages_alphabetic);
367 } else {
368 $allRows = $inMenuPages;
369 }
370 // Traverse the records:
371 foreach ($allRows as $row) {
372 $a++;
373 $newID = $row['uid'];
374 // Reserve space.
375 $this->tree[] = array();
376 end($this->tree);
377 // Get the key for this space
378 $treeKey = key($this->tree);
379 $LN = $a == $c ? 'blank' : 'line';
380 // If records should be accumulated, do so
381 if ($this->setRecs) {
382 $this->recs[$row['uid']] = $row;
383 }
384 // Accumulate the id of the element in the internal arrays
385 $this->ids[] = ($idH[$row['uid']]['uid'] = $row['uid']);
386 $this->ids_hierarchy[$depth][] = $row['uid'];
387 // Make a recursive call to the next level
388 if ($depth > 1 && $this->expandNext($newID) && !$row['php_tree_stop']) {
389 $nextCount = $this->getTree($newID, $depth - 1, $blankLineCode . ',' . $LN, $row['_SUBCSSCLASS']);
390 if (!empty($this->buffer_idH)) {
391 $idH[$row['uid']]['subrow'] = $this->buffer_idH;
392 }
393 // Set "did expand" flag
394 $exp = 1;
395 } else {
396 $nextCount = $this->getCount($newID);
397 // Clear "did expand" flag
398 $exp = 0;
399 }
400 // Set HTML-icons, if any:
401 if ($this->makeHTML) {
402 if ($row['_FIRST_NOT_IN_MENU']) {
403 $HTML = '<img' . IconUtility::skinImg($this->backPath, 'gfx/ol/line.gif') . ' alt="" /><br/><img' . IconUtility::skinImg($this->backPath, 'gfx/ol/line.gif') . ' alt="" /><i>Not shown in menu' . $label_shownAlphabetically . ':</i><br>';
404 } else {
405 $HTML = '';
406 }
407 $HTML .= $this->PMicon($row, $a, $c, $nextCount, $exp);
408 $HTML .= $this->wrapStop($this->getIcon($row), $row);
409 }
410 // Finally, add the row/HTML content to the ->tree array in the reserved key.
411 $this->tree[$treeKey] = array(
412 'row' => $row,
413 'HTML' => $HTML,
414 'hasSub' => $nextCount && $this->expandNext($newID),
415 'isFirst' => $a == 1,
416 'isLast' => FALSE,
417 'invertedDepth' => $depth,
418 'blankLineCode' => $blankLineCode,
419 'bank' => $this->bank
420 );
421 }
422 if ($a) {
423 $this->tree[$treeKey]['isLast'] = TRUE;
424 }
425 $this->getDataFree($res);
426 $this->buffer_idH = $idH;
427 return $c;
428 }
429
430 }