From 76a0e00d050c502f068dabb11f3f8d05163043de Mon Sep 17 00:00:00 2001
From: Manuel Selbach <manuel_selbach@yahoo.de>
Date: Mon, 23 Mar 2020 21:12:22 +0100
Subject: [PATCH] [BUGFIX] Cast label field for search in recycler

With this change all field will be cast to a datatype, that is
searchable with `like` to prevent errors.

Before that change e.g. for table `sys_file_reference` the field
`uid_local` is configured as label field. The query that is built,
will then try to do a like search on that field, which breaks on
certain DBMS.

Resolves: #81802
Resolves: #82837
Releases: master, 9.5
Change-Id: I65cc11e6c6388919a34b45a8738d8e1c64881983
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/63885
Tested-by: Andreas Fernandez <a.fernandez@scripting-base.de>
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Georg Ringer <georg.ringer@gmail.com>
Reviewed-by: Andreas Fernandez <a.fernandez@scripting-base.de>
Reviewed-by: Georg Ringer <georg.ringer@gmail.com>
---
 .../Classes/Database/Query/QueryBuilder.php   | 44 ++++++++++++++++
 .../Unit/Database/Query/QueryBuilderTest.php  | 52 +++++++++++++++++++
 .../Classes/RecordList/DatabaseRecordList.php |  2 +-
 .../Classes/Domain/Model/DeletedRecords.php   |  5 +-
 4 files changed, 100 insertions(+), 3 deletions(-)

diff --git a/typo3/sysext/core/Classes/Database/Query/QueryBuilder.php b/typo3/sysext/core/Classes/Database/Query/QueryBuilder.php
index cfbb5b18f034..e17b9f6e903e 100644
--- a/typo3/sysext/core/Classes/Database/Query/QueryBuilder.php
+++ b/typo3/sysext/core/Classes/Database/Query/QueryBuilder.php
@@ -15,6 +15,10 @@ namespace TYPO3\CMS\Core\Database\Query;
  * The TYPO3 project - inspiring people to share!
  */
 
+use Doctrine\DBAL\Platforms\MySqlPlatform;
+use Doctrine\DBAL\Platforms\OraclePlatform;
+use Doctrine\DBAL\Platforms\PostgreSqlPlatform;
+use Doctrine\DBAL\Platforms\SqlitePlatform;
 use Doctrine\DBAL\Platforms\SQLServerPlatform;
 use Doctrine\DBAL\Query\Expression\CompositeExpression;
 use TYPO3\CMS\Core\Database\Connection;
@@ -1034,6 +1038,46 @@ class QueryBuilder
         return $this->getConnection()->quoteColumnValuePairs($input);
     }
 
