[BUGFIX] Unify return types for PDO drivers 03/62003/11
authorManuel Selbach <manuel_selbach@yahoo.de>
Tue, 15 Oct 2019 15:23:50 +0000 (17:23 +0200)
committerOliver Hader <oliver.hader@typo3.org>
Tue, 22 Oct 2019 08:02:22 +0000 (10:02 +0200)
With this change custom drivers for PDO get introduced, to unify the return
types from the database. Main topic is, to get string instead of resource
e.g. on pdo_ drivers for BLOB-fields.

Furthermore the test script for local testing gets expanded with the ability
to test on different database driver (if wished) and the option to test
lastest version of microsoft mssql server 2017.

Resolves: #88272
Releases: master, 9.5
Change-Id: I5d2cd2ccb7646f389219754be72ae6a02ae79559
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/62003
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Oliver Hader <oliver.hader@typo3.org>
Reviewed-by: Oliver Hader <oliver.hader@typo3.org>
12 files changed:
Build/Scripts/duplicateExceptionCodeCheck.sh
Build/Scripts/runTests.sh
Build/testing-docker/local/docker-compose.yml
typo3/sysext/core/Classes/Database/ConnectionPool.php
typo3/sysext/core/Classes/Database/Driver/PDOConnection.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Driver/PDOMySql/Driver.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Driver/PDOPgSql/Driver.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Driver/PDOSqlite/Driver.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Driver/PDOSqlsrv/Connection.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Driver/PDOSqlsrv/Driver.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Driver/PDOSqlsrv/Statement.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Driver/PDOStatement.php [new file with mode: 0644]

index bcb2179..c17536f 100755 (executable)
@@ -18,6 +18,8 @@ ignoreFiles+="sysext/core/Tests/Acceptance/Support/_generated/InstallTesterActio
 # an exception in here throws a code from a previous exception/error
 ignoreFiles+="sysext/extbase/Classes/Core/Bootstrap.php"
 ignoreFiles+="sysext/form/Classes/Mvc/Property/Exception/TypeConverterException.php"
+ignoreFiles+="sysext/core/Classes/Database/Driver/PDOStatement.php"
+ignoreFiles+="sysext/core/Classes/Database/Driver/PDOConnection.php"
 
 foundNewFile=0
 oldFilename=""
index b00a085..f1f3c9d 100755 (executable)
@@ -31,6 +31,8 @@ setUpDockerComposeDotEnv() {
     echo "SCRIPT_VERBOSE=${SCRIPT_VERBOSE}" >> .env
     echo "PHPUNIT_RANDOM=${PHPUNIT_RANDOM}" >> .env
     echo "CGLCHECK_DRY_RUN=${CGLCHECK_DRY_RUN}" >> .env
+    # Set a custom database driver provided by option: -a
+    [[ ! -z "$DATABASE_DRIVER" ]] && echo "DATABASE_DRIVER=${DATABASE_DRIVER}" >> .env
 }
 
 # Load help text into $HELP
@@ -46,6 +48,16 @@ Usage: $0 [options] [file]
 No arguments: Run all unit tests with PHP 7.2
 
 Options:
+    -a <mysqli|pdo_mysql|sqlsrv|pdo_sqlsrv>
+        Only with -s functional
+        Specifies to use another driver, following combinations are available:
+            - mariadb
+                - mysqli (default)
+                - pdo_mysql
+            - mssql
+                - sqlsrv (default)
+                - pdo_sqlsrv
+
     -s <...>
         Specifies which test suite to run
             - acceptance: backend acceptance tests
@@ -81,6 +93,7 @@ Options:
         Specifies on which DBMS tests are performed
             - mariadb (default): use mariadb
             - mssql: use mssql microsoft sql server
+            - mssql2017latest: use latest version of microsoft sql server 2017
             - postgres: use postgres
             - sqlite: use sqlite
 
