iconFactory = GeneralUtility::makeInstance(IconFactory::class); $this->queryBuilder = $this->getQueryBuilderForTable($table); $this->queryBuilder->getRestrictions() ->removeAll() ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace)); $this->table = $table; $this->tcaSchema = GeneralUtility::makeInstance(TcaSchemaFactory::class)->get($this->table); $this->config = $config; // get a list of all the pages that should be looked on if (isset($config['pidList'])) { $pageIds = GeneralUtility::intExplode(',', (string)$config['pidList'], true); $depth = (int)($config['pidDepth'] ?? 0); $availablePageIds = $this->getAvailablePageIds($pageIds, $depth); $this->allowedPages = array_unique(array_merge($this->allowedPages, $availablePageIds)); } if (isset($config['maxItemsInResultList'])) { $this->maxItems = (int)$config['maxItemsInResultList']; } $backendUser = $this->getBackendUser(); $backendUser->initializeWebmountsForElementBrowser(); if ($this->table === 'pages') { $this->queryBuilder->andWhere( QueryHelper::stripLogicalOperatorPrefix($backendUser->getPagePermsClause(Permission::PAGE_SHOW)), $this->queryBuilder->expr()->or( $this->queryBuilder->expr()->eq('language_tag', $this->queryBuilder->createNamedParameter(\Local\Multilanguage\Service\DefaultLanguageTagService::getTag())), $this->queryBuilder->expr()->eq('language_tag', $this->queryBuilder->createNamedParameter('')), ) ); } if (isset($config['addWhere'])) { $this->queryBuilder->andWhere( QueryHelper::stripLogicalOperatorPrefix($config['addWhere']) ); } } /** * Queries a table for records and completely processes them * * Returns a two-dimensional array of almost finished records; the only need to be put into a
  • -structure * * If you subclass this class, you will most likely only want to overwrite the functions called from here, but not * this function itself * * @param array $params * @param int $recursionCounter The parent object * @return array Array of rows or FALSE if nothing found */ public function queryTable(array &$params, int $recursionCounter = 0): array { $maxQueryResults = 50; $rows = []; $this->params = &$params; $start = $recursionCounter * $maxQueryResults; $this->prepareSelectStatement(); $selectQueryBuilder = clone $this->queryBuilder; $selectQueryBuilder = $this->prepareOrderByStatement($selectQueryBuilder); $result = $selectQueryBuilder->select($this->table . '.*') ->from($this->table) ->setFirstResult($start) ->setMaxResults($maxQueryResults) ->executeQuery(); $countQueryBuilder = clone $this->queryBuilder; $allRowsCount = $countQueryBuilder ->count($this->table . '.uid') ->from($this->table) ->executeQuery() ->fetchOne(); if ($allRowsCount) { while ($row = $result->fetchAssociative()) { // check if we already have collected the maximum number of records if (count($rows) > $this->maxItems) { break; } $this->manipulateRecord($row); $this->makeWorkspaceOverlay($row); // check if the user has access to the record if (!is_array($row) || !$this->checkRecordAccess($row, $row['uid'])) { continue; } $icon = $this->iconFactory->getIconForRecord($this->table, $row, IconSize::SMALL); $uid = ($row['t3ver_oid'] ?? 0) > 0 ? $row['t3ver_oid'] : $row['uid']; $path = $this->getRecordPath($row, $uid); $label = $this->getLabel($row); $entry = [ 'table' => $this->mmForeignTable ?: $this->table, 'label' => strip_tags($label), 'path' => $path, 'uid' => $uid, 'icon' => [ 'identifier' => $icon->getIdentifier(), 'overlay' => $icon->getOverlayIcon()?->getIdentifier(), ], ]; $rows[$this->table . '_' . $uid] = $this->renderRecord($row, $entry); } // if there are less records than we need, call this function again to get more records if (count($rows) < $this->maxItems && $allRowsCount >= $maxQueryResults && $recursionCounter < $this->maxItems) { $tmp = self::queryTable($params, ++$recursionCounter); $rows = array_merge($tmp, $rows); } } return $rows; } /** * Prepare the statement for selecting the records which will be returned to the selector. May also return some * other records (e.g. from a mm-table) which will be used later on to select the real records */ protected function prepareSelectStatement(): void { $expressionBuilder = $this->queryBuilder->expr(); $searchString = $this->params['value']; if ($searchString !== '') { $splitStrings = $this->splitSearchString($searchString); $constraints = []; foreach ($splitStrings as $splitString) { $constraints[] = $this->buildConstraintBlock($splitString); } foreach ($constraints as $constraint) { $this->queryBuilder->andWhere($expressionBuilder->and($constraint)); } } if ($this->allowedPages !== []) { $this->queryBuilder->andWhere( $expressionBuilder->in('pid', array_map(intval(...), $this->allowedPages)) ); } // append an additional search condition comment if (isset($this->config['searchCondition']) && $this->config['searchCondition'] !== '') { $this->queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($this->config['searchCondition'])); } } /** * Creates OR constraints for each split searchWord. * * @return string|CompositeExpression */ protected function buildConstraintBlock($searchString) { $expressionBuilder = $this->queryBuilder->expr(); $selectParts = $expressionBuilder->or(); if (MathUtility::canBeInterpretedAsInteger($searchString) && (int)$searchString > 0) { $selectParts = $selectParts->with($expressionBuilder->eq('uid', (int)$searchString)); } $searchWholePhrase = !isset($this->config['searchWholePhrase']) || $this->config['searchWholePhrase']; $likeCondition = ($searchWholePhrase ? '%' : '') . $this->queryBuilder->escapeLikeWildcards($searchString) . '%'; // Search in all fields given by label or label_alt $selectFields = []; if (isset($this->config['additionalSearchFields'])) { $selectFields = GeneralUtility::trimExplode(',', $this->config['additionalSearchFields'], true); } $labelCapability = $this->tcaSchema->getCapability(TcaSchemaCapability::Label); $selectFields = array_unique(array_merge($selectFields, $labelCapability->getAllLabelFieldNames())); foreach ($selectFields as $field) { $selectParts = $selectParts->with($expressionBuilder->like($field, $this->queryBuilder->createPositionalParameter($likeCondition))); } return $selectParts; } /** * Splits the search string by space * This allows searching for 'elements basic' and will find results like "elements rte basic" * To search for whole phrases enclose by double-quotes: '"elements basic"', results in empty result */ protected function splitSearchString(string $searchString): array { return str_getcsv($searchString, ' ', '"', '\\'); } /** * Get array of page ids from given page id and depth * * @param array $entryPointPageIds List of possible page IDs. * @param int $depth Depth to go down. * @return array of all page ids */ protected function getAvailablePageIds(array $entryPointPageIds, int $depth = 0): array { if ($depth === 0) { return $entryPointPageIds; } $pageIds = []; $repository = GeneralUtility::makeInstance(PageTreeRepository::class); $pages = $repository->getFlattenedPages($entryPointPageIds, $depth); foreach ($pages as $page) { $pageIds[] = (int)$page['uid']; } return $pageIds; } /** * Prepares the clause by which the result elements are sorted. See description of ORDER BY in * SQL standard for reference. */ protected function prepareOrderByStatement(QueryBuilder $queryBuilder): QueryBuilder { if (empty($this->config['orderBy'])) { /** @var LabelCapability $labelCapability */ $labelCapability = $this->tcaSchema->getCapability(TcaSchemaCapability::Label); if ($labelCapability->hasPrimaryField()) { $queryBuilder->addOrderBy($labelCapability->getPrimaryFieldName()); } } else { foreach (QueryHelper::parseOrderBy($this->config['orderBy']) as $orderPair) { [$fieldName, $order] = $orderPair; $queryBuilder->addOrderBy($fieldName, $order); } } return $queryBuilder; } /** * Manipulate a record before using it to render the selector; may be used to replace a MM-relation etc. * * @param array $row */ protected function manipulateRecord(&$row) {} /** * Selects whether the logged in Backend User is allowed to read a specific record */ protected function checkRecordAccess(array $row, int $uid): bool { $backendUser = $this->getBackendUser(); $retValue = true; $table = $this->mmForeignTable ?: $this->table; if ($table === 'pages') { if (!BackendUtility::readPageAccess($uid, $backendUser->getPagePermsClause(Permission::PAGE_SHOW))) { $retValue = false; } } elseif (!is_array(BackendUtility::readPageAccess($row['pid'], $backendUser->getPagePermsClause(Permission::PAGE_SHOW)))) { $retValue = false; } return $retValue; } /** * Overlay the given record with its workspace-version, if any * * @param array $row The record to get the workspace version for * @param-out false|array|null $row */ protected function makeWorkspaceOverlay(array &$row): void { // Check for workspace-versions if ($this->getBackendUser()->workspace !== 0 && $this->tcaSchema->hasCapability(TcaSchemaCapability::Workspace)) { BackendUtility::workspaceOL($this->mmForeignTable ?: $this->table, $row); } } /** * Returns the path for a record. Is the whole path for all records except pages - for these the last part is cut * off, because it contains the pagetitle itself, which would be double information * * The path is returned uncut, cutting has to be done by calling function. * * @param array $row The row * @param int $uid UID of the record * @return string The record-path */ protected function getRecordPath(array &$row, $uid): string { $titleLimit = max($this->config['maxPathTitleLength'] ?? 0, 0); if (($this->mmForeignTable ?: $this->table) === 'pages') { $path = BackendUtility::getRecordPath($uid, '', $titleLimit); // For pages we only want the first (n-1) parts of the path, // because the n-th part is the page itself $path = substr($path, 0, (int)strrpos($path, '/', -2)) . '/'; } else { $path = BackendUtility::getRecordPath($row['pid'], '', $titleLimit); } return $path; } /** * Returns a label for a given record; usually only a wrapper for \TYPO3\CMS\Backend\Utility\BackendUtility::getRecordTitle * * @param array $row The record to get the label for * @return string The label */ protected function getLabel(array $row): string { return BackendUtility::getRecordTitle($this->mmForeignTable ?: $this->table, $row, true); } /** * Calls a user function for rendering the page. * * This user function should manipulate $entry * * @param array $row The row * @param array $entry The entry to render * @return array The rendered entry (will be put into a
  • later on */ protected function renderRecord(array $row, array $entry): array { // Call renderlet if available (normal pages etc. usually don't have one) if (($this->config['renderFunc'] ?? '') !== '') { $params = [ 'table' => $this->table, 'uid' => $row['uid'], 'row' => $row, 'entry' => &$entry, ]; GeneralUtility::callUserFunction($this->config['renderFunc'], $params, $this); } return $entry; } protected function getLanguageService(): LanguageService { return $GLOBALS['LANG']; } public function getBackendUser(): BackendUserAuthentication { return $GLOBALS['BE_USER']; } protected function getQueryBuilderForTable(string $table): QueryBuilder { return GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); } }