languageService = $this->languageServiceFactory->createFromUserPreferences($this->getBackendUser()); $this->userPermissions = $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW); } public function getFilterLabel(): string { return $this->languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:liveSearch.pageRecordProvider.filterLabel'); } public function count(SearchDemand $searchDemand): int { $searchDemand = $this->parseCommand($searchDemand); $queryBuilder = $this->getQueryBuilderForTable($searchDemand); return (int)$queryBuilder?->count('*')->executeQuery()->fetchOne(); } /** * @return ResultItem[] */ public function find(SearchDemand $searchDemand): array { $this->pageIdList = $this->getPageIdList(); $result = []; $remainingItems = $searchDemand->getLimit(); $searchDemand = $this->parseCommand($searchDemand); $tableResult = $this->findByTable($searchDemand, $remainingItems); $result[] = $tableResult; return array_merge([], ...$result); } private function parseCommand(SearchDemand $searchDemand): SearchDemand { $commandQuery = null; $query = $searchDemand->getQuery(); if ($this->queryParser->isValidCommand($query)) { $commandQuery = $query; } elseif ($this->queryParser->isValidPageJump($query)) { $commandQuery = $this->queryParser->getCommandForPageJump($query); } if ($commandQuery !== null) { $tableName = $this->queryParser->getTableNameFromCommand($query); if ($tableName === 'pages') { $extractedQueryString = $this->queryParser->getSearchQueryValue($commandQuery); $searchDemand = new SearchDemand([ new DemandProperty(DemandPropertyName::query, $extractedQueryString), ...array_filter( $searchDemand->getProperties(), static fn(DemandProperty $demandProperty): bool => $demandProperty->getName() !== DemandPropertyName::query ), ]); } } return $searchDemand; } private function getQueryBuilderForTable(SearchDemand $searchDemand): ?QueryBuilder { $queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages'); $queryBuilder->getRestrictions() ->removeAll() ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace, true)); $constraints = $this->buildConstraintsForTable($searchDemand->getQuery(), $queryBuilder); $event = $this->eventDispatcher->dispatch(new ModifyConstraintsForLiveSearchEvent($constraints, 'pages', $searchDemand)); $constraints = $event->getConstraints(); if ($constraints === []) { return null; } $queryBuilder ->from('pages') ->where( $queryBuilder->expr()->or(...$constraints) ); if ($this->userPermissions) { $queryBuilder->andWhere($this->userPermissions); } if ($this->pageIdList !== []) { $queryBuilder->andWhere( $queryBuilder->expr()->in( 'pid', $queryBuilder->createNamedParameter($this->pageIdList, ArrayParameterType::INTEGER) ) ); } $event = $this->eventDispatcher->dispatch(new ModifyQueryForLiveSearchEvent($queryBuilder, 'pages')); return $event->getQueryBuilder(); } /** * @return ResultItem[] */ private function findByTable(SearchDemand $searchDemand, int $limit): array { $queryBuilder = $this->getQueryBuilderForTable($searchDemand); if ($queryBuilder === null) { return []; } $queryBuilder ->select('*') ->setFirstResult($searchDemand->getOffset()) ->setMaxResults($limit) ->addOrderBy('uid', 'DESC'); $queryBuilder->addOrderBy('uid', 'DESC'); $items = []; $result = $queryBuilder->executeQuery(); $schema = $this->tcaSchemaFactory->get('pages'); $hasWorkspaceCapability = $schema->hasCapability(TcaSchemaCapability::Workspace); while ($row = $result->fetchAssociative()) { BackendUtility::workspaceOL('pages', $row); if (!is_array($row)) { continue; } $actions = []; $editActionLink = $this->getEditActionLink($row); if ($editActionLink !== '') { $actions[DatabaseRecordActionType::EDIT->value] = (new ResultItemAction(DatabaseRecordActionType::EDIT->value)) ->setLabel($this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.edit')) ->setIcon($this->iconFactory->getIcon('actions-open', IconSize::SMALL)) ->setUrl($editActionLink); } $layoutActionLink = $this->getLayoutActionLink($row); if ($layoutActionLink !== '') { $actions[DatabaseRecordActionType::LAYOUT->value] = (new ResultItemAction(DatabaseRecordActionType::LAYOUT->value)) ->setLabel($this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.view.layout')) ->setIcon($this->iconFactory->getIcon('actions-viewmode-layout', IconSize::SMALL)) ->setUrl($layoutActionLink); } $listActionLink = $this->getRecordsActionLink($row); if ($listActionLink !== '') { $actions[DatabaseRecordActionType::LIST->value] = (new ResultItemAction(DatabaseRecordActionType::LIST->value)) ->setLabel($this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showList')) ->setIcon($this->iconFactory->getIcon('actions-list', IconSize::SMALL)) ->setUrl($listActionLink); } $previewActionLink = $this->getPreviewActionLink($row); if ($previewActionLink !== '') { $actions[DatabaseRecordActionType::PREVIEW->value] = (new ResultItemAction(DatabaseRecordActionType::PREVIEW->value)) ->setLabel($this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage')) ->setIcon($this->iconFactory->getIcon('actions-file-view', IconSize::SMALL)) ->setUrl($previewActionLink); } // Find the default action $defaultActionIdentifier = DatabaseRecordActionType::fromUserForTable($this->getBackendUser(), 'pages'); $defaultAction = $actions[$defaultActionIdentifier->value] ?? null; $icon = $this->iconFactory->getIconForRecord('pages', $row, IconSize::SMALL); $items[] = (new ResultItem(self::class)) ->setItemTitle(BackendUtility::getRecordTitle('pages', $row)) ->setTypeLabel($schema->getTitle($this->languageService->sL(...))) ->setIcon($icon) ->setActions(...array_values($actions)) ->setDefaultAction($defaultAction) ->setLanguage($this->resolveLanguage($row['l10n_source'] > 0 ? $row['l10n_source'] : $row['uid'], (int)$row['language_tag'])) ->setExtraData([ 'breadcrumb' => BackendUtility::getRecordPath($row['pid'], 'AND ' . $this->userPermissions, 0), 'inWorkspace' => $hasWorkspaceCapability && $row['t3ver_wsid'] > 0, ]) ->setInternalData([ 'row' => $row, ]) ; } return $items; } /** * List of available page uids for user, empty array for admin users. * * @return int[] */ private function getPageIdList(): array { if ($this->getBackendUser()->isAdmin()) { return []; } $mounts = $this->getBackendUser()->getWebmounts(); $pageList = $mounts; $repository = GeneralUtility::makeInstance(PageTreeRepository::class); $repository->setAdditionalWhereClause($this->userPermissions); $pages = $repository->getFlattenedPages($mounts, self::RECURSIVE_PAGE_LEVEL); foreach ($pages as $page) { $pageList[] = (int)$page['uid']; } return $pageList; } /** * @return CompositeExpression[] */ private function buildConstraintsForTable(string $queryString, QueryBuilder $queryBuilder): array { $platform = $queryBuilder->getConnection()->getDatabasePlatform(); $isPostgres = $platform instanceof DoctrinePostgreSQLPlatform; $fieldsToSearchWithin = $this->searchableSchemaFieldsCollector->getFields('pages'); [$subSchemaDivisorFieldName, $fieldsSubSchemaTypes] = $this->searchableSchemaFieldsCollector->getSchemaFieldSubSchemaTypes('pages'); $constraints = []; // If the search string is a simple integer, assemble an equality comparison if (MathUtility::canBeInterpretedAsInteger($queryString)) { // Add uid and pid constraint $constraints[] = $queryBuilder->expr()->eq( 'uid', $queryBuilder->createNamedParameter($queryString, Connection::PARAM_INT) ); $constraints[] = $queryBuilder->expr()->eq( 'pid', $queryBuilder->createNamedParameter($queryString, Connection::PARAM_INT) ); foreach ($fieldsToSearchWithin as $fieldName => $field) { // Assemble the search condition only if the field is an integer if ($field instanceof NumberFieldType || $field instanceof DateTimeFieldType) { $searchConstraint = $queryBuilder->expr()->eq( $fieldName, $queryBuilder->createNamedParameter($queryString, Connection::PARAM_INT) ); } else { // Otherwise assemble a like condition $searchConstraint = $queryBuilder->expr()->like( $fieldName, $queryBuilder->createNamedParameter( '%' . $queryBuilder->escapeLikeWildcards($queryString) . '%' ) ); } // If this table has subtypes (e.g. tt_content.CType), we want to ensure that only CType that contain // e.g. "bodytext" in their list of fields, to search through them. This is important when a field // is filled but its type has been changed. if ($subSchemaDivisorFieldName !== '' && isset($fieldsSubSchemaTypes[$fieldName]) && $fieldsSubSchemaTypes[$fieldName] !== [] ) { // Using `IN()` with a string-value quoted list is fine for all database systems, even when // used on integer-typed fields and no additional work required here to mitigate something. $searchConstraint = $queryBuilder->expr()->and( $searchConstraint, $queryBuilder->expr()->in( $subSchemaDivisorFieldName, $queryBuilder->quoteArrayBasedValueListToStringList($fieldsSubSchemaTypes[$fieldName]) ), ); } $constraints[] = $searchConstraint; } } else { $like = '%' . $queryBuilder->escapeLikeWildcards($queryString) . '%'; foreach ($fieldsToSearchWithin as $fieldName => $field) { $fieldConfig = $field->getConfiguration(); // Enforce case-insensitive comparison by lower-casing field and value, unrelated to charset/collation // on MySQL/MariaDB, for example if column collation is `utf8mb4_bin` - which would be case-sensitive. $preparedFieldName = $isPostgres ? $queryBuilder->castFieldToTextType($fieldName) : $queryBuilder->quoteIdentifier($fieldName); $searchConstraint = $queryBuilder->expr()->comparison( 'LOWER(' . $preparedFieldName . ')', 'LIKE', $queryBuilder->createNamedParameter(mb_strtolower($like)) ); // If this table has subtypes (e.g. tt_content.CType), we want to ensure that only CType that contain // e.g. "bodytext" in their list of fields, to search through them. This is important when a field // is filled but its type has been changed. if ($subSchemaDivisorFieldName !== '' && isset($fieldsSubSchemaTypes[$fieldName]) && $fieldsSubSchemaTypes[$fieldName] !== [] ) { // Using `IN()` with a string-value quoted list is fine for all database systems, even when // used on integer-typed fields and no additional work required here to mitigate something. $searchConstraint = $queryBuilder->expr()->and( $searchConstraint, $queryBuilder->expr()->in( $subSchemaDivisorFieldName, $queryBuilder->quoteArrayBasedValueListToStringList($fieldsSubSchemaTypes[$fieldName]) ), ); } $constraints[] = $searchConstraint; } } return $constraints; } /** * Build a backend edit link based on given page. * * @param array $row Current page row from database. * @return string Link to open an edit window for page. * @see \TYPO3\CMS\Backend\Utility\BackendUtility::readPageAccess() */ private function getEditActionLink(array $row): string { $backendUser = $this->getBackendUser(); $editLink = ''; $permissionSet = new Permission($backendUser->calcPerms(BackendUtility::readPageAccess($row['uid'], $this->userPermissions) ?: [])); $schema = $this->tcaSchemaFactory->get('pages'); if (!$schema->hasCapability(TcaSchemaCapability::AccessReadOnly) && ( $backendUser->isAdmin() || ( $permissionSet->editContentPermissionIsGranted() && !$schema->hasCapability(TcaSchemaCapability::AccessAdminOnly) && $backendUser->check('tables_modify', 'pages') && $backendUser->checkRecordEditAccess('pages', $row)->isAllowed ) ) ) { $returnUrl = (string)$this->uriBuilder->buildUriFromRoute('web_layout', ['id' => $row['uid']]); $editLink = (string)$this->uriBuilder->buildUriFromRoute('record_edit', [ 'edit[pages][' . $row['uid'] . ']' => 'edit', 'returnUrl' => $returnUrl, ]); } return $editLink; } /** * Build a link to the page layout for the given record. * * @param array $row Current record row from database. * @return string Link to open an edit window for record. */ private function getLayoutActionLink(array $row): string { $showLink = ''; if ($this->hasPagesAccess($row)) { $parameter = [ 'id' => $row['language_tag'] === 0 ? $row['uid'] : $row['l10n_parent'], 'languages' => [$row['language_tag']], ]; $showLink = (string)$this->uriBuilder->buildUriFromRoute('web_layout', $parameter); } return $showLink; } /** * Build a link to the record list based on given record. * * @param array $row Current record row from database. * @return string Link to open an edit window for record. */ private function getRecordsActionLink(array $row): string { $showLink = ''; if ($this->hasPagesAccess($row)) { $parameter = [ 'id' => $row['language_tag'] === 0 ? $row['uid'] : $row['l10n_parent'], 'languages' => [$row['language_tag']], ]; $showLink = ((string)$this->uriBuilder->buildUriFromRoute('records', $parameter)) . '#t3-table-pages'; } return $showLink; } /** * Build a preview link to display the record in the frontend. * * @param array $row Current record row from database. * @return string Link to open an edit window for record. */ private function getPreviewActionLink(array $row): string { $previewLink = ''; if ($this->hasPagesAccess($row)) { $previewUriBuilder = PreviewUriBuilder::create($row); if ($previewUriBuilder->isPreviewable()) { $previewLink = (string)$previewUriBuilder->buildUri(); } } return $previewLink; } private function hasPagesAccess(array $row): bool { $backendUser = $this->getBackendUser(); $permissionSet = new Permission($backendUser->calcPerms(BackendUtility::getRecord('pages', $row['uid']) ?? [])); $pagesSchema = $this->tcaSchemaFactory->get('pages'); return $backendUser->isAdmin() || ( $permissionSet->showPagePermissionIsGranted() && !$pagesSchema->hasCapability(TcaSchemaCapability::AccessAdminOnly) && $backendUser->check('tables_select', 'pages') ); } private function resolveLanguage(int $pageUid, int $languageId): ?array { try { $siteLanguage = $this->siteFinder->getSiteByPageId($pageUid)->getLanguageById($languageId); return [ 'id' => $siteLanguage->getLanguageId(), 'title' => $siteLanguage->getTitle(), 'iconIdentifier' => $siteLanguage->getFlagIdentifier(), ]; } catch (SiteNotFoundException|\InvalidArgumentException) { return null; } } private function getBackendUser(): BackendUserAuthentication { return $GLOBALS['BE_USER']; } }