From 8bc6fc8b2219d96231d78d3f7dd1e0e8224a0f72 Mon Sep 17 00:00:00 2001 From: Benni Mack <benni@typo3.org> Date: Tue, 12 Mar 2024 00:08:58 +0100 Subject: [PATCH] [TASK] Remove leftover query parsing code in cObj MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some code in ContentObjectRenderer is not needed anymore as the query parts are not parsed anymore. Resolves: #103371 Releases: main Change-Id: Ie313c7006d2f103c48c9bac97abbd4343d277052 Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/83408 Tested-by: Stefan Bürk <stefan@buerk.tech> Tested-by: Christian Kuhn <lolli@schwarzbu.ch> Tested-by: core-ci <typo3@b13.com> Reviewed-by: Stefan Bürk <stefan@buerk.tech> Tested-by: Oliver Bartsch <bo@cedev.de> Reviewed-by: Oliver Bartsch <bo@cedev.de> Reviewed-by: Andreas Kienast <a.fernandez@scripting-base.de> Tested-by: Benni Mack <benni@typo3.org> Tested-by: Andreas Kienast <a.fernandez@scripting-base.de> Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch> Reviewed-by: Benni Mack <benni@typo3.org> --- .../Classes/Database/Query/QueryBuilder.php | 22 +--- .../ContentObject/ContentObjectRenderer.php | 124 +++--------------- .../ContentObjectRendererTest.php | 69 ++++------ 3 files changed, 45 insertions(+), 170 deletions(-) diff --git a/typo3/sysext/core/Classes/Database/Query/QueryBuilder.php b/typo3/sysext/core/Classes/Database/Query/QueryBuilder.php index aac7aa1003c8..47a4831f1dd8 100644 --- a/typo3/sysext/core/Classes/Database/Query/QueryBuilder.php +++ b/typo3/sysext/core/Classes/Database/Query/QueryBuilder.php @@ -44,7 +44,6 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\MathUtility; use TYPO3\CMS\Extbase\Persistence\Generic\Storage\Typo3DbBackend; use TYPO3\CMS\Extbase\Tests\Functional\Persistence\Generic\Storage\Typo3DbQueryParserTest; -use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; /** * Object-oriented approach to building SQL queries. @@ -1217,8 +1216,7 @@ class QueryBuilder extends ConcreteQueryBuilder /** * Returns selected fields from internal query state. * - * @see ContentObjectRenderer::getQueryArray() - * @internal only, used for Extbase internal handling, ContentObject Query handling and core tests. Don't use it. + * @internal only, used for Extbase internal handling and core tests. Don't use it. * @return string[] */ public function getSelect(): array @@ -1230,10 +1228,9 @@ class QueryBuilder extends ConcreteQueryBuilder /** * Returns from tables from internal query state. * - * @see ContentObjectRenderer::getQueryArray() * @see Typo3DbBackend::getObjectDataByQuery() * @see Typo3DbBackend::getObjectCountByQuery() - * @internal only, used for Extbase internal handling, ContentObject Query handling and core tests. Don't use it. + * @internal only, used for Extbase internal handling and core tests. Don't use it. * @return From[] */ public function getFrom() @@ -1245,9 +1242,8 @@ class QueryBuilder extends ConcreteQueryBuilder /** * Returns where expressions from internal query state. * - * @see ContentObjectRenderer::getQueryArray() * @see Typo3DbQueryParserTest - * @internal only, used for Extbase internal handling, ContentObject Query handling and core tests. Don't use it. + * @internal only, used for Extbase internal handling and core tests. Don't use it. * @return CompositeExpression|string|null */ public function getWhere(): CompositeExpression|string|null @@ -1259,8 +1255,7 @@ class QueryBuilder extends ConcreteQueryBuilder /** * Returns having expressions from internal query state. * - * @see ContentObjectRenderer::getQueryArray() - * @internal only, used for Extbase internal handling, ContentObject Query handling and core tests. Don't use it. + * @internal only, used for Extbase internal handling and core tests. Don't use it. * @return CompositeExpression|string|null */ public function getHaving(): CompositeExpression|string|null @@ -1273,10 +1268,9 @@ class QueryBuilder extends ConcreteQueryBuilder * Returns order-by definitions from internal query state. * * @return string[] - * @see ContentObjectRenderer::getQueryArray() * @see RelationHandler::readForeignField() * @see Typo3DbQueryParserTest - * @internal only, used for Extbase internal handling, ContentObject Query handling and core tests. Don't use it. + * @internal only, used for Extbase internal handling and core tests. Don't use it. */ public function getOrderBy(): array { @@ -1287,10 +1281,9 @@ class QueryBuilder extends ConcreteQueryBuilder /** * Returns selected group-by definitions from internal query state. * - * @see ContentObjectRenderer::getQueryArray() * @see Typo3DbQueryParserTest * @return string[] - * @internal only, used for Extbase internal handling, ContentObject Query handling and core tests. Don't use it. + * @internal only, used for Extbase internal handling and core tests. Don't use it. */ public function getGroupBy(): array { @@ -1301,9 +1294,8 @@ class QueryBuilder extends ConcreteQueryBuilder /** * Returns the list of joins, indexed by `from-alias` from the internal query state. * - * @see ContentObjectRenderer::getQueryArray() * @return array<string, Join[]> - * @internal only, used for Extbase internal handling, ContentObject Query handling and core tests. Don't use it. + * @internal only, used for Extbase internal handling and core tests. Don't use it. */ public function getJoin(): array { diff --git a/typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php b/typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php index e0f5d7127531..4b5b16fc5279 100644 --- a/typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php +++ b/typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php @@ -17,7 +17,6 @@ namespace TYPO3\CMS\Frontend\ContentObject; use Doctrine\DBAL\Exception as DBALException; use Doctrine\DBAL\Query\From; -use Doctrine\DBAL\Query\Join; use Doctrine\DBAL\Result; use Psr\Container\ContainerInterface; use Psr\EventDispatcher\EventDispatcherInterface; @@ -30,6 +29,7 @@ use TYPO3\CMS\Core\Context\Context; use TYPO3\CMS\Core\Context\LanguageAspect; use TYPO3\CMS\Core\Core\Environment; use TYPO3\CMS\Core\Crypto\HashService; +use TYPO3\CMS\Core\Database\Connection; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder; use TYPO3\CMS\Core\Database\Query\QueryBuilder; @@ -4688,8 +4688,8 @@ class ContentObjectRenderer implements LoggerAwareInterface */ public function exec_getQuery($table, $conf) { - $statement = $this->getQuery($table, $conf); $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table); + $statement = $this->getQuery($connection, $table, $conf); return $connection->executeQuery($statement); } @@ -4729,22 +4729,21 @@ class ContentObjectRenderer implements LoggerAwareInterface } /** - * Creates and returns a SELECT query for records from $table and with conditions based on the configuration in the $conf array - * Implements the "select" function in TypoScript + * Creates and returns a SELECT query for records from $table and with conditions + * based on the configuration in the $conf array. + * Implements the "select" function in TypoScript. * * @param string $table See ->exec_getQuery() * @param array $conf See ->exec_getQuery() - * @param bool $returnQueryArray If set, the function will return the query not as a string but array with the various parts. RECOMMENDED! - * @return mixed A SELECT query if $returnQueryArray is FALSE, otherwise the SELECT query in an array as parts. + * @return string A SELECT query * @throws \RuntimeException * @throws \InvalidArgumentException * @internal * @see numRows() */ - public function getQuery($table, $conf, $returnQueryArray = false) + public function getQuery(Connection $connection, string $table, array $conf): string { // Resolve stdWrap in these properties first - $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table); $properties = [ 'pidInList', 'uidInList', @@ -4777,7 +4776,7 @@ class ContentObjectRenderer implements LoggerAwareInterface } } // Handle PDO-style named parameter markers first - $queryMarkers = $this->getQueryMarkers($table, $conf); + $queryMarkers = $this->getQueryMarkers($connection, $conf); // Replace the markers in the non-stdWrap properties foreach ($queryMarkers as $marker => $markerValue) { $properties = [ @@ -4819,7 +4818,7 @@ class ContentObjectRenderer implements LoggerAwareInterface $conf['pidInList'] = 'this'; } - $queryParts = $this->getQueryConstraints($table, $conf); + $queryParts = $this->getQueryConstraints($connection, $table, $conf); $queryBuilder = $connection->createQueryBuilder(); // @todo Check against getQueryConstraints, can probably use FrontendRestrictions @@ -4843,14 +4842,14 @@ class ContentObjectRenderer implements LoggerAwareInterface // Fields: if ($conf['selectFields'] ?? false) { - $queryBuilder->selectLiteral($this->sanitizeSelectPart($conf['selectFields'], $table)); + $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 = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); + $countQueryBuilder = $connection->createQueryBuilder(); $countQueryBuilder->getRestrictions()->removeAll(); $countQueryBuilder->count('*') ->from($table) @@ -4920,92 +4919,9 @@ class ContentObjectRenderer implements LoggerAwareInterface // @todo for exec_Query / getQuery it's the best we can do. $query = str_replace('###' . $marker . '###', $markerValue, $query); } - - return $returnQueryArray ? $this->getQueryArray($queryBuilder) : $query; - } - - /** - * 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 = []; - $fromParts = $queryBuilder->getFrom(); - $joinParts = $queryBuilder->getJoin(); - - // Loop through all FROM clauses - foreach ($fromParts 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, - $joinParts, - $knownAliases - ); - } - - $queryParts['SELECT'] = implode(', ', $queryBuilder->getSelect()); - $queryParts['FROM'] = implode(', ', $fromClauses); - $queryParts['WHERE'] = (string)$queryBuilder->getWhere() ?: ''; - $queryParts['GROUPBY'] = implode(', ', $queryBuilder->getGroupBy()); - $queryParts['ORDERBY'] = implode(', ', $queryBuilder->getOrderBy()); - if ($queryBuilder->getFirstResult() > 0) { - $queryParts['LIMIT'] = $queryBuilder->getFirstResult() . ',' . $queryBuilder->getMaxResults(); - } elseif ($queryBuilder->getMaxResults() > 0) { - $queryParts['LIMIT'] = $queryBuilder->getMaxResults(); - } - - return $queryParts; + return $query; } - /** - * Helper to transform the QueryBuilder join part into a SQL fragment. - * - * @param array<string, Join[]> $joinParts - * @param array<string, bool> $knownAliases - * - * @throws \RuntimeException - */ - protected function getQueryArrayJoinHelper(string $fromAlias, array $joinParts, array &$knownAliases): string - { - $sql = ''; - - if (isset($joinParts[$fromAlias])) { - foreach ($joinParts[$fromAlias] as $join) { - /** @var Join $join */ - if (array_key_exists($join->alias, $knownAliases)) { - throw new \RuntimeException( - 'Non unique join alias: "' . $join->alias . '" found.', - 1472748872 - ); - } - $sql .= ' ' . strtoupper($join->type) - . ' JOIN ' . $join->table . ' ' . $join->alias - . ' ON ' . ((string)$join->condition); - $knownAliases[$join->alias] = true; - } - - foreach ($joinParts[$fromAlias] as $join) { - $sql .= $this->getQueryArrayJoinHelper($join->alias, $joinParts, $knownAliases); - } - } - - return $sql; - } /** * Helper function for getQuery(), creating the WHERE clause of the SELECT query * @@ -5015,10 +4931,9 @@ class ContentObjectRenderer implements LoggerAwareInterface * @throws \InvalidArgumentException * @see getQuery() */ - protected function getQueryConstraints(string $table, array $conf): array + protected function getQueryConstraints(Connection $connection, string $table, array $conf): array { - // Init: - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); + $queryBuilder = $connection->createQueryBuilder(); $expressionBuilder = $queryBuilder->expr(); $request = $this->getRequest(); $contentPid = $request->getAttribute('frontend.page.information')->getContentFromPid(); @@ -5205,16 +5120,12 @@ class ContentObjectRenderer implements LoggerAwareInterface * 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($selectPart, $table) + protected function sanitizeSelectPart(Connection $connection, string $selectPart, string $table) { - $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table); - // Pattern matching parts $matchStart = '/(^\\s*|,\\s*|' . $table . '\\.)'; $matchEnd = '(\\s*,|\\s*$)/'; @@ -5280,19 +5191,16 @@ class ContentObjectRenderer implements LoggerAwareInterface * 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($table, $conf) + public function getQueryMarkers(Connection $connection, $conf) { 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, '.'); diff --git a/typo3/sysext/frontend/Tests/Functional/ContentObject/ContentObjectRendererTest.php b/typo3/sysext/frontend/Tests/Functional/ContentObject/ContentObjectRendererTest.php index 55297841507b..22c1db10780b 100644 --- a/typo3/sysext/frontend/Tests/Functional/ContentObject/ContentObjectRendererTest.php +++ b/typo3/sysext/frontend/Tests/Functional/ContentObject/ContentObjectRendererTest.php @@ -87,36 +87,28 @@ final class ContentObjectRendererTest extends FunctionalTestCase 'testing empty conf' => [ 'tt_content', [], - [ - 'SELECT' => '*', - ], + '*', ], 'testing #17284: adding uid/pid for workspaces' => [ 'tt_content', [ 'selectFields' => 'header,bodytext', ], - [ - 'SELECT' => 'header,bodytext, [tt_content].[uid] AS [uid], [tt_content].[pid] AS [pid], [tt_content].[t3ver_state] AS [t3ver_state]', - ], + 'header,bodytext, [tt_content].[uid] AS [uid], [tt_content].[pid] AS [pid], [tt_content].[t3ver_state] AS [t3ver_state]', ], 'testing #17284: no need to add' => [ 'tt_content', [ 'selectFields' => 'tt_content.*', ], - [ - 'SELECT' => 'tt_content.*', - ], + 'tt_content.*', ], 'testing #17284: no need to add #2' => [ 'tt_content', [ 'selectFields' => '*', ], - [ - 'SELECT' => '*', - ], + '*', ], 'testing #29783: joined tables, prefix tablename' => [ 'tt_content', @@ -124,63 +116,49 @@ final class ContentObjectRendererTest extends FunctionalTestCase 'selectFields' => 'tt_content.header,be_users.username', 'join' => 'be_users ON tt_content.cruser_id = be_users.uid', ], - [ - 'SELECT' => 'tt_content.header,be_users.username, [tt_content].[uid] AS [uid], [tt_content].[pid] AS [pid], [tt_content].[t3ver_state] AS [t3ver_state]', - ], + 'tt_content.header,be_users.username, [tt_content].[uid] AS [uid], [tt_content].[pid] AS [pid], [tt_content].[t3ver_state] AS [t3ver_state]', ], 'testing #34152: single count(*), add nothing' => [ 'tt_content', [ 'selectFields' => 'count(*)', ], - [ - 'SELECT' => 'count(*)', - ], + 'count(*)', ], 'testing #34152: single max(crdate), add nothing' => [ 'tt_content', [ 'selectFields' => 'max(crdate)', ], - [ - 'SELECT' => 'max(crdate)', - ], + 'max(crdate)', ], 'testing #34152: single min(crdate), add nothing' => [ 'tt_content', [ 'selectFields' => 'min(crdate)', ], - [ - 'SELECT' => 'min(crdate)', - ], + 'min(crdate)', ], 'testing #34152: single sum(is_siteroot), add nothing' => [ 'tt_content', [ 'selectFields' => 'sum(is_siteroot)', ], - [ - 'SELECT' => 'sum(is_siteroot)', - ], + 'sum(is_siteroot)', ], 'testing #34152: single avg(crdate), add nothing' => [ 'tt_content', [ 'selectFields' => 'avg(crdate)', ], - [ - 'SELECT' => 'avg(crdate)', - ], + 'avg(crdate)', ], 'single distinct, add nothing' => [ 'tt_content', [ 'selectFields' => 'DISTINCT crdate', ], - [ - 'SELECT' => 'DISTINCT crdate', - ], + 'DISTINCT crdate', ], 'testing #96321: pidInList=root does not raise PHP 8 warning' => [ 'tt_content', @@ -189,9 +167,7 @@ final class ContentObjectRendererTest extends FunctionalTestCase 'recursive' => '5', 'pidInList' => 'root', ], - [ - 'SELECT' => '*', - ], + '*', ], ]; } @@ -201,7 +177,7 @@ final class ContentObjectRendererTest extends FunctionalTestCase */ #[DataProvider('getQueryDataProvider')] #[Test] - public function getQuery(string $table, array $conf, array $expected): void + public function getQuery(string $table, array $conf, string $expected): void { $GLOBALS['TCA'] = [ 'pages' => [ @@ -230,18 +206,17 @@ final class ContentObjectRendererTest extends FunctionalTestCase $request = $request->withAttribute('frontend.page.information', $pageInformation); $subject->setRequest($request); - $result = $subject->getQuery($table, $conf, true); + $connection = (new ConnectionPool())->getConnectionForTable('tt_content'); + $result = $subject->getQuery($connection, $table, $conf); - $databasePlatform = (new ConnectionPool())->getConnectionForTable('tt_content')->getDatabasePlatform(); + $databasePlatform = $connection->getDatabasePlatform(); $identifierQuoteCharacter = (new PlatformHelper())->getIdentifierQuoteCharacter($databasePlatform); - foreach ($expected as $field => $value) { - // Replace the TYPO3 quote character with the actual quote character for the DBMS, - if ($field === 'SELECT') { - $quoteChar = $identifierQuoteCharacter; - $value = str_replace(['[', ']'], [$quoteChar, $quoteChar], $value); - } - self::assertEquals($value, $result[$field]); - } + // strip select * from part between SELECT and FROM + $selectValue = preg_replace('/SELECT (.*) FROM.*/', '$1', $result); + // Replace the TYPO3 quote character with the actual quote character for the DBMS + $quoteChar = $identifierQuoteCharacter; + $expected = str_replace(['[', ']'], [$quoteChar, $quoteChar], $expected); + self::assertEquals($expected, $selectValue); } #[Test] -- GitLab