‪TYPO3CMS  ‪main
PageRecordProvider.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
20 use Doctrine\DBAL\ArrayParameterType;
21 use Psr\EventDispatcher\EventDispatcherInterface;
29 use TYPO3\CMS\Backend\Utility\BackendUtility;
33 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
40 use TYPO3\CMS\Core\Imaging\IconSize;
47 
54 {
55  private const ‪RECURSIVE_PAGE_LEVEL = 99;
56 
58  protected string ‪$userPermissions;
59  protected array ‪$pageIdList = [];
60 
61  public function ‪__construct(
62  protected readonly EventDispatcherInterface $eventDispatcher,
63  protected readonly ‪IconFactory $iconFactory,
64  protected readonly ‪LanguageServiceFactory $languageServiceFactory,
65  protected readonly ‪UriBuilder $uriBuilder,
66  protected readonly ‪QueryParser $queryParser,
67  protected readonly ‪SiteFinder $siteFinder,
68  ) {
69  $this->languageService = $this->languageServiceFactory->createFromUserPreferences($this->‪getBackendUser());
70  $this->userPermissions = $this->‪getBackendUser()->getPagePermsClause(‪Permission::PAGE_SHOW);
71  }
72 
73  public function ‪getFilterLabel(): string
74  {
75  return $this->languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:liveSearch.pageRecordProvider.filterLabel');
76  }
77 
78  public function ‪count(‪SearchDemand $searchDemand): int
79  {
80  $searchDemand = $this->‪parseCommand($searchDemand);
81  $queryBuilder = $this->‪getQueryBuilderForTable($searchDemand);
82  return (int)$queryBuilder?->count('*')->executeQuery()->fetchOne();
83  }
84 
88  public function ‪find(‪SearchDemand $searchDemand): array
89  {
90  $this->pageIdList = $this->‪getPageIdList();
91  $result = [];
92 
93  $remainingItems = $searchDemand->‪getLimit();
94  $searchDemand = $this->‪parseCommand($searchDemand);
95  $tableResult = $this->‪findByTable($searchDemand, $remainingItems);
96 
97  $result[] = $tableResult;
98 
99  return array_merge([], ...$result);
100  }
101 
102  protected function ‪parseCommand(‪SearchDemand $searchDemand): ‪SearchDemand
103  {
104  $commandQuery = null;
105  $query = $searchDemand->‪getQuery();
106 
107  if ($this->queryParser->isValidCommand($query)) {
108  $commandQuery = $query;
109  } elseif ($this->queryParser->isValidPageJump($query)) {
110  $commandQuery = $this->queryParser->getCommandForPageJump($query);
111  }
112 
113  if ($commandQuery !== null) {
114  $tableName = $this->queryParser->getTableNameFromCommand($query);
115  if ($tableName === 'pages') {
116  $extractedQueryString = $this->queryParser->getSearchQueryValue($commandQuery);
117  $searchDemand = new ‪SearchDemand([
118  new ‪DemandProperty(DemandPropertyName::query, $extractedQueryString),
119  ...array_filter(
120  $searchDemand->‪getProperties(),
121  static fn(‪DemandProperty $demandProperty): bool => $demandProperty->‪getName() !== DemandPropertyName::query
122  ),
123  ]);
124  }
125  }
126 
127  return $searchDemand;
128  }
129 
130  protected function ‪getQueryBuilderForTable(‪SearchDemand $searchDemand): ?QueryBuilder
131  {
132  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
133  ->getQueryBuilderForTable('pages');
134  $queryBuilder->getRestrictions()
135  ->removeByType(HiddenRestriction::class)
136  ->removeByType(StartTimeRestriction::class)
137  ->removeByType(EndTimeRestriction::class);
138 
139  $constraints = $this->‪buildConstraintsForTable($searchDemand->‪getQuery(), $queryBuilder);
140  if ($constraints === []) {
141  return null;
142  }
143 
144  $queryBuilder
145  ->from('pages')
146  ->where(
147  $queryBuilder->expr()->or(...$constraints)
148  );
149 
150  if ($this->userPermissions) {
151  $queryBuilder->andWhere($this->userPermissions);
152  }
153 
154  if ($this->pageIdList !== []) {
155  $queryBuilder->andWhere(
156  $queryBuilder->expr()->in(
157  'pid',
158  $queryBuilder->createNamedParameter($this->pageIdList, ArrayParameterType::INTEGER)
159  )
160  );
161  }
162 
163  $event = $this->eventDispatcher->dispatch(new ‪ModifyQueryForLiveSearchEvent($queryBuilder, 'pages'));
164 
165  return $event->getQueryBuilder();
166  }
167 
171  protected function ‪findByTable(‪SearchDemand $searchDemand, int $limit): array
172  {
173  $queryBuilder = $this->‪getQueryBuilderForTable($searchDemand);
174  if ($queryBuilder === null) {
175  return [];
176  }
177 
178  $queryBuilder
179  ->select('*')
180  ->setFirstResult($searchDemand->‪getOffset())
181  ->setMaxResults($limit)
182  ->addOrderBy('uid', 'DESC');
183 
184  $queryBuilder->addOrderBy('uid', 'DESC');
185 
186  $items = [];
187  $result = $queryBuilder->executeQuery();
188  while ($row = $result->fetchAssociative()) {
189  BackendUtility::workspaceOL('pages', $row);
190  if (!is_array($row)) {
191  continue;
192  }
193 
194  $flagIconData = [];
195  try {
196  $site = $this->siteFinder->getSiteByPageId($row['l10n_source'] > 0 ? $row['l10n_source'] : $row['uid']);
197  $siteLanguage = $site->getLanguageById($row['sys_language_uid']);
198  $flagIconData = [
199  'identifier' => $siteLanguage->getFlagIdentifier(),
200  'title' => $siteLanguage->getTitle(),
201  ];
202  } catch (‪SiteNotFoundException|\InvalidArgumentException) {
203  // intended fall-thru, perhaps broken data in database or pages without (=deleted) site config
204  }
205 
206  $actions = [
207  (new ‪ResultItemAction('open_page_details'))
208  ->setLabel($this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showList'))
209  ->setIcon($this->iconFactory->getIcon('actions-list', IconSize::SMALL))
210  ->setUrl($this->‪getShowLink($row)),
211  ];
212 
213  $pageLanguage = (int)($row['sys_language_uid'] ?? 0);
214  $previewUrl = ‪PreviewUriBuilder::create($pageLanguage === 0 ? (int)$row['uid'] : (int)$row['l10n_parent'])
215  ->withRootLine(BackendUtility::BEgetRootLine($row['uid']))
216  ->withLanguage($pageLanguage)
217  ->buildUri();
218  if ($previewUrl !== null) {
219  $actions[] = (new ‪ResultItemAction('preview_page'))
220  ->setLabel($this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage'))
221  ->setIcon($this->iconFactory->getIcon('actions-file-view', IconSize::SMALL))
222  ->setUrl((string)$previewUrl);
223  }
224 
225  $icon = $this->iconFactory->getIconForRecord('pages', $row, IconSize::SMALL);
226  $items[] = (new ‪ResultItem(self::class))
227  ->setItemTitle(BackendUtility::getRecordTitle('pages', $row))
228  ->setTypeLabel($this->languageService->sL(‪$GLOBALS['TCA']['pages']['ctrl']['title']))
229  ->setIcon($icon)
230  ->setActions(...$actions)
231  ->setExtraData([
232  'breadcrumb' => BackendUtility::getRecordPath($row['pid'], 'AND ' . $this->userPermissions, 0),
233  'flagIcon' => $flagIconData,
234  ])
235  ->setInternalData([
236  'row' => $row,
237  ])
238  ;
239  }
240 
241  return $items;
242  }
243 
249  protected function ‪getPageIdList(): array
250  {
251  if ($this->‪getBackendUser()->isAdmin()) {
252  return [];
253  }
254  $mounts = $this->‪getBackendUser()->returnWebmounts();
255  $pageList = $mounts;
256  $repository = GeneralUtility::makeInstance(PageTreeRepository::class);
257  $repository->setAdditionalWhereClause($this->userPermissions);
258  $pages = $repository->getFlattenedPages($mounts, self::RECURSIVE_PAGE_LEVEL);
259  foreach ($pages as $page) {
260  $pageList[] = (int)$page['uid'];
261  }
262  return $pageList;
263  }
264 
270  protected function ‪extractSearchableFieldsFromTable(): array
271  {
272  // Get the list of fields to search in from the TCA, if any
273  if (isset(‪$GLOBALS['TCA']['pages']['ctrl']['searchFields'])) {
274  $fieldListArray = ‪GeneralUtility::trimExplode(',', ‪$GLOBALS['TCA']['pages']['ctrl']['searchFields'], true);
275  } else {
276  $fieldListArray = [];
277  }
278  // Add special fields
279  if ($this->‪getBackendUser()->isAdmin()) {
280  $fieldListArray[] = 'uid';
281  $fieldListArray[] = 'pid';
282  }
283  return $fieldListArray;
284  }
285 
286  protected function ‪buildConstraintsForTable(string $queryString, QueryBuilder $queryBuilder): array
287  {
288  $fieldsToSearchWithin = $this->‪extractSearchableFieldsFromTable();
289  if ($fieldsToSearchWithin === []) {
290  return [];
291  }
292 
293  $constraints = [];
294 
295  // If the search string is a simple integer, assemble an equality comparison
296  if (‪MathUtility::canBeInterpretedAsInteger($queryString)) {
297  foreach ($fieldsToSearchWithin as $fieldName) {
298  if ($fieldName !== 'uid'
299  && $fieldName !== 'pid'
300  && !isset(‪$GLOBALS['TCA']['pages']['columns'][$fieldName])
301  ) {
302  continue;
303  }
304  $fieldConfig = ‪$GLOBALS['TCA']['pages']['columns'][$fieldName]['config'] ?? [];
305  $fieldType = $fieldConfig['type'] ?? '';
306 
307  // Assemble the search condition only if the field is an integer, or is uid or pid
308  if ($fieldName === 'uid'
309  || $fieldName === 'pid'
310  || ($fieldType === 'number' && ($fieldConfig['format'] ?? 'integer') === 'integer')
311  || ($fieldType === 'datetime' && !in_array($fieldConfig['dbType'] ?? '', ‪QueryHelper::getDateTimeTypes(), true))
312  ) {
313  $constraints[] = $queryBuilder->expr()->eq(
314  $fieldName,
315  $queryBuilder->createNamedParameter($queryString, ‪Connection::PARAM_INT)
316  );
317  } elseif ($this->‪fieldTypeIsSearchable($fieldType)) {
318  // Otherwise and if the field makes sense to be searched, assemble a like condition
319  $constraints[] = $queryBuilder->expr()->like(
320  $fieldName,
321  $queryBuilder->createNamedParameter(
322  '%' . $queryBuilder->escapeLikeWildcards($queryString) . '%'
323  )
324  );
325  }
326  }
327  } else {
328  $like = '%' . $queryBuilder->escapeLikeWildcards($queryString) . '%';
329  foreach ($fieldsToSearchWithin as $fieldName) {
330  if (!isset(‪$GLOBALS['TCA']['pages']['columns'][$fieldName])) {
331  continue;
332  }
333  $fieldConfig = ‪$GLOBALS['TCA']['pages']['columns'][$fieldName]['config'] ?? [];
334  $fieldType = $fieldConfig['type'] ?? '';
335 
336  // Check whether search should be case-sensitive or not
337  $searchConstraint = $queryBuilder->expr()->and(
338  $queryBuilder->expr()->comparison(
339  'LOWER(' . $queryBuilder->quoteIdentifier($fieldName) . ')',
340  'LIKE',
341  $queryBuilder->createNamedParameter(mb_strtolower($like))
342  )
343  );
344 
345  if (is_array($fieldConfig['search'] ?? false)) {
346  if (in_array('case', $fieldConfig['search'], true)) {
347  // Replace case insensitive default constraint
348  $searchConstraint = $queryBuilder->expr()->and(
349  $queryBuilder->expr()->like(
350  $fieldName,
351  $queryBuilder->createNamedParameter($like)
352  )
353  );
354  }
355  // Apply additional condition, if any
356  if ($fieldConfig['search']['andWhere'] ?? false) {
357  $searchConstraint = $searchConstraint->with(
358  ‪QueryHelper::stripLogicalOperatorPrefix(‪QueryHelper::quoteDatabaseIdentifiers($queryBuilder->getConnection(), $fieldConfig['search']['andWhere']))
359  );
360  }
361  }
362  // Assemble the search condition only if the field makes sense to be searched
363  if ($this->‪fieldTypeIsSearchable($fieldType) && $searchConstraint->count() !== 0) {
364  $constraints[] = $searchConstraint;
365  }
366  }
367  }
368 
369  return $constraints;
370  }
371 
372  protected function ‪fieldTypeIsSearchable(string $fieldType): bool
373  {
374  $searchableFieldTypes = [
375  'input',
376  'text',
377  'flex',
378  'email',
379  'link',
380  'color',
381  'slug',
382  ];
383 
384  return in_array($fieldType, $searchableFieldTypes, true);
385  }
386 
393  protected function ‪getShowLink(array $row): string
394  {
395  $backendUser = $this->‪getBackendUser();
396  $showLink = '';
397  $permissionSet = new ‪Permission($this->‪getBackendUser()->calcPerms(BackendUtility::getRecord('pages', $row['pid']) ?? []));
398  // "View" link - Only with proper permissions
399  if ($backendUser->isAdmin()
400  || (
401  $permissionSet->showPagePermissionIsGranted()
402  && !(‪$GLOBALS['TCA']['pages']['ctrl']['adminOnly'] ?? false)
403  && $backendUser->check('tables_select', 'pages')
404  )
405  ) {
406  $showLink = (string)$this->uriBuilder->buildUriFromRoute('web_list', ['id' => $row['uid']]);
407  }
408  return $showLink;
409  }
410 
412  {
413  return ‪$GLOBALS['BE_USER'];
414  }
415 }
‪TYPO3\CMS\Core\Localization\LanguageServiceFactory
Definition: LanguageServiceFactory.php:25
‪TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction
Definition: HiddenRestriction.php:27
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT
‪const PARAM_INT
Definition: Connection.php:52
‪TYPO3\CMS\Backend\Search\LiveSearch\PageRecordProvider\RECURSIVE_PAGE_LEVEL
‪const RECURSIVE_PAGE_LEVEL
Definition: PageRecordProvider.php:55
‪TYPO3\CMS\Backend\Search\LiveSearch\PageRecordProvider\fieldTypeIsSearchable
‪fieldTypeIsSearchable(string $fieldType)
Definition: PageRecordProvider.php:372
‪TYPO3\CMS\Core\Database\Query\QueryHelper\quoteDatabaseIdentifiers
‪static quoteDatabaseIdentifiers(Connection $connection, string $sql)
Definition: QueryHelper.php:224
‪TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction
Definition: EndTimeRestriction.php:27
‪TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction
Definition: StartTimeRestriction.php:27
‪TYPO3\CMS\Backend\Search\LiveSearch\QueryParser
Definition: QueryParser.php:27
‪TYPO3\CMS\Backend\Search\LiveSearch\SearchDemand\SearchDemand\getProperties
‪DemandProperty[] getProperties()
Definition: SearchDemand.php:56
‪TYPO3\CMS\Backend\Search\LiveSearch\PageRecordProvider\$userPermissions
‪string $userPermissions
Definition: PageRecordProvider.php:58
‪TYPO3\CMS\Core\Exception\SiteNotFoundException
Definition: SiteNotFoundException.php:25
‪TYPO3\CMS\Core\Site\SiteFinder
Definition: SiteFinder.php:31
‪TYPO3\CMS\Backend\Search\LiveSearch\PageRecordProvider\getFilterLabel
‪getFilterLabel()
Definition: PageRecordProvider.php:73
‪TYPO3\CMS\Core\Imaging\IconFactory
Definition: IconFactory.php:34
‪TYPO3\CMS\Backend\Search\Event\ModifyQueryForLiveSearchEvent
Definition: ModifyQueryForLiveSearchEvent.php:26
‪TYPO3\CMS\Backend\Search\LiveSearch\PageRecordProvider
Definition: PageRecordProvider.php:54
‪TYPO3\CMS\Backend\Search\LiveSearch\SearchDemand\DemandPropertyName
‪DemandPropertyName
Definition: DemandPropertyName.php:21
‪TYPO3\CMS\Core\Type\Bitmask\Permission
Definition: Permission.php:26
‪TYPO3\CMS\Backend\Search\LiveSearch\PageRecordProvider\parseCommand
‪parseCommand(SearchDemand $searchDemand)
Definition: PageRecordProvider.php:102
‪TYPO3\CMS\Backend\Search\LiveSearch\SearchDemand\DemandProperty
Definition: DemandProperty.php:21
‪TYPO3\CMS\Core\Utility\MathUtility\canBeInterpretedAsInteger
‪static bool canBeInterpretedAsInteger(mixed $var)
Definition: MathUtility.php:69
‪TYPO3\CMS\Backend\Search\LiveSearch\PageRecordProvider\$pageIdList
‪array $pageIdList
Definition: PageRecordProvider.php:59
‪TYPO3\CMS\Core\Database\Query\QueryHelper
Definition: QueryHelper.php:32
‪TYPO3\CMS\Backend\Search\LiveSearch\SearchDemand\SearchDemand
Definition: SearchDemand.php:29
‪TYPO3\CMS\Backend\Search\LiveSearch
Definition: DatabaseRecordProvider.php:18
‪TYPO3\CMS\Backend\Search\LiveSearch\SearchDemand\SearchDemand\getQuery
‪getQuery()
Definition: SearchDemand.php:66
‪TYPO3\CMS\Backend\Search\LiveSearch\PageRecordProvider\getPageIdList
‪int[] getPageIdList()
Definition: PageRecordProvider.php:249
‪TYPO3\CMS\Core\Database\Query\QueryHelper\getDateTimeTypes
‪static array getDateTimeTypes()
Definition: QueryHelper.php:211
‪TYPO3\CMS\Backend\Routing\UriBuilder
Definition: UriBuilder.php:44
‪TYPO3\CMS\Backend\Search\LiveSearch\PageRecordProvider\$languageService
‪LanguageService $languageService
Definition: PageRecordProvider.php:57
‪TYPO3\CMS\Backend\Search\LiveSearch\SearchDemand\SearchDemand\getOffset
‪getOffset()
Definition: SearchDemand.php:76
‪TYPO3\CMS\Backend\Routing\PreviewUriBuilder\create
‪static static create(int $pageId)
Definition: PreviewUriBuilder.php:65
‪TYPO3\CMS\Backend\Search\LiveSearch\PageRecordProvider\extractSearchableFieldsFromTable
‪string[] extractSearchableFieldsFromTable()
Definition: PageRecordProvider.php:270
‪TYPO3\CMS\Backend\Search\LiveSearch\PageRecordProvider\getBackendUser
‪getBackendUser()
Definition: PageRecordProvider.php:411
‪TYPO3\CMS\Core\Authentication\BackendUserAuthentication
Definition: BackendUserAuthentication.php:62
‪TYPO3\CMS\Core\Type\Bitmask\Permission\PAGE_SHOW
‪const PAGE_SHOW
Definition: Permission.php:35
‪TYPO3\CMS\Backend\Search\LiveSearch\SearchDemand\DemandProperty\getName
‪getName()
Definition: DemandProperty.php:27
‪TYPO3\CMS\Backend\Search\LiveSearch\PageRecordProvider\findByTable
‪ResultItem[] findByTable(SearchDemand $searchDemand, int $limit)
Definition: PageRecordProvider.php:171
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:41
‪TYPO3\CMS\Backend\Search\LiveSearch\SearchDemand\SearchDemand\getLimit
‪getLimit()
Definition: SearchDemand.php:71
‪TYPO3\CMS\Core\Database\Query\QueryHelper\stripLogicalOperatorPrefix
‪static string stripLogicalOperatorPrefix(string $constraint)
Definition: QueryHelper.php:171
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Backend\Search\LiveSearch\PageRecordProvider\getQueryBuilderForTable
‪getQueryBuilderForTable(SearchDemand $searchDemand)
Definition: PageRecordProvider.php:130
‪TYPO3\CMS\Backend\Routing\PreviewUriBuilder
Definition: PreviewUriBuilder.php:44
‪TYPO3\CMS\Backend\Tree\Repository\PageTreeRepository
Definition: PageTreeRepository.php:41
‪TYPO3\CMS\Backend\Search\LiveSearch\PageRecordProvider\buildConstraintsForTable
‪buildConstraintsForTable(string $queryString, QueryBuilder $queryBuilder)
Definition: PageRecordProvider.php:286
‪TYPO3\CMS\Backend\Search\LiveSearch\PageRecordProvider\find
‪ResultItem[] find(SearchDemand $searchDemand)
Definition: PageRecordProvider.php:88
‪TYPO3\CMS\Core\Utility\MathUtility
Definition: MathUtility.php:24
‪TYPO3\CMS\Core\Localization\LanguageService
Definition: LanguageService.php:46
‪TYPO3\CMS\Backend\Search\LiveSearch\ResultItem
Definition: ResultItem.php:28
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Backend\Search\LiveSearch\PageRecordProvider\count
‪count(SearchDemand $searchDemand)
Definition: PageRecordProvider.php:78
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Backend\Search\LiveSearch\PageRecordProvider\__construct
‪__construct(protected readonly EventDispatcherInterface $eventDispatcher, protected readonly IconFactory $iconFactory, protected readonly LanguageServiceFactory $languageServiceFactory, protected readonly UriBuilder $uriBuilder, protected readonly QueryParser $queryParser, protected readonly SiteFinder $siteFinder,)
Definition: PageRecordProvider.php:61
‪TYPO3\CMS\Backend\Search\LiveSearch\PageRecordProvider\getShowLink
‪string getShowLink(array $row)
Definition: PageRecordProvider.php:393
‪TYPO3\CMS\Core\Utility\GeneralUtility\trimExplode
‪static list< string > trimExplode(string $delim, string $string, bool $removeEmptyValues=false, int $limit=0)
Definition: GeneralUtility.php:822
‪TYPO3\CMS\Backend\Search\LiveSearch\ResultItemAction
Definition: ResultItemAction.php:28
‪TYPO3\CMS\Backend\Search\LiveSearch\SearchProviderInterface
Definition: SearchProviderInterface.php:28