. * * ========================= * * This file contains the DatabaseConnector class * * @category API * @package DmarcSrg * @author Aleksey Andreev (liuch) * @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3 */ namespace Liuch\DmarcSrg\Database\Mariadb; use Liuch\DmarcSrg\ErrorHandler; use Liuch\DmarcSrg\Database\DatabaseConnector; use Liuch\DmarcSrg\Exception\SoftException; use Liuch\DmarcSrg\Exception\RuntimeException; use Liuch\DmarcSrg\Exception\DatabaseFatalException; use Liuch\DmarcSrg\Exception\DatabaseExceptionFactory; use Liuch\DmarcSrg\Exception\DatabaseNotFoundException; class Connector extends DatabaseConnector { protected $dbh = null; /** * Returns an instance of PDO class * * @return \PDO */ public function dbh(): object { $this->ensureConnection(); return $this->dbh; } /** * Returns the name of the database * * @return string */ public function dbName(): string { return $this->name; } /** * Returns information about the database as an array. * * @return array May contain the following fields: * `tables` - an array of tables with their properties; * `correct` - true if the database is correct; * `version` - the current version of the database structure; * `message` - a state message; * `error_code` - an error code; */ public function state(): array { $this->ensureConnection(); $res = []; $p_len = strlen($this->prefix); if ($p_len > 0) { $like_str = ' WHERE NAME LIKE "' . str_replace('_', '\\_', $this->prefix) . '%"'; } else { $like_str = ''; } try { $tables = []; $st = $this->dbh->query( 'SHOW TABLE STATUS FROM `' . str_replace('`', '', $this->name) . '`' . $like_str ); while ($row = $st->fetch(\PDO::FETCH_ASSOC)) { $tname = $row['Name']; $rcnt = $this->dbh->query('SELECT COUNT(*) FROM `' . $tname . '`')->fetch(\PDO::FETCH_NUM)[0]; $tables[substr($tname, $p_len)] = [ 'engine' => $row['Engine'], 'rows' => intval($rcnt), 'data_length' => intval($row['Data_length']), 'index_length' => intval($row['Index_length']), 'create_time' => $row['Create_time'], 'update_time' => $row['Update_time'] ]; } foreach (array_keys(self::$schema) as $table) { if (!isset($tables[$table])) { $tables[$table] = false; } } $exist_cnt = 0; $absent_cnt = 0; $tables_res = []; foreach ($tables as $tname => $tval) { $t = null; if ($tval) { $t = $tval; $t['exists'] = true; if (isset(self::$schema[$tname])) { ++$exist_cnt; $t['message'] = 'Ok'; } else { $t['message'] = 'Unknown table'; } } else { ++$absent_cnt; $t = [ 'error_code' => 1, 'message' => 'Not exist' ]; } $t['name'] = $tname; $tables_res[] = $t; } $res['tables'] = $tables_res; if ($absent_cnt === 0) { $res['correct'] = true; $res['message'] = 'Ok'; try { $res['version'] = $this->getMapper('setting')->value('version'); } catch (DatabaseNotFoundException $e) { } } else { $res['error_code'] = -1; if ($exist_cnt == 0) { $res['message'] = 'The database schema is not initiated'; } else { $res['message'] = 'Incomplete set of the tables'; } } } catch (\PDOException $e) { $res = array_replace($res, ErrorHandler::exceptionResult( new DatabaseFatalException('Failed to get the database information', -1, $e) )); } catch (RuntimeException $e) { $res = array_replace($res, ErrorHandler::exceptionResult($e)); } return $res; } /** * Inites the database. * * This method creates needed tables and indexes in the database. * The method will fail if the database already have tables with the table prefix. * * @param $version The current version of the database schema * * @return void */ public function initDb(string $version): void { $this->ensureConnection(); try { $st = $this->dbh->query($this->sqlShowTablesQuery()); try { if ($st->fetch()) { if (empty($this->tablePrefix())) { throw new SoftException('The database is not empty', -4); } else { throw new SoftException('Database tables already exist with the given prefix', -4); } } foreach (self::$schema as $t_name => &$t_schema) { $this->createDbTable($this->tablePrefix($t_name), $t_schema); } unset($t_schema); } finally { $st->closeCursor(); } $st = $this->dbh->prepare( 'INSERT INTO `' . $this->tablePrefix('system') . '` (`key`, `value`) VALUES ("version", ?)' ); $st->bindValue(1, $version, \PDO::PARAM_STR); $st->execute(); $st->closeCursor(); } catch (\PDOException $e) { throw new DatabaseFatalException('Failed to create required tables in the database', -1, $e); } } /** * Cleans up the database * * Drops tables with the table prefix in the database or all tables in the database * if no table prefix is set. * * @return void */ public function cleanDb(): void { $this->ensureConnection(); try { $db = $this->dbh; $db->query('SET foreign_key_checks = 0'); $st = $db->query($this->sqlShowTablesQuery()); while ($table = $st->fetchColumn(0)) { $db->query('DROP TABLE `' . $table . '`'); } $st->closeCursor(); $db->query('SET foreign_key_checks = 1'); } catch (\PDOException $e) { throw new DatabaseFatalException('Failed to drop the database tables', -1, $e); } } /** * Sets the database connection if it hasn't connected yet. * * @return void */ private function ensureConnection(): void { if (!$this->dbh) { try { $this->dbh = new \PDO( "mysql:host={$this->host};dbname={$this->name};charset=utf8", $this->user, $this->password, [ \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION ] ); $this->dbh->query('SET time_zone = "+00:00"'); } catch (\PDOException $e) { throw DatabaseExceptionFactory::fromException($e); } } } /** * Return SHOW TABLES SQL query string for tables with the table prefix * * @return string */ private function sqlShowTablesQuery(): string { $res = 'SHOW TABLES'; $prefix = $this->tablePrefix(); if (strlen($prefix) > 0) { $res .= ' WHERE `tables_in_' . str_replace('`', '', $this->name) . '` LIKE "' . str_replace('_', '\\_', $prefix) . '%"'; } return $res; } /** * Creates a table in the database. * * @param string $name Table name * @param array $definitions Table structure * * @return void */ private function createDbTable(string $name, array $definitions): void { $query = 'CREATE TABLE `' . $name . '` ('; $col_num = 0; foreach ($definitions['columns'] as $column) { if ($col_num > 0) { $query .= ', '; } $query .= '`' . $column['name'] . '` ' . $column['definition']; $col_num += 1; } $query .= ', ' . $definitions['additional'] . ') ' . $definitions['table_options']; $this->dbh->query($query); } private static $schema = [ 'system' => [ 'columns' => [ [ 'name' => 'key', 'definition' => 'varchar(64) NOT NULL' ], [ 'name' => 'value', 'definition' => 'varchar(255) DEFAULT NULL' ] ], 'additional' => 'PRIMARY KEY (`key`)', 'table_options' => 'ENGINE=InnoDB DEFAULT CHARSET=utf8' ], 'domains' => [ 'columns' => [ [ 'name' => 'id', 'definition' => 'int(10) unsigned NOT NULL AUTO_INCREMENT' ], [ 'name' => 'fqdn', 'definition' => 'varchar(255) NOT NULL' ], [ 'name' => 'active', 'definition' => 'boolean NOT NULL' ], [ 'name' => 'description', 'definition' => 'TEXT NULL' ], [ 'name' => 'created_time', 'definition' => 'datetime NOT NULL' ], [ 'name' => 'updated_time', 'definition' => 'datetime NOT NULL' ] ], 'additional' => 'PRIMARY KEY (`id`), UNIQUE KEY `fqdn` (`fqdn`)', 'table_options' => 'ENGINE=InnoDB DEFAULT CHARSET=utf8' ], 'reports' => [ 'columns' => [ [ 'name' => 'id', 'definition' => 'int(10) unsigned NOT NULL AUTO_INCREMENT' ], [ 'name' => 'domain_id', 'definition' => 'int(10) NOT NULL' ], [ 'name' => 'begin_time', 'definition' => 'datetime NOT NULL' ], [ 'name' => 'end_time', 'definition' => 'datetime NOT NULL' ], [ 'name' => 'loaded_time', 'definition' => 'datetime NOT NULL' ], [ 'name' => 'org', 'definition' => 'varchar(255) NOT NULL' ], [ 'name' => 'external_id', 'definition' => 'varchar(255) NOT NULL' ], [ 'name' => 'email', 'definition' => 'varchar(255) NOT NULL' ], [ 'name' => 'extra_contact_info', 'definition' => 'varchar(255) NULL' ], [ 'name' => 'error_string', 'definition' => 'text NULL' ], [ 'name' => 'policy_adkim', 'definition' => 'varchar(20) NULL' ], [ 'name' => 'policy_aspf', 'definition' => 'varchar(20) NULL' ], [ 'name' => 'policy_p', 'definition' => 'varchar(20) NULL' ], [ 'name' => 'policy_sp', 'definition' => 'varchar(20) NULL' ], [ 'name' => 'policy_np', 'definition' => 'varchar(20) NULL' ], [ 'name' => 'policy_pct', 'definition' => 'varchar(20) NULL' ], [ 'name' => 'policy_fo', 'definition' => 'varchar(20) NULL' ], [ 'name' => 'seen', 'definition' => 'boolean NOT NULL' ] ], 'additional' => 'PRIMARY KEY (`id`),' . ' UNIQUE KEY `external_id` (`domain_id`, `external_id`),' . ' KEY (`begin_time`), KEY (`end_time`),' . ' KEY `org` (`org`, `begin_time`)', 'table_options' => 'ENGINE=InnoDB DEFAULT CHARSET=utf8' ], 'rptrecords' => [ 'columns' => [ [ 'name' => 'id', 'definition' => 'int(10) unsigned NOT NULL AUTO_INCREMENT' ], [ 'name' => 'report_id', 'definition' => 'int(10) unsigned NOT NULL' ], [ 'name' => 'ip', 'definition' => 'varbinary(16) NOT NULL' ], [ 'name' => 'rcount', 'definition' => 'int(10) unsigned NOT NULL' ], [ 'name' => 'disposition', 'definition' => 'tinyint unsigned NOT NULL' ], [ 'name' => 'reason', 'definition' => 'text NULL' ], [ 'name' => 'dkim_auth', 'definition' => 'text NULL' ], [ 'name' => 'spf_auth', 'definition' => 'text NULL' ], [ 'name' => 'dkim_align', 'definition' => 'tinyint unsigned NOT NULL' ], [ 'name' => 'spf_align', 'definition' => 'tinyint unsigned NOT NULL' ], [ 'name' => 'envelope_to', 'definition' => 'varchar(255) NULL' ], [ 'name' => 'envelope_from', 'definition' => 'varchar(255) NULL' ], [ 'name' => 'header_from', 'definition' => 'varchar(255) NULL' ] ], 'additional' => 'PRIMARY KEY (`id`), KEY (`report_id`), KEY (`ip`)', 'table_options' => 'ENGINE=InnoDB DEFAULT CHARSET=utf8' ], 'reportlog' => [ 'columns' => [ [ 'name' => 'id', 'definition' => 'int(10) unsigned NOT NULL AUTO_INCREMENT' ], [ 'name' => 'domain', 'definition' => 'varchar(255) NULL' ], [ 'name' => 'external_id', 'definition' => 'varchar(255) NULL' ], [ 'name' => 'event_time', 'definition' => 'datetime NOT NULL' ], [ 'name' => 'filename', 'definition' => 'varchar(255) NULL' ], [ 'name' => 'source', 'definition' => 'tinyint unsigned NOT NULL' ], [ 'name' => 'success', 'definition' => 'boolean NOT NULL' ], [ 'name' => 'message', 'definition' => 'text NULL' ] ], 'additional' => 'PRIMARY KEY (`id`), KEY(`event_time`)', 'table_options' => 'ENGINE=InnoDB DEFAULT CHARSET=utf8' ] ]; }