+    /**
+     * Creates a cast of the $fieldName to a text datatype depending on the database management system.
+     *
+     * @param string $fieldName The fieldname will be quoted and casted according to database platform automatically
+     * @return string
+     */
+    public function castFieldToTextType(string $fieldName): string
+    {
+        $databasePlatform = $this->connection->getDatabasePlatform();
+        // https://dev.mysql.com/doc/refman/5.7/en/cast-functions.html#function_convert
+        if ($databasePlatform instanceof MySqlPlatform) {
+            return sprintf('CONVERT(%s, CHAR)', $this->connection->quoteIdentifier($fieldName));
+        }
+        // https://www.postgresql.org/docs/current/sql-createcast.html
+        if ($databasePlatform instanceof PostgreSqlPlatform) {
+            return sprintf('%s::text', $this->connection->quoteIdentifier($fieldName));
+        }
+        // https://www.sqlite.org/lang_expr.html#castexpr
+        if ($databasePlatform instanceof SqlitePlatform) {
+            return sprintf('CAST(%s as TEXT)', $this->connection->quoteIdentifier($fieldName));
+        }
+        // https://docs.microsoft.com/en-us/sql/t-sql/functions/cast-and-convert-transact-sql?view=sql-server-ver15#implicit-conversions
+        if ($databasePlatform instanceof SQLServerPlatform) {
+            return sprintf('CAST(%s as VARCHAR)', $this->connection->quoteIdentifier($fieldName));
+        }
+        // https://docs.oracle.com/javadb/10.8.3.0/ref/rrefsqlj33562.html
+        if ($databasePlatform instanceof OraclePlatform) {
+            return sprintf('CAST(%s as VARCHAR)', $this->connection->quoteIdentifier($fieldName));
+        }
+
+        throw new \RuntimeException(
+            sprintf(
+                '%s is not implemented for the used database platform "%s", yet!',
+                __METHOD__,
+                get_class($this->connection->getDatabasePlatform())
+            ),
+            1584637096
+        );
+    }
+
     /**
      * Unquote a single identifier (no dot expansion). Used to unquote the table names
      * from the expressionBuilder so that the table can be found in the TCA definition.
diff --git a/typo3/sysext/core/Tests/Unit/Database/Query/QueryBuilderTest.php b/typo3/sysext/core/Tests/Unit/Database/Query/QueryBuilderTest.php
index 424ef1ff69ef..929a52f3bf70 100644
--- a/typo3/sysext/core/Tests/Unit/Database/Query/QueryBuilderTest.php
+++ b/typo3/sysext/core/Tests/Unit/Database/Query/QueryBuilderTest.php
@@ -17,7 +17,9 @@ namespace TYPO3\CMS\Core\Tests\Unit\Database\Query;
 
 use Doctrine\DBAL\Platforms\AbstractPlatform;
 use Doctrine\DBAL\Platforms\MySqlPlatform;
+use Doctrine\DBAL\Platforms\OraclePlatform;
 use Doctrine\DBAL\Platforms\PostgreSqlPlatform;
+use Doctrine\DBAL\Platforms\SqlitePlatform;
 use Doctrine\DBAL\Platforms\SQLServerPlatform;
 use Prophecy\Argument;
 use TYPO3\CMS\Core\Database\Connection;
@@ -1392,4 +1394,54 @@ class QueryBuilderTest extends UnitTestCase
             ],
         ];
     }
+
+    public function castFieldToTextTypeDataProvider(): array
+    {
+        return [
+            'Test cast for MySqlPlatform' => [
+                new MySqlPlatform(),
+                'CONVERT(aField, CHAR)'
+            ],
+            'Test cast for PostgreSqlPlatform' => [
+                new PostgreSqlPlatform(),
+                'aField::text'
+            ],
+            'Test cast for SqlitePlatform' => [
+                new SqlitePlatform(),
+                'CAST(aField as TEXT)'
+            ],
+            'Test cast for SQLServerPlatform' => [
+                new SQLServerPlatform(),
+                'CAST(aField as VARCHAR)'
+            ],
+            'Test cast for OraclePlatform' => [
+                new OraclePlatform(),
+                'CAST(aField as VARCHAR)'
+            ],
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider castFieldToTextTypeDataProvider
+     *
+     * @param AbstractPlatform $platform
+     * @param string $expectation
+     */
+    public function castFieldToTextType(AbstractPlatform $platform, string $expectation): void
+    {
+        $this->connection->quoteIdentifier('aField')
+            ->shouldBeCalled()
+            ->willReturnArgument(0);
+
+        $this->connection->getDatabasePlatform()->willReturn($platform);
+
+        $concreteQueryBuilder = new \Doctrine\DBAL\Query\QueryBuilder($this->connection->reveal());
+
+        $subject = new QueryBuilder($this->connection->reveal(), null, $concreteQueryBuilder);
+        $result = $subject->castFieldToTextType('aField');
+
+        $this->connection->quoteIdentifier('aField')->shouldHaveBeenCalled();
+        self::assertSame($expectation, $result);
+    }
 }
diff --git a/typo3/sysext/recordlist/Classes/RecordList/DatabaseRecordList.php b/typo3/sysext/recordlist/Classes/RecordList/DatabaseRecordList.php
index 42b6cb260a3b..ecba9e9f9c7e 100644
--- a/typo3/sysext/recordlist/Classes/RecordList/DatabaseRecordList.php
+++ b/typo3/sysext/recordlist/Classes/RecordList/DatabaseRecordList.php
@@ -3343,7 +3343,7 @@ class DatabaseRecordList
                 $evalRules = $fieldConfig['eval'] ?: '';
                 $searchConstraint = $expressionBuilder->andX(
                     $expressionBuilder->comparison(
-                        'LOWER(' . $queryBuilder->quoteIdentifier($fieldName) . ')',
+                        'LOWER(' . $queryBuilder->castFieldToTextType($fieldName) . ')',
                         'LIKE',
                         'LOWER(' . $like . ')'
                     )
diff --git a/typo3/sysext/recycler/Classes/Domain/Model/DeletedRecords.php b/typo3/sysext/recycler/Classes/Domain/Model/DeletedRecords.php
index 4cb22a0c9dba..8b030569ed97 100644
--- a/typo3/sysext/recycler/Classes/Domain/Model/DeletedRecords.php
+++ b/typo3/sysext/recycler/Classes/Domain/Model/DeletedRecords.php
@@ -270,8 +270,9 @@ class DeletedRecords
         // create the filter WHERE-clause
         $filterConstraint = null;
         if (trim($filter) !== '') {
-            $filterConstraint = $queryBuilder->expr()->like(
-                $GLOBALS['TCA'][$table]['ctrl']['label'],
+            $filterConstraint = $queryBuilder->expr()->comparison(
+                $queryBuilder->castFieldToTextType($GLOBALS['TCA'][$table]['ctrl']['label']),
+                'LIKE',
                 $queryBuilder->createNamedParameter(
                     '%' . $queryBuilder->escapeLikeWildcards($filter) . '%',
                     \PDO::PARAM_STR
-- 
GitLab