checkIf($processorConfiguration['if.'])) { return $processedData; } // the table to query, if none given, exit $tableName = $cObj->stdWrapValue('table', $processorConfiguration); if (empty($tableName)) { return $processedData; } if (isset($processorConfiguration['table.'])) { unset($processorConfiguration['table.']); } if (isset($processorConfiguration['table'])) { unset($processorConfiguration['table']); } $this->request = $cObj->getRequest(); $this->tableName = $tableName; $this->cObj = clone $cObj; // @extensionScannerIgnoreLine $this->cObj->start($cObj->data, $tableName); $this->cObj->setRequest($this->request); // The variable to be used within the result $targetVariableName = $cObj->stdWrapValue('as', $processorConfiguration, 'records'); // Execute a SQL statement to fetch the records $records = $this->getRecords($tableName, $processorConfiguration); $request = $cObj->getRequest(); $processedRecordVariables = []; foreach ($records as $key => $record) { /** @var ContentObjectRenderer $recordContentObjectRenderer */ $recordContentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class); $recordContentObjectRenderer->setRequest($request); $recordContentObjectRenderer->start($record, $tableName); $processedRecordVariables[$key] = ['data' => $record]; $processedRecordVariables[$key] = $this->contentDataProcessor->process( $recordContentObjectRenderer, $processorConfiguration, $processedRecordVariables[$key] ); } $processedData[$targetVariableName] = $processedRecordVariables; return $processedData; } protected function getRecords(string $tableName, array $queryConfiguration): array { $records = []; $statement = $this->exec_getQuery($tableName, $queryConfiguration); $pageRepository = $this->getPageRepository(); while ($row = $statement->fetchAssociative()) { // Versioning preview: $pageRepository->versionOL($tableName, $row, true); // Language overlay: if (is_array($row)) { $row = $pageRepository->getLanguageOverlay($tableName, $row); } // Might be unset in the language overlay if (is_array($row)) { $records[] = $row; } } return $records; } protected function exec_getQuery(string $table, array $conf): Result { $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table); $statement = $this->getQuery($connection, $table, $conf); return $connection->executeQuery($statement); } public function getQuery(Connection $connection, $table, $conf): string { // Resolve stdWrap in these properties first $properties = [ 'pidInList', 'uidInList', 'languageField', 'selectFields', 'max', 'begin', 'groupBy', 'orderBy', 'join', 'leftjoin', 'rightjoin', 'recursive', 'where', ]; foreach ($properties as $property) { $conf[$property] = trim( isset($conf[$property . '.']) ? (string)$this->cObj->stdWrap($conf[$property] ?? '', $conf[$property . '.'] ?? []) : (string)($conf[$property] ?? '') ); if ($conf[$property] === '') { unset($conf[$property]); } elseif (in_array($property, ['languageField', 'selectFields', 'join', 'leftjoin', 'rightjoin', 'where'], true)) { $conf[$property] = QueryHelper::quoteDatabaseIdentifiers($connection, $conf[$property]); } if (isset($conf[$property . '.'])) { // stdWrapping already done, so remove the sub-array unset($conf[$property . '.']); } } // Handle PDO-style named parameter markers first $queryMarkers = $this->cObj->getQueryMarkers($connection, $conf); // Replace the markers in the non-stdWrap properties foreach ($queryMarkers as $marker => $markerValue) { $properties = [ 'uidInList', 'selectFields', 'where', 'max', 'begin', 'groupBy', 'orderBy', 'join', 'leftjoin', 'rightjoin', ]; foreach ($properties as $property) { if ($conf[$property] ?? false) { $conf[$property] = str_replace('###' . $marker . '###', $markerValue, $conf[$property]); } } } // Construct WHERE clause: // Handle recursive function for the pidInList if (isset($conf['recursive'])) { $conf['recursive'] = (int)$conf['recursive']; if ($conf['recursive'] > 0) { $pidList = GeneralUtility::trimExplode(',', $conf['pidInList'], true); array_walk($pidList, function (&$storagePid) { if ($storagePid === 'this') { $storagePid = $this->getRequest()->getAttribute('frontend.page.information')->getId(); } }); $pageRepository = $this->getPageRepository(); $expandedPidList = $pageRepository->getPageIdsRecursive($pidList, $conf['recursive']); $conf['pidInList'] = implode(',', $expandedPidList); } } if ((string)($conf['pidInList'] ?? '') === '') { $conf['pidInList'] = 'this'; } $queryParts = $this->getQueryConstraints($connection, $table, $conf); $queryBuilder = $connection->createQueryBuilder(); // @todo Check against getQueryConstraints, can probably use FrontendRestrictions // @todo here and remove enableFields there. $queryBuilder->getRestrictions()->removeAll(); $queryBuilder->select('*')->from($table); if ($queryParts['where'] ?? false) { $queryBuilder->where($queryParts['where']); } if (($queryParts['groupBy'] ?? false) && is_array($queryParts['groupBy'])) { $queryBuilder->groupBy(...$queryParts['groupBy']); } if (is_array($queryParts['orderBy'] ?? false)) { foreach ($queryParts['orderBy'] as $orderBy) { $queryBuilder->addOrderBy(...$orderBy); } } // Fields: if ($conf['selectFields'] ?? false) { $queryBuilder->selectLiteral($this->sanitizeSelectPart($connection, $conf['selectFields'], $table)); } // Setting LIMIT: if (($conf['max'] ?? false) || ($conf['begin'] ?? false)) { // Finding the total number of records, if used: if (str_contains(strtolower(($conf['begin'] ?? '') . ($conf['max'] ?? '')), 'total')) { $countQueryBuilder = $connection->createQueryBuilder(); $countQueryBuilder->getRestrictions()->removeAll(); $countQueryBuilder->count('*') ->from($table) ->where($queryParts['where']); if ($queryParts['groupBy']) { $countQueryBuilder->groupBy(...$queryParts['groupBy']); } try { $count = $countQueryBuilder->executeQuery()->fetchOne(); if (isset($conf['max'])) { $conf['max'] = str_ireplace('total', $count, (string)$conf['max']); } if (isset($conf['begin'])) { $conf['begin'] = str_ireplace('total', $count, (string)$conf['begin']); } } catch (DBALException $e) { $this->getTimeTracker()->setTSlogMessage($e->getPrevious()->getMessage()); return ''; } } if (isset($conf['begin']) && $conf['begin'] > 0) { $conf['begin'] = MathUtility::forceIntegerInRange((int)ceil($this->cObj->calc($conf['begin'])), 0); $queryBuilder->setFirstResult($conf['begin']); } if (isset($conf['max'])) { $conf['max'] = MathUtility::forceIntegerInRange((int)ceil($this->cObj->calc($conf['max'])), 0); $queryBuilder->setMaxResults($conf['max'] ?: 100000); } } // Setting up tablejoins: if ($conf['join'] ?? false) { $joinParts = QueryHelper::parseJoin($conf['join']); $queryBuilder->join( $table, $joinParts['tableName'], $joinParts['tableAlias'], $joinParts['joinCondition'] ); } elseif ($conf['leftjoin'] ?? false) { $joinParts = QueryHelper::parseJoin($conf['leftjoin']); $queryBuilder->leftJoin( $table, $joinParts['tableName'], $joinParts['tableAlias'], $joinParts['joinCondition'] ); } elseif ($conf['rightjoin'] ?? false) { $joinParts = QueryHelper::parseJoin($conf['rightjoin']); $queryBuilder->rightJoin( $table, $joinParts['tableName'], $joinParts['tableAlias'], $joinParts['joinCondition'] ); } // Convert the QueryBuilder object into a SQL statement. $query = $queryBuilder->getSQL(); // Replace the markers in the queryParts to handle stdWrap enabled properties foreach ($queryMarkers as $marker => $markerValue) { // @todo Ugly hack that needs to be cleaned up, with the current architecture // @todo for exec_Query / getQuery it's the best we can do. $query = str_replace('###' . $marker . '###', (string)$markerValue, $query); } return $query; } /** * Helper function for getQuery(), creating the WHERE clause of the SELECT query * * @param Connection $connection * @param string $table The table name * @param array $conf The TypoScript configuration properties * @return array Associative array containing the prepared data for WHERE, ORDER BY and GROUP BY fragments * @throws AspectNotFoundException * @throws ContentRenderingException * @see getQuery() */ protected function getQueryConstraints(Connection $connection, string $table, array $conf): array { $queryBuilder = $connection->createQueryBuilder(); $expressionBuilder = $queryBuilder->expr(); $request = $this->getRequest(); $contentPid = $request->getAttribute('frontend.page.information')->getContentFromPid(); $constraints = []; $pid_uid_flag = 0; $enableFieldsIgnore = []; $queryParts = [ 'where' => null, 'groupBy' => null, 'orderBy' => null, ]; $isInWorkspace = GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('workspace', 'isOffline'); $considerMovePointers = ( $isInWorkspace && $table !== 'pages' && !empty($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) ); if (trim($conf['uidInList'] ?? '')) { $listArr = GeneralUtility::intExplode(',', str_replace('this', (string)$contentPid, $conf['uidInList'])); // If moved records shall be considered, select via t3ver_oid if ($considerMovePointers) { $constraints[] = (string)$expressionBuilder->or( $expressionBuilder->in($table . '.uid', $listArr), $expressionBuilder->and( $expressionBuilder->eq( $table . '.t3ver_state', VersionState::MOVE_POINTER->value ), $expressionBuilder->in($table . '.t3ver_oid', $listArr) ) ); } else { $constraints[] = (string)$expressionBuilder->in($table . '.uid', $listArr); } $pid_uid_flag++; } // Static_* tables are allowed to be fetched from root page if (str_starts_with($table, 'static_')) { $pid_uid_flag++; } if (trim($conf['pidInList'])) { $listArr = GeneralUtility::intExplode(',', str_replace('this', (string)$contentPid, $conf['pidInList'])); // Removes all pages which are not visible for the user! $listArr = $this->cObj->checkPidArray($listArr); if (GeneralUtility::inList($conf['pidInList'], 'root')) { $listArr[] = 0; } if (GeneralUtility::inList($conf['pidInList'], '-1')) { $listArr[] = -1; $enableFieldsIgnore['pid'] = true; } if (!empty($listArr)) { $constraints[] = $expressionBuilder->in($table . '.pid', array_map('intval', $listArr)); $pid_uid_flag++; } else { // If not uid and not pid then uid is set to 0 - which results in nothing!! $pid_uid_flag = 0; } } // If not uid and not pid then uid is set to 0 - which results in nothing!! if (!$pid_uid_flag && trim($conf['pidInList'] ?? '') !== 'ignore') { $constraints[] = $expressionBuilder->eq($table . '.uid', 0); } $where = trim((string)$this->cObj->stdWrapValue('where', $conf)); if ($where) { $constraints[] = QueryHelper::stripLogicalOperatorPrefix($where); } // Check if the default language should be fetched (= doing overlays), or if only the records of a language should be fetched // but only do this for TCA tables that have languages enabled $languageConstraint = $this->getLanguageRestriction($expressionBuilder, $table, $conf, GeneralUtility::makeInstance(Context::class)); if ($languageConstraint !== null) { $constraints[] = $languageConstraint; } // default constraints from TCA $pageRepository = $this->getPageRepository(); $constraints = array_merge( $constraints, array_values($pageRepository->getDefaultConstraints($table, $enableFieldsIgnore)) ); // MAKE WHERE: if ($constraints !== []) { $queryParts['where'] = $expressionBuilder->and(...$constraints); } // GROUP BY $groupBy = trim((string)$this->cObj->stdWrapValue('groupBy', $conf)); if ($groupBy) { $queryParts['groupBy'] = QueryHelper::parseGroupBy($groupBy); } // ORDER BY $orderByString = trim((string)$this->cObj->stdWrapValue('orderBy', $conf)); if ($orderByString) { $queryParts['orderBy'] = QueryHelper::parseOrderBy($orderByString); } // Return result: return $queryParts; } /** * Adds parts to the WHERE clause that are related to language. * This only works on TCA tables which have the [ctrl][languageField] field set or if they * have select.languageField = my_language_field set explicitly. * * It is also possible to disable the language restriction for a query by using select.languageField = 0, * if select.languageField is not explicitly set, the TCA default values are taken. * * If the table is "localizeable" (= any of the criteria above is met), then the DB query is restricted: * * If the current language aspect has overlays enabled, then the only records with language "0" or "-1" are * fetched (the overlays are taken care of later-on). * if the current language has overlays but also records without localization-parent (free mode) available, * then these are fetched as well. This can explicitly set via select.includeRecordsWithoutDefaultTranslation = 1 * which overrules the overlayType within the language aspect. * * If the language aspect has NO overlays enabled, it behaves as in "free mode" (= only fetch the records * for the current language. * * @throws AspectNotFoundException */ protected function getLanguageRestriction( ExpressionBuilder $expressionBuilder, string $table, array $conf, Context $context ): string|CompositeExpression|null { $languageField = ''; $localizationParentField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] ?? null; // Check if the table is translatable, and set the language field by default from the TCA information if (!empty($conf['languageField']) || !isset($conf['languageField'])) { if (isset($conf['languageField']) && !empty($GLOBALS['TCA'][$table]['columns'][$conf['languageField']])) { $languageField = $conf['languageField']; } elseif (!empty($GLOBALS['TCA'][$table]['ctrl']['languageField']) && !empty($localizationParentField)) { $languageField = $table . '.' . $GLOBALS['TCA'][$table]['ctrl']['languageField']; } } // No language restriction enabled explicitly or available via TCA if (empty($languageField)) { return null; } /** @var LanguageAspect $languageAspect */ $languageAspect = $context->getAspect('language'); if ($languageAspect->doOverlays() && !empty($localizationParentField)) { // Sys language content is set to zero/-1 - and it is expected that whatever routine processes the output will // OVERLAY the records with localized versions! $languageQuery = $expressionBuilder->in($languageField, [0, -1]); // Use this option to include records that don't have a default language counterpart ("free mode") // (originalpointerfield is 0 and the language field contains the requested language) if (isset($conf['includeRecordsWithoutDefaultTranslation']) || !empty($conf['includeRecordsWithoutDefaultTranslation.'])) { $includeRecordsWithoutDefaultTranslation = isset($conf['includeRecordsWithoutDefaultTranslation.']) ? $this->cObj->stdWrap($conf['includeRecordsWithoutDefaultTranslation'], $conf['includeRecordsWithoutDefaultTranslation.']) : $conf['includeRecordsWithoutDefaultTranslation']; $includeRecordsWithoutDefaultTranslation = trim((string)$includeRecordsWithoutDefaultTranslation); $includeRecordsWithoutDefaultTranslation = $includeRecordsWithoutDefaultTranslation !== '' && $includeRecordsWithoutDefaultTranslation !== '0'; } else { // Option was not explicitly set, check what's in for the language overlay type. $includeRecordsWithoutDefaultTranslation = $languageAspect->getOverlayType() === $languageAspect::OVERLAYS_ON_WITH_FLOATING; } if ($includeRecordsWithoutDefaultTranslation) { $languageQuery = $expressionBuilder->or( $languageQuery, $expressionBuilder->and( $expressionBuilder->eq($table . '.' . $localizationParentField, 0), $expressionBuilder->eq($languageField, $languageAspect->getContentId()) ) ); } return $languageQuery; } // No overlays = only fetch records given for the requested language and "all languages" return (string)$expressionBuilder->in($languageField, [$languageAspect->getContentId(), -1]); } /** * Helper function for getQuery, sanitizing the select part * * This functions checks if the necessary fields are part of the select * and adds them if necessary. * * @return string Sanitized select part * @internal * @see getQuery */ protected function sanitizeSelectPart(Connection $connection, string $selectPart, string $table): string { // Pattern matching parts $matchStart = '/(^\\s*|,\\s*|' . $table . '\\.)'; $matchEnd = '(\\s*,|\\s*$)/'; $necessaryFields = ['uid', 'pid']; $wsFields = ['t3ver_state']; $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField'] ?? false; if (isset($GLOBALS['TCA'][$table]) && !preg_match($matchStart . '\\*' . $matchEnd, $selectPart) && !preg_match('/(count|max|min|avg|sum)\\([^\\)]+\\)|distinct/i', $selectPart)) { foreach ($necessaryFields as $field) { $match = $matchStart . $field . $matchEnd; if (!preg_match($match, $selectPart)) { $selectPart .= ', ' . $connection->quoteIdentifier($table . '.' . $field) . ' AS ' . $connection->quoteIdentifier($field); } } if (is_string($languageField)) { $match = $matchStart . $languageField . $matchEnd; if (!preg_match($match, $selectPart)) { $selectPart .= ', ' . $connection->quoteIdentifier($table . '.' . $languageField) . ' AS ' . $connection->quoteIdentifier($languageField); } } if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS'] ?? false) { foreach ($wsFields as $field) { $match = $matchStart . $field . $matchEnd; if (!preg_match($match, $selectPart)) { $selectPart .= ', ' . $connection->quoteIdentifier($table . '.' . $field) . ' AS ' . $connection->quoteIdentifier($field); } } } } return $selectPart; } protected function getPageRepository(): PageRepository { return GeneralUtility::makeInstance(PageRepository::class); } protected function getTimeTracker(): TimeTracker { return GeneralUtility::makeInstance(TimeTracker::class); } public function getRequest(): ServerRequestInterface { if ($this->request instanceof ServerRequestInterface) { return $this->request; } throw new ContentRenderingException( 'PSR-7 request is missing in ContentObjectRenderer. Inject with start(), setRequest() or provide via $GLOBALS[\'TYPO3_REQUEST\'].', 1607172972 ); } }