39f7d29252af407f07857df36b5d65ea2e67aa64
[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 = '
137 <!-- TYPO3 tree structure. -->
138 <ul class="tree" id="treeRoot">
139 ';
140 // -- evaluate AJAX request
141 // IE takes anchor as parameter
142 $PM = GeneralUtility::_GP('PM');
143 if (($PMpos = strpos($PM, '#')) !== FALSE) {
144 $PM = substr($PM, 0, $PMpos);
145 }
146 $PM = explode('_', $PM);
147 if (TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_AJAX && is_array($PM) && count($PM) === 4 && $PM[2] != 0) {
148 if ($PM[1]) {
149 $expandedPageUid = $PM[2];
150 $ajaxOutput = '';
151 // We don't know yet. Will be set later.
152 $invertedDepthOfAjaxRequestedItem = 0;
153 $doExpand = TRUE;
154 } else {
155 $collapsedPageUid = $PM[2];
156 $doCollapse = TRUE;
157 }
158 }
159 // We need to count the opened <ul>'s every time we dig into another level,
160 // so we know how many we have to close when all children are done rendering
161 $closeDepth = array();
162 foreach ($treeArr as $k => $v) {
163 $classAttr = $v['row']['_CSSCLASS'];
164 $uid = $v['row']['uid'];
165 $idAttr = htmlspecialchars($this->domIdPrefix . $this->getId($v['row']) . '_' . $v['bank']);
166 $itemHTML = '';
167 // If this item is the start of a new level,
168 // then a new level <ul> is needed, but not in ajax mode
169 if ($v['isFirst'] && !$doCollapse && !($doExpand && $expandedPageUid == $uid)) {
170 $itemHTML = '<ul>';
171 }
172 // Add CSS classes to the list item
173 if ($v['hasSub']) {
174 $classAttr .= $classAttr ? ' expanded' : 'expanded';
175 }
176 if ($v['isLast']) {
177 $classAttr .= $classAttr ? ' last' : 'last';
178 }
179 $itemHTML .= '
180 <li id="' . $idAttr . '"' . ($classAttr ? ' class="' . $classAttr . '"' : '') . '><div class="treeLinkItem">' . $v['HTML'] . $this->wrapTitle($this->getTitleStr($v['row'], $titleLen), $v['row'], $v['bank']) . '</div>
181 ';
182 if (!$v['hasSub']) {
183 $itemHTML .= '</li>';
184 }
185 // We have to remember if this is the last one
186 // on level X so the last child on level X+1 closes the <ul>-tag
187 if ($v['isLast'] && !($doExpand && $expandedPageUid == $uid)) {
188 $closeDepth[$v['invertedDepth']] = 1;
189 }
190 // If this is the last one and does not have subitems, we need to close
191 // the tree as long as the upper levels have last items too
192 if ($v['isLast'] && !$v['hasSub'] && !$doCollapse && !($doExpand && $expandedPageUid == $uid)) {
193 for ($i = $v['invertedDepth']; $closeDepth[$i] == 1; $i++) {
194 $closeDepth[$i] = 0;
195 $itemHTML .= '</ul></li>';
196 }
197 }
198 // Ajax request: collapse
199 if ($doCollapse && $collapsedPageUid == $uid) {
200 $this->ajaxStatus = TRUE;
201 return $itemHTML;
202 }
203 // ajax request: expand
204 if ($doExpand && $expandedPageUid == $uid) {
205 $ajaxOutput .= $itemHTML;
206 $invertedDepthOfAjaxRequestedItem = $v['invertedDepth'];
207 } elseif ($invertedDepthOfAjaxRequestedItem) {
208 if ($v['invertedDepth'] < $invertedDepthOfAjaxRequestedItem) {
209 $ajaxOutput .= $itemHTML;
210 } else {
211 $this->ajaxStatus = TRUE;
212 return $ajaxOutput;
213 }
214 }
215 $out .= $itemHTML;
216 }
217 if ($ajaxOutput) {
218 $this->ajaxStatus = TRUE;
219 return $ajaxOutput;
220 }
221 // Finally close the first ul
222 $out .= '</ul>';
223 return $out;
224 }
225
226 /**
227 * Generate the plus/minus icon for the browsable tree.
228 *
229 * @param array $row Record for the entry
230 * @param int $a The current entry number
231 * @param int $c The total number of entries. If equal to $a, a "bottom" element is returned.
232 * @param int $nextCount The number of sub-elements to the current element.
233 * @param bool $exp The element was expanded to render subelements if this flag is set.
234 * @return string Image tag with the plus/minus icon.
235 * @access private
236 * @see \TYPO3\CMS\Backend\Tree\View\PageTreeView::PMicon()
237 */
238 public function PMicon($row, $a, $c, $nextCount, $exp) {
239 $PM = $nextCount ? ($exp ? 'minus' : 'plus') : 'join';
240 $BTM = $a == $c ? 'bottom' : '';
241 $icon = '<img' . IconUtility::skinImg($this->backPath, ('gfx/ol/' . $PM . $BTM . '.gif'), 'width="18" height="16"') . ' alt="" />';
242 if ($nextCount) {
243 $cmd = $this->bank . '_' . ($exp ? '0_' : '1_') . $row['uid'] . '_' . $this->treeName;
244 $icon = $this->PMiconATagWrap($icon, $cmd, !$exp);
245 }
246 return $icon;
247 }
248
249 /**
250 * Wrap the plus/minus icon in a link
251 *
252 * @param string $icon HTML string to wrap, probably an image tag.
253 * @param string $cmd Command for 'PM' get var
254 * @return bool $isExpand Link-wrapped input string
255 * @access private
256 */
257 public function PMiconATagWrap($icon, $cmd, $isExpand = TRUE) {
258 if ($this->thisScript) {
259 // Activate dynamic ajax-based tree
260 $js = htmlspecialchars('Tree.load(' . GeneralUtility::quoteJSvalue($cmd) . ', ' . (int)$isExpand . ', this);');
261 return '<a class="pm" onclick="' . $js . '">' . $icon . '</a>';
262 } else {
263 return $icon;
264 }
265 }
266
267 /**
268 * Will create and return the HTML code for a browsable tree
269 * Is based on the mounts found in the internal array ->MOUNTS (set in the constructor)
270 *
271 * @return string HTML code for the browsable tree
272 */
273 public function getBrowsableTree() {
274 // Get stored tree structure AND updating it if needed according to incoming PM GET var.
275 $this->initializePositionSaving();
276 // Init done:
277 $titleLen = (int)$this->BE_USER->uc['titleLen'];
278 $treeArr = array();
279 // Traverse mounts:
280 foreach ($this->MOUNTS as $idx => $uid) {
281 // Set first:
282 $this->bank = $idx;
283 $isOpen = $this->stored[$idx][$uid] || $this->expandFirst || $uid === '0';
284 // Save ids while resetting everything else.
285 $curIds = $this->ids;
286 $this->reset();
287 $this->ids = $curIds;
288 // Set PM icon for root of mount:
289 $cmd = $this->bank . '_' . ($isOpen ? '0_' : '1_') . $uid . '_' . $this->treeName;
290 // Only, if not for uid 0
291 if ($uid) {
292 $icon = '<img' . IconUtility::skinImg($this->backPath, ('gfx/ol/' . ($isOpen ? 'minus' : 'plus') . 'only.gif')) . ' alt="" />';
293 $firstHtml = $this->PMiconATagWrap($icon, $cmd, !$isOpen);
294 }
295 // Preparing rootRec for the mount
296 if ($uid) {
297 $rootRec = $this->getRecord($uid);
298 $firstHtml .= $this->getIcon($rootRec);
299 } else {
300 // Artificial record for the tree root, id=0
301 $rootRec = $this->getRootRecord($uid);
302 $firstHtml .= $this->getRootIcon($rootRec);
303 }
304 if (is_array($rootRec)) {
305 // In case it was swapped inside getRecord due to workspaces.
306 $uid = $rootRec['uid'];
307 // Add the root of the mount to ->tree
308 $this->tree[] = array('HTML' => $firstHtml, 'row' => $rootRec, 'bank' => $this->bank, 'hasSub' => TRUE, 'invertedDepth' => 1000);
309 // If the mount is expanded, go down:
310 if ($isOpen) {
311 // Set depth:
312 if ($this->addSelfId) {
313 $this->ids[] = $uid;
314 }
315 $this->getTree($uid, 999, '', $rootRec['_SUBCSSCLASS']);
316 }
317 // Add tree:
318 $treeArr = array_merge($treeArr, $this->tree);
319 }
320 }
321 return $this->printTree($treeArr);
322 }
323
324 /**
325 * Fetches the data for the tree
326 *
327 * @param int $uid Item id for which to select subitems (parent id)
328 * @param int $depth Max depth (recursivity limit)
329 * @param string $blankLineCode ? (internal)
330 * @param string $subCSSclass
331 * @return int The count of items on the level
332 */
333 public function getTree($uid, $depth = 999, $blankLineCode = '', $subCSSclass = '') {
334 // Buffer for id hierarchy is reset:
335 $this->buffer_idH = array();
336 // Init vars
337 $depth = (int)$depth;
338 $HTML = '';
339 $a = 0;
340 $res = $this->getDataInit($uid, $subCSSclass);
341 $c = $this->getDataCount($res);
342 $crazyRecursionLimiter = 999;
343 $inMenuPages = array();
344 $outOfMenuPages = array();
345 $outOfMenuPagesTextIndex = array();
346 while ($crazyRecursionLimiter > 0 && ($row = $this->getDataNext($res, $subCSSclass))) {
347 $crazyRecursionLimiter--;
348 // Not in menu:
349 if ($this->ext_separateNotinmenuPages && ($row['doktype'] == \TYPO3\CMS\Frontend\Page\PageRepository::DOKTYPE_BE_USER_SECTION || $row['doktype'] >= 200 || $row['nav_hide'])) {
350 $outOfMenuPages[] = $row;
351 $outOfMenuPagesTextIndex[] = ($row['doktype'] >= 200 ? 'zzz' . $row['doktype'] . '_' : '') . $row['title'];
352 } else {
353 $inMenuPages[] = $row;
354 }
355 }
356 $label_shownAlphabetically = '';
357 if (!empty($outOfMenuPages)) {
358 // Sort out-of-menu pages:
359 $outOfMenuPages_alphabetic = array();
360 if ($this->ext_alphasortNotinmenuPages) {
361 asort($outOfMenuPagesTextIndex);
362 $label_shownAlphabetically = ' (alphabetic)';
363 }
364 foreach ($outOfMenuPagesTextIndex as $idx => $txt) {
365 $outOfMenuPages_alphabetic[] = $outOfMenuPages[$idx];
366 }
367 // Merge:
368 $outOfMenuPages_alphabetic[0]['_FIRST_NOT_IN_MENU'] = TRUE;
369 $allRows = array_merge($inMenuPages, $outOfMenuPages_alphabetic);
370 } else {
371 $allRows = $inMenuPages;
372 }
373 // Traverse the records:
374 foreach ($allRows as $row) {
375 $a++;
376 $newID = $row['uid'];
377 // Reserve space.
378 $this->tree[] = array();
379 end($this->tree);
380 // Get the key for this space
381 $treeKey = key($this->tree);
382 $LN = $a == $c ? 'blank' : 'line';
383 // If records should be accumulated, do so
384 if ($this->setRecs) {
385 $this->recs[$row['uid']] = $row;
386 }
387 // Accumulate the id of the element in the internal arrays
388 $this->ids[] = ($idH[$row['uid']]['uid'] = $row['uid']);
389 $this->ids_hierarchy[$depth][] = $row['uid'];
390 // Make a recursive call to the next level
391 if ($depth > 1 && $this->expandNext($newID) && !$row['php_tree_stop']) {
392 $nextCount = $this->getTree($newID, $depth - 1, $blankLineCode . ',' . $LN, $row['_SUBCSSCLASS']);
393 if (!empty($this->buffer_idH)) {
394 $idH[$row['uid']]['subrow'] = $this->buffer_idH;
395 }
396 // Set "did expand" flag
397 $exp = 1;
398 } else {
399 $nextCount = $this->getCount($newID);
400 // Clear "did expand" flag
401 $exp = 0;
402 }
403 // Set HTML-icons, if any:
404 if ($this->makeHTML) {
405 if ($row['_FIRST_NOT_IN_MENU']) {
406 $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>';
407 } else {
408 $HTML = '';
409 }
410 $HTML .= $this->PMicon($row, $a, $c, $nextCount, $exp);
411 $HTML .= $this->wrapStop($this->getIcon($row), $row);
412 }
413 // Finally, add the row/HTML content to the ->tree array in the reserved key.
414 $this->tree[$treeKey] = array(
415 'row' => $row,
416 'HTML' => $HTML,
417 'hasSub' => $nextCount && $this->expandNext($newID),
418 'isFirst' => $a == 1,
419 'isLast' => FALSE,
420 'invertedDepth' => $depth,
421 'blankLineCode' => $blankLineCode,
422 'bank' => $this->bank
423 );
424 }
425 if ($a) {
426 $this->tree[$treeKey]['isLast'] = TRUE;
427 }
428 $this->getDataFree($res);
429 $this->buffer_idH = $idH;
430 return $c;
431 }
432
433 }