@@ -185,8 +198,11 @@ OPTIND=1
 # Array for invalid options
 INVALID_OPTIONS=();
 # Simple option parsing based on getopts (! not getopt)
-while getopts ":s:d:p:e:xy:o:nhuv" OPT; do
+while getopts ":a:s:d:p:e:xy:o:nhuv" OPT; do
     case ${OPT} in
+        a)
+            DATABASE_DRIVER=${OPTARG}
+            ;;
         s)
             TEST_SUITE=${OPTARG}
             ;;
@@ -371,15 +387,23 @@ case ${TEST_SUITE} in
         setUpDockerComposeDotEnv
         case ${DBMS} in
             mariadb)
+                [[ ! -z "$DATABASE_DRIVER" ]] && echo "Using driver: ${DATABASE_DRIVER}"
                 docker-compose run prepare_functional_mariadb10
                 docker-compose run functional_mariadb10
                 SUITE_EXIT_CODE=$?
                 ;;
             mssql)
+                [[ ! -z "$DATABASE_DRIVER" ]] && echo "Using driver: ${DATABASE_DRIVER}"
                 docker-compose run prepare_functional_mssql2017cu9
                 docker-compose run functional_mssql2017cu9
                 SUITE_EXIT_CODE=$?
                 ;;
+            mssql2017latest)
+                [[ ! -z "$DATABASE_DRIVER" ]] && echo "Using driver: ${DATABASE_DRIVER}"
+                docker-compose run prepare_functional_mssql2017latest
+                docker-compose run functional_mssql2017latest
+                SUITE_EXIT_CODE=$?
+                ;;
             postgres)
                 docker-compose run prepare_functional_postgres10
                 docker-compose run functional_postgres10
index 068d599..dab7a7a 100644 (file)
@@ -19,6 +19,13 @@ services:
     # No tmpfs setup here since mssql fails on tmpfs o_direct.
     # This makes mssql sloooow for functionals.
 
+  mssql2017latest:
+    image: mcr.microsoft.com/mssql/server:2017-latest-ubuntu
+    environment:
+      ACCEPT_EULA: Y
+      SA_PASSWORD: "Test1234!"
+      MSSQL_PID: Developer
+
   postgres10:
     image: postgres:10
     environment:
@@ -510,6 +517,7 @@ services:
       - /etc/passwd:/etc/passwd:ro
       - /etc/group:/etc/group:ro
     environment:
+      typo3DatabaseDriver: "${DATABASE_DRIVER:-mysqli}"
       typo3DatabaseName: func_test
       typo3DatabaseUsername: root
       typo3DatabasePassword: funcp
@@ -559,7 +567,7 @@ services:
       - /etc/passwd:/etc/passwd:ro
       - /etc/group:/etc/group:ro
     environment:
-      typo3DatabaseDriver: sqlsrv
+      typo3DatabaseDriver: "${DATABASE_DRIVER:-sqlsrv}"
       typo3DatabaseName: func
       typo3DatabasePassword: "Test1234!"
       typo3DatabaseUsername: SA
@@ -585,6 +593,59 @@ services:
         fi
       "
 
