From 4670dfc8497d39b34774e8202a700935bf5ce182 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 15 Jan 2019 10:51:55 -0500 Subject: [PATCH] Handle connection errors --- lib/Db/MySQL/Driver.php | 15 +++++------- lib/Db/MySQL/ExceptionBuilder.php | 31 ++++++++++++++++++++++++ lib/Db/MySQL/PDODriver.php | 14 ++++++++--- lib/Db/PostgreSQL/PDODriver.php | 2 +- locale/en.php | 2 +- tests/cases/Db/MySQL/TestCreation.php | 31 ++++++++++++++++++++++++ tests/cases/Db/MySQLPDO/TestCreation.php | 31 ++++++++++++++++++++++++ 7 files changed, 112 insertions(+), 14 deletions(-) create mode 100644 tests/cases/Db/MySQL/TestCreation.php create mode 100644 tests/cases/Db/MySQLPDO/TestCreation.php diff --git a/lib/Db/MySQL/Driver.php b/lib/Db/MySQL/Driver.php index 8357e489..ea358b17 100644 --- a/lib/Db/MySQL/Driver.php +++ b/lib/Db/MySQL/Driver.php @@ -35,7 +35,7 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { $port = Arsse::$conf->dbMySQLPost ?? 3306; $db = Arsse::$conf->dbMySQLDb ?? "arsse"; // make the connection - $this->makeConnection($user, $pass, $db, $host, $port, $socket); + $this->makeConnection($db, $user, $pass, $host, $port, $socket); // set session variables foreach (static::makeSetupQueries() as $q) { $this->exec($q); @@ -157,15 +157,12 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { } protected function makeConnection(string $db, string $user, string $password, string $host, int $port, string $socket) { - try { - $this->db = new \mysqli($host, $user, $password, $db, $port, $socket); - if ($this->db->connect_errno) { - echo $this->db->connect_errno.": ".$this->db->connect_error; - } - $this->db->set_charset("utf8mb4"); - } catch (\Exception $e) { - throw $e; + $this->db = @new \mysqli($host, $user, $password, $db, $port, $socket); + if ($this->db->connect_errno) { + list($excClass, $excMsg, $excData) = $this->buildConnectionException($this->db->connect_errno, $this->db->connect_error); + throw new $excClass($excMsg, $excData); } + $this->db->set_charset("utf8mb4"); } public function exec(string $query): bool { diff --git a/lib/Db/MySQL/ExceptionBuilder.php b/lib/Db/MySQL/ExceptionBuilder.php index 53cf6713..8f5be9c3 100644 --- a/lib/Db/MySQL/ExceptionBuilder.php +++ b/lib/Db/MySQL/ExceptionBuilder.php @@ -23,4 +23,35 @@ trait ExceptionBuilder { return [Exception::class, 'engineErrorGeneral', $msg]; } } + + public static function buildConnectionException($code, string $msg): array { + switch ($code) { + case 1045: + // @codeCoverageIgnoreStart + case 1043: + case 1044: + case 1046: + case 1049: + case 2001: + case 2002: + case 2003: + case 2004: + case 2005: + case 2007: + case 2009: + case 2010: + case 2011: + case 2012: + case 2015: + case 2016: + case 2017: + case 2018: + case 2026: + case 2028: + // @codeCoverageIgnoreEnd + return [Exception::class, 'connectionFailure', ['engine' => "MySQL", 'message' => $msg]]; + default: + return [Exception::class, 'engineErrorGeneral', $msg]; // @codeCoverageIgnore + } + } } diff --git a/lib/Db/MySQL/PDODriver.php b/lib/Db/MySQL/PDODriver.php index 4c47a297..6d378a8b 100644 --- a/lib/Db/MySQL/PDODriver.php +++ b/lib/Db/MySQL/PDODriver.php @@ -28,9 +28,17 @@ class PDODriver extends Driver { "socket=$socket", "port=$port", ]); - $this->db = new \PDO($dsn, $user, $password, [ - \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, - ]); + try { + $this->db = new \PDO($dsn, $user, $password, [ + \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, + ]); + } catch (\PDOException $e) { + $msg = $e->getMessage(); + $code = (int) substr($msg, 17, 4); + $msg = substr($msg, 23); + list($excClass, $excMsg, $excData) = $this->buildConnectionException($code, $msg); + throw new $excClass($excMsg, $excData); + } } public function __destruct() { diff --git a/lib/Db/PostgreSQL/PDODriver.php b/lib/Db/PostgreSQL/PDODriver.php index c4eebd56..ca3f33eb 100644 --- a/lib/Db/PostgreSQL/PDODriver.php +++ b/lib/Db/PostgreSQL/PDODriver.php @@ -31,7 +31,7 @@ class PDODriver extends Driver { if ($e->getCode() == 7) { switch (substr($e->getMessage(), 9, 5)) { case "08006": - throw new Exception("connectionFailure", ["PostgreSQL", substr($e->getMessage(), 28)]); + throw new Exception("connectionFailure", ['engine' => "PostgreSQL", 'message' => substr($e->getMessage(), 28)]); default: throw $e; // @codeCoverageIgnore } diff --git a/locale/en.php b/locale/en.php index fb312cb8..f00c29b5 100644 --- a/locale/en.php +++ b/locale/en.php @@ -124,7 +124,7 @@ return [ 'Exception.JKingWeb/Arsse/Db/Exception.fileUnusable' => 'Insufficient permissions to open database file "{0}" for reading or writing', 'Exception.JKingWeb/Arsse/Db/Exception.fileUncreatable' => 'Insufficient permissions to create new database file "{0}"', 'Exception.JKingWeb/Arsse/Db/Exception.fileCorrupt' => 'Database file "{0}" is corrupt or not a valid database', - 'Exception.JKingWeb/Arsse/Db/Exception.connectionFailure' => 'Could not connect to {0} database: {1}', + 'Exception.JKingWeb/Arsse/Db/Exception.connectionFailure' => 'Could not connect to {engine} database: {message}', 'Exception.JKingWeb/Arsse/Db/Exception.paramTypeInvalid' => 'Prepared statement parameter type "{0}" is invalid', 'Exception.JKingWeb/Arsse/Db/Exception.paramTypeUnknown' => 'Prepared statement parameter type "{0}" is valid, but not implemented', 'Exception.JKingWeb/Arsse/Db/Exception.paramTypeMissing' => 'Prepared statement parameter type for parameter #{0} was not specified', diff --git a/tests/cases/Db/MySQL/TestCreation.php b/tests/cases/Db/MySQL/TestCreation.php new file mode 100644 index 00000000..e0280577 --- /dev/null +++ b/tests/cases/Db/MySQL/TestCreation.php @@ -0,0 +1,31 @@ + + * @covers \JKingWeb\Arsse\Db\MySQL\ExceptionBuilder */ +class TestCreation extends \JKingWeb\Arsse\Test\AbstractTest { + public function setUp() { + if (!Driver::requirementsMet()) { + $this->markTestSkipped("MySQL extension not loaded"); + } + } + + public function testFailToConnect() { + // for the sake of simplicity we don't distinguish between failure modes, but the MySQL-supplied error messages do + self::setConf([ + 'dbMySQLPass' => (string) rand(), + ]); + $this->assertException("connectionFailure", "Db"); + new Driver; + } +} diff --git a/tests/cases/Db/MySQLPDO/TestCreation.php b/tests/cases/Db/MySQLPDO/TestCreation.php new file mode 100644 index 00000000..2b96b3db --- /dev/null +++ b/tests/cases/Db/MySQLPDO/TestCreation.php @@ -0,0 +1,31 @@ + + * @covers \JKingWeb\Arsse\Db\MySQL\ExceptionBuilder */ +class TestCreation extends \JKingWeb\Arsse\Test\AbstractTest { + public function setUp() { + if (!Driver::requirementsMet()) { + $this->markTestSkipped("PDO-MySQL extension not loaded"); + } + } + + public function testFailToConnect() { + // for the sake of simplicity we don't distinguish between failure modes, but the MySQL-supplied error messages do + self::setConf([ + 'dbMySQLPass' => (string) rand(), + ]); + $this->assertException("connectionFailure", "Db"); + new Driver; + } +}