Add in software files and templates

This commit is contained in:
2023-06-21 14:19:40 +01:00
parent f42fdb947c
commit 5228fc5e9f
143 changed files with 23175 additions and 2 deletions

View File

@@ -0,0 +1,543 @@
<?php
/**
* dmarc-srg - A php parser, viewer and summary report generator for incoming DMARC reports.
* Copyright (C) 2020 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/>.
*/
namespace Liuch\DmarcSrg\Database;
use Liuch\DmarcSrg\Settings\SettingString;
class Database
{
public const REQUIRED_VERSION = '2.0';
private $conn;
private static $instance = null;
private function __construct()
{
$this->conn = null;
$this->establishConnection();
}
public static function connection()
{
if (!self::$instance) {
self::$instance = new self();
}
return self::$instance->conn;
}
public static function type()
{
global $database;
return $database['type'];
}
public static function name()
{
global $database;
return $database['name'];
}
public static function location()
{
global $database;
return $database['host'];
}
/**
* Returns the prefix for tables of the database
*
* @param string $postfix String to be concatenated with the prefix.
* Usually, this is a table name.
*
* @return string
*/
public static function tablePrefix(string $postfix = ''): string
{
global $database;
return ($database['table_prefix'] ?? '') . $postfix;
}
/**
* Returns information about the database as an array.
*
* @return array May contain the following fields:
* `tables` - an array of tables with their properties;
* `needs_upgrade` - true if the database needs upgrading;
* `correct` - true if the database is correct;
* `type` - the database type;
* `name` - the database name;
* `location` - the database location;
* `version` - the current version of the database structure;
* `message` - a state message;
* `error_code` - an error code;
*/
public static function state(): array
{
$res = [];
try {
$prefix = self::tablePrefix();
$p_len = strlen($prefix);
if ($p_len > 0) {
$like_str = ' WHERE NAME LIKE "' . str_replace('_', '\\_', $prefix) . '%"';
} else {
$like_str = '';
}
$db = self::connection();
$tables = [];
$st = $db->query(
'SHOW TABLE STATUS FROM `' . str_replace('`', '', self::name()) . '`' . $like_str
);
while ($row = $st->fetch(\PDO::FETCH_ASSOC)) {
$tnm = $row['Name'];
$st2 = $db->query('SELECT COUNT(*) FROM `' . $tnm . '`');
$rows = $st2->fetch(\PDO::FETCH_NUM)[0];
$tables[substr($tnm, $p_len)] = [
'engine' => $row['Engine'],
'rows' => intval($rows),
'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_sys = 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 += 1;
$t['message'] = 'Ok';
if (!$exist_sys && $tname === 'system') {
$exist_sys = true;
}
} else {
$t['message'] = 'Unknown table';
}
} else {
$absent_cnt += 1;
$t = [
'error_code' => 1,
'message' => 'Not exist'
];
}
$t['name'] = $tname;
$tables_res[] = $t;
}
$res['tables'] = $tables_res;
$ver = $exist_sys ? (new SettingString('version'))->value() : null;
if ($exist_sys && $ver !== self::REQUIRED_VERSION) {
self::setDbMessage('The database structure needs upgrading', 0, $res);
$res['needs_upgrade'] = true;
} elseif ($absent_cnt == 0) {
$res['correct'] = true;
self::setDbMessage('Ok', 0, $res);
} else {
if ($exist_cnt == 0) {
self::setDbMessage('The database schema is not initiated', -1, $res);
} else {
self::setDbMessage('Incomplete set of the tables', -1, $res);
}
}
if ($ver) {
$res['version'] = $ver;
}
} catch (\Exception $e) {
$res['error_code'] = $e->getCode();
$res['message'] = $e->getMessage();
}
$res['type'] = self::type();
$res['name'] = self::name();
$res['location'] = self::location();
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.
*
* @return array Result array with `error_code` and `message` fields.
*/
public static function initDb(): array
{
try {
$db = self::connection();
$st = $db->query(self::sqlShowTablesQuery());
try {
if ($st->fetch()) {
if (empty(self::tablePrefix())) {
throw new \Exception('The database is not empty', -4);
} else {
throw new \Exception('Database tables already exist with the given prefix', -4);
}
}
foreach (array_keys(self::$schema) as $table) {
self::createDbTable(self::tablePrefix($table), self::$schema[$table]);
}
} finally {
$st->closeCursor();
}
$st = $db->prepare(
'INSERT INTO `' . self::tablePrefix('system') . '` (`key`, `value`) VALUES ("version", ?)'
);
$st->bindValue(1, self::REQUIRED_VERSION, \PDO::PARAM_STR);
$st->execute();
$st->closeCursor();
} catch (\Exception $e) {
return [
'error_code' => $e->getCode(),
'message' => $e->getMessage()
];
}
return [ 'message' => 'The database has been initiated' ];
}
/**
* 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 array Result array with `error_code` and `message` fields.
*/
public static function dropTables(): array
{
try {
$db = self::connection();
$db->query('SET foreign_key_checks = 0');
$st = $db->query(self::sqlShowTablesQuery());
while ($table = $st->fetchColumn(0)) {
$db->query('DROP TABLE `' . $table . '`');
}
$st->closeCursor();
$db->query('SET foreign_key_checks = 1');
} catch (\PDOException $e) {
return [
'error_code' => $e->getCode(),
'message' => $e->getMessage()
];
}
return [ 'message' => 'Database tables have been dropped' ];
}
private function establishConnection()
{
global $database;
try {
$dsn = "{$database['type']}:host={$database['host']};dbname={$database['name']};charset=utf8";
$this->conn = new \PDO(
$dsn,
$database['user'],
$database['password'],
[ \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION ]
);
$this->conn->query('SET time_zone = "+00:00"');
} catch (\Exception $e) {
throw new \Exception($e->getMessage(), -1);
}
}
/**
* Return SHOW TABLES SQL query string for tables with the table prefix
*
* @return string
*/
private static function sqlShowTablesQuery(): string
{
$res = 'SHOW TABLES';
$prefix = self::tablePrefix();
if (strlen($prefix) > 0) {
$res .= ' WHERE `tables_in_' . str_replace('`', '', self::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 static 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'];
self::connection()->query($query);
}
/**
* Sets the database message and error code for the state array
*
* @param string $message Message string
* @param int $err_code Error code
* @param array $state Database state array
*
* @return void
*/
private static function setDbMessage(string $message, int $err_code, array &$state): void
{
$state['message'] = $message;
if ($err_code !== 0) {
$state['error_code'] = $err_code;
}
}
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_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'
]
];
}

View File

@@ -0,0 +1,131 @@
<?php
/**
* dmarc-srg - A php parser, viewer and summary report generator for incoming DMARC reports.
* Copyright (C) 2022 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 abstract 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;
use Liuch\DmarcSrg\Exception\LogicException;
abstract class DatabaseConnector
{
protected static $names = [
'domain' => 'DomainMapper',
'report' => 'ReportMapper',
'report-log' => 'ReportLogMapper',
'setting' => 'SettingMapper',
'statistics' => 'StatisticsMapper',
'upgrader' => 'UpgraderMapper'
];
protected $host = null;
protected $name = null;
protected $user = null;
protected $password = null;
protected $prefix = '';
protected $mappers = [];
/**
* The constructor
*
* @param array $conf Configuration data from the conf.php file
*/
public function __construct(array $conf)
{
$this->host = $conf['host'] ?? '';
$this->name = $conf['name'] ?? '';
$this->user = $conf['user'] ?? '';
$this->password = $conf['password'] ?? '';
$this->prefix = $conf['table_prefix'] ?? '';
}
/**
* Returns an instance of PDO class
*
* @return PDO
*/
abstract public function dbh(): object;
/**
* Returns the database state as an array
*
* @return array
*/
abstract public function state(): array;
/**
* Returns a data mapper by its name.
*
* @param string $name Mapper name
*
* @return object
*/
public function getMapper(string $name): object
{
if (isset($this->mappers[$name])) {
return $this->mappers[$name];
}
if (!isset(self::$names[$name])) {
throw new LogicException('Unknown mapper name: ' . $name);
}
$mapper_name = (new \ReflectionClass($this))->getNamespaceName() . '\\' . self::$names[$name];
$mapper = new $mapper_name($this);
$this->mappers[$name] = $mapper;
return $mapper;
}
/**
* Inites the database.
*
* @return void
*/
abstract public function initDb(string $version): void;
/**
* Cleans up the database
*
* @return void
*/
abstract public function cleanDb(): void;
/**
* Returns the prefix for tables of the database
*
* @param string $postfix String to be concatenated with the prefix.
* Usually, this is a table name.
*
* @return string
*/
public function tablePrefix(string $postfix = ''): string
{
return $this->prefix . $postfix;
}
}

View File