+  prepare_functional_mssql2017latest:
+    image: alpine:3.8
+    links:
+      - mssql2017latest
+      - redis4
+      - memcached1-5
+    command: >
+      /bin/sh -c "
+        if [ ${SCRIPT_VERBOSE} -eq 1 ]; then
+          set -x
+        fi
+        echo Waiting for database start...;
+        while ! nc -z mssql2017latest 1433; do
+          sleep 1;
+        done;
+        echo Database is up;
+      "
+
+  functional_mssql2017latest:
+    image: typo3gmbh/${DOCKER_PHP_IMAGE}:latest
+    user: ${HOST_UID}
+    volumes:
+      - ${CORE_ROOT}:${CORE_ROOT}
+      - ${HOST_HOME}:${HOST_HOME}
+      - /etc/passwd:/etc/passwd:ro
+      - /etc/group:/etc/group:ro
+    environment:
+      typo3DatabaseDriver: "${DATABASE_DRIVER:-sqlsrv}"
+      typo3DatabaseName: func
+      typo3DatabasePassword: "Test1234!"
+      typo3DatabaseUsername: SA
+      typo3DatabasePort: 1433
+      typo3DatabaseCharset: utf-8
+      typo3DatabaseHost: mssql2017latest
+      typo3TestingRedisHost: redis4
+      typo3TestingMemcachedHost: memcached1-5
+    working_dir: ${CORE_ROOT}
+    command: >
+      /bin/sh -c "
+        if [ ${SCRIPT_VERBOSE} -eq 1 ]; then
+          set -x
+        fi
+        php -v | grep '^PHP'
+        if [ ${PHP_XDEBUG_ON} -eq 0 ]; then
+          php -n -c /etc/php/cli-no-xdebug/php.ini \
+            vendor/phpunit/phpunit/phpunit -c vendor/typo3/testing-framework/Resources/Core/Build/FunctionalTests.xml ${EXTRA_TEST_OPTIONS} --exclude-group not-mssql ${TEST_FILE};
+        else
+          DOCKER_HOST=`route -n | awk '/^0.0.0.0/ { print $$2 }'`
+          XDEBUG_CONFIG=\"remote_port=${PHP_XDEBUG_PORT} remote_enable=1 remote_host=$${DOCKER_HOST}\" \
+            vendor/phpunit/phpunit/phpunit -c vendor/typo3/testing-framework/Resources/Core/Build/FunctionalTests.xml ${EXTRA_TEST_OPTIONS} --exclude-group not-mssql ${TEST_FILE};
+        fi
+      "
+
   prepare_functional_postgres10:
     image: alpine:3.8
     links:
index 6890e8d..7b9a5dd 100644 (file)
@@ -18,6 +18,10 @@ namespace TYPO3\CMS\Core\Database;
 use Doctrine\DBAL\DriverManager;
 use Doctrine\DBAL\Events;
 use Doctrine\DBAL\Types\Type;
+use TYPO3\CMS\Core\Database\Driver\PDOMySql\Driver as PDOMySqlDriver;
+use TYPO3\CMS\Core\Database\Driver\PDOPgSql\Driver as PDOPgSqlDriver;
+use TYPO3\CMS\Core\Database\Driver\PDOSqlite\Driver as PDOSqliteDriver;
+use TYPO3\CMS\Core\Database\Driver\PDOSqlsrv\Driver as PDOSqlsrvDriver;
 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
 use TYPO3\CMS\Core\Database\Schema\EventListener\SchemaAlterTableListener;
 use TYPO3\CMS\Core\Database\Schema\EventListener\SchemaColumnDefinitionListener;
@@ -57,6 +61,21 @@ class ConnectionPool
     ];
 
     /**
+     * List of custom drivers and their mappings to the driver classes.
+     *
+     * @var string[]
+     */
+    protected static $driverMap = [
+        'pdo_mysql' => PDOMySqlDriver::class,
+        'pdo_sqlite' => PDOSqliteDriver::class,
+        'pdo_pgsql' => PDOPgSqlDriver::class,
+        'pdo_sqlsrv' => PDOSqlsrvDriver::class,
+        // TODO: not supported yet, need to be checked later
+//        'pdo_oci' => PDOOCIDriver::class,
+//        'drizzle_pdo_mysql' => DrizzlePDOMySQLDriver::class,
+    ];
+
+    /**
      * Creates a connection object based on the specified table name.
      *
      * This is the official entry point to get a database connection to ensure
@@ -131,6 +150,22 @@ class ConnectionPool
     }
 
     /**
+     * Map custom driver class for certain driver
+     *
+     * @param array $connectionParams
+     * @return array
+     */
+    protected function mapCustomDriver(array $connectionParams): array
+    {
+        // if no custom driver is provided, map TYPO3 specific drivers
+        if (!isset($connectionParams['driverClass']) && isset(static::$driverMap[$connectionParams['driver']])) {
+            $connectionParams['driverClass'] = static::$driverMap[$connectionParams['driver']];
+        }
+
+        return $connectionParams;
+    }
+
+    /**
      * Creates a connection object based on the specified parameters
      *
      * @param array $connectionParams
@@ -143,11 +178,7 @@ class ConnectionPool
             $connectionParams['charset'] = 'utf8';
         }
 
-        // Force consistent handling of binary objects across database platforms
-        // MySQL returns strings by default, PostgreSQL streams.
-        if (strpos($connectionParams['driver'], 'pdo_') === 0) {
-            $connectionParams['driverOptions'][\PDO::ATTR_STRINGIFY_FETCHES] = true;
-        }
+        $connectionParams = $this->mapCustomDriver($connectionParams);
 
         /** @var Connection $conn */
         $conn = DriverManager::getConnection($connectionParams);
