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