[!!!][FEATURE] Replace ExtJS page tree
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Tree / Pagetree / DataProvider.php
1 <?php
2 namespace TYPO3\CMS\Backend\Tree\Pagetree;
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\BackendUtility;
18 use TYPO3\CMS\Core\Database\ConnectionPool;
19 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
20 use TYPO3\CMS\Core\Database\Query\QueryHelper;
21 use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction;
22 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
23 use TYPO3\CMS\Core\Utility\GeneralUtility;
24
25 /**
26 * Page tree data provider.
27 */
28 class DataProvider extends \TYPO3\CMS\Backend\Tree\AbstractTreeDataProvider
29 {
30 /**
31 * Node limit that should be loaded for this request per mount
32 *
33 * @var int
34 */
35 protected $nodeLimit = 0;
36
37 /**
38 * Current amount of nodes
39 *
40 * @var int
41 */
42 protected $nodeCounter = 0;
43
44 /**
45 * TRUE to show the path of each mountpoint in the tree
46 *
47 * @var bool
48 */
49 protected $showRootlineAboveMounts = false;
50
51 /**
52 * Hidden Records
53 *
54 * @var array<string>
55 */
56 protected $hiddenRecords = [];
57
58 /**
59 * Process collection hook objects
60 *
61 * @var array<\TYPO3\CMS\Backend\Tree\Pagetree\CollectionProcessorInterface>
62 */
63 protected $processCollectionHookObjects = [];
64
65 /**
66 * Constructor
67 *
68 * @param int $nodeLimit (optional)
69 */
70 public function __construct($nodeLimit = null)
71 {
72 $nodeLimit = $nodeLimit ?? $GLOBALS['TYPO3_CONF_VARS']['BE']['pageTree']['preloadLimit'];
73 $this->nodeLimit = abs((int)$nodeLimit);
74
75 $this->showRootlineAboveMounts = $GLOBALS['BE_USER']->getTSConfigVal('options.pageTree.showPathAboveMounts');
76
77 $this->hiddenRecords = GeneralUtility::trimExplode(',', $GLOBALS['BE_USER']->getTSConfigVal('options.hideRecords.pages'));
78 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/tree/pagetree/class.t3lib_tree_pagetree_dataprovider.php']['postProcessCollections'] ?? [] as $className) {
79 /** @var $hookObject \TYPO3\CMS\Backend\Tree\Pagetree\CollectionProcessorInterface */
80 $hookObject = GeneralUtility::makeInstance($className);
81 if ($hookObject instanceof \TYPO3\CMS\Backend\Tree\Pagetree\CollectionProcessorInterface) {
82 $this->processCollectionHookObjects[] = $hookObject;
83 }
84 }
85 }
86
87 /**
88 * Returns the root node.
89 *
90 * @return \TYPO3\CMS\Backend\Tree\TreeNode the root node
91 */
92 public function getRoot()
93 {
94 /** @var $node \TYPO3\CMS\Backend\Tree\Pagetree\PagetreeNode */
95 $node = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Tree\Pagetree\PagetreeNode::class);
96 $node->setId('root');
97 $node->setExpanded(true);
98 return $node;
99 }
100
101 /**
102 * Fetches the sub-nodes of the given node
103 *
104 * @param \TYPO3\CMS\Backend\Tree\TreeNode $node
105 * @param int $mountPoint
106 * @param int $level internally used variable as a recursion limiter
107 * @return \TYPO3\CMS\Backend\Tree\TreeNodeCollection
108 */
109 public function getNodes(\TYPO3\CMS\Backend\Tree\TreeNode $node, $mountPoint = 0, $level = 0)
110 {
111 /** @var $nodeCollection \TYPO3\CMS\Backend\Tree\Pagetree\PagetreeNodeCollection */
112 $nodeCollection = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Tree\Pagetree\PagetreeNodeCollection::class);
113 if ($level >= 99 || ($node->getStopPageTree() && $node->getId() !== $mountPoint)) {
114 return $nodeCollection;
115 }
116 $isVirtualRootNode = false;
117 $subpages = $this->getSubpages($node->getId());
118 // check if fetching subpages the "root"-page
119 // and in case of a virtual root return the mountpoints as virtual "subpages"
120 if ((int)$node->getId() === 0) {
121 // check no temporary mountpoint is used
122 if (!(int)$GLOBALS['BE_USER']->uc['pageTree_temporaryMountPoint']) {
123 $mountPoints = array_map('intval', $GLOBALS['BE_USER']->returnWebmounts());
124 $mountPoints = array_unique($mountPoints);
125 if (!in_array(0, $mountPoints, true)) {
126 // using a virtual root node
127 // so then return the mount points here as "subpages" of the first node
128 $isVirtualRootNode = true;
129 $subpages = [];
130 foreach ($mountPoints as $webMountPoint) {
131 $subpage = BackendUtility::getRecordWSOL('pages', $webMountPoint, '*', '', true, true);
132 $subpage['isMountPoint'] = true;
133 $subpages[] = $subpage;
134 }
135 }
136 }
137 }
138 if (is_array($subpages) && !empty($subpages)) {
139 $lastRootline = [];
140 foreach ($subpages as $subpage) {
141 if (in_array($subpage['t3ver_oid'] ?: $subpage['uid'], $this->hiddenRecords)) {
142 continue;
143 }
144 // must be calculated before getRecordWSOL(),
145 // because the information is lost otherwise
146 $isMountPoint = $subpage['isMountPoint'] === true;
147 if ($isVirtualRootNode) {
148 $mountPoint = (int)$subpage['t3ver_oid'] ?: $subpage['uid'];
149 }
150 $subNode = Commands::getNewNode($subpage, $mountPoint);
151 $subNode->setIsMountPoint($isMountPoint);
152 if ($isMountPoint && $this->showRootlineAboveMounts) {
153 if ($subpage['pid'] > 0) {
154 $rootline = Commands::getMountPointPath($subpage['pid']);
155 } else {
156 $rootline = Commands::getMountPointPath($subpage['uid']);
157 }
158 if ($lastRootline !== $rootline) {
159 $subNode->setReadableRootline($rootline);
160 }
161 $lastRootline = $rootline;
162 }
163 if ($this->nodeCounter < $this->nodeLimit) {
164 $childNodes = $this->getNodes($subNode, $mountPoint, $level + 1);
165 $subNode->setChildNodes($childNodes);
166 $this->nodeCounter += $childNodes->count();
167 } else {
168 $subNode->setLeaf(!$this->hasNodeSubPages((int)$subNode->getId()));
169 }
170 if (!$GLOBALS['BE_USER']->isAdmin() && (int)$subpage['editlock'] === 1) {
171 $subNode->setLabelIsEditable(false);
172 }
173 $nodeCollection->append($subNode);
174 }
175 }
176 foreach ($this->processCollectionHookObjects as $hookObject) {
177 /** @var $hookObject \TYPO3\CMS\Backend\Tree\Pagetree\CollectionProcessorInterface */
178 $hookObject->postProcessGetNodes($node, $mountPoint, $level, $nodeCollection);
179 }
180 return $nodeCollection;
181 }
182
183 /**
184 * Returns a node collection of filtered nodes
185 *
186 * @param \TYPO3\CMS\Backend\Tree\TreeNode $node
187 * @param string $searchFilter
188 * @param int $mountPoint
189 * @return \TYPO3\CMS\Backend\Tree\Pagetree\PagetreeNodeCollection the filtered nodes
190 */
191 public function getFilteredNodes(\TYPO3\CMS\Backend\Tree\TreeNode $node, $searchFilter, $mountPoint = 0)
192 {
193 /** @var $nodeCollection \TYPO3\CMS\Backend\Tree\Pagetree\PagetreeNodeCollection */
194 $nodeCollection = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Tree\Pagetree\PagetreeNodeCollection::class);
195 $records = $this->getPagesByQuery($searchFilter);
196 if (!is_array($records) || empty($records)) {
197 return $nodeCollection;
198 }
199 if (count($records) > 500) {
200 return $nodeCollection;
201 }
202 // check no temporary mountpoint is used
203 $mountPoints = (int)$GLOBALS['BE_USER']->uc['pageTree_temporaryMountPoint'];
204 if (!$mountPoints) {
205 $mountPoints = array_map('intval', $GLOBALS['BE_USER']->returnWebmounts());
206 $mountPoints = array_unique($mountPoints);
207 } else {
208 $mountPoints = [$mountPoints];
209 }
210 $isNumericSearchFilter = is_numeric($searchFilter) && $searchFilter > 0;
211 $searchFilterQuoted = preg_quote($searchFilter, '/');
212 $nodeId = (int)$node->getId();
213 $processedRecordIds = [];
214 foreach ($records as $record) {
215 $uid = (int)$record['t3ver_oid'] ?: $record['uid'];
216 if (in_array($uid, $this->hiddenRecords) || in_array($uid, $processedRecordIds, true)
217 || (
218 (int)$record['pid'] === -1 && (
219 (int)$record['t3ver_wsid'] === 0
220 || (int)$record['t3ver_wsid'] !== (int)$GLOBALS['BE_USER']->workspace
221 )
222 )
223 ) {
224 continue;
225 }
226 $processedRecordIds[] = $uid;
227
228 $rootline = BackendUtility::BEgetRootLine(
229 $uid,
230 '',
231 $GLOBALS['BE_USER']->workspace != 0,
232 [
233 'hidden',
234 'starttime',
235 'endtime',
236 ]
237 );
238 $rootline = array_reverse($rootline);
239 if (!in_array(0, $mountPoints, true)) {
240 $isInsideMountPoints = false;
241 foreach ($rootline as $rootlineElement) {
242 if (in_array((int)$rootlineElement['uid'], $mountPoints, true)) {
243 $isInsideMountPoints = true;
244 break;
245 }
246 }
247 if (!$isInsideMountPoints) {
248 continue;
249 }
250 }
251 $reference = $nodeCollection;
252 $inFilteredRootline = false;
253 $amountOfRootlineElements = count($rootline);
254 // render the root line elements up to the search result
255 for ($i = 0; $i < $amountOfRootlineElements; ++$i) {
256 $rootlineElement = $rootline[$i];
257 $rootlineElement['uid'] = (int)$rootlineElement['uid'];
258 $isInWebMount = (int)$GLOBALS['BE_USER']->isInWebMount($rootlineElement['uid']);
259 if (!$isInWebMount
260 || ($rootlineElement['uid'] === (int)$mountPoints[0]
261 && $rootlineElement['uid'] !== $isInWebMount)
262 ) {
263 continue;
264 }
265 if ((int)$rootlineElement['pid'] === $nodeId
266 || $rootlineElement['uid'] === $nodeId
267 || ($rootlineElement['uid'] === $isInWebMount
268 && in_array($rootlineElement['uid'], $mountPoints, true))
269 ) {
270 $inFilteredRootline = true;
271 }
272 if (!$inFilteredRootline || $rootlineElement['uid'] === $mountPoint) {
273 continue;
274 }
275 $ident = (int)$rootlineElement['sorting'] . (int)$rootlineElement['uid'];
276 if ($reference && $reference->offsetExists($ident)) {
277 /** @var $refNode \TYPO3\CMS\Backend\Tree\Pagetree\PagetreeNode */
278 $refNode = $reference->offsetGet($ident);
279 $refNode->setExpanded(true);
280 $refNode->setLeaf(false);
281 $reference = $refNode->getChildNodes();
282 if ($reference == null) {
283 $reference = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Tree\Pagetree\PagetreeNodeCollection::class);
284 $refNode->setChildNodes($reference);
285 }
286 } else {
287 $refNode = Commands::getNewNode($rootlineElement, $mountPoint);
288 $replacement = '<span class="typo3-pagetree-filteringTree-highlight">$1</span>';
289 if ($isNumericSearchFilter && (int)$rootlineElement['uid'] === (int)$searchFilter) {
290 $text = str_replace('$1', $refNode->getText(), $replacement);
291 } else {
292 $text = preg_replace('/(' . $searchFilterQuoted . ')/iu', $replacement, $refNode->getText());
293 }
294 $refNode->setText($text, $refNode->getTextSourceField(), $refNode->getPrefix(), $refNode->getSuffix());
295 /** @var $childCollection \TYPO3\CMS\Backend\Tree\Pagetree\PagetreeNodeCollection */
296 $childCollection = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Tree\Pagetree\PagetreeNodeCollection::class);
297 if ($i + 1 >= $amountOfRootlineElements) {
298 $childNodes = $this->getNodes($refNode, $mountPoint);
299 foreach ($childNodes as $childNode) {
300 /** @var $childNode \TYPO3\CMS\Backend\Tree\Pagetree\PagetreeNode */
301 $childRecord = $childNode->getRecord();
302 $childIdent = (int)$childRecord['sorting'] . (int)$childRecord['uid'];
303 $childCollection->offsetSet($childIdent, $childNode);
304 }
305 $refNode->setChildNodes($childNodes);
306 }
307 $refNode->setChildNodes($childCollection);
308 $reference->offsetSet($ident, $refNode);
309 $reference->ksort();
310 $reference = $childCollection;
311 }
312 }
313 }
314 foreach ($this->processCollectionHookObjects as $hookObject) {
315 /** @var $hookObject \TYPO3\CMS\Backend\Tree\Pagetree\CollectionProcessorInterface */
316 $hookObject->postProcessFilteredNodes($node, $searchFilter, $mountPoint, $nodeCollection);
317 }
318 return $nodeCollection;
319 }
320
321 /**
322 * Returns the page tree mounts for the current user
323 *
324 * Note: If you add the search filter parameter, the nodes will be filtered by this string.
325 *
326 * @param string $searchFilter
327 * @return \TYPO3\CMS\Backend\Tree\Pagetree\PagetreeNodeCollection
328 */
329 public function getTreeMounts($searchFilter = '')
330 {
331 /** @var $nodeCollection \TYPO3\CMS\Backend\Tree\Pagetree\PagetreeNodeCollection */
332 $nodeCollection = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Tree\Pagetree\PagetreeNodeCollection::class);
333 $isTemporaryMountPoint = false;
334 $rootNodeIsVirtual = false;
335 $mountPoints = (int)$GLOBALS['BE_USER']->uc['pageTree_temporaryMountPoint'];
336 if (!$mountPoints) {
337 $mountPoints = array_map('intval', $GLOBALS['BE_USER']->returnWebmounts());
338 $mountPoints = array_unique($mountPoints);
339 if (!in_array(0, $mountPoints, true)) {
340 $rootNodeIsVirtual = true;
341 // use a virtual root
342 // the real mountpoints will be fetched in getNodes() then
343 // since those will be the "subpages" of the virtual root
344 $mountPoints = [0];
345 }
346 } else {
347 $isTemporaryMountPoint = true;
348 $mountPoints = [$mountPoints];
349 }
350 if (empty($mountPoints)) {
351 return $nodeCollection;
352 }
353
354 foreach ($mountPoints as $mountPoint) {
355 if ($mountPoint === 0) {
356 $record = [
357 'uid' => 0,
358 'title' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?: 'TYPO3'
359 ];
360 $subNode = Commands::getNewNode($record);
361 $subNode->setLabelIsEditable(false);
362 if ($rootNodeIsVirtual) {
363 $subNode->setType('virtual_root');
364 $subNode->setIsDropTarget(false);
365 } else {
366 $subNode->setType('pages_root');
367 $subNode->setIsDropTarget(true);
368 }
369 } else {
370 if (in_array($mountPoint, $this->hiddenRecords)) {
371 continue;
372 }
373 $record = BackendUtility::getRecordWSOL('pages', $mountPoint);
374 if (!$record) {
375 continue;
376 }
377 $subNode = Commands::getNewNode($record, $mountPoint);
378 if ($this->showRootlineAboveMounts && !$isTemporaryMountPoint) {
379 $rootline = Commands::getMountPointPath($record['uid']);
380 $subNode->setReadableRootline($rootline);
381 }
382 }
383 if (count($mountPoints) <= 1) {
384 $subNode->setExpanded(true);
385 $subNode->setCls('typo3-pagetree-node-notExpandable');
386 }
387 $subNode->setIsMountPoint(true);
388 $subNode->setDraggable(false);
389 if ($searchFilter === '') {
390 $childNodes = $this->getNodes($subNode, $mountPoint);
391 } else {
392 $childNodes = $this->getFilteredNodes($subNode, $searchFilter, $mountPoint);
393 $subNode->setExpanded(true);
394 }
395 $subNode->setChildNodes($childNodes);
396 $nodeCollection->append($subNode);
397 }
398 foreach ($this->processCollectionHookObjects as $hookObject) {
399 /** @var $hookObject \TYPO3\CMS\Backend\Tree\Pagetree\CollectionProcessorInterface */
400 $hookObject->postProcessGetTreeMounts($searchFilter, $nodeCollection);
401 }
402 return $nodeCollection;
403 }
404
405 /**
406 * Sets the Doctrine where clause for fetching pages
407 *
408 * @param QueryBuilder $queryBuilder
409 * @param string $searchFilter
410 * @return QueryBuilder
411 */
412 protected function setWhereClause(QueryBuilder $queryBuilder, $searchFilter = ''): QueryBuilder
413 {
414 $expressionBuilder = $queryBuilder->expr();
415 $queryBuilder->where(
416 QueryHelper::stripLogicalOperatorPrefix($GLOBALS['BE_USER']->getPagePermsClause(1)),
417 // Only show records in default language
418 $expressionBuilder->eq('sys_language_uid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
419 );
420
421 if ($searchFilter !== '') {
422 $searchParts = $expressionBuilder->orX();
423 if (is_numeric($searchFilter) && $searchFilter > 0) {
424 $searchParts->add(
425 $expressionBuilder->eq('uid', $queryBuilder->createNamedParameter($searchFilter, \PDO::PARAM_INT))
426 );
427 }
428 $searchFilter = '%' . $queryBuilder->escapeLikeWildcards($searchFilter) . '%';
429 $useNavTitle = $GLOBALS['BE_USER']->getTSConfigVal('options.pageTree.showNavTitle');
430
431 if ($useNavTitle) {
432 $searchWhereAlias = $expressionBuilder->orX(
433 $expressionBuilder->like(
434 'nav_title',
435 $queryBuilder->createNamedParameter($searchFilter, \PDO::PARAM_STR)
436 ),
437 $expressionBuilder->andX(
438 $expressionBuilder->eq(
439 'nav_title',
440 $queryBuilder->createNamedParameter('', \PDO::PARAM_STR)
441 ),
442 $expressionBuilder->like(
443 'title',
444 $queryBuilder->createNamedParameter($searchFilter, \PDO::PARAM_STR)
445 )
446 )
447 );
448 $searchParts->add($searchWhereAlias);
449 } else {
450 $searchParts->add(
451 $expressionBuilder->like(
452 'title',
453 $queryBuilder->createNamedParameter($searchFilter, \PDO::PARAM_STR)
454 )
455 );
456 }
457
458 // Also search for the alias
459 $searchParts->add(
460 $expressionBuilder->like(
461 'alias',
462 $queryBuilder->createNamedParameter($searchFilter, \PDO::PARAM_STR)
463 )
464 );
465 $queryBuilder->andWhere($searchParts);
466 }
467 return $queryBuilder;
468 }
469
470 /**
471 * Returns all sub-pages of a given ID
472 *
473 * @param int $id
474 * @return array
475 */
476 protected function getSubpages(int $id): array
477 {
478 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
479 $queryBuilder->getRestrictions()
480 ->removeAll()
481 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
482 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
483 $result = [];
484 $queryBuilder->select('*')
485 ->from('pages')
486 ->where(
487 QueryHelper::stripLogicalOperatorPrefix($GLOBALS['BE_USER']->getPagePermsClause(1)),
488 // Only show records in default language
489 $queryBuilder->expr()->eq('sys_language_uid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
490 )
491 ->orderBy('sorting');
492 if ((int)$id >= 0) {
493 $queryBuilder->andWhere(
494 $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT))
495 );
496 }
497 $queryResult = $queryBuilder->execute();
498 while ($row = $queryResult->fetch()) {
499 BackendUtility::workspaceOL('pages', $row, -99, true);
500 if ($row) {
501 $result[] = $row;
502 }
503 }
504 return $result;
505 }
506
507 /**
508 * Returns all pages with a query.
509 *
510 * @param string $searchFilter
511 * @return array
512 */
513 protected function getPagesByQuery(string $searchFilter = ''): array
514 {
515 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
516 $queryBuilder->getRestrictions()
517 ->removeAll()
518 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
519 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
520 $result = [];
521 $queryBuilder = $this->setWhereClause($queryBuilder, $searchFilter);
522 $queryResult = $queryBuilder->select('*')
523 ->from('pages')
524 ->orderBy('sorting')
525 ->execute();
526 while ($row = $queryResult->fetch()) {
527 BackendUtility::workspaceOL('pages', $row, -99, true);
528 if ($row) {
529 $result[] = $row;
530 }
531 }
532 return $result;
533 }
534
535 /**
536 * Returns true if the node has children.
537 *
538 * @param int $id
539 * @return bool
540 */
541 protected function hasNodeSubPages(int $id): bool
542 {
543 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
544 $queryBuilder->getRestrictions()
545 ->removeAll()
546 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
547 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
548 $queryBuilder->count('uid')
549 ->from('pages')
550 ->where(
551 QueryHelper::stripLogicalOperatorPrefix($GLOBALS['BE_USER']->getPagePermsClause(1)),
552 // Only show records in default language
553 $queryBuilder->expr()->eq('sys_language_uid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
554 );
555 if ((int)$id >= 0) {
556 $queryBuilder->andWhere(
557 $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT))
558 );
559 }
560 $count = $queryBuilder->execute()
561 ->fetchColumn(0);
562 return (bool)$count;
563 }
564 }