diff --git a/typo3/sysext/core/Classes/Database/Driver/PDOConnection.php b/typo3/sysext/core/Classes/Database/Driver/PDOConnection.php
new file mode 100644 (file)
index 0000000..37e5b61
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Core\Database\Driver;
+
+/*
+ * 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!
+ */
+
+use Doctrine\DBAL\Driver\PDOConnection as DoctrineDbalPDOConnection;
+use Doctrine\DBAL\Driver\PDOException;
+use PDO;
+
+/**
+ * This is a full "clone" of the class of package doctrine/dbal. Scope is to use the PDOConnection of TYPO3.
+ * All private methods have to be checked on every release of doctrine/dbal.
+ */
+class PDOConnection extends DoctrineDbalPDOConnection
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function __construct($dsn, $user = null, $password = null, ?array $options = null)
+    {
+        try {
+            parent::__construct($dsn, $user, $password, $options);
+            $this->setAttribute(PDO::ATTR_STATEMENT_CLASS, [PDOStatement::class, []]);
+        } catch (\PDOException $exception) {
+            throw new PDOException($exception);
+        }
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Driver/PDOMySql/Driver.php b/typo3/sysext/core/Classes/Database/Driver/PDOMySql/Driver.php
new file mode 100644 (file)
index 0000000..7085742
--- /dev/null
@@ -0,0 +1,48 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Core\Database\Driver\PDOMySql;
+
+/*
+ * 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!
+ */
+
+use Doctrine\DBAL\DBALException;
+use Doctrine\DBAL\Driver\PDOMySql\Driver as DoctrinePDOMySqlDriver;
+use PDOException;
+use TYPO3\CMS\Core\Database\Driver\PDOConnection;
+
+/**
+ * This is a full "clone" of the class of package doctrine/dbal. Scope is to use the PDOConnection of TYPO3.
+ * All private methods have to be checked on every release of doctrine/dbal.
+ */
+class Driver extends DoctrinePDOMySqlDriver
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function connect(array $params, $username = null, $password = null, array $driverOptions = [])
+    {
+        try {
+            $conn = new PDOConnection(
+                $this->constructPdoDsn($params),
+                $username,
+                $password,
+                $driverOptions
+            );
+        } catch (PDOException $e) {
+            throw DBALException::driverException($this, $e);
+        }
+
+        return $conn;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Driver/PDOPgSql/Driver.php b/typo3/sysext/core/Classes/Database/Driver/PDOPgSql/Driver.php
new file mode 100644 (file)
index 0000000..9a11999
--- /dev/null
@@ -0,0 +1,119 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Core\Database\Driver\PDOPgSql;
+
+/*
+ * 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!
+ */
+
+use Doctrine\DBAL\DBALException;
+use Doctrine\DBAL\Driver\PDOPgSql\Driver as DoctrineDbalPDOPgSqlDriver;
+use PDO;
+use PDOException;
+use TYPO3\CMS\Core\Database\Driver\PDOConnection;
+
+/**
+ * This is a full "clone" of the class of package doctrine/dbal. Scope is to use the PDOConnection of TYPO3.
+ * All private methods have to be checked on every release of doctrine/dbal.
+ */
+class Driver extends DoctrineDbalPDOPgSqlDriver
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function connect(array $params, $username = null, $password = null, array $driverOptions = [])
+    {
+        try {
+            $pdo = new PDOConnection(
+                $this->_constructPdoDsn($params),
+                $username,
+                $password,
+                $driverOptions
+            );
+
+            if (defined('PDO::PGSQL_ATTR_DISABLE_PREPARES')
+                && (
+                    ! isset($driverOptions[PDO::PGSQL_ATTR_DISABLE_PREPARES])
+                    || $driverOptions[PDO::PGSQL_ATTR_DISABLE_PREPARES] === true
+                )
+            ) {
+                $pdo->setAttribute(PDO::PGSQL_ATTR_DISABLE_PREPARES, true);
+            }
+
+            /* defining client_encoding via SET NAMES to avoid inconsistent DSN support
+             * - the 'client_encoding' connection param only works with postgres >= 9.1
+             * - passing client_encoding via the 'options' param breaks pgbouncer support
+             */
+            if (isset($params['charset'])) {
+                $pdo->exec('SET NAMES \'' . $params['charset'] . '\'');
+            }
+
+            return $pdo;
+        } catch (PDOException $e) {
+            throw DBALException::driverException($this, $e);
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    private function _constructPdoDsn(array $params)
+    {
+        $dsn = 'pgsql:';
+
+        if (isset($params['host']) && $params['host'] !== '') {
+            $dsn .= 'host=' . $params['host'] . ';';
+        }
+
+        if (isset($params['port']) && $params['port'] !== '') {
+            $dsn .= 'port=' . $params['port'] . ';';
+        }
+
+        if (isset($params['dbname'])) {
+            $dsn .= 'dbname=' . $params['dbname'] . ';';
+        } elseif (isset($params['default_dbname'])) {
+            $dsn .= 'dbname=' . $params['default_dbname'] . ';';
+        } else {
+            // Used for temporary connections to allow operations like dropping the database currently connected to.
+            // Connecting without an explicit database does not work, therefore "postgres" database is used
+            // as it is mostly present in every server setup.
+            $dsn .= 'dbname=postgres;';
+        }
+
+        if (isset($params['sslmode'])) {
+            $dsn .= 'sslmode=' . $params['sslmode'] . ';';
+        }
+
+        if (isset($params['sslrootcert'])) {
+            $dsn .= 'sslrootcert=' . $params['sslrootcert'] . ';';
+        }
+
+        if (isset($params['sslcert'])) {
+            $dsn .= 'sslcert=' . $params['sslcert'] . ';';
+        }
+
+        if (isset($params['sslkey'])) {
+            $dsn .= 'sslkey=' . $params['sslkey'] . ';';
+        }
+
+        if (isset($params['sslcrl'])) {
+            $dsn .= 'sslcrl=' . $params['sslcrl'] . ';';
+        }
+
+        if (isset($params['application_name'])) {
+            $dsn .= 'application_name=' . $params['application_name'] . ';';
+        }
+
+        return $dsn;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Driver/PDOSqlite/Driver.php b/typo3/sysext/core/Classes/Database/Driver/PDOSqlite/Driver.php
new file mode 100644 (file)
index 0000000..31c5ef9
--- /dev/null
@@ -0,0 +1,60 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Core\Database\Driver\PDOSqlite;
+
+/*
+ * 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!
+ */
+
+use Doctrine\DBAL\DBALException;
+use Doctrine\DBAL\Driver\PDOSqlite\Driver as DoctrinePDOSqliteDriver;
+use PDOException;
+use TYPO3\CMS\Core\Database\Driver\PDOConnection;
+
+/**
+ * This is a full "clone" of the class of package doctrine/dbal. Scope is to use the PDOConnection of TYPO3.
+ * All private methods have to be checked on every release of doctrine/dbal.
+ */
+class Driver extends DoctrinePDOSqliteDriver
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function connect(array $params, $username = null, $password = null, array $driverOptions = [])
+    {
+        if (isset($driverOptions['userDefinedFunctions'])) {
+            $this->_userDefinedFunctions = array_merge(
+                $this->_userDefinedFunctions,
+                $driverOptions['userDefinedFunctions']
+            );
+            unset($driverOptions['userDefinedFunctions']);
+        }
+
+        try {
+            $pdo = new PDOConnection(
+                $this->_constructPdoDsn($params),
+                $username,
+                $password,
+                $driverOptions
+            );
+        } catch (PDOException $ex) {
+            throw DBALException::driverException($this, $ex);
+        }
+
+        foreach ($this->_userDefinedFunctions as $fn => $data) {
+            $pdo->sqliteCreateFunction($fn, $data['callback'], $data['numArgs']);
+        }
+
+        return $pdo;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Driver/PDOSqlsrv/Connection.php b/typo3/sysext/core/Classes/Database/Driver/PDOSqlsrv/Connection.php
new file mode 100644 (file)
index 0000000..d830017
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Core\Database\Driver\PDOSqlsrv;
+
+/*
+ * 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!
+ */
+
+use PDO;
+
+/**
+ * This is a full "clone" of the class of package doctrine/dbal. Scope is to use the PDOConnection of TYPO3.
+ * All private methods have to be checked on every release of doctrine/dbal.
+ */
+class Connection extends \Doctrine\DBAL\Driver\PDOSqlsrv\Connection
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function __construct($dsn, $user = null, $password = null, ?array $options = null)
+    {
+        parent::__construct($dsn, $user, $password, $options);
+        $this->setAttribute(PDO::ATTR_STATEMENT_CLASS, [Statement::class, []]);
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Driver/PDOSqlsrv/Driver.php b/typo3/sysext/core/Classes/Database/Driver/PDOSqlsrv/Driver.php
new file mode 100644 (file)
index 0000000..b427038
--- /dev/null
@@ -0,0 +1,106 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Core\Database\Driver\PDOSqlsrv;
+
+/*
+ * 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!
+ */
+
+/**
+ * This is a full "clone" of the class of package doctrine/dbal. Scope is to use the PDOConnection of TYPO3.
+ * All private methods have to be checked on every release of doctrine/dbal.
+ */
+class Driver extends \Doctrine\DBAL\Driver\PDOSqlsrv\Driver
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function connect(array $params, $username = null, $password = null, array $driverOptions = [])
+    {
+        [$driverOptions, $connectionOptions] = $this->splitOptions($driverOptions);
+
+        return new Connection(
+            $this->_constructPdoDsn($params, $connectionOptions),
+            $username,
+            $password,
+            $driverOptions
+        );
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    private function _constructPdoDsn(array $params, array $connectionOptions)
+    {
+        $dsn = 'sqlsrv:server=';
+
+        if (isset($params['host'])) {
+            $dsn .= $params['host'];
+        }
+
+        if (isset($params['port']) && ! empty($params['port'])) {
+            $dsn .= ',' . $params['port'];
+        }
+
+        if (isset($params['dbname'])) {
+            $connectionOptions['Database'] = $params['dbname'];
+        }
+
+        if (isset($params['MultipleActiveResultSets'])) {
+            $connectionOptions['MultipleActiveResultSets'] = $params['MultipleActiveResultSets'] ? 'true' : 'false';
+        }
+
+        return $dsn . $this->getConnectionOptionsDsn($connectionOptions);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    private function splitOptions(array $options): array
+    {
+        $driverOptions     = [];
+        $connectionOptions = [];
+
+        foreach ($options as $optionKey => $optionValue) {
+            if (is_int($optionKey)) {
+                $driverOptions[$optionKey] = $optionValue;
+            } else {
+                $connectionOptions[$optionKey] = $optionValue;
+            }
+        }
+
+        return [$driverOptions, $connectionOptions];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    private function getConnectionOptionsDsn(array $connectionOptions): string
+    {
+        $connectionOptionsDsn = '';
+
+        foreach ($connectionOptions as $paramName => $paramValue) {
+            $connectionOptionsDsn .= sprintf(';%s=%s', $paramName, $paramValue);
+        }
+
+        return $connectionOptionsDsn;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getName()
+    {
+        return parent::getName();
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Driver/PDOSqlsrv/Statement.php b/typo3/sysext/core/Classes/Database/Driver/PDOSqlsrv/Statement.php
new file mode 100644 (file)
index 0000000..d4afdff
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Core\Database\Driver\PDOSqlsrv;
+
+/*
+ * 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!
+ */
+
+use Doctrine\DBAL\ParameterType;
+use PDO;
+use TYPO3\CMS\Core\Database\Driver\PDOStatement;
+
+/**
+ * This is a full "clone" of the class of package doctrine/dbal. Scope is to use the PDOConnection of TYPO3.
+ * All private methods have to be checked on every release of doctrine/dbal.
+ */
+class Statement extends PDOStatement
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null, $driverOptions = null)
+    {
+        if (($type === ParameterType::LARGE_OBJECT || $type === ParameterType::BINARY)
+            && $driverOptions === null
+        ) {
+            $driverOptions = PDO::SQLSRV_ENCODING_BINARY;
+        }
+
+        return parent::bindParam($column, $variable, $type, $length, $driverOptions);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function bindValue($param, $value, $type = ParameterType::STRING)
+    {
+        return $this->bindParam($param, $value, $type);
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Driver/PDOStatement.php b/typo3/sysext/core/Classes/Database/Driver/PDOStatement.php
new file mode 100644 (file)
index 0000000..ed54de8
--- /dev/null
@@ -0,0 +1,94 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Core\Database\Driver;
+
+/*
+ * 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!
+ */
+
+use Doctrine\DBAL\Driver\PDOException;
+use Doctrine\DBAL\Driver\PDOStatement as DoctrineDbalPDOStatement;
+use PDO;
+
+class PDOStatement extends DoctrineDbalPDOStatement
+{
+    /**
+     * Map resources to string like is done for e.g. in mysqli driver
+     *
+     * @param mixed $record
+     * @return mixed
+     */
+    protected function mapResourceToString($record)
+    {
+        if (is_array($record)) {
+            return array_map(
+                static function ($value) {
+                    if (is_resource($value)) {
+                        $value = stream_get_contents($value);
+                    }
+
+                    return $value;
+                },
+                $record
+            );
+        }
+
+        return $record;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function fetch($fetchMode = null, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
+    {
+        try {
+            $record = parent::fetch($fetchMode, $cursorOrientation, $cursorOffset);
+            $record = $this->mapResourceToString($record);
+            return $record;
+        } catch (\PDOException $exception) {
+            throw new PDOException($exception);
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
+    {
+        try {
+            $records = parent::fetchAll($fetchMode, $fetchArgument, $ctorArgs);
+
+            if ($records !== false) {
+                $records = array_map([$this, 'mapResourceToString'], $records);
+            }
+
+            return $records;
+        } catch (\PDOException $exception) {
+            throw new PDOException($exception);
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function fetchColumn($columnIndex = 0)
+    {
+        try {
+            $record = parent::fetchColumn($columnIndex);
+            $record = $this->mapResourceToString($record);
+            return $record;
+        } catch (\PDOException $exception) {
+            throw new PDOException($exception);
+        }
+    }
+}