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