@@ -0,0 +1,184 @@
<?php
/**
* dmarc-srg - A php parser, viewer and summary report generator for incoming DMARC reports.
* Copyright (C) 2022 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 DatabaseController 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;
use Liuch\DmarcSrg\Exception\RuntimeException;
use Liuch\DmarcSrg\Exception\DatabaseFatalException;
/**
* Proxy class for accessing a database of the selected type
*/
class DatabaseController
{
public const REQUIRED_VERSION = '3.0';
private $conf_data = null;
private $connector = null;
/**
* The constructor
*
* @param Core $core Instace of the Core class
* @param class $connector The connector class of the current database
*/
public function __construct($core, $connector = null)
{
$this->conf_data = $core->config('database');
$this->connector = $connector;
}
/**
* Returns the database type
*
* @return string
*/
public function type(): string
{
return $this->conf_data['type'] ?? '';
}
/**
* Returns the database name
*
* @return string
*/
public function name(): string
{
return $this->conf_data['name'] ?? '';
}
/**
* Returns the database host
*
* @return string
*/
public function location(): string
{
return $this->conf_data['host'] ?? '';
}
/**
* Returns information about the database as an array.
*
* @return array May contain the following fields:
* `tables` - an array of tables with their properties;
* `needs_upgrade` - true if the database needs upgrading;
* `correct` - true if the database is correct;
* `type` - the database type;
* `name` - the database name;
* `location` - the database location;
* `version` - the current version of the database structure;
* `message` - a state message;
* `error_code` - an error code;
*/
public function state(): array
{
$this->ensureConnector();
$res = $this->connector->state();
$res['type'] = $this->type();
$res['name'] = $this->name();
$res['location'] = $this->location();
if (($res['correct'] ?? false) && ($res['version'] ?? 'null') !== self::REQUIRED_VERSION) {
$res['correct'] = false;
$res['message'] = 'The database structure needs upgrading';
$res['needs_upgrade'] = true;
}
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.
*
* @return array Result array with `error_code` and `message` fields.
*/
public function initDb(): array
{
$this->ensureConnector();
$this->connector->initDb(self::REQUIRED_VERSION);
return [ 'message' => 'The database has been initiated' ];
}
/**
* 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 array Result array with `error_code` and `message` fields.
*/
public function cleanDb(): array
{
$this->ensureConnector();
$this->connector->cleanDb();
return [ 'message' => 'The database tables have been dropped' ];
}
/**
* Returns a data mapper by its name from the current database connector
*
* @param string $name Mapper name
*
* @return object
*/
public function getMapper(string $name): object
{
$this->ensureConnector();
return $this->connector->getMapper($name);
}
/**
* Finds the connector of the specified database type and initializes it
* if it hasn't already been initialized
*
* @return void
*/
private function ensureConnector(): void
{
if (!$this->connector) {
switch ($this->conf_data['type']) {
case 'mysql':
case 'mariadb':
$type = 'mariadb';
break;
default:
throw new RuntimeException('Unknown database type: ' . $this->conf_data['type']);
$type = null;
break;
}
$c_name = __NAMESPACE__ . '\\' . \ucfirst($type) . '\\Connector';
$this->connector = new $c_name($this->conf_data);
}
}
}

View File

@@ -0,0 +1,121 @@
<?php
/**
* dmarc-srg - A php parser, viewer and summary report generator for incoming DMARC reports.
* Copyright (C) 2020 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/>.
*/
namespace Liuch\DmarcSrg\Database;
use PDO;
use Exception;
use Liuch\DmarcSrg\Settings\SettingString;
class DatabaseUpgrader
{
public static function go()
{
$ver = (new SettingString('version'))->value();
if ($ver == '') {
$ver = 'null';
}
while ($ver !== Database::REQUIRED_VERSION) {
if (!isset(self::$upways['ver_' . $ver])) {
throw new Exception('Upgrading failed: There is no way to upgrade from ' . $ver . ' to ' . Database::REQUIRED_VERSION, -1);
}
$um = self::$upways['ver_' . $ver];
$ver = self::$um();
}
}
private static $upways = [
'ver_null' => 'upNull',
'ver_0.1' => 'up01',
'ver_1.0' => 'up10'
];
private static function upNull()
{
$db = Database::connection();
$db->beginTransaction();
try {
$db->query(
'INSERT INTO `' . Database::tablePrefix('system') . '` (`key`, `value`) VALUES ("version", "0.1")'
);
$db->commit();
} catch (Exception $e) {
$db->rollBack();
throw $e;
}
return '0.1';
}
private static function up01()
{
$db = Database::connection();
$db->beginTransaction();
try {
$dom_tn = Database::tablePrefix('domains');
if (!self::columnExists($db, $dom_tn, 'active')) {
$db->query('ALTER TABLE `' . $dom_tn . '` ADD COLUMN `active` boolean NOT NULL AFTER `fqdn`');
}
if (!self::columnExists($db, $dom_tn, 'created_time')) {
$db->query('ALTER TABLE `' . $dom_tn . '` ADD COLUMN `created_time` datetime NOT NULL');
}
if (!self::columnExists($db, $dom_tn, 'updated_time')) {
$db->query('ALTER TABLE `' . $dom_tn . '` ADD COLUMN `updated_time` datetime NOT NULL');
}
$db->query('UPDATE `' . $dom_tn . '` SET `active` = TRUE, `created_time` = NOW(), `updated_time` = NOW()');
$db->query('UPDATE `' . Database::tablePrefix('system') . '` SET `value` = "1.0" WHERE `key` = "version"');
$db->commit();
} catch (Exception $e) {
$db->rollBack();
throw $e;
}
return '1.0';
}
private static function up10()
{
$db = Database::connection();
$db->beginTransaction();
try {
$sys_tn = Database::tablePrefix('system');
$db->query('ALTER TABLE `' . $sys_tn . '` MODIFY COLUMN `key` varchar(64) NOT NULL');
$db->query('UPDATE `' . $sys_tn . '` SET `value` = "2.0" WHERE `key` = "version"');
$db->commit();
} catch (Exception $d) {
$db->rollBack();
throw $e;
}
return '2.0';
}
private static function columnExists($db, $table, $column)
{
$st = $db->prepare('SELECT NULL FROM `INFORMATION_SCHEMA`.`COLUMNS` WHERE `table_schema` = ? AND `table_name` = ? AND `column_name` = ?');
$st->bindValue(1, Database::name(), PDO::PARAM_STR);
$st->bindValue(2, $table, PDO::PARAM_STR);
$st->bindValue(3, $column, PDO::PARAM_STR);
$st->execute();
$res = $st->fetch(PDO::FETCH_NUM);
$st->closeCursor();
return $res ? true : false;
}
}

View File

@@ -0,0 +1,96 @@
<?php
/**
* dmarc-srg - A php parser, viewer and summary report generator for incoming DMARC reports.
* Copyright (C) 2022 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 DomainMapperInterface
*
* @category API
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg\Database;
interface DomainMapperInterface
{
/**
* Return true if the domain exists or false otherwise.
*
* @param array $data Array with domain data to search
*
* @return bool
*/
public function exists(array &$data): bool;
/**
* Fetch the domain data from the database by its id or name
*
* @param array $data Domain data to update
*
* @return void
*/
public function fetch(array &$data): void;
/**
* Saves domain data to the database (updates or inserts an record)
*
* @param array $data Domain data
*
* @return void
*/
public function save(array &$data): void;
/**
* Deletes the domain from the database
*
* Deletes the domain if there are no reports for this domain in the database.
*
* @param array $data Domain data
*
* @return void
*/
public function delete(array &$data): void;
/**
* Returns a list of domains data from the database
*
* @return array
*/
public function list(): array;
/**
* Returns an ordered array with domain names from the database
*
* @return array
*/
public function names(): array;
/**
* Returns the total number of domains in the database
*
* @param int $max The maximum number of records to count. 0 means no limitation.
*
* @return int The total number of domains
*/
public function count(int $max = 0): int;
}

View File

@@ -0,0 +1,511 @@
<?php
/**
* dmarc-srg - A php parser, viewer and summary report generator for incoming DMARC reports.
* Copyright (C) 2022 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\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'
]
];
}

View File

@@ -0,0 +1,332 @@
<?php
/**
* dmarc-srg - A php parser, viewer and summary report generator for incoming DMARC reports.
* Copyright (C) 2022 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 DomainMapper 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\DateTime;
use Liuch\DmarcSrg\Database\DomainMapperInterface;
use Liuch\DmarcSrg\Exception\SoftException;
use Liuch\DmarcSrg\Exception\DatabaseFatalException;
use Liuch\DmarcSrg\Exception\DatabaseNotFoundException;
/**
* DomainMapper class implementation for MariaDB
*/
class DomainMapper implements DomainMapperInterface
{
private $connector = null;
/**
* The constructor
*
* @param Connector $connector DatabaseConnector
*/
public function __construct(object $connector)
{
$this->connector = $connector;
}
/**
* Return true if the domain exists or false otherwise.
*
* @param array $data Array with domain data to search
*
* @return bool
*/
public function exists(array &$data): bool
{
try {
$st = $this->connector->dbh()->prepare(
'SELECT `id` FROM `' . $this->connector->tablePrefix('domains') .
'` WHERE ' . $this->sqlCondition($data)
);
$this->sqlBindValue($st, 1, $data);
$st->execute();
$res = $st->fetch(\PDO::FETCH_NUM);
$st->closeCursor();
if (!$res) {
return false;
}
$data['id'] = intval($res[0]);
} catch (\PDOException $e) {
throw new DatabaseFatalException('Failed to get domain ID', -1, $e);
}
return true;
}
/**
* Fetch the domain data from the database by its id or name
*
* @param array $data Domain data to update
*
* @return void
*/
public function fetch(array &$data): void
{
try {
$st = $this->connector->dbh()->prepare(
'SELECT `id`, `fqdn`, `active`, `description`, `created_time`, `updated_time` FROM `'
. $this->connector->tablePrefix('domains') . '` WHERE ' . $this->sqlCondition($data)
);
$this->sqlBindValue($st, 1, $data);
$st->execute();
$res = $st->fetch(\PDO::FETCH_NUM);
$st->closeCursor();
if (!$res) {
throw new DatabaseNotFoundException('Domain not found');
}
$data['id'] = intval($res[0]);
$data['fqdn'] = $res[1];
$data['active'] = boolval($res[2]);
$data['description'] = $res[3];
$data['created_time'] = new DateTime($res[4]);
$data['updated_time'] = new DateTime($res[5]);
} catch (\PDOException $e) {
throw new DatabaseFatalException('Failed to fetch the domain data', -1, $e);
}
}
/**
* Saves domain data to the database (updates or inserts an record)
*
* @param array $data Domain data
*
* @return void
*/
public function save(array &$data): void
{
$db = $this->connector->dbh();
$data['updated_time'] = new DateTime();
if ($this->exists($data)) {
try {
$st = $db->prepare(
'UPDATE `' . $this->connector->tablePrefix('domains')
. '` SET `active` = ?, `description` = ?, `updated_time` = ? WHERE `id` = ?'
);
$st->bindValue(1, $data['active'], \PDO::PARAM_BOOL);
$st->bindValue(2, $data['description'], \PDO::PARAM_STR);
$st->bindValue(3, $data['updated_time']->format('Y-m-d H:i:s'), \PDO::PARAM_STR);
$st->bindValue(4, $data['id'], \PDO::PARAM_INT);
$st->execute();
$st->closeCursor();
} catch (\PDOException $e) {
throw new DababaseException('Failed to update the domain data', -1, $e);
}
} else {
try {
$active = $data['active'] ?? false;
$data['created_time'] = $data['updated_time'];
if (is_null($data['description'])) {
$sql1 = '';
$sql2 = '';
} else {
$sql1 = ', `description`';
$sql2 = ', ?';
}
$st = $db->prepare(
'INSERT INTO `' . $this->connector->tablePrefix('domains')
. '` (`fqdn`, `active`' . $sql1 . ', `created_time`, `updated_time`)'
. ' VALUES (?, ?' . $sql2 . ', ?, ?)'
);
$idx = 0;
$st->bindValue(++$idx, $data['fqdn'], \PDO::PARAM_STR);
$st->bindValue(++$idx, $active, \PDO::PARAM_BOOL);
if (!is_null($data['description'])) {
$st->bindValue(++$idx, $data['description'], \PDO::PARAM_STR);
}
$st->bindValue(++$idx, $data['created_time']->format('Y-m-d H:i:s'), \PDO::PARAM_STR);
$st->bindValue(++$idx, $data['updated_time']->format('Y-m-d H:i:s'), \PDO::PARAM_STR);
$st->execute();
$st->closeCursor();
$data['id'] = intval($db->lastInsertId());
$data['active'] = $active;
} catch (\PDOException $e) {
throw new DatabaseFatalException('Failed to insert the domain data', -1, $e);
}
}
}
/**
* Deletes the domain from the database
*
* Deletes the domain if there are no reports for this domain in the database.
*
* @param array $data Domain data
*
* @return void
*/
public function delete(array &$data): void
{
$db = $this->connector->dbh();
$db->beginTransaction();
try {
$filter = [ 'domain' => $data['id'] ];
$limit = [ 'offset' => 0, 'count' => 0 ];
$r_count = $this->connector->getMapper('report')->count($filter, $limit);
if ($r_count > 0) {
switch ($r_count) {
case 1:
$s1 = 'is';
$s2 = '';
break;
default:
$s1 = 'are';
$s2 = 's';
break;
}
throw new SoftException(
"Failed to delete: there {$s1} {$r_count} incoming report{$s2} for this domain"
);
}
$st = $db->prepare('DELETE FROM `' . $this->connector->tablePrefix('domains') . '` WHERE `id` = ?');
$st->bindValue(1, $data['id'], \PDO::PARAM_INT);
$st->execute();
$st->closeCursor();
$db->commit();
} catch (\PDOException $e) {
$db->rollBack();
throw new DatabaseFatalException('Failed to delete the domain', -1, $e);
} catch (\Exception $e) {
$db->rollBack();
throw $e;
}
}
/**
* Returns a list of domains data from the database
*
* @return array
*/
public function list(): array
{
$list = [];
try {
$st = $this->connector->dbh()->query(
'SELECT `id`, `fqdn`, `active`, `description`, `created_time`, `updated_time` FROM `'
. $this->connector->tablePrefix('domains') . '`'
);
while ($row = $st->fetch(\PDO::FETCH_NUM)) {
$list[] = [
'id' => intval($row[0]),
'fqdn' => $row[1],
'active' => boolval($row[2]),
'description' => $row[3],
'created_time' => new DateTime($row[4]),
'updated_time' => new DateTime($row[5])
];
}
$st->closeCursor();
} catch (\PDOException $e) {
throw new DatabaseFatalException('Failed to get the domain list', -1, $e);
}
return $list;
}
/**
* Returns an ordered array with domain names from the database
*
* @return array
*/
public function names(): array
{
$res = [];
try {
$st = $this->connector->dbh()->query(
'SELECT `fqdn` FROM `' . $this->connector->tablePrefix('domains') . '` ORDER BY `fqdn`',
\PDO::FETCH_NUM
);
while ($name = $st->fetchColumn(0)) {
$res[] = $name;
}
$st->closeCursor();
} catch (\PDOException $e) {
throw new DatabaseFatalException('Failed to get a list of domain names', -1, $e);
}
return $res;
}
/**
* Returns the total number of domains in the database
*
* @param int $max The maximum number of records to count. 0 means no limitation.
*
* @return int The total number of domains
*/
public function count(int $max = 0): int
{
$number = 0;
try {
$query_str = 'SELECT COUNT(*) FROM `' . $this->connector->tablePrefix('domains') . '`';
if ($max > 0) {
$query_str .= " LIMIT {$max}";
}
$st = $this->connector->dbh()->query($query_str, \PDO::FETCH_NUM);
$number = intval($st->fetchColumn(0));
$st->closeCursor();
} catch (\PDOException $e) {
throw new DatabaseFatalException('Failed to get the number of domains', -1, $e);
}
return $number;
}
/**
* Returns a condition string for a WHERE statement based on existing domain data
*
* @param array $data Domain data
*
* @return string Condition string
*/
private function sqlCondition(array &$data): string
{
if (isset($data['id'])) {
return '`id` = ?';
}
return '`fqdn` = ?';
}
/**
* Binds values for SQL queries based on existing domain data
*
* @param PDOStatement $st PDO Statement to bind to
* @param ind $pos Start position for binding
* @param array $data Domain data
*
* @return void
*/
private function sqlBindValue($st, int $pos, array &$data): void
{
if (isset($data['id'])) {
$st->bindValue($pos, $data['id'], \PDO::PARAM_INT);
} else {
$st->bindValue($pos, $data['fqdn'], \PDO::PARAM_STR);
}
}
}

View File

@@ -0,0 +1,311 @@
<?php
/**
* dmarc-srg - A php parser, viewer and summary report generator for incoming DMARC reports.
* Copyright (C) 2022 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 ReportLogMapper 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\DateTime;
use Liuch\DmarcSrg\Database\ReportLogMapperInterface;
use Liuch\DmarcSrg\Exception\DatabaseFatalException;
use Liuch\DmarcSrg\Exception\DatabaseNotFoundException;
/**
* ReportLogMapper class implementation for MariaDB
*/
class ReportLogMapper implements ReportLogMapperInterface
{
private $connector = null;
/**
* The constructor
*
* @param Connector $connector DatabaseConnector
*/
public function __construct(object $connector)
{
$this->connector = $connector;
}
/**
* Fetches data of report log item from the database by id
*
* @param Report log data
*
* @return void
*/
public function fetch(array &$data): void
{
try {
$st = $this->connector->dbh()->prepare(
'SELECT `domain`, `external_id`, `event_time`, `filename`, `source`, `success`, `message` FROM `'
. $this->connector->tablePrefix('reportlog') . '` WHERE `id` = ?'
);
$st->bindValue(1, $data['id'], \PDO::PARAM_INT);
$st->execute();
if (!($row = $st->fetch(\PDO::FETCH_NUM))) {
throw new DatabaseNotFoundException();
}
$data['domain'] = $row[0];
$data['external_id'] = $row[1];
$data['event_time'] = new DateTime($row[2]);
$data['filename'] = $row[3];
$data['source'] = intval($row[4]);
$data['success'] = boolval($row[5]);
$data['message'] = $row[6];
$st->closeCursor();
} catch (\PDOException $e) {
throw new DatabaseFatalException('Failed to get the log item', -1, $e);
}
}
/**
* Saves data of report log item to the database
*
* @return void
*/
public function save(array &$data): void
{
$db = $this->connector->dbh();
try {
$id = $data['id'];
if (is_null($id)) {
$st = $db->prepare(
'INSERT INTO `' . $this->connector->tablePrefix('reportlog')
. '` (`domain`, `external_id`, `event_time`, `filename`, `source`, `success`, `message`)'
. ' VALUES (?, ?, ?, ?, ?, ?, ?)'
);
} else {
$st = $db->prepare(
'UPDATE `' . $this->connector->tablePrefix('reportlog')
. '` SET `domain` = ?, `external_id` = ?, `event_time` = ?, `filename` = ?,'
. ' `source` = ?, `success` = ?, `message` = ? WHERE `id` = ?'
);
$st->bindValue(8, $id, \PDO::PARAM_INT);
}
$ts = $data['event_time'] ?? (new DateTime());
$st->bindValue(1, $data['domain'], \PDO::PARAM_STR);
$st->bindValue(2, $data['external_id'], \PDO::PARAM_STR);
$st->bindValue(3, $ts->format('Y-m-d H:i:s'), \PDO::PARAM_STR);
$st->bindValue(4, $data['filename'], \PDO::PARAM_STR);
$st->bindValue(5, $data['source'], \PDO::PARAM_INT);
$st->bindValue(6, $data['success'], \PDO::PARAM_BOOL);
$st->bindValue(7, $data['message'], \PDO::PARAM_STR);
$st->execute();
if (is_null($id)) {
$data['id'] = intval($db->lastInsertId());
}
$st->closeCursor();
$data['event_time'] = $ts;
} catch (\PDOException $e) {
throw new DatabaseFatalException('Failed to save a report log item');
}
}
/**
* Returns a list of report log items with given criteria
*
* @param array $filter Key-value array:
* 'from_time' => DateTime
* 'till_time' => DateTime
* @param array $order Key-value array with order options:
* 'direction' => string, 'ascent' or 'descent'
* @param array $limit Key-value array:
* 'offset' => int
* 'count' => int
*
* @return array
*/
public function list(array &$filter, array &$order, array &$limit): array
{
$list = [];
try {
$st = $this->connector->dbh()->prepare(
'SELECT `id`, `domain`, `event_time`, `source`, `success`, `message` FROM `'
. $this->connector->tablePrefix('reportlog') . '`'
. $this->sqlCondition($filter)
. $this->sqlOrder($order)
. $this->sqlLimit($limit)
);
$this->sqlBindValues($st, $filter, $limit);
$st->execute();
while ($row = $st->fetch(\PDO::FETCH_NUM)) {
$list[] = [
'id' => intval($row[0]),
'domain' => $row[1],
'event_time' => new DateTime($row[2]),
'source' => intval($row[3]),
'success' => boolval($row[4]),
'message' => $row[5]
];
}
$st->closeCursor();
} catch (\PDOException $e) {
throw new DatabaseFatalException('Failed to get the logs', -1, $e);
}
return $list;
}
/**
* Returns the number of report log items matching the specified filter and limits
*
* @param array $filter Key-value array with filtering parameters
* @param array $limit Key-value array with limits
*
* @return int
*/
public function count(array &$filter, array &$limit): int
{
$cnt = 0;
try {
$st = $this->connector->dbh()->prepare(
'SELECT COUNT(*) FROM `' . $this->connector->tablePrefix('reportlog') . '`'
. $this->sqlCondition($filter)
. $this->sqlLimit($limit)
);
$this->sqlBindValues($st, $filter, $limit);
$st->execute();
$cnt = intval($st->fetch(\PDO::FETCH_NUM)[0]);
$st->closeCursor();
} catch (\PDOException $e) {
throw new DatabaseFatalException('Failed to get the log data', -1, $e);
}
return $cnt;
}
/**
* Deletes report log items from the database
*
* @param array $filter Key-value array with filtering parameters
* @param array $order Key-value array with order options:
* 'direction' => string, 'ascent' or 'descent'
* @param array $limit Key-value array with limits
*
* @return void
*/
public function delete(array &$filter, array &$order, array &$limit): void
{
try {
$st = $this->connector->dbh()->prepare(
'DELETE FROM `' . $this->connector->tablePrefix('reportlog') . '`'
. $this->sqlCondition($filter)
. $this->sqlOrder($order)
. $this->sqlLimit($limit)
);
$this->sqlBindValues($st, $filter, $limit);
$st->execute();
$st->closeCursor();
} catch (\PDOException $e) {
throw new DatabaseFatalException('Failed to remove the log data', -1, $e);
}
}
/**
* Returns a string with an SQL condition 'WHERE ...'
*
* @param array $filter Key-value with filtering paremeters
*
* @return string
*/
private function sqlCondition(array &$filter): string
{
$res = '';
if (!is_null($filter['from_time']) || !is_null($filter['till_time'])) {
$res = ' WHERE';
$till_time = $filter['till_time'];
if (!is_null($filter['from_time'])) {
$res .= ' `event_time` >= ?';
if (!is_null($till_time)) {
$res .= ' AND';
}
}
if (!is_null($till_time)) {
$res .= ' `event_time` < ?';
}
}
return $res;
}
/**
* Returns 'ORDER BY ...' part of the SQL query
*
* @param array $order Key-value array with ordering options
*
* @return string
*/
private function sqlOrder(array &$order): string
{
return ' ORDER BY `event_time` ' . ($order['direction'] === 'descent' ? 'DESC' : 'ASC');
}
/**
* Returns 'LIMIT ...' part of the SQL string
*
* @param array $limit Key-value array with keys 'offset' and 'count'
*
* @return string
*/
private function sqlLimit(array &$limit): string
{
$res = '';
if ($limit['count'] > 0) {
$res = ' LIMIT ?';
if ($limit['offset'] > 0) {
$res .= ', ?';
}
}
return $res;
}
/**
* Binds the values of the filter and the limit to SQL query
*
* @param PDOStatement $st Prepared SOL statement to bind to
* @param array $filter Key-value array with filter data
* @param array $limit Key-value array with limit data
*
* @return void
*/
private function sqlBindValues($st, array &$filter, array &$limit): void
{
$pos = 0;
if (!is_null($filter['from_time'])) {
$st->bindValue(++$pos, $filter['from_time']->format('Y-m-d H:i:s'), \PDO::PARAM_STR);
}
if (!is_null($filter['till_time'])) {
$st->bindValue(++$pos, $filter['till_time']->format('Y-m-d H:i:s'), \PDO::PARAM_STR);
}
if ($limit['count'] > 0) {
if ($limit['offset'] > 0) {
$st->bindValue(++$pos, $limit['offset'], \PDO::PARAM_INT);
}
$st->bindValue(++$pos, $limit['count'], \PDO::PARAM_INT);
}
}
}

View File

@@ -0,0 +1,753 @@
<?php
/**
* dmarc-srg - A php parser, viewer and summary report generator for incoming DMARC reports.
* Copyright (C) 2022 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 ReportMapper 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\Core;
use Liuch\DmarcSrg\Common;
use Liuch\DmarcSrg\DateTime;
use Liuch\DmarcSrg\Settings\SettingsList;
use Liuch\DmarcSrg\Database\ReportMapperInterface;
use Liuch\DmarcSrg\Exception\SoftException;
use Liuch\DmarcSrg\Exception\LogicException;
use Liuch\DmarcSrg\Exception\DatabaseFatalException;
use Liuch\DmarcSrg\Exception\DatabaseNotFoundException;
/**
* ReportMapper class implementation for MariaDB
*/
class ReportMapper implements ReportMapperInterface
{
private $connector = null;
private static $allowed_domains = null;
/**
* The constructor
*
* @param Connector $connector DatabaseConnector
*/
public function __construct(object $connector)
{
$this->connector = $connector;
}
/**
* Fetches report data from the database and stores it in the passed array
*
* @param array $data Array with report data. To identify the report,
* the array must contain at least two fields:
* `report_id` - External report id from the xml file
* `domain` - Fully Qualified Domain Name without a trailing dot
*
* @return void
*/
public function fetch(array &$data): void
{
$db = $this->connector->dbh();
try {
$st = $db->prepare(
'SELECT `rp`.`id`, `begin_time`, `end_time`, `loaded_time`, `org`, `email`, `extra_contact_info`,'
. ' `error_string`, `policy_adkim`, `policy_aspf`, `policy_p`, `policy_sp`, `policy_np`,'
. ' `policy_pct`, `policy_fo`'
. ' FROM `' . $this->connector->tablePrefix('reports') . '` AS `rp`'
. ' INNER JOIN `' . $this->connector->tablePrefix('domains')
. '` AS `dom` ON `dom`.`id` = `rp`.`domain_id`'
. ' WHERE `fqdn` = ? AND `external_id` = ?'
);
$st->bindValue(1, $data['domain'], \PDO::PARAM_STR);
$st->bindValue(2, $data['report_id'], \PDO::PARAM_STR);
$st->execute();
if (!($res = $st->fetch(\PDO::FETCH_NUM))) {
throw new DatabaseNotFoundException('The report is not found');
}
$id = intval($res[0]);
$data['date'] = [
'begin' => new DateTime($res[1]),
'end' => new DateTime($res[2])
];
$data['loaded_time'] = new DateTime($res[3]);
$data['org_name'] = $res[4];
$data['email'] = $res[5];
$data['extra_contact_info'] = $res[6];
$data['error_string'] = json_decode($res[7] ?? '', true);
$data['policy'] = [
'adkim' => $res[8],
'aspf' => $res[9],
'p' => $res[10],
'sp' => $res[11],
'np' => $res[12],
'pct' => $res[13],
'fo' => $res[14]
];
$order_str = $this->sqlOrderRecords();
$st = $db->prepare(
'SELECT `report_id`, `ip`, `rcount`, `disposition`, `reason`, `dkim_auth` , `spf_auth`, `dkim_align`,'
. ' `spf_align`, `envelope_to`, `envelope_from`, `header_from`'
. ' FROM `' . $this->connector->tablePrefix('rptrecords') . '` WHERE `report_id` = ?' . $order_str
);
$st->bindValue(1, $id, \PDO::PARAM_INT);
$st->execute();
$data['records'] = [];
while ($res = $st->fetch(\PDO::FETCH_NUM)) {
$data['records'][] = [
'ip' => inet_ntop($res[1]),
'count' => intval($res[2]),
'disposition' => Common::$disposition[$res[3]],
'reason' => json_decode($res[4] ?? '', true),
'dkim_auth' => json_decode($res[5] ?? '', true),
'spf_auth' => json_decode($res[6] ?? '', true),
'dkim_align' => Common::$align_res[$res[7]],
'spf_align' => Common::$align_res[$res[8]],
'envelope_to' => $res[9],
'envelope_from' => $res[10],
'header_from' => $res[11]
];
}
$st->closeCursor();
} catch (\PDOException $e) {
throw new DatabaseFatalException('Failed to get the report from DB', -1, $e);
}
}
/**
* Inserts report data into the database.
*
* @param array $data Report data
*
* @return void
*/
public function save(array &$data): void
{
$db = $this->connector->dbh();
$db->beginTransaction();
try {
$domain_data = [ 'fqdn' => strtolower($data['domain']) ];
$domain_mapper = $this->connector->getMapper('domain');
try {
$domain_mapper->fetch($domain_data);
if (!$domain_data['active']) {
throw new SoftException('Failed to add an incoming report: the domain is inactive');
}
} catch (DatabaseNotFoundException $e) {
// The domain is not found. Let's try to add it automatically.
$this->insertDomain($domain_data, $domain_mapper);
}
$ct = new DateTime();
$st = $db->prepare(
'INSERT INTO `' . $this->connector->tablePrefix('reports')
. '` (`domain_id`, `begin_time`, `end_time`, `loaded_time`, `org`, `external_id`, `email`,'
. ' `extra_contact_info`, `error_string`, `policy_adkim`, `policy_aspf`, `policy_p`,'
. ' `policy_sp`, `policy_np`, `policy_pct`, `policy_fo`, `seen`)'
. ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)'
);
$st->bindValue(1, $domain_data['id'], \PDO::PARAM_INT);
$st->bindValue(2, $data['begin_time']->format('Y-m-d H:i:s'), \PDO::PARAM_STR);
$st->bindValue(3, $data['end_time']->format('Y-m-d H:i:s'), \PDO::PARAM_STR);
$st->bindValue(4, $ct->format('Y-m-d H:i:s'), \PDO::PARAM_STR);
$st->bindValue(5, $data['org'], \PDO::PARAM_STR);
$st->bindValue(6, $data['external_id'], \PDO::PARAM_STR);
$st->bindValue(7, $data['email'], \PDO::PARAM_STR);
$st->bindValue(8, $data['extra_contact_info'], \PDO::PARAM_STR);
self::sqlBindJson($st, 9, $data['error_string']);
$st->bindValue(10, $data['policy_adkim'], \PDO::PARAM_STR);
$st->bindValue(11, $data['policy_aspf'], \PDO::PARAM_STR);
$st->bindValue(12, $data['policy_p'], \PDO::PARAM_STR);
$st->bindValue(13, $data['policy_sp'], \PDO::PARAM_STR);
$st->bindValue(14, $data['policy_np'], \PDO::PARAM_STR);
$st->bindValue(15, $data['policy_pct'], \PDO::PARAM_STR);
$st->bindValue(16, $data['policy_fo'], \PDO::PARAM_STR);
$st->execute();
$new_id = intval($db->lastInsertId());
$st->closeCursor();
$st = $db->prepare(
'INSERT INTO `' . $this->connector->tablePrefix('rptrecords')
. '` (`report_id`, `ip`, `rcount`, `disposition`, `reason`, `dkim_auth`, `spf_auth`, `dkim_align`,'
. ' `spf_align`, `envelope_to`, `envelope_from`, `header_from`)'
. ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
);
foreach ($data['records'] as &$rec_data) {
$st->bindValue(1, $new_id, \PDO::PARAM_INT);
$st->bindValue(2, inet_pton($rec_data['ip']), \PDO::PARAM_STR);
$st->bindValue(3, $rec_data['rcount'], \PDO::PARAM_INT);
$st->bindValue(4, array_search($rec_data['disposition'], Common::$disposition), \PDO::PARAM_INT);
self::sqlBindJson($st, 5, $rec_data['reason']);
self::sqlBindJson($st, 6, $rec_data['dkim_auth']);
self::sqlBindJson($st, 7, $rec_data['spf_auth']);
$st->bindValue(8, array_search($rec_data['dkim_align'], Common::$align_res), \PDO::PARAM_INT);
$st->bindValue(9, array_search($rec_data['spf_align'], Common::$align_res), \PDO::PARAM_INT);
$st->bindValue(10, $rec_data['envelope_to'], \PDO::PARAM_STR);
$st->bindValue(11, $rec_data['envelope_from'], \PDO::PARAM_STR);
$st->bindValue(12, $rec_data['header_from'], \PDO::PARAM_STR);
$st->execute();
}
unset($rec_data);
$db->commit();
$data['loaded_time'] = $ct;
} catch (\PDOException $e) {
$db->rollBack();
if ($e->getCode() == '23000') {
throw new SoftException('This report has already been loaded');
}
throw new DatabaseFatalException('Failed to insert the report', -1, $e);
} catch (\Exception $e) {
$db->rollBack();
throw $e;
}
}
/**
* Sets report record property in database.
*
* It has nothing to do with the fields of the report itself.
*
* @param array $data Report data
* @param string $name Property name. Currently only `seen` is supported.
* @param variant $value Property value
*
* @return void
*/
public function setProperty(array &$data, string $name, $value): void
{
if ($name !== 'seen' && gettype($value) !== 'boolean') {
throw new LogicException('Incorrect parameters');
}
try {
$st = $this->connector->dbh()->prepare(
'UPDATE `' . $this->connector->tablePrefix('reports') . '` AS `rp`'
. ' INNER JOIN `' . $this->connector->tablePrefix('domains') . '` AS `dom`'
. ' ON `rp`.`domain_id` = `dom`.`id` SET `seen` = ? WHERE `fqdn` = ? AND `external_id` = ?'
);
$st->bindValue(1, $value, \PDO::PARAM_BOOL);
$st->bindValue(2, $data['domain'], \PDO::PARAM_STR);
$st->bindValue(3, $data['report_id'], \PDO::PARAM_STR);
$st->execute();
$st->closeCursor();
} catch (\PDOException $e) {
throw new DatabaseFatalException('Failed to update the DB record', -1, $e);
}
}
/**
* Returns a list of reports with specified parameters
*
* This method returns a list of reports that depends on the $filter, $order and $limit.
*
* @param array $filter Key-value array with filtering parameters
* @param array $order Key-value array:
* 'field' => string, 'begin_time'
* 'direction' => string, 'ascent' or 'descent'
* @param array $limit Key-value array with two keys: `offset` and `count`
*
* @return array
*/
public function list(array &$filter, array &$order, array &$limit): array
{
$db = $this->connector->dbh();
$list = [];
$f_data = $this->prepareFilterData($filter);
$order_str = $this->sqlOrderList($order);
$cond_str0 = $this->sqlConditionList($f_data, ' AND ', 0);
$cond_str1 = $this->sqlConditionList($f_data, ' HAVING ', 1);
$limit_str = $this->sqlLimit($limit);
try {
$st = $db->prepare(
'SELECT `org`, `begin_time`, `end_time`, `fqdn`, external_id, `seen`, SUM(`rcount`) AS `rcount`,'
. ' MIN(`dkim_align`) AS `dkim_align`, MIN(`spf_align`) AS `spf_align`,'
. ' MIN(`disposition`) AS `disposition` FROM `' . $this->connector->tablePrefix('rptrecords')
. '` AS `rr` RIGHT JOIN (SELECT `rp`.`id`, `org`, `begin_time`, `end_time`, `external_id`,'
. ' `fqdn`, `seen` FROM `' . $this->connector->tablePrefix('reports')
. '` AS `rp` INNER JOIN `' . $this->connector->tablePrefix('domains')
. '` AS `d` ON `d`.`id` = `rp`.`domain_id`' . $cond_str0 . $order_str
. ') AS `rp` ON `rp`.`id` = `rr`.`report_id` GROUP BY `rp`.`id`'
. $cond_str1 . $order_str . $limit_str
);
$this->sqlBindValues($st, $f_data, $limit);
$st->execute();
while ($row = $st->fetch(\PDO::FETCH_NUM)) {
$list[] = [
'org_name' => $row[0],
'date' => [
'begin' => new DateTime($row[1]),
'end' => new DateTime($row[2])
],
'domain' => $row[3],
'report_id' => $row[4],
'seen' => (bool) $row[5],
'messages' => $row[6],
'dkim_align' => Common::$align_res[$row[7]],
'spf_align' => Common::$align_res[$row[8]],
'disposition' => Common::$disposition[$row[9]]
];
}
$st->closeCursor();
} catch (\PDOException $e) {
throw new DatabaseFatalException('Failed to get the report list', -1, $e);
}
return $list;
}
/**
* Returns the number of reports matching the specified filter and limits
*
* @param array $filter Key-value array with filtering parameters
* @param array $limit Key-value array with two keys: `offset` and `count`
*
* @return int
*/
public function count(array &$filter, array &$limit): int
{
$cnt = 0;
$f_data = $this->prepareFilterData($filter);
try {
$st = $this->connector->dbh()->prepare(
'SELECT COUNT(*) FROM `' . $this->connector->tablePrefix('reports') . '` AS `rp`'
. $this->sqlConditionList($f_data, ' WHERE ', 0)
);
$l_empty = [ 'offset' => 0, 'count' => 0 ];
$this->sqlBindValues($st, $f_data, $l_empty);
$st->execute();
$cnt = intval($st->fetch(\PDO::FETCH_NUM)[0]);
$st->closeCursor();
$offset = $limit['offset'];
if ($offset > 0) {
$cnt -= $offset;
if ($cnt < 0) {
$cnt = 0;
}
}
$max = $limit['count'];
if ($max > 0 && $max < $cnt) {
$cnt = $max;
}
} catch (\PDOException $e) {
throw new DatabaseFatalException('Failed to get the number of reports', -1, $e);
}
return $cnt;
}
/**
* Deletes reports from the database
*
* It deletes repors form the database. The filter options `dkim` and `spf` do not affect this.
*
* @param array $filter Key-value array with filtering parameters
* @param array $order Key-value array:
* 'field' => string, 'begin_time'
* 'direction' => string, 'ascent' or 'descent'
* @param array $limit Key-value array with two keys: `offset` and `count`
*
* @return void
*/
public function delete(array &$filter, array &$order, array &$limit): void
{
$f_data = $this->prepareFilterData($filter);
$cond_str = $this->sqlConditionList($f_data, ' WHERE ', 0);
$order_str = $this->sqlOrderList($order);
$limit_str = $this->sqlLimit($limit);
$db = $this->connector->dbh();
$db->beginTransaction();
try {
$st = $db->prepare(
'DELETE `rr` FROM `' . $this->connector->tablePrefix('rptrecords')
. '` AS `rr` INNER JOIN (SELECT `id` FROM `' . $this->connector->tablePrefix('reports') . '`'
. $cond_str . $order_str . $limit_str . ') AS `rp` ON `rp`.`id` = `rr`.`report_id`'
);
$this->sqlBindValues($st, $f_data, $limit);
$st->execute();
$st->closeCursor();
$st = $db->prepare(
'DELETE FROM `' . $this->connector->tablePrefix('reports') . "`{$cond_str}{$order_str}{$limit_str}"
);
$this->sqlBindValues($st, $f_data, $limit);
$st->execute();
$st->closeCursor();
$db->commit();
} catch (\PDOException $e) {
$db->rollBack();
throw new DatabaseFatalException('Failed to delete reports', -1, $e);
} catch (\Exception $e) {
$db->rollBack();
throw $e;
}
}
/**
* Returns a list of months with years of the form: 'yyyy-mm' for which there is at least one report
*
* @return array
*/
public function months(): array
{
$res = [];
$rep_tn = $this->connector->tablePrefix('reports');
try {
$st = $this->connector->dbh()->query(
'SELECT DISTINCT DATE_FORMAT(`date`, "%Y-%m") AS `month` FROM'
. ' ((SELECT DISTINCT `begin_time` AS `date` FROM `' . $rep_tn
. '`) UNION (SELECT DISTINCT `end_time` AS `date` FROM `' . $rep_tn
. '`)) AS `r` ORDER BY `month` DESC'
);
while ($row = $st->fetch(\PDO::FETCH_NUM)) {
$res[] = $row[0];
}
$st->closeCursor();
} catch (\PDOException $e) {
throw new DatabaseFatalException('Failed to get a list of months', -1, $e);
}
return $res;
}
/**
* Returns a list of reporting organizations from which there is at least one report
*
* @return array
*/
public function organizations(): array
{
$res = [];
$rep_tn = $this->connector->tablePrefix('reports');
try {
$st = $this->connector->dbh()->query(
'SELECT DISTINCT `org` FROM `' . $rep_tn . '` ORDER BY `org`'
);
while ($row = $st->fetch(\PDO::FETCH_NUM)) {
$res[] = $row[0];
}
$st->closeCursor();
} catch (\PDOException $e) {
throw new DatabaseFatalException('Failed to get a list of organizations', -1, $e);
}
return $res;
}
/**
* Returns `ORDER BY ...` part of the SQL query for report records
*
* @return string
*/
private function sqlOrderRecords(): string
{
$o_set = explode(',', SettingsList::getSettingByName('report-view.sort-records-by')->value());
switch ($o_set[0]) {
case 'ip':
$fname = 'ip';
break;
case 'message-count':
default:
$fname = 'rcount';
break;
}
$dir = $o_set[1] === 'descent' ? 'DESC' : 'ASC';
return " ORDER BY `{$fname}` {$dir}";
}
/**
* Checks if the domain exists and adds it to the database if necessary
*
* It automatically adds the domain if there are no domains in the database
* or if the domain match the `allowed_domains` reqular expression in the configuration file.
* Otherwise, throws a SoftException.
*
* @param array $data Domain data
* @param object $mapper Domain mapper
*
* @return void
*/
private function insertDomain(array &$data, $mapper): void
{
$mapper = $this->connector->getMapper('domain');
if ($mapper->count(1) !== 0) {
if (is_null(self::$allowed_domains)) {
$allowed = Core::instance()->config('fetcher/allowed_domains', '');
if (!empty($allowed)) {
self::$allowed_domains = "<{$allowed}>i";
}
}
try {
$add = !empty(self::$allowed_domains) && preg_match(self::$allowed_domains, $data['fqdn']) === 1;
} catch (\ErrorException $e) {
$add = false;
Core::instance()->logger()->warning(
'The allow_domains parameter in the settings has an incorrect regular expression value.'
);
}
if (!$add) {
throw new SoftException('Failed to add an incoming report: unknown domain: ' . $data['fqdn']);
}
}
$data['active'] = true;
$data['description'] = 'The domain was added automatically.';
$mapper->save($data);
}
/**
* Binds a nullable array to an SQL query as a json string
*
* @param PDOStatement $st DB statement object
* @param int $idx Bind position
* @param array $data JSON data or null
*
* @return void
*/
private static function sqlBindJson($st, int $idx, $data): void
{
if (is_null($data)) {
$val = null;
$type = \PDO::PARAM_NULL;
} else {
$val = json_encode($data);
$type = \PDO::PARAM_STR;
}
$st->bindValue($idx, $val, $type);
}
/**
* Returns `ORDER BY ...` part of the SQL query
*
* @param array $order Key-value array with ordering options
*
* @return string
*/
private function sqlOrderList(array &$order): string
{
$dir = $order['direction'] === 'ascent' ? 'ASC' : 'DESC';
return " ORDER BY `{$order['field']}` {$dir}";
}
/**
* The valid filter item names
*/
private static $filters_available = [
'domain', 'month', 'before_time', 'organization', 'dkim', 'spf', 'status'
];
/**
* Returns prepared filter data for sql queries
*
* @param array $filter Key-value array with filter options
*
* @return array
*/
private function prepareFilterData(array &$filter): array
{
$filters = [];
for ($i = 0; $i < 2; ++$i) {
$filters[] = [
'a_str' => [],
'bindings' => []
];
}
foreach (self::$filters_available as $fn) {
if (isset($filter[$fn])) {
$fv = $filter[$fn];
switch (gettype($fv)) {
case 'string':
if (!empty($fv)) {
if ($fn == 'domain') {
$filters[0]['a_str'][] = '`rp`.`domain_id` = ?';
$d_data = [ 'fqdn' => $fv ];
$this->connector->getMapper('domain')->fetch($d_data);
$filters[0]['bindings'][] = [ $d_data['id'], \PDO::PARAM_INT ];
} elseif ($fn == 'month') {
$ma = explode('-', $fv);
if (count($ma) != 2) {
throw new SoftException('Report list filter: Incorrect date format');
}
$year = (int)$ma[0];
$month = (int)$ma[1];
if ($year < 0 || $month < 1 || $month > 12) {
throw new SoftException('Report list filter: Incorrect month or year value');
}
$filters[0]['a_str'][] = '`begin_time` < ? AND `end_time` >= ?';
$date1 = new DateTime("{$year}-{$month}-01");
$date2 = (clone $date1)->modify('first day of next month');
$date1->add(new \DateInterval('PT10S'));
$date2->sub(new \DateInterval('PT10S'));
$filters[0]['bindings'][] = [ $date2->format('Y-m-d H:i:s'), \PDO::PARAM_STR ];
$filters[0]['bindings'][] = [ $date1->format('Y-m-d H:i:s'), \PDO::PARAM_STR ];
} elseif ($fn == 'organization') {
$filters[0]['a_str'][] = '`org` = ?';
$filters[0]['bindings'][] = [ $fv, \PDO::PARAM_STR ];
} elseif ($fn == 'dkim') {
if ($fv === Common::$align_res[0]) {
$val = 0;
} else {
$val = count(Common::$align_res) - 1;
if ($fv !== Common::$align_res[$val]) {
throw new SoftException('Report list filter: Incorrect DKIM value');
}
}
$filters[1]['a_str'][] = '`dkim_align` = ?';
$filters[1]['bindings'][] = [ $val, \PDO::PARAM_INT ];
} elseif ($fn == 'spf') {
if ($fv === Common::$align_res[0]) {
$val = 0;
} else {
$val = count(Common::$align_res) - 1;
if ($fv !== Common::$align_res[$val]) {
throw new SoftException('Report list filter: Incorrect SPF value');
}
}
$filters[1]['a_str'][] = '`spf_align` = ?';
$filters[1]['bindings'][] = [ $val, \PDO::PARAM_INT ];
} elseif ($fn == 'status') {
if ($fv === 'read') {
$val = true;
} elseif ($fv === 'unread') {
$val = false;
} else {
throw new SoftException('Report list filter: Incorrect status value');
}
$filters[0]['a_str'][] = '`seen` = ?';
$filters[0]['bindings'][] = [ $val, \PDO::PARAM_BOOL ];
}
}
break;
case 'object':
if ($fn == 'domain') {
$filters[0]['a_str'][] = '`rp`.`domain_id` = ?';
$filters[0]['bindings'][] = [ $fv->id(), \PDO::PARAM_INT ];
} elseif ($fn == 'before_time') {
$filters[0]['a_str'][] = '`begin_time` < ?';
$filters[0]['bindings'][] = [ $fv->format('Y-m-d H:i:s'), \PDO::PARAM_STR ];
}
break;
case 'integer':
if ($fn == 'domain') {
$filters[0]['a_str'][] = '`rp`.`domain_id` = ?';
$filters[0]['bindings'][] = [ $fv, \PDO::PARAM_INT ];
}
break;
}
}
}
$f_data = [];
for ($i = 0; $i < count($filters); ++$i) {
$filter = &$filters[$i];
if (count($filter['a_str']) > 0) {
$f_data[$i] = [
'str' => implode(' AND ', $filter['a_str']),
'bindings' => $filter['bindings']
];
}
unset($filter);
}
return $f_data;
}
/**
* Returns the SQL condition for a filter by filter id
*
* @param array $f_data Array with prepared filter data
* @param string $prefix Prefix, which will be added to the beginning of the condition string,
* but only in the case when the condition string is not empty.
* @param int $f_id Index of the filter
*
* @return string the condition string
*/
private function sqlConditionList(array &$f_data, string $prefix, int $f_idx): string
{
return isset($f_data[$f_idx]) ? ($prefix . $f_data[$f_idx]['str']) : '';
}
/**
* Returns `LIMIT ...` part of the SQL query
*
* @param array $limit Key-value array with two keys: `offset` and `count`
*
* @return string
*/
private function sqlLimit(array &$limit): string
{
$res = '';
if ($limit['count'] > 0) {
$res = ' LIMIT ?';
if ($limit['offset'] > 0) {
$res .= ', ?';
}
}
return $res;
}
/**
* Binds the values of the filter and the limit to SQL query
*
* @param PDOStatement $st Prepared SQL statement to bind to
* @param array $f_data Array with prepared filter data
* @param array $limit Key-value array with two keys: `offset` and `count`
*
* @return void
*/
private function sqlBindValues($st, array &$f_data, array &$limit): void
{
$pos = 0;
if (isset($f_data[0])) {
$this->sqlBindFilterValues($st, $f_data, 0, $pos);
}
if (isset($f_data[1])) {
$this->sqlBindFilterValues($st, $f_data, 1, $pos);
}
if ($limit['count'] > 0) {
if ($limit['offset'] > 0) {
$st->bindValue(++$pos, $limit['offset'], \PDO::PARAM_INT);
}
$st->bindValue(++$pos, $limit['count'], \PDO::PARAM_INT);
}
}
/**
* Binds the values of the specified filter item to SQL query
*
* @param PDOStatement $st Prepared SQL statement to bind to
* @param array $f_data Array with prepared filter data
* @param int $filter_idx Index of the filter to bind to
* @param int $bind_pos Start bind position (pointer). It will be increaded with each binding.
*
* @return void
*/
private function sqlBindFilterValues($st, array &$f_data, int $filter_idx, int &$bind_pos): void
{
foreach ($f_data[$filter_idx]['bindings'] as &$bv) {
$st->bindValue(++$bind_pos, $bv[0], $bv[1]);
}
}
}

View File

@@ -0,0 +1,130 @@
<?php
/**
* dmarc-srg - A php parser, viewer and summary report generator for incoming DMARC reports.
* Copyright (C) 2022 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 SettingMapper 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\Database\SettingMapperInterface;
use Liuch\DmarcSrg\Exception\DatabaseFatalException;
use Liuch\DmarcSrg\Exception\DatabaseNotFoundException;
/**
* SettingMapper class implementation for MariaDB
*/
class SettingMapper implements SettingMapperInterface
{
private $connector = null;
/**
* The constructor
*
* @param Connector $connector DatabaseConnector
*/
public function __construct(object $connector)
{
$this->connector = $connector;
}
/**
* Returns setting value as a string by key
*
* @param string $key
*
* @return string
*/
public function value(string $key): string
{
try {
$st = $this->connector->dbh()->prepare(
'SELECT `value` FROM `' . $this->connector->tablePrefix('system') . '` WHERE `key` = ?'
);
$st->bindValue(1, $key, \PDO::PARAM_STR);
$st->execute();
if (!$res = $st->fetch(\PDO::FETCH_NUM)) {
throw new DatabaseNotFoundException('Setting not found: ' . $key);
}
$st->closeCursor();
return $res[0];
} catch (\PDOException $e) {
throw new DatabaseFatalException('Failed to get a setting', -1, $e);
}
}
/**
* Returns a key-value array of the setting list like this:
* [ 'name1' => 'value1', 'name2' => 'value2' ]
*
* @return array
*/
public function list(): array
{
$res = [];
try {
$st = $this->connector->dbh()->query(
'SELECT `key`, `value` FROM `' . $this->connector->tablePrefix('system') . '` ORDER BY `key`'
);
while ($row = $st->fetch(\PDO::FETCH_NUM)) {
$res[$row[0]] = $row[1];
}
$st->closeCursor();
} catch (\PDOException $e) {
throw new DatabaseFatalException('Failed to get a list of the settings', -1, $e);
}
return $res;
}
/**
* Saves the setting to the database
*
* Updates the value of the setting in the database if the setting exists there or insert a new record otherwise.
*
* @param string $name Setting name
* @param string $value Setting value
*
* @return void
*/
public function save(string $name, string $value): void
{
$db = $this->connector->dbh();
try {
$st = $db->prepare(
'INSERT INTO `' . $this->connector->tablePrefix('system') .
'` (`key`, `value`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `value` = ?'
);
$st->bindValue(1, $name, \PDO::PARAM_STR);
$st->bindValue(2, $value, \PDO::PARAM_STR);
$st->bindValue(3, $value, \PDO::PARAM_STR);
$st->execute();
$st->closeCursor();
} catch (\PDOException $e) {
throw new DatabaseFatalException('Failed to update a setting', -1, $e);
}
}
}

View File

@@ -0,0 +1,222 @@
<?php
/**
* dmarc-srg - A php parser, viewer and summary report generator for incoming DMARC reports.
* Copyright (C) 2022 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 StatisticsMapper 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\Database\StatisticsMapperInterface;
use Liuch\DmarcSrg\Exception\DatabaseFatalException;
/**
* StatisticsMapper class implementation for MariaDB
*/
class StatisticsMapper implements StatisticsMapperInterface
{
private $connector = null;
/**
* The constructor
*
* @param Connector $connector DatabaseConnector
*/
public function __construct(object $connector)
{
$this->connector = $connector;
}
/**
* Returns summary information for the specified domain and date range
*
* @param Domain|null $domain Domain for which the information is needed. Null is for all domains.
* @param array $range Array with two dates
*
* @return array Array with Summary information:
* 'emails' => [
* 'total' => total email processed (int)
* 'dkim_spf_aligned' => Both DKIM and SPF aligned (int)
* 'dkim_aligned' => Only DKIM aligned (int)
* 'spf_aligned' => Only SPF aligned (int)
* ];
*/
public function summary($domain, array &$range): array
{
$is_domain = $domain ? true : false;
$db = $this->connector->dbh();
try {
$st = $db->prepare(
'SELECT SUM(`rcount`), SUM(IF(`dkim_align` = 2 AND `spf_align` = 2, `rcount`, 0)),'
. ' SUM(IF(`dkim_align` = 2 AND `spf_align` <> 2, `rcount`, 0)),'
. ' SUM(IF(`dkim_align` <> 2 AND `spf_align` = 2, `rcount`, 0))'
. ' FROM `' . $this->connector->tablePrefix('rptrecords') . '` AS `rr`'
. ' INNER JOIN `' . $this->connector->tablePrefix('reports')
. '` AS `rp` ON `rr`.`report_id` = `rp`.`id`'
. $this->sqlCondition($is_domain)
);
$this->sqlBindValues($st, $domain, $range);
$st->execute();
$row = $st->fetch(\PDO::FETCH_NUM);
$ems = [
'total' => intval($row[0]),
'dkim_spf_aligned' => intval($row[1]),
'dkim_aligned' => intval($row[2]),
'spf_aligned' => intval($row[3])
];
$st->closeCursor();
$st = $db->prepare(
'SELECT COUNT(*) FROM (SELECT `org` FROM `' . $this->connector->tablePrefix('reports') . '`'
. $this->sqlCondition($is_domain) . ' GROUP BY `org`) AS `orgs`'
);
$this->sqlBindValues($st, $domain, $range);
$st->execute();
$row = $st->fetch(\PDO::FETCH_NUM);
$st->closeCursor();
} catch (\PDOException $e) {
throw new DatabaseFatalException('Failed to get summary information', -1, $e);
}
return [
'emails' => $ems,
'organizations' => intval($row[0])
];
}
/**
* Returns a list of ip-addresses from which the e-mail messages were received, with some statistics for each one
*
* @param Domain|null $domain Domain for which the information is needed. Null is for all domains.
* @param array $range Array with two dates
*
* @return array A list of ip-addresses with fields `ip`, `emails`, `dkim_aligned`, `spf_aligned`
*/
public function ips($domain, array &$range): array
{
try {
$st = $this->connector->dbh()->prepare(
'SELECT `ip`, SUM(`rcount`) AS `rcount`, SUM(IF(`dkim_align` = 2, `rcount`, 0)) AS `dkim_aligned`,'
. ' SUM(IF(`spf_align` = 2, `rcount`, 0)) AS `spf_aligned`'
. ' FROM `' . $this->connector->tablePrefix('rptrecords') . '` AS `rr`'
. ' INNER JOIN `' . $this->connector->tablePrefix('reports')
. '` AS `rp` ON `rr`.`report_id` = `rp`.`id`'
. $this->sqlCondition($domain ? true : false) . ' GROUP BY `ip` ORDER BY `rcount` DESC'
);
$this->sqlBindValues($st, $domain, $range);
$st->execute();
$res = [];
while ($row = $st->fetch(\PDO::FETCH_NUM)) {
$res[] = [
'ip' => inet_ntop($row[0]),
'emails' => intval($row[1]),
'dkim_aligned' => intval($row[2]),
'spf_aligned' => intval($row[3])
];
}
$st->closeCursor();
} catch (\PDOException $e) {
throw new DatabaseFatalException('Failed to get IPs summary information', -1, $e);
}
return $res;
}
/**
* Returns a list of organizations that sent the reports with some statistics for each one
*
* @param Domain|null $domain Domain for which the information is needed. Null is for all domains.
* @param array $range Array with two dates
*
* @return array List of organizations with fields `name`, `reports`, `emails`
*/
public function organizations($domain, array &$range): array
{
try {
$st = $this->connector->dbh()->prepare(
'SELECT `org`, COUNT(*), SUM(`rr`.`rcount`) AS `rcount`'
. ' FROM `' . $this->connector->tablePrefix('reports') . '` AS `rp`'
. ' INNER JOIN (SELECT `report_id`, SUM(`rcount`) AS `rcount` FROM `'
. $this->connector->tablePrefix('rptrecords')
. '` GROUP BY `report_id`) AS `rr` ON `rp`.`id` = `rr`.`report_id`'
. $this->sqlCondition($domain ? true : false)
. ' GROUP BY `org` ORDER BY `rcount` DESC'
);
$this->sqlBindValues($st, $domain, $range);
$st->execute();
$res = [];
while ($row = $st->fetch(\PDO::FETCH_NUM)) {
$res[] = [
'name' => $row[0],
'reports' => intval($row[1]),
'emails' => intval($row[2])
];
}
$st->closeCursor();
} catch (\PDOException $e) {
throw new DatabaseFatalException('Failed to get summary information of reporting organizations', -1, $e);
}
return $res;
}
/**
* Returns a condition string for WHERE statement
*
* @param bool $with_domain Is it needed to add a condition for a domain
*
* @return string Condition string
*/
private function sqlCondition($with_domain): string
{
$res = ' WHERE ';
if ($with_domain) {
$res .= 'domain_id = ? AND ';
}
$res .= '`begin_time` < ? AND `end_time` >= ?';
return $res;
}
/**
* Binds values for SQL queries
*
* @param PDOStatement $st PDO Statement to bind to
* @param Domain|null $domain Domain for the condition
* @param array $range Date range for the condition
*
* @return void
*/
private function sqlBindValues(object $st, $domain, array &$range): void
{
$pnum = 0;
if ($domain) {
$st->bindValue(++$pnum, $domain->id(), \PDO::PARAM_INT);
}
$ds1 = (clone $range['date1'])->add(new \DateInterval('PT10S'))->format('Y-m-d H:i:s');
$ds2 = (clone $range['date2'])->sub(new \DateInterval('PT10S'))->format('Y-m-d H:i:s');
$st->bindValue(++$pnum, $ds2, \PDO::PARAM_STR);
$st->bindValue(++$pnum, $ds1, \PDO::PARAM_STR);
}
}

View File

@@ -0,0 +1,236 @@
<?php
/**
* dmarc-srg - A php parser, viewer and summary report generator for incoming DMARC reports.
* Copyright (C) 2022 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 UpgraderMapper 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\Database\UpgraderMapperInterface;
use Liuch\DmarcSrg\Exception\SoftException;
use Liuch\DmarcSrg\Exception\DatabaseFatalException;
use Liuch\DmarcSrg\Exception\DatabaseNotFoundException;
/**
* UpgraderMapper class implementation for MariaDB
*/
class UpgraderMapper implements UpgraderMapperInterface
{
private $connector = null;
/**
* The constructor
*
* @param Connector $connector DatabaseConnector
*/
public function __construct(object $connector)
{
$this->connector = $connector;
}
/**
* Starts upgrading the database structure
*
* @param string $target Target version of the database structure to upgrade to
*
* @return void
*/
public function go(string $target): void
{
try {
$cur_ver = $this->connector->getMapper('setting')->value('version');
} catch (DatabaseNotFoundException $e) {
$cur_ver = 'null';
}
while ($cur_ver !== $target) {
if (!isset(self::$upways['ver_' . $cur_ver])) {
throw new SoftException(
"Upgrading failed: There is no way to upgrade from {$cur_ver} to {$target}"
);
}
$um = self::$upways['ver_' . $cur_ver];
$cur_ver = $this->$um();
}
}
/**
* Upgrades the database structure from None to 0.1
*
* @return string New version of the database structure
*/
private function upNull(): string
{
$db = $this->connector->dbh();
$db->beginTransaction();
try {
$db->query(
'INSERT INTO `' . $this->connector->tablePrefix('system')
. '` (`key`, `value`) VALUES ("version", "0.1")'
);
$db->commit();
} catch (\PDOException $e) {
$db->rollBack();
throw $this->dbFatalException($e);
} catch (\Exception $e) {
$db->rollBack();
throw $e;
}
return '0.1';
}
/**
* Upgrades the database structure from 0.1 to 1.0
*
* @return string New version of the database structure
*/
private function up01(): string
{
$db = $this->connector->dbh();
// Transaction would be useful here but it doesn't work with ALTER TABLE in MySQL/MariaDB
try {
$dom_tn = $this->connector->tablePrefix('domains');
if (!$this->columnExists($db, $dom_tn, 'active')) {
$db->query(
'ALTER TABLE `' . $dom_tn . '` ADD COLUMN `active` boolean NOT NULL AFTER `fqdn`'
);
}
if (!$this->columnExists($db, $dom_tn, 'created_time')) {
$db->query(
'ALTER TABLE `' . $dom_tn . '` ADD COLUMN `created_time` datetime NOT NULL'
);
}
if (!$this->columnExists($db, $dom_tn, 'updated_time')) {
$db->query(
'ALTER TABLE `' . $dom_tn . '` ADD COLUMN `updated_time` datetime NOT NULL'
);
}
$db->query(
'UPDATE `' . $dom_tn . '` SET `active` = TRUE, `created_time` = NOW(), `updated_time` = NOW()'
);
$db->query(
'UPDATE `' . $this->connector->tablePrefix('system') . '` SET `value` = "1.0" WHERE `key` = "version"'
);
} catch (\PDOException $e) {
throw $this->dbFatalException($e);
}
return '1.0';
}
/**
* Upgrades the database structure from 1.0 to 2.0
*
* @return string New version of the database structure
*/
private function up10(): string
{
$db = $this->connector->dbh();
// Transaction would be useful here but it doesn't work with ALTER TABLE in MySQL/MariaDB
try {
$sys_tn = $this->connector->tablePrefix('system');
$db->query(
'ALTER TABLE `' . $sys_tn . '` MODIFY COLUMN `key` varchar(64) NOT NULL'
);
$db->query(
'UPDATE `' . $sys_tn . '` SET `value` = "2.0" WHERE `key` = "version"'
);
} catch (\PDOException $e) {
throw $this->dbFatalException($e);
}
return '2.0';
}
/**
* Upgrades the database structure from v2.0 to v3.0
*
* @return string New version of the database structure
*/
private function up20(): string
{
$db = $this->connector->dbh();
// Transaction would be useful here but it doesn't work with ALTER TABLE in MySQL/MariaDB
try {
$rep_tn = $this->connector->tablePrefix('reports');
if (!$this->columnExists($db, $rep_tn, 'policy_np')) {
$db->query(
'ALTER TABLE `' . $rep_tn . '` ADD COLUMN `policy_np` varchar(20) NULL AFTER `policy_sp`'
);
}
$sys_tn = $this->connector->tablePrefix('system');
$db->query(
'UPDATE `' . $sys_tn . '` SET `value` = "3.0" WHERE `key` = "version"'
);
} catch (\PDOException $e) {
throw $this->dbFatalException($e);
}
return '3.0';
}
/**
* Checks if the spefied column exists in the spefied table of the database
*
* @param object $db Connection handle of the database
* @param string $table Table name with the prefix
* @param string $columb Column name
*
* @return bool
*/
private function columnExists($db, string $table, string $column): bool
{
$st = $db->prepare(
'SELECT NULL FROM `INFORMATION_SCHEMA`.`COLUMNS`'
. ' WHERE `table_schema` = ? AND `table_name` = ? AND `column_name` = ?'
);
$st->bindValue(1, $this->connector->dbName(), \PDO::PARAM_STR);
$st->bindValue(2, $table, \PDO::PARAM_STR);
$st->bindValue(3, $column, \PDO::PARAM_STR);
$st->execute();
$res = $st->fetch(\PDO::FETCH_NUM);
$st->closeCursor();
return $res ? true : false;
}
/**
* Return an instance of DatabaseFatalException
*
* @param Exception $e The original exception
*
* @return DatabaseFatalException
*/
private function dbFatalException($e)
{
return new DatabaseFatalException('Failed to upgrade the database structure', -1, $e);
}
private static $upways = [
'ver_null' => 'upNull',
'ver_0.1' => 'up01',
'ver_1.0' => 'up10',
'ver_2.0' => 'up20'
];
}

View File

@@ -0,0 +1,89 @@
<?php
/**
* dmarc-srg - A php parser, viewer and summary report generator for incoming DMARC reports.
* Copyright (C) 2022 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 ReportLogMapperInterface
*
* @category API
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg\Database;
interface ReportLogMapperInterface
{
/**
* Fetches data of report log item from the database by id
*
* @param Report log data
*
* @return void
*/
public function fetch(array &$data): void;
/**
* Saves data of report log item to the database
*
* @return void
*/
public function save(array &$data): void;
/**
* Returns a list of report log items with given criteria
*
* @param array $filter Key-value array:
* 'from_time' => DateTime
* 'till_time' => DateTime
* @param array $order Key-value array with order options:
* 'direction' => string, 'ascent' or 'descent'
* @param array $limit Key-value array:
* 'offset' => int
* 'count' => int
*
* @return array
*/
public function list(array &$filter, array &$order, array &$limit): array;
/**
* Returns the number of report log items matching the specified filter and limits
*
* @param array $filter Key-value array with filtering parameters
* @param array $limit Key-value array with limits
*
* @return int
*/
public function count(array &$filter, array &$limit): int;
/**
* Deletes report log items from the database
*
* @param array $filter Key-value array with filtering parameters
* @param array $order Key-value array with order options:
* 'direction' => string, 'ascent' or 'descent'
* @param array $limit Key-value array with limits
*
* @return void
*/
public function delete(array &$filter, array &$order, array &$limit): void;
}

View File

@@ -0,0 +1,123 @@
<?php
/**
* dmarc-srg - A php parser, viewer and summary report generator for incoming DMARC reports.
* Copyright (C) 2022 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 ReportMapperInterface
*
* @category API
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg\Database;
interface ReportMapperInterface
{
/**
* Fetches report data from the database and stores it in the passed array
*
* @param array $data Array with report data. To identify the report,
* the array must contain at least two fields:
* `report_id` - External report id from the xml file
* `domain` - Fully Qualified Domain Name without a trailing dot
*
* @return void
*/
public function fetch(array &$data): void;
/**
* Inserts report data into the database.
*
* @param array $data Report data
*
* @return void
*/
public function save(array &$data): void;
/**
* Sets report record property in database.
*
* It has nothing to do with the fields of the report itself.
*
* @param array $data Report data
* @param string $name Property name. Currently only `seen` is supported.
* @param variant $value Property value
*
* @return void
*/
public function setProperty(array &$data, string $name, $value): void;
/**
* Returns a list of reports with specified parameters
*
* This method returns a list of reports that depends on the $filter, $order and $limit.
*
* @param array $filter Key-value array with filtering parameters
* @param array $order Key-value array:
* 'field' => string, 'begin_time'
* 'direction' => string, 'ascent' or 'descent'
* @param array $limit Key-value array with two keys: `offset` and `count`
*
* @return array
*/
public function list(array &$filter, array &$order, array &$limit): array;
/**
* Returns the number of reports matching the specified filter and limits
*
* @param array $filter Key-value array with filtering parameters
* @param array $limit Key-value array with two keys: `offset` and `count`
*
* @return int
*/
public function count(array &$filter, array &$limit): int;
/**
* Deletes reports from the database
*
* It deletes repors form the database. The filter options `dkim` and `spf` do not affect this.
*
* @param array $filter Key-value array with filtering parameters
* @param array $order Key-value array:
* 'field' => string, 'begin_time'
* 'direction' => string, 'ascent' or 'descent'
* @param array $limit Key-value array with two keys: `offset` and `count`
*
* @return void
*/
public function delete(array &$filter, array &$order, array &$limit): void;
/**
* Returns a list of months with years of the form: 'yyyy-mm' for which there is at least one report
*
* @return array
*/
public function months(): array;
/**
* Returns a list of reporting organizations from which there is at least one report
*
* @return array
*/
public function organizations(): array;
}

View File

@@ -0,0 +1,64 @@
<?php
/**
* dmarc-srg - A php parser, viewer and summary report generator for incoming DMARC reports.
* Copyright (C) 2022 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 SettingMapperInterface
*
* @category API
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg\Database;
interface SettingMapperInterface
{
/**
* Returns setting value as a string by key
*
* @param string $key
*
* @return string
*/
public function value(string $key): string;
/**
* Returns a key-value array of the setting list like this:
* [ 'name1' => 'value1', 'name2' => 'value2' ]
*
* @return array
*/
public function list(): array;
/**
* Saves the setting to the database
*
* Updates the value of the setting in the database if the setting exists there or insert a new record otherwise.
*
* @param string $name Setting name
* @param string $value Setting value
*
* @return void
*/
public function save(string $name, string $value): void;
}

View File

@@ -0,0 +1,71 @@
<?php
/**
* dmarc-srg - A php parser, viewer and summary report generator for incoming DMARC reports.
* Copyright (C) 2022 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 StatisticsMapperInterface
*
* @category API
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg\Database;
interface StatisticsMapperInterface
{
/**
* Returns summary information for the specified domain and date range
*
* @param Domain|null $domain Domain for which the information is needed. Null is for all domains.
* @param array $range Array with two dates
*
* @return array Array with Summary information:
* 'emails' => [
* 'total' => total email processed (int)
* 'dkim_spf_aligned' => Both DKIM and SPF aligned (int)
* 'dkim_aligned' => Only DKIM aligned (int)
* 'spf_aligned' => Only SPF aligned (int)
* ];
*/
public function summary($domain, array &$range): array;
/**
* Returns a list of ip-addresses from which the e-mail messages were received, with some statistics for each one
*
* @param Domain|null $domain Domain for which the information is needed. Null is for all domains.
* @param array $range Array with two dates
*
* @return array A list of ip-addresses with fields `ip`, `emails`, `dkim_aligned`, `spf_aligned`
*/
public function ips($domain, array &$range): array;
/**
* Returns a list of organizations that sent the reports with some statistics for each one
*
* @param Domain|null $domain Domain for which the information is needed. Null is for all domains.
* @param array $range Array with two dates
*
* @return array List of organizations with fields `name`, `reports`, `emails`
*/
public function organizations($domain, array &$range): array;
}

View File

@@ -0,0 +1,44 @@
<?php
/**
* dmarc-srg - A php parser, viewer and summary report generator for incoming DMARC reports.
* Copyright (C) 2022 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 UpgraderMapperInterface
*
* @category API
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg\Database;
interface UpgraderMapperInterface
{
/**
* Starts upgrading the database structure
*
* @param string $target Target version of the database structure to upgrade to
*
* @return void
*/
public function go(string $target): void;
}