From 817021e0728d8b4493b9e4e208e01cb89a432559 Mon Sep 17 00:00:00 2001 From: Thorsten Rinne Date: Sun, 9 Feb 2025 13:59:27 +0100 Subject: [PATCH] feat: added experimental support for PDO_MYSQL --- CHANGELOG.md | 1 + Dockerfile | 4 +- composer.json | 1 + phpmyfaq/src/phpMyFAQ/Category.php | 7 +- .../SystemInformationController.php | 3 +- phpmyfaq/src/phpMyFAQ/Database.php | 2 +- .../src/phpMyFAQ/Database/DatabaseDriver.php | 6 +- phpmyfaq/src/phpMyFAQ/Database/Mysqli.php | 2 +- phpmyfaq/src/phpMyFAQ/Database/PdoMysql.php | 323 ++++++++++++++++++ phpmyfaq/src/phpMyFAQ/Database/Pgsql.php | 6 +- phpmyfaq/src/phpMyFAQ/Database/Sqlite3.php | 3 +- phpmyfaq/src/phpMyFAQ/Database/Sqlsrv.php | 3 +- phpmyfaq/src/phpMyFAQ/System.php | 6 +- 13 files changed, 345 insertions(+), 22 deletions(-) create mode 100644 phpmyfaq/src/phpMyFAQ/Database/PdoMysql.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 29a2063ede..fba4900d8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ This is a log of major user-visible changes in each phpMyFAQ release. - added Symfony Routing for administration backend (Thorsten) - added code snippets plugin with syntax highlighting in WYSIWYG editor (Thorsten) - added plugin administration backend (Thorsten) +- added experimental support for PDO_MYSQL (Thorsten) - improved online update feature (Thorsten) - migrated from WYSIWYG editor from TinyMCE to Jodit Editor (Thorsten) - migrated from JavaScript to TypeScript (Thorsten) diff --git a/Dockerfile b/Dockerfile index 10159d264f..2dde8065ca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,12 @@ # -# This image uses a php:8.4.2-apache base image and do not have any phpMyFAQ code with it. +# This image uses a php:8.4-apache base image and do not have any phpMyFAQ code with it. # It's for development only, it's meant to be run with docker-compose # ##################################### #=== Unique stage without payload === ##################################### -FROM php:8.4.2-apache +FROM php:8.4-apache #=== Install gd PHP dependencie === RUN set -x \ diff --git a/composer.json b/composer.json index afaeb11c29..156f401521 100644 --- a/composer.json +++ b/composer.json @@ -65,6 +65,7 @@ "suggest": { "ext-ldap": "*", "ext-openssl": "*", + "ext-pdo": "*", "ext-pgsql": "*", "ext-sqlite3": "*", "ext-sqlsrv": "*" diff --git a/phpmyfaq/src/phpMyFAQ/Category.php b/phpmyfaq/src/phpMyFAQ/Category.php index e58714f931..ab622c37dd 100755 --- a/phpmyfaq/src/phpMyFAQ/Category.php +++ b/phpmyfaq/src/phpMyFAQ/Category.php @@ -172,11 +172,8 @@ public function getOrderedCategories(bool $withPermission = true, bool $withInac } if ($this->language !== null && preg_match("/^[a-z\-]{2,}$/", $this->language)) { - $where .= $where === '' || $where === '0' ? ' - WHERE' : ' - AND'; - $where .= " - fc.lang = '" . $this->configuration->getDb()->escape($this->language) . "'"; + $where .= $where === '' || $where === '0' ? ' WHERE' : ' AND'; + $where .= sprintf(' fc.lang = \'%s\'', $this->configuration->getDb()->escape($this->language)); } $query = sprintf( diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Administration/SystemInformationController.php b/phpmyfaq/src/phpMyFAQ/Controller/Administration/SystemInformationController.php index a888df0b04..4808062965 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Administration/SystemInformationController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Administration/SystemInformationController.php @@ -74,8 +74,7 @@ public function index(Request $request): Response 'Web server Interface' => strtoupper(PHP_SAPI), 'PHP Version' => PHP_VERSION, 'PHP Extensions' => implode(', ', get_loaded_extensions()), - 'PHP Session path' => session_save_path(), - 'Database Server' => Database::getType(), + 'Database Driver' => Database::getType(), 'Database Server Version' => $this->configuration->getDb()->serverVersion(), 'Database Client Version' => $this->configuration->getDb()->clientVersion(), 'Elasticsearch Version' => $esInformation diff --git a/phpmyfaq/src/phpMyFAQ/Database.php b/phpmyfaq/src/phpMyFAQ/Database.php index 307b567a1b..60b72d8d3c 100755 --- a/phpmyfaq/src/phpMyFAQ/Database.php +++ b/phpmyfaq/src/phpMyFAQ/Database.php @@ -50,7 +50,7 @@ public static function factory(string $type): ?DatabaseDriver self::$dbType = $type; if (str_starts_with($type, 'pdo_')) { - $class = 'phpMyFAQ\Database\Pdo_' . ucfirst(substr($type, 4)); + $class = 'phpMyFAQ\Database\Pdo' . ucfirst(substr($type, 4)); } else { $class = 'phpMyFAQ\Database\\' . ucfirst($type); } diff --git a/phpmyfaq/src/phpMyFAQ/Database/DatabaseDriver.php b/phpmyfaq/src/phpMyFAQ/Database/DatabaseDriver.php index 9b59c560b5..e244689be4 100644 --- a/phpmyfaq/src/phpMyFAQ/Database/DatabaseDriver.php +++ b/phpmyfaq/src/phpMyFAQ/Database/DatabaseDriver.php @@ -65,10 +65,10 @@ public function fetchObject(mixed $result): mixed; /** * Fetch a result row as an array. * - * - * @return array + * @param mixed $result + * @return array|false|null */ - public function fetchArray(mixed $result): ?array; + public function fetchArray(mixed $result): array|false|null; /** * Fetch a result row. diff --git a/phpmyfaq/src/phpMyFAQ/Database/Mysqli.php b/phpmyfaq/src/phpMyFAQ/Database/Mysqli.php index 2c31b94d37..1d204a2075 100644 --- a/phpmyfaq/src/phpMyFAQ/Database/Mysqli.php +++ b/phpmyfaq/src/phpMyFAQ/Database/Mysqli.php @@ -14,6 +14,7 @@ * @copyright 2005-2025 phpMyFAQ Team * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 * @link https://www.phpmyfaq.de + * @since 2005-12-13 */ namespace phpMyFAQ\Database; @@ -22,7 +23,6 @@ use mysqli_sql_exception; use phpMyFAQ\Database; use phpMyFAQ\Core\Exception; -use phpMyFAQ\Utils; use SensitiveParameter; /** diff --git a/phpmyfaq/src/phpMyFAQ/Database/PdoMysql.php b/phpmyfaq/src/phpMyFAQ/Database/PdoMysql.php new file mode 100644 index 0000000000..b3ba197c5d --- /dev/null +++ b/phpmyfaq/src/phpMyFAQ/Database/PdoMysql.php @@ -0,0 +1,323 @@ + + * @copyright 2025 phpMyFAQ Team + * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 + * @link https://www.phpmyfaq.de + * @since 2025-02-09 + */ + +namespace phpMyFAQ\Database; + +use PDO; +use PDOException; +use PDOStatement; +use phpMyFAQ\Core\Exception; +use SensitiveParameter; + +/** + * Class PdoDatabase + * + * @package phpMyFAQ\Database + */ +class PdoMysql implements DatabaseDriver +{ + /** + * @var string[] Tables. + */ + public array $tableNames = []; + + /** + * The connection object. + * + * @var PDO|null + */ + private ?PDO $conn = null; + + /** + * The query log string. + */ + private string $sqlLog = ''; + + /** + * Connects to the database. + * + * @param string $host Hostname or path to socket + * @param string $user Username + * @param string $password Password + * @param string $database Database name + * @param int|null $port + * @return null|bool true, if connected, otherwise false + * @throws Exception + */ + public function connect( + string $host, + #[\SensitiveParameter] string $user, + #[SensitiveParameter] string $password, + string $database = '', + int|null $port = null + ): ?bool { + $dsn = "mysql:host=$host;dbname=$database;port=$port;charset=utf8mb4"; + try { + $this->conn = new PDO($dsn, $user, $password); + $this->conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + } catch (PDOException $e) { + throw new Exception($e->getMessage()); + } + + return true; + } + + /** + * Returns the error string. + */ + public function error(): string + { + return $this->conn->errorInfo()[2] ?? ''; + } + + /** + * Escapes a string for use in a query. + */ + public function escape(string $string): string + { + return $string; + } + + /** + * Fetch a result row as an associative array. + */ + public function fetchArray(mixed $result): array|false|null + { + return $result->fetch(PDO::FETCH_ASSOC); + } + + /** + * Fetch a result row. + */ + public function fetchRow(mixed $result): mixed + { + return $result->fetch(PDO::FETCH_NUM)[0] ?? false; + } + + /** + * Fetches a complete result as an object. + * + * @param mixed $result Result set + * @throws Exception + */ + public function fetchAll(mixed $result): ?array + { + if (false === $result) { + throw new Exception('Error while fetching result: ' . $this->error()); + } + + return $result->fetchAll(PDO::FETCH_OBJ); + } + + /** + * Fetch a result row as an object. + * This function fetches a result row as an object. + * + * @throws Exception + */ + public function fetchObject(mixed $result): mixed + { + return $result->fetch(PDO::FETCH_OBJ); + } + + /** + * Number of rows in a result. + */ + public function numRows(mixed $result): int + { + return $result->rowCount(); + } + + /** + * Logs the queries. + */ + public function log(): string + { + return $this->sqlLog; + } + + /** + * This function returns the table status. + * + * @param string $prefix Table prefix + * @return string[] + */ + public function getTableStatus(string $prefix = ''): array + { + $status = []; + foreach ($this->getTableNames($prefix) as $table) { + $status[$table] = $this->getOne('SELECT count(*) FROM ' . $table); + } + + return $status; + } + + /** + * Returns an array with all table names. + * + * @todo Have to be refactored because of https://github.com/thorsten/phpMyFAQ/issues/965 + * + * @param string $prefix Table prefix + * + * @return string[] + */ + public function getTableNames(string $prefix = ''): array + { + return $this->tableNames = [ + $prefix . 'faqadminlog', + $prefix . 'faqattachment', + $prefix . 'faqattachment_file', + $prefix . 'faqbackup', + $prefix . 'faqbookmarks', + $prefix . 'faqcaptcha', + $prefix . 'faqcategories', + $prefix . 'faqcategoryrelations', + $prefix . 'faqcategory_group', + $prefix . 'faqcategory_news', + $prefix . 'faqcategory_order', + $prefix . 'faqcategory_user', + $prefix . 'faqchanges', + $prefix . 'faqcomments', + $prefix . 'faqconfig', + $prefix . 'faqdata', + $prefix . 'faqdata_group', + $prefix . 'faqdata_revisions', + $prefix . 'faqdata_tags', + $prefix . 'faqdata_user', + $prefix . 'faqforms', + $prefix . 'faqglossary', + $prefix . 'faqgroup', + $prefix . 'faqgroup_right', + $prefix . 'faqinstances', + $prefix . 'faqinstances_config', + $prefix . 'faqnews', + $prefix . 'faqquestions', + $prefix . 'faqright', + $prefix . 'faqsearches', + $prefix . 'faqseo', + $prefix . 'faqsessions', + $prefix . 'faqstopwords', + $prefix . 'faqtags', + $prefix . 'faquser', + $prefix . 'faquserdata', + $prefix . 'faquserlogin', + $prefix . 'faquser_group', + $prefix . 'faquser_right', + $prefix . 'faqvisits', + $prefix . 'faqvoting', + ]; + } + + /** + * Returns just one row. + */ + private function getOne(string $query): string + { + $stmt = $this->conn->query($query); + $row = $stmt->fetch(PDO::FETCH_NUM); + + return $row[0]; + } + + /** + * This function is a replacement for MySQL's auto-increment so that + * we don't need it anymore. + * + * @param string $table The name of the table + * @param string $columnId The name of the ID column + * @throws Exception + */ + public function nextId(string $table, string $columnId): int + { + $select = sprintf( + ' + SELECT + MAX(%s) AS current_id + FROM + %s', + $columnId, + $table + ); + + $stmt = $this->query($select); + $current = $stmt->fetch(PDO::FETCH_NUM); + + return $current[0] + 1; + } + + /** + * This function sends a query to the database. + * + * @return PDOStatement|false $result + * @throws Exception + */ + public function query(string $query, int $offset = 0, int $rowcount = 0): mixed + { + $this->sqlLog .= $query; + + if (0 < $rowcount) { + $query .= sprintf(' LIMIT %d,%d', $offset, $rowcount); + } + + try { + $result = $this->conn->query($query); + } catch (PDOException $e) { + throw new Exception($e->getMessage()); + } + + if (false === $result) { + $this->sqlLog .= $this->conn->errorCode() . ': ' . $this->error(); + } + + return $result; + } + + /** + * Returns the client version string. + */ + public function clientVersion(): string + { + return $this->conn->getAttribute(PDO::ATTR_CLIENT_VERSION); + } + + /** + * Returns the server version string. + */ + public function serverVersion(): string + { + return $this->conn->getAttribute(PDO::ATTR_SERVER_VERSION); + } + + /** + * Closes the connection to the database. + */ + public function close(): void + { + $this->conn = null; + } + + public function __destruct() + { + $this->close(); + } + + public function now(): string + { + return 'NOW()'; + } +} diff --git a/phpmyfaq/src/phpMyFAQ/Database/Pgsql.php b/phpmyfaq/src/phpMyFAQ/Database/Pgsql.php index e50637f885..e307be7f71 100644 --- a/phpmyfaq/src/phpMyFAQ/Database/Pgsql.php +++ b/phpmyfaq/src/phpMyFAQ/Database/Pgsql.php @@ -1,7 +1,7 @@ * @author Tom Rochester - * @copyright 2003-2025 phpMyFAQ Team + * @copyright 2005-2025 phpMyFAQ Team * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 * @link https://www.phpmyfaq.de + * @since 2005-12-13 */ namespace phpMyFAQ\Database; @@ -22,7 +23,6 @@ use PgSql\Connection; use PgSql\Result; use phpMyFAQ\Database; -use phpMyFAQ\Utils; /** * Class Pgsql diff --git a/phpmyfaq/src/phpMyFAQ/Database/Sqlite3.php b/phpmyfaq/src/phpMyFAQ/Database/Sqlite3.php index fe028a6d54..5569e68b5c 100644 --- a/phpmyfaq/src/phpMyFAQ/Database/Sqlite3.php +++ b/phpmyfaq/src/phpMyFAQ/Database/Sqlite3.php @@ -1,7 +1,7 @@ [ self::VERSION_MINIMUM_PHP, - 'MySQL v8 / MariaDB v10 / Percona Server v8 / Galera Cluster v4 for MySQL', + 'MySQL v8 / MariaDB v10 / Percona Server v8 / Galera Cluster v4 (ext/mysqli)', ], 'pgsql' => [ self::VERSION_MINIMUM_PHP, @@ -125,6 +125,10 @@ class System self::VERSION_MINIMUM_PHP, 'MS SQL Server 2016 or later', ], + 'pdo_mysql' => [ + self::VERSION_MINIMUM_PHP, + 'MySQL v8 / MariaDB v10 / Percona Server v8 / Galera Cluster v4 (PDO_MYSQL, experimental)', + ], ]; /**