ew_base/Classes/DataProcessing/DatabaseQueryProcessor.php
2024-12-14 14:03:20 +01:00

832 lines
34 KiB
PHP

<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace Evoweb\EwBase\DataProcessing;
use Doctrine\DBAL\Exception as DBALException;
use Doctrine\DBAL\Result;
use TYPO3\CMS\Backend\FrontendBackendUserAuthentication;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Context\LanguageAspect;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Database\Query\QueryHelper;
use TYPO3\CMS\Core\Database\Query\Restriction\DocumentTypeExclusionRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer;
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Versioning\VersionState;
use TYPO3\CMS\Frontend\ContentObject\ContentDataProcessor;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
/**
* Fetch records from the database, using the default .select syntax from TypoScript.
*
* This way, e.g. a FLUIDTEMPLATE cObject can iterate over the array of records.
*
* Example TypoScript configuration:
*
* 10 = TYPO3\CMS\Frontend\DataProcessing\DatabaseQueryProcessor
* 10 {
* table = tt_address
* pidInList = 123
* where = company="Acme" AND first_name="Ralph"
* orderBy = sorting DESC
* as = addresses
* dataProcessing {
* 10 = TYPO3\CMS\Frontend\DataProcessing\FilesProcessor
* 10 {
* references.fieldName = image
* }
* }
* }
*
* where "as" means the variable to be containing the result-set from the DB query.
*/
class DatabaseQueryProcessor implements DataProcessorInterface
{
protected ContentDataProcessor $contentDataProcessor;
protected ContentObjectRenderer $cObj;
/**
* Constructor
*/
public function __construct()
{
$this->contentDataProcessor = GeneralUtility::makeInstance(ContentDataProcessor::class);
}
/**
* Fetches records from the database as an array
*
* @param ContentObjectRenderer $cObj The data of the content element or page
* @param array $contentObjectConfiguration The configuration of Content Object
* @param array $processorConfiguration The configuration of this processor
* @param array $processedData Key/value store of processed data (e.g. to be passed to a Fluid View)
*
* @return array the processed data as key/value store
*/
public function process(
ContentObjectRenderer $cObj,
array $contentObjectConfiguration,
array $processorConfiguration,
array $processedData
) {
$this->cObj = $cObj;
if (isset($processorConfiguration['if.']) && !$cObj->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']);
}
// 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) {
$recordContentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$recordContentObjectRenderer->start($record, $tableName, $request);
$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);
$tsfe = $this->getTypoScriptFrontendController();
while ($row = $statement->fetchAssociative()) {
// Versioning preview:
$tsfe->sys_page->versionOL($tableName, $row, true);
// Language overlay:
if (is_array($row)) {
$row = $tsfe->sys_page->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
{
$statement = $this->getQuery($table, $conf);
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
return $connection->executeQuery($statement);
}
public function getQuery($table, $conf, $returnQueryArray = false)
{
// Resolve stdWrap in these properties first
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
$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->getQueryMarkers($table, $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 . '###', (string)$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->getTypoScriptFrontendController()->id;
}
});
$expandedPidList = $this->getTypoScriptFrontendController()->sys_page->getPageIdsRecursive($pidList, $conf['recursive']);
$conf['pidInList'] = implode(',', $expandedPidList);
}
}
if ((string)($conf['pidInList'] ?? '') === '') {
$conf['pidInList'] = 'this';
}
$queryParts = $this->getQueryConstraints($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) {
$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($conf['selectFields'], $table));
}
// Setting LIMIT:
$error = false;
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 = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
$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());
$error = true;
}
}
if (!$error) {
if (isset($conf['begin']) && $conf['begin'] > 0) {
$conf['begin'] = MathUtility::forceIntegerInRange((int)ceil($this->calc($conf['begin'])), 0);
$queryBuilder->setFirstResult($conf['begin']);
}
if (isset($conf['max'])) {
$conf['max'] = MathUtility::forceIntegerInRange((int)ceil($this->calc($conf['max'])), 0);
$queryBuilder->setMaxResults($conf['max'] ?: 100000);
}
}
}
if (!$error) {
// 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 $returnQueryArray ? $this->getQueryArray($queryBuilder) : $query;
}
return '';
}
/**
* Helper to transform a QueryBuilder object into a queryParts array that can be used
* with exec_SELECT_queryArray
*
* @return array
* @throws \RuntimeException
*/
protected function getQueryArray(QueryBuilder $queryBuilder)
{
$fromClauses = [];
$knownAliases = [];
$queryParts = [];
// Loop through all FROM clauses
foreach ($queryBuilder->getQueryPart('from') as $from) {
if ($from['alias'] === null) {
$tableSql = $from['table'];
$tableReference = $from['table'];
} else {
$tableSql = $from['table'] . ' ' . $from['alias'];
$tableReference = $from['alias'];
}
$knownAliases[$tableReference] = true;
$fromClauses[$tableReference] = $tableSql . $this->getQueryArrayJoinHelper(
$tableReference,
$queryBuilder->getQueryPart('join'),
$knownAliases
);
}
$queryParts['SELECT'] = implode(', ', $queryBuilder->getQueryPart('select'));
$queryParts['FROM'] = implode(', ', $fromClauses);
$queryParts['WHERE'] = (string)$queryBuilder->getQueryPart('where') ?: '';
$queryParts['GROUPBY'] = implode(', ', $queryBuilder->getQueryPart('groupBy'));
$queryParts['ORDERBY'] = implode(', ', $queryBuilder->getQueryPart('orderBy'));
if ($queryBuilder->getFirstResult() > 0) {
$queryParts['LIMIT'] = $queryBuilder->getFirstResult() . ',' . $queryBuilder->getMaxResults();
} elseif ($queryBuilder->getMaxResults() > 0) {
$queryParts['LIMIT'] = $queryBuilder->getMaxResults();
}
return $queryParts;
}
/**
* Helper to transform the QueryBuilder join part into a SQL fragment.
*
* @throws \RuntimeException
*/
protected function getQueryArrayJoinHelper(string $fromAlias, array $joinParts, array &$knownAliases): string
{
$sql = '';
if (isset($joinParts['join'][$fromAlias])) {
foreach ($joinParts['join'][$fromAlias] as $join) {
if (array_key_exists($join['joinAlias'], $knownAliases)) {
throw new \RuntimeException(
'Non unique join alias: "' . $join['joinAlias'] . '" found.',
1472748872
);
}
$sql .= ' ' . strtoupper($join['joinType'])
. ' JOIN ' . $join['joinTable'] . ' ' . $join['joinAlias']
. ' ON ' . ((string)$join['joinCondition']);
$knownAliases[$join['joinAlias']] = true;
}
foreach ($joinParts['join'][$fromAlias] as $join) {
$sql .= $this->getQueryArrayJoinHelper($join['joinAlias'], $joinParts, $knownAliases);
}
}
return $sql;
}
/**
* Builds list of marker values for handling PDO-like parameter markers in select parts.
* Marker values support stdWrap functionality thus allowing a way to use stdWrap functionality in various
* properties of 'select' AND prevents SQL-injection problems by quoting and escaping of numeric values, strings,
* NULL values and comma separated lists.
*
* @param string $table Table to select records from
* @param array $conf Select part of CONTENT definition
* @return array List of values to replace markers with
* @internal
* @see getQuery()
*/
public function getQueryMarkers(string $table, array $conf): array
{
if (!isset($conf['markers.']) || !is_array($conf['markers.'])) {
return [];
}
// Parse markers and prepare their values
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
$markerValues = [];
foreach ($conf['markers.'] as $dottedMarker => $dummy) {
$marker = rtrim($dottedMarker, '.');
if ($dottedMarker != $marker . '.') {
continue;
}
// Parse definition
// todo else value is always null
$tempValue = isset($conf['markers.'][$dottedMarker])
? $this->cObj->stdWrap($conf['markers.'][$dottedMarker]['value'] ?? '', $conf['markers.'][$dottedMarker])
: $conf['markers.'][$dottedMarker]['value'];
// Quote/escape if needed
if (is_numeric($tempValue)) {
if ((int)$tempValue == $tempValue) {
// Handle integer
$markerValues[$marker] = (int)$tempValue;
} else {
// Handle float
$markerValues[$marker] = (float)$tempValue;
}
} elseif ($tempValue === null) {
// It represents NULL
$markerValues[$marker] = 'NULL';
} elseif (!empty($conf['markers.'][$dottedMarker]['commaSeparatedList'])) {
// See if it is really a comma separated list of values
$explodeValues = GeneralUtility::trimExplode(',', $tempValue);
if (count($explodeValues) > 1) {
// Handle each element of list separately
$tempArray = [];
foreach ($explodeValues as $listValue) {
if (is_numeric($listValue)) {
if ((int)$listValue == $listValue) {
$tempArray[] = (int)$listValue;
} else {
$tempArray[] = (float)$listValue;
}
} else {
// If quoted, remove quotes before
// escaping.
if (preg_match('/^\'([^\']*)\'$/', $listValue, $matches)) {
$listValue = $matches[1];
} elseif (preg_match('/^\\"([^\\"]*)\\"$/', $listValue, $matches)) {
$listValue = $matches[1];
}
$tempArray[] = $connection->quote($listValue);
}
}
$markerValues[$marker] = implode(',', $tempArray);
} else {
// Handle remaining values as string
$markerValues[$marker] = $connection->quote($tempValue);
}
} else {
// Handle remaining values as string
$markerValues[$marker] = $connection->quote($tempValue);
}
}
return $markerValues;
}
/**
* Helper function for getQuery(), creating the WHERE clause of the SELECT query
*
* @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 \InvalidArgumentException
* @see getQuery()
*/
protected function getQueryConstraints(string $table, array $conf): array
{
// Init:
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
$expressionBuilder = $queryBuilder->expr();
$tsfe = $this->getTypoScriptFrontendController();
$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)$tsfe->contentPid, $conf['uidInList']));
// If moved records shall be considered, select via t3ver_oid
if ($considerMovePointers) {
$constraints[] = (string)$expressionBuilder->orX(
$expressionBuilder->in($table . '.uid', $listArr),
$expressionBuilder->andX(
$expressionBuilder->eq(
$table . '.t3ver_state',
(int)(string)VersionState::cast(VersionState::MOVE_POINTER)
),
$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 (strpos($table, 'static_') === 0) {
$pid_uid_flag++;
}
if (trim($conf['pidInList'])) {
$listArr = GeneralUtility::intExplode(',', str_replace('this', (string)$tsfe->contentPid, $conf['pidInList']));
// Removes all pages which are not visible for the user!
$listArr = $this->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;
}
// Enablefields
if ($table === 'pages') {
$constraints[] = QueryHelper::stripLogicalOperatorPrefix($tsfe->sys_page->where_hid_del);
$constraints[] = QueryHelper::stripLogicalOperatorPrefix($tsfe->sys_page->where_groupAccess);
} else {
$constraints[] = QueryHelper::stripLogicalOperatorPrefix($tsfe->sys_page->enableFields($table, -1, $enableFieldsIgnore));
}
// MAKE WHERE:
if (count($constraints) !== 0) {
$queryParts['where'] = $expressionBuilder->andX(...$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;
}
/**
* Removes Page UID numbers from the input array which are not available due to enableFields() or the list of bad doktype numbers ($this->checkPid_badDoktypeList)
*
* @param int[] $pageIds Array of Page UID numbers for select and for which pages with enablefields and bad doktypes should be removed.
* @return array Returns the array of remaining page UID numbers
* @internal
*/
public function checkPidArray(array $pageIds): array
{
if (empty($pageIds)) {
return [];
}
$restrictionContainer = GeneralUtility::makeInstance(FrontendRestrictionContainer::class);
$restrictionContainer->add(GeneralUtility::makeInstance(
DocumentTypeExclusionRestriction::class,
GeneralUtility::intExplode(',', (string)$this->cObj->checkPid_badDoktypeList, true)
));
return $this->getTypoScriptFrontendController()->sys_page->filterAccessiblePageIds($pageIds, $restrictionContainer);
}
/**
* 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.
*
* @param ExpressionBuilder $expressionBuilder
* @param string $table
* @param array $conf
* @param Context $context
* @return string|\TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression|null
* @throws \TYPO3\CMS\Core\Context\Exception\AspectNotFoundException
*/
protected function getLanguageRestriction(ExpressionBuilder $expressionBuilder, string $table, array $conf, Context $context)
{
$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($includeRecordsWithoutDefaultTranslation) !== '';
} 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->orX(
$languageQuery,
$expressionBuilder->andX(
$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 $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.
*
* @param string $selectPart Select part
* @param string $table Table to select from
* @return string Sanitized select part
* @internal
* @see getQuery
*/
protected function sanitizeSelectPart(string $selectPart, string $table): string
{
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
// Pattern matching parts
$matchStart = '/(^\\s*|,\\s*|' . $table . '\\.)';
$matchEnd = '(\\s*,|\\s*$)/';
$necessaryFields = ['uid', 'pid'];
$wsFields = ['t3ver_state'];
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 ($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;
}
/**
* Performs basic mathematical evaluation of the input string. Does NOT take parenthesis and operator precedence into account! (for that, see \TYPO3\CMS\Core\Utility\MathUtility::calculateWithPriorityToAdditionAndSubtraction())
*
* @param string $val The string to evaluate. Example: "3+4*10/5" will generate "35". Only integer numbers can be used.
* @return int The result (might be a float if you did a division of the numbers).
* @see \TYPO3\CMS\Core\Utility\MathUtility::calculateWithPriorityToAdditionAndSubtraction()
*/
public function calc($val)
{
$parts = GeneralUtility::splitCalc($val, '+-*/');
$value = 0;
foreach ($parts as $part) {
$theVal = $part[1];
$sign = $part[0];
if ((string)(int)$theVal === (string)$theVal) {
$theVal = (int)$theVal;
} else {
$theVal = 0;
}
if ($sign === '-') {
$value -= $theVal;
}
if ($sign === '+') {
$value += $theVal;
}
if ($sign === '/') {
if ((int)$theVal) {
$value /= (int)$theVal;
}
}
if ($sign === '*') {
$value *= $theVal;
}
}
return $value;
}
/**
* @return TimeTracker
*/
protected function getTimeTracker()
{
return GeneralUtility::makeInstance(TimeTracker::class);
}
/**
* Returns the current BE user.
*
* @return FrontendBackendUserAuthentication
*/
protected function getFrontendBackendUser()
{
return $GLOBALS['BE_USER'];
}
/**
* @return TypoScriptFrontendController|null
*/
protected function getTypoScriptFrontendController()
{
return $GLOBALS['TSFE'] ?? null;
}
}