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