%PDF- %PDF-
Direktori : /www/varak.net/dmarc.varak.net/classes/Database/Mariadb/ |
Current File : //www/varak.net/dmarc.varak.net/classes/Database/Mariadb/Connector.php |
<?php /** * dmarc-srg - A php parser, viewer and summary report generator for incoming DMARC reports. * Copyright (C) 2022-2025 Aleksey Andreev (liuch) * * Available at: * https://github.com/liuch/dmarc-srg * * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation, either version 3 of the License. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see <http://www.gnu.org/licenses/>. * * ========================= * * 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\ErrorCodes; 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; protected $ansiMode = true; /** * Returns an instance of PDO class * * @return \PDO */ public function dbh(): object { $this->ensureConnection(); return $this->dbh; } /** * 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 { $this->setANSIMode(false); $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 = []; $system_exs = false; 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'; } if ($tname === 'system') { $system_exs = true; } } 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'; } else { if ($exist_cnt == 0) { $res['error_code'] = -1; $res['message'] = 'The database schema is not initiated'; } else { $res['error_code'] = ErrorCodes::INCORRECT_TABLE_SET; $res['message'] = 'Incomplete set of the tables'; } } if ($system_exs) { try { $this->setANSIMode(true); $res['version'] = $this->getMapper('setting')->value('version', 0); $this->setANSIMode(false); } catch (DatabaseNotFoundException $e) { } } } 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)); } finally { $this->setANSIMode(true); } return $res; } /** * Initiates 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 string $version The current version of the database schema * * @return void */ public function initDb(string $version): void { $this->ensureConnection(); try { $this->setANSIMode(false); $st = $this->dbh->query($this->sqlShowTablesQuery()); try { if ($st->fetch()) { if (empty($this->tablePrefix())) { throw new SoftException('The database is not empty', ErrorCodes::DB_NOT_EMPTY); } else { throw new SoftException( 'Database tables already exist with the given prefix', ErrorCodes::DB_NOT_EMPTY ); } } 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`, user_id, value) VALUES ("version", 0, ?)' ); $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); } finally { $this->setANSIMode(true); } } /** * 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 { $this->setANSIMode(false); $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); } finally { $this->setANSIMode(true); } } /** * Enables or disables ANSI query mode for the current datatabase connection * * @param bool $on True turns ANSI mode on, False turns it off * * @return void */ public function setANSIMode(bool $on): void { if ($on !== $this->ansiMode) { $this->ansiMode = $on; if ($this->dbh) { if ($on) { $this->dbh->query('SET SESSION sql_mode=\'ANSI\''); } else { $this->dbh->query('SET SESSION sql_mode=@prev_sql_mode'); } } } } /** * 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 @prev_sql_mode=@@sql_mode'); if ($this->ansiMode) { $this->dbh->query('SET SESSION sql_mode=\'ANSI\''); } $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' => 'user_id', 'definition' => 'int(10) unsigned NOT NULL DEFAULT 0' ], [ 'name' => 'value', 'definition' => 'varchar(255) DEFAULT NULL' ] ], 'additional' => 'PRIMARY KEY (user_id, `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' ], 'users' => [ 'columns' => [ [ 'name' => 'id', 'definition' => 'int(10) unsigned NOT NULL AUTO_INCREMENT' ], [ 'name' => 'name', 'definition' => 'varchar(32) NOT NULL' ], [ 'name' => 'level', 'definition' => 'smallint unsigned NOT NULL' ], [ 'name' => 'enabled', 'definition' => 'boolean NOT NULL' ], [ 'name' => 'password', 'definition' => 'varchar(255) NULL' ], [ 'name' => 'email', 'definition' => 'varchar(64) NULL' ], [ 'name' => 'key', 'definition' => 'varchar(64) NULL' ], [ 'name' => 'session', 'definition' => 'int(10) unsigned NOT NULL' ], [ 'name' => 'created_time', 'definition' => 'datetime NOT NULL' ], [ 'name' => 'updated_time', 'definition' => 'datetime NOT NULL' ] ], 'additional' => 'PRIMARY KEY (id), UNIQUE KEY name (name)', 'table_options' => 'ENGINE=InnoDB DEFAULT CHARSET=utf8' ], 'userdomains' => [ 'columns' => [ [ 'name' => 'domain_id', 'definition' => 'int(10) unsigned NOT NULL' ], [ 'name' => 'user_id', 'definition' => 'int(10) unsigned NOT NULL' ] ], 'additional' => 'PRIMARY KEY (domain_id, user_id)', '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 org_time_id_u (domain_id, begin_time, org, 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' => 'user_id', 'definition' => 'int(10) unsigned NOT NULL DEFAULT 0' ], [ '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), KEY user_id (user_id, event_time)', 'table_options' => 'ENGINE=InnoDB DEFAULT CHARSET=utf8' ] ]; }