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