832 lines
34 KiB
PHP
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;
|
|
}
|
|
}
|