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,99 @@
<?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/>.
*
* =========================
*
* This file contains Admin class
*
* @category API
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg;
use Liuch\DmarcSrg\Mail\MailBoxes;
use Liuch\DmarcSrg\Directories\DirectoryList;
use Liuch\DmarcSrg\Exception\LogicException;
/**
* It's the main class for accessing administration functions.
*/
class Admin
{
private $core = null;
/**
* The constructor of the class
*
* @param Core $core Instace of the Core class
*/
public function __construct($core)
{
$this->core = $core;
}
/**
* Returns information about the database, directories, and mailboxes as an array.
*
* @return array Contains fields: `database`, `state`, `mailboxes`, `directories`.
*/
public function state(): array
{
$res = [];
$res['database'] = $this->core->database()->state();
$res['mailboxes'] = (new MailBoxes())->list();
$res['directories'] = array_map(function ($dir) {
return $dir->toArray();
}, (new DirectoryList())->list());
if ($res['database']['correct'] ?? false) {
$res['state'] = 'Ok';
} else {
$res['state'] = 'Err';
}
return $res;
}
/**
* Checks the availability of report sources.
*
* @param int $id Id of the checked source. If $id == 0 then all available sources with the passed type
* will be checked.
* @param string $type Type of the checked source.
*
* @return array Result array with `error_code` and `message` fields.
* For one resource and if there is no error,
* a field `status` will be added to the result.
*/
public function checkSource(int $id, string $type): array
{
switch ($type) {
case 'mailbox':
return (new MailBoxes())->check($id);
case 'directory':
return (new DirectoryList())->check($id);
}
throw new LogicException('Unknown resource type');
}
}

View File

@@ -0,0 +1,147 @@
<?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/>.
*
* =========================
*
* This file contains Auth class.
*
* @category API
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg;
use Liuch\DmarcSrg\Exception\AuthException;
/**
* Class for working with authentication data.
*/
class Auth
{
private $core = null;
/**
* The constructor
*
* @param Core $core
*/
public function __construct(object $core)
{
$this->core = $core;
}
/**
* Checks if authentication is enabled.
*
* The method examines the key `password` in $admin array from the config file.
* It must exist and not be null.
*
* @return bool
*/
public function isEnabled(): bool
{
return $this->core->config('admin/password') !== null;
}
/**
* The authetication with a username and password.
*
* This method checks the password passed in $password and creates a user session.
* This method throws an exception if the passed password is wrong.
* The password with an empty string is always wrong!
*
* @param string $username - Must be an empty string, it is currently not used.
* @param string $password - Must not be an empty string.
*
* @return array Array with `error_code` and `message` fields.
*/
public function login(string $username, string $password): array
{
if ($username !== '' || $this->core->config('admin/password') === '' || !$this->isAdminPassword($password)) {
throw new AuthException('Authentication failed. Try again');
}
$this->core->userId(0);
return [
'error_code' => 0,
'message' => 'Authentication succeeded'
];
}
/**
* Removes the current user's session.
*
* @return array Array with `error_code` and `message` fields.
*/
public function logout(): array
{
$this->core->destroySession();
return [
'error_code' => 0,
'message' => 'Logged out successfully'
];
}
/**
* Checks if the user session exists.
*
* This method throws an exception if authentication needed.
*
* @return void
*/
public function isAllowed(): void
{
if ($this->isEnabled()) {
if ($this->core->userId() === false) {
throw new AuthException('Authentication needed', -2);
}
}
}
/**
* Checks if the passed password is the admin password.
*
* Throws an exception if the passed password is not the admin password.
*
* @param string $password Password to check
*
* @return void
*/
public function checkAdminPassword(string $password): void
{
if (!$this->isAdminPassword($password)) {
throw new AuthException('Incorrect password');
}
}
/**
* Checks if $password equals the admin password.
*
* @param string $password Password to check
*
* @return bool
*/
private function isAdminPassword(string $password): bool
{
return $password === $this->core->config('admin/password');
}
}

View File

@@ -0,0 +1,50 @@
<?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/>.
*
* =========================
*
* This file contains common classes
*
* @category Common
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg;
/**
* Static common arrays
*/
class Common
{
/**
* This array needs for converting the align result text constant to integer value and back
* in Report and ReportList classes
*/
public static $align_res = [ 'fail', 'unknown', 'pass' ];
/**
* This array needs for converting the the disposition result text constant to integer value and back
* in Report and ReportList classes
*/
public static $disposition = [ 'reject', 'quarantine', 'none' ];
}

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 class Config
*
* @category API
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg;
use Liuch\DmarcSrg\Exception\LogicException;
/**
* This class is for storing configuration data to avoid using globas variables
*/
class Config
{
private $data = [];
/**
* The constructor
*
* @param string $config_file A php config file to load.
*/
public function __construct(string $config_file)
{
require($config_file);
foreach ([
'debug', 'database', 'mailboxes', 'directories',
'admin', 'mailer', 'fetcher', 'cleaner'
] as $key
) {
$this->data[$key] = $$key ?? null;
}
}
/**
* Returns config value by its name
*
* @param string $name Setting name. Hierarchy supported via '/'
* @param mixed $default Value to be returned if the required config item is missing or null
*
* @return mixed
*/
public function get(string $name, $default = null)
{
$nm_i = 0;
$path = explode('/', $name);
$data = $this->data;
do {
$key = $path[$nm_i++];
if (empty($key)) {
throw new LogicException('Incorrect setting name: ' .$name);
}
if (!isset($data[$key])) {
return $default;
}
$data = $data[$key];
if (!isset($path[$nm_i])) {
return $data ?? $default;
}
} while (gettype($data) === 'array');
return $default;
}
}

View File

@@ -0,0 +1,362 @@
<?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/>.
*
* =========================
*
* This file contains the Core class
*
* @category API
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg;
use Liuch\DmarcSrg\Exception\SoftException;
use Liuch\DmarcSrg\Exception\LogicException;
/**
* It's class for accessing to most methods for working with http, json data,
* the user session, getting instances of some classes
*/
class Core
{
public const APP_VERSION = '1.7';
private const SESSION_NAME = 'session';
private const HTML_FILE_NAME = 'index.html';
private $modules = [];
private static $instance = null;
/**
* The constructor
*
* @param array $params Array with modules to be bind to
*/
public function __construct($params)
{
foreach ([ 'admin', 'auth', 'config', 'database', 'ehandler', 'status' ] as $key) {
if (isset($params[$key])) {
$this->modules[$key] = $params[$key];
}
}
self::$instance = $this;
}
/**
* Returns the method of the current http request.
*
* @return string http method
*/
public static function method(): string
{
return $_SERVER['REQUEST_METHOD'];
}
/**
* Returns array of request headers in lowercase mode.
*
* @return array
*/
public static function getHeaders(): array
{
return array_change_key_case(getallheaders(), CASE_LOWER);
}
/**
* Sets or gets the current user's id
*
* In case $id is null, the method returns the current user's id.
* In case $id is integer value, the method sets this value as the current user's id.
* It returns false if there is an error.
*
* @param int|void $id User id to set it.
*
* @return int|bool User id or false in case of error.
*/
public function userId($id = null)
{
$start_f = false;
if ((self::cookie(self::SESSION_NAME) !== '' || $id !== null) && session_status() !== PHP_SESSION_ACTIVE) {
$start_f = true;
self::sessionStart();
}
$res = null;
if (gettype($id) === 'integer') {
$_SESSION['user_id'] = $id;
}
$res = isset($_SESSION['user_id']) ? intval($_SESSION['user_id']) : false;
if ($start_f) {
session_write_close();
}
return $res;
}
/**
* Deletes the session of the current user and the corresponding cookie.
*
* @return void
*/
public function destroySession(): void
{
if (self::cookie(self::SESSION_NAME)) {
if (session_status() !== PHP_SESSION_ACTIVE) {
self::sessionStart();
}
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$scp = session_get_cookie_params();
$cep = [
'expires' => time() - 42000,
'path' => $scp['path'],
'domain' => $scp['domain'],
'secure' => $scp['secure'],
'httponly' => $scp['httponly'],
'samesite' => $scp['samesite']
];
setcookie(self::SESSION_NAME, '', $cep);
session_write_close();
}
}
}
/**
* Returns true if the http request asks for json data.
*
* @return bool
*/
public static function isJson(): bool
{
$headers = self::getHeaders();
return (isset($headers['accept']) && $headers['accept'] === 'application/json');
}
/**
* Sends the html file to the client.
*
* @return void
*/
public static function sendHtml(): void
{
if (file_exists(Core::HTML_FILE_NAME)) {
readfile(Core::HTML_FILE_NAME);
}
}
/**
* Sends data from an array as json string to the client.
*
* @param array $data - Data to send.
*
* @return void
*/
public static function sendJson(array $data): void
{
$res_str = json_encode($data);
if ($res_str === false) {
$res_str = '[]';
}
header('content-type: application/json; charset=UTF-8');
echo $res_str;
}
/**
* Sends a Bad Request response to the client.
*
* @return void
*/
public static function sendBad(): void
{
http_response_code(400);
echo 'Bad request';
}
/**
* Retrieves json data from the request and return it as an array.
*
* Returns an array with data or null if there is an error.
*
* @return array|null Data from the request
*/
public static function getJsonData()
{
$res = null;
$headers = self::getHeaders();
if (isset($headers['content-type']) && $headers['content-type'] === 'application/json') {
$str = file_get_contents('php://input');
if ($str) {
$res = json_decode($str, true);
}
}
return $res;
}
/**
* Returns an instance of the class Auth.
*
* @return Auth
*/
public function auth()
{
return $this->getModule('auth', true);
}
/**
* Returns an instance of the class Status.
*
* @return Status instance of Status
*/
public function status()
{
return $this->getModule('status', true);
}
/**
* Returns an instance of the class Admin.
*
* @return Admin instance of Admin
*/
public function admin()
{
return $this->getModule('admin', true);
}
/**
* Returns an instance of the class Database.
*
* @return DatabaseController
*/
public function database()
{
return $this->getModule('database', true);
}
/**
* Returns an instance of the class ErrorHandler
*
* @return ErrorHandler
*/
public function errorHandler()
{
return $this->getModule('ehandler', true);
}
/**
* Returns the current logger.
* Just a proxy method to return the logger from ErrorHandler
*
* @return LoggerInterface
*/
public function logger()
{
return $this->errorHandler()->logger();
}
/**
* Returns instance of the object
*
* @return self
*/
public static function instance()
{
return self::$instance;
}
/**
* Returns the config value by its name
*
* @param string $name Config item name. Hierarchy supported via '/'
* @param mixed $default Value to be returned if the required config item is missing or null
*
* @return mixed
*/
public function config(string $name, $default = null)
{
return $this->getModule('config', false)->get($name, $default);
}
/**
* Gets or sets a cookie with the specified name.
*
* @param string $name the cookie name to get or to set
* @param string|null $value
* @param array|null $params
*
* @return string|boolean The cookie value or false if there is an error
*/
private static function cookie($name, $value = null, $params = null)
{
if (!$value) {
return isset($_COOKIE[$name]) ? $_COOKIE[$name] : '';
}
if (setcookie($name, $value, $params)) {
return $value;
}
return false;
}
/**
* Starts the user session
*
* @return void
*/
private static function sessionStart(): void
{
if (!session_start(
[
'name' => self::SESSION_NAME,
'cookie_path' => dirname($_SERVER['REQUEST_URI']),
'cookie_httponly' => true,
'cookie_samesite' => 'Strict'
]
)
) {
throw new SoftException('Failed to start a user session');
}
}
/**
* Returns a module instance by its name. Lazy initialization is used.
*
* @param string $name Module name
* @param bool $core Whether to pass $this to the constructor
*
* @return object
*/
private function getModule(string $name, bool $core)
{
$module = $this->modules[$name] ?? null;
switch (gettype($module)) {
case 'array':
if ($core) {
$module = new $module[0]($this, ...($module[1] ?? []));
} else {
$module = new $module[0](...($module[1] ?? []));
}
$this->modules[$name] = $module;
break;
case 'NULL':
throw new LogicException('Attempt to initiate an unloaded module ' . $name);
}
return $module;
}
}

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;
}

View File

@@ -0,0 +1,43 @@
<?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/>.
*
* =========================
*
* This file contains the class DateTime
*
* @category API
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg;
/**
* This class extends the standard class to get ISO 8601 value in json_encode.
*/
class DateTime extends \DateTime implements \JsonSerializable
{
public function jsonSerialize(): string
{
return $this->format(\DateTime::ATOM);
}
}

View File

@@ -0,0 +1,165 @@
<?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/>.
*
* =========================
*
* This file contains the class Directory
*
* @category API
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg\Directories;
use Liuch\DmarcSrg\ErrorHandler;
use Liuch\DmarcSrg\Exception\SoftException;
use Liuch\DmarcSrg\Exception\LogicException;
use Liuch\DmarcSrg\Exception\RuntimeException;
/**
* This class is designed to work with the report directories which are listed in the configuration file.
*/
class Directory
{
private $id = null;
private $name = null;
private $location = null;
/**
* It's the constructor of the class
*
* @param int $id Id of the directory. In fact, it is a serial number in the configuration file.
* @param array $data An array with the following fields:
* `location` (string) - Location of the directory in the file system.
* `name` (string) - Name of the directory. It is optional.
*
* @return void
*/
public function __construct(int $id, array $data)
{
if (isset($data['name']) && gettype($data['name']) !== 'string') {
throw new LogicException('Directory name must be either null or a string value');
}
if (!isset($data['location']) || gettype($data['location']) !== 'string') {
throw new LogicException('Directory location must be a string value');
}
if (empty($data['location'])) {
throw new LogicException('Directory location must not be an empty string');
}
$this->id = $id;
$this->name = $data['name'] ?? null;
$this->location = $data['location'];
if (empty($this->name)) {
$this->name = 'Directory ' . $this->id;
}
if (substr($this->location, -1) !== '/') {
$this->location .= '/';
}
}
/**
* Returns an array with directory configuration data.
*
* @return array
*/
public function toArray(): array
{
return [
'id' => $this->id,
'name' => $this->name,
'location' => $this->location
];
}
/**
* Checks the existence and accessibility of the directory. Returns the result as an array.
*
* @return array
*/
public function check(): array
{
try {
self::checkPath($this->location, true);
self::checkPath($this->location . 'failed/', false);
} catch (RuntimeException $e) {
return ErrorHandler::exceptionResult($e);
}
return [
'error_code' => 0,
'message' => 'Successfully',
'status' => [
'files' => $this->count()
]
];
}
/**
* Returns the total number of files in the directory.
*
* @return int
*/
public function count(): int
{
$cnt = 0;
try {
$fs = new \FilesystemIterator($this->location);
} catch (\Exception $e) {
throw new RuntimeException("Error accessing directory {$this->location}", -1, $e);
}
foreach ($fs as $entry) {
if ($entry->isFile()) {
++$cnt;
}
}
return $cnt;
}
/**
* Checks accessibility of a directory by its path. Throws an exception in case of any error.
*
* @param string $path Path to the directory to check.
* @param bool $existence If true, the absence of the directory causes an error.
*
* @return void
*/
private static function checkPath(string $path, bool $existence): void
{
if (!file_exists($path)) {
if ($existence) {
throw new SoftException($path . ' directory does not exist!');
}
return;
}
if (!is_dir($path)) {
throw new SoftException($path . ' is not a directory!');
}
if (!is_readable($path)) {
throw new SoftException($path . ' directory is not readable!');
}
if (!is_writable($path)) {
throw new SoftException($path . ' directory is not writable!');
}
}
}

View File

@@ -0,0 +1,147 @@
<?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/>.
*
* =========================
*
* This file contains the class DirectoryList
*
* @category API
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg\Directories;
use Liuch\DmarcSrg\Core;
use Liuch\DmarcSrg\Exception\LogicException;
/**
* This class is designed to work with the list of report directories which are listed in the configuration file.
*/
class DirectoryList
{
private $list = null;
/**
* Returns a list of directories for the setting file
*
* @return array Array with instances of Directory class
*/
public function list(): array
{
$this->ensureList();
return $this->list;
}
/**
* Returns an instance of the Directory class by its Id
*
* @param int $id Id of the required directory
*
* @return Directory
*/
public function directory(int $id)
{
$this->ensureList();
if ($id <= 0 || $id > count($this->list)) {
throw new LogicException('Incorrect directory Id');
}
return $this->list[$id - 1];
}
/**
* Checks the accessibility of the specified directory or all the directories from configuration file if $id is 0.
*
* @param int $id Directory Id to check
*
* @return array Result array with `error_code` and `message` fields. For one directory and if there is no error,
* a field `status` will be added to the result.
*/
public function check(int $id): array
{
if ($id !== 0) {
$dir = $this->directory($id);
return $dir->check();
}
$this->ensureList();
$results = [];
$err_cnt = 0;
$dir_cnt = count($this->list);
for ($i = 0; $i < $dir_cnt; ++$i) {
$r = $this->list[$i]->check();
if ($r['error_code'] !== 0) {
++$err_cnt;
}
$results[] = $r;
}
$res = [];
if ($err_cnt === 0) {
$res['error_code'] = 0;
$res['message'] = 'Successfully';
} else {
$res['error_code'] = -1;
$res['message'] = sprintf('%d of %d directories have failed the check', $err_cnt, $dir_cnt);
}
$res['results'] = $results;
return $res;
}
/**
* Creates an array of directories from the configuration file if it does not exist
* for using in other methods of the class.
*
* @return void
*/
private function ensureList(): void
{
if (!is_null($this->list)) {
return;
}
$directories = Core::instance()->config('directories');
$this->list = [];
if (is_array($directories)) {
$cnt = count($directories);
if ($cnt > 0) {
if (isset($directories[0])) {
$id = 1;
for ($i = 0; $i < $cnt; ++$i) {
try {
$this->list[] = new Directory($id, $directories[$i]);
++$id;
} catch (LogicException $d) {
// Just ignore this directory setting.
}
}
} else {
try {
$this->list[] = new Directory(1, $directories);
} catch (LogicException $e) {
// Just ignore this directory setting.
}
}
}
}
}
}

View File

@@ -0,0 +1,286 @@
<?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/>.
*
* =========================
*
* This file contains the class Domain
*
* @category API
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg\Domains;
use Liuch\DmarcSrg\Core;
use Liuch\DmarcSrg\Exception\SoftException;
use Liuch\DmarcSrg\Exception\LogicException;
use Liuch\DmarcSrg\Exception\DatabaseNotFoundException;
/**
* It's a class for accessing to stored domains data
*
* This class is designed for storing and manipulating domain data.
* All queries to the datatabase are made in lazy mode.
*/
class Domain
{
private $db = null;
private $ex_f = null;
private $data = [
'id' => null,
'fqdn' => null,
'active' => null,
'description' => null,
'created_time' => null,
'updated_time' => null
];
/**
* It's a constructor of the class
*
* Some examples of using:
* (new Domain(1))->fqdn(); - will return the fully qualified domain name for the domain with id = 1
* (new Domain('example.com'))->description(); - will return the description for the domain example.com
* (new Domain([ 'fqdn' => 'example.com', 'description' => 'an expample domain' ])->save(); - will add
* this domain to the database if it does not exist in it.
*
* @param int|string|array $data Some domain data to identify it
* int value is treated as domain id
* string value is treated as a FQDN
* array has these fields: `id`, `fqdn`, `active`, `description`
* and usually uses for creating a new domain item.
* Note: The values of the fields `created_time` and `updated_time`
* will be ignored while saving to the database.
* @param DatabaseController $db The database controller
*
* @return void
*/
public function __construct($data, $db = null)
{
$this->db = $db ?? Core::instance()->database();
switch (gettype($data)) {
case 'integer':
$this->data['id'] = $data;
return;
case 'string':
$this->data['fqdn'] = strtolower($data);
$this->checkFqdn();
return;
case 'array':
if (isset($data['id'])) {
if (gettype($data['id']) !== 'integer') {
break;
}
$this->data['id'] = $data['id'];
}
if (isset($data['fqdn'])) {
if (gettype($data['fqdn']) !== 'string') {
break;
}
$this->data['fqdn'] = strtolower($data['fqdn']);
$this->checkFqdn();
}
if (isset($data['active'])) {
if (gettype($data['active']) !== 'boolean') {
break;
}
$this->data['active'] = $data['active'];
} else {
$this->data['active'] = false;
}
if (isset($data['description'])) {
if (gettype($data['description']) !== 'string') {
break;
}
$this->data['description'] = $data['description'];
}
if (isset($data['created_time'])) {
if (gettype($data['created_time']) !== 'object') {
break;
}
$this->data['created_time'] = $data['created_time'];
}
if (isset($data['updated_time'])) {
if (gettype($data['updated_time']) !== 'object') {
break;
}
$this->data['updated_time'] = $data['updated_time'];
}
if (!is_null($this->data['id']) || !is_null($this->data['fqdn'])) {
return;
}
}
throw new LogicException('Wrong domain data');
}
/**
* Returns true if the domain exists in the database or false otherwise
*
* @return bool Whether the domain exists
*/
public function exists(): bool
{
if (is_null($this->ex_f)) {
$this->ex_f = $this->db->getMapper('domain')->exists($this->data);
}
return $this->ex_f;
}
/**
* Returns the domain id
*
* @return int The domain id
*/
public function id(): int
{
if (is_null($this->data['id'])) {
$this->fetchData();
}
return $this->data['id'];
}
/**
* Returns the domain's FQDN
*
* @return string FQDN for the domain
*/
public function fqdn(): string
{
if (is_null($this->data['fqdn'])) {
$this->fetchData();
}
return $this->data['fqdn'];
}
/**
* Whether the domain is active or not
*
* When the domain is inactive, all incoming reports for it are ignored
* but the domain will still be included in summary reports.
*
* @return bool
*/
public function active(): bool
{
if (is_null($this->data['active'])) {
$this->fetchData();
}
return $this->data['active'];
}
/**
* Returns the domain's description
*
* @return string|null The description of the domain if it exists or null otherwise
*/
public function description()
{
if (is_null($this->data['id']) || is_null($this->data['fqdn'])) {
$this->fetchData();
}
return $this->data['description'];
}
/**
* Returns an array with domain data
*
* @return array Domain data
*/
public function toArray(): array
{
if (is_null($this->data['id']) || is_null($this->data['fqdn'])) {
$this->fetchData();
}
$res = $this->data;
unset($res['id']);
return $res;
}
/**
* Saves the domain to the database
*
* Updates the domain's description in the database if the domain exists or insert a new record otherwise.
* The domain id is ignored in the insert mode.
*
* @return void
*/
public function save(): void
{
$this->db->getMapper('domain')->save($this->data);
$this->ex_f = true;
}
/**
* Deletes the domain from the database
*
* Deletes the domain if there are no reports for this domain in the database.
* If you want to stop handling reports for this domain, just make it inactive.
*
* @return void
*/
public function delete(): void
{
if (is_null($this->data['id'])) {
$this->fetchData();
}
$this->db->getMapper('domain')->delete($this->data);
$this->ex_f = false;
}
/**
* Removes the trailing dot from the domain name and checks it for an empty value.
*
* @return void
*/
private function checkFqdn(): void
{
$fqdn = trim($this->data['fqdn']);
if (substr($fqdn, -1) === '.') {
$fqdn = trim(substr($fqdn, 0, -1));
}
if ($fqdn === '') {
throw new SoftException('The domain name must not be an empty string');
}
$this->data['fqdn'] = $fqdn;
}
/**
* Fetches the domain data from the database by its id or name
*
* @return void
*/
private function fetchData(): void
{
if ($this->ex_f === false) {
return;
}
try {
$this->db->getMapper('domain')->fetch($this->data);
$this->ex_f = true;
} catch (DatabaseNotFoundException $e) {
$this->ex_f = false;
throw new SoftException('Domain not found');
}
}
}

View File

@@ -0,0 +1,79 @@
<?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/>.
*
* =========================
*
* This file contains the class DomainList
*
* @category API
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg\Domains;
use Liuch\DmarcSrg\Core;
/**
* This class is designed to work with the list of domains
*/
class DomainList
{
private $db = null;
/**
* The constructor
*
* @param DatabaseController $db The database controller
*/
public function __construct($db = null)
{
$this->db = $db ?? Core::instance()->database();
}
/**
* Returns a list of domains from the database
*
* @return array Array with instances of Domain class
*/
public function getList(): array
{
$list = [];
foreach ($this->db->getMapper('domain')->list() as $dd) {
$list[] = new Domain($dd, $this->db);
}
return [
'domains' => $list,
'more' => false
];
}
/**
* Returns an ordered array with domain names from the database
*
* @return array Array of strings
*/
public function names(): array
{
return $this->db->getMapper('domain')->names();
}
}

View File

@@ -0,0 +1,156 @@
<?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 ErrorHandler class
*
* @category Common
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg;
use Liuch\DmarcSrg\Log\LoggerInterface;
use Liuch\DmarcSrg\Log\LoggerAwareInterface;
use Liuch\DmarcSrg\Exception\SoftException;
/**
* Uncaught exception handler
*/
class ErrorHandler implements LoggerAwareInterface
{
private $core = null;
private $logger = null;
/**
* The constructor
*
* @param Core $core
*/
public function __construct(object $core)
{
$this->core = $core;
}
/**
* Handle uncaught exceptions. Used by set_exception_handler and set_error_handler functions
*
* @param Throwable $e an exception to handle. For set_error_handler it is ErrorException.
*
* @return void
*/
public function handleException(\Throwable $e): void
{
$debug = $this->core->config('debug', 0);
if ($this->logger) {
$this->logger->error(strval($e));
}
if (php_sapi_name() === 'cli') {
echo self::getText($e, $debug);
exit(1);
} else {
Core::sendJson(self::getResult($e, $debug));
}
}
/**
* Returns an result array based on the passed exception's data.
* If the debug mode is enabled, the `debug_info` field will be added to the result.
*
* @param Throwable $e an exception for which the result is generated
*
* @return array
*/
public static function exceptionResult(\Throwable $e): array
{
return self::getResult($e, Core::instance()->config('debug', 0));
}
/**
* Returns information about the passed exception as text.
* If the debug is enabled, debug information will be added.
*
* @param Throwable $e an exception for which the text is generated
*
* @return string
*/
public static function exceptionText(\Throwable $e): string
{
return self::getText($e, Core::instance()->config('debug', 0));
}
/**
* Sets a logger to log uncaught exceptions and errors
*/
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
/**
* Returns the current logger
*/
public function logger()
{
return $this->logger;
}
private static function getResult(\Throwable $e, int $debug): array
{
$code = $e->getCode();
if ($code === 0) {
$code = -1;
}
$res = [
'error_code' => $code,
'message' => $e->getMessage()
];
if ($debug &&
(Core::instance()->userId() !== false || php_sapi_name() === 'cli') &&
!($e instanceof SoftException)
) {
$prev = $e->getPrevious();
$res['debug_info'] = [
'code' => ($prev ?? $e)->getCode(),
'content' => strval($prev ?? $e)
];
}
return $res;
}
private static function getText(\Throwable $e, int $debug): string
{
$msg = 'Error: ' . $e->getMessage() . ' (' . $e->getCode() . ')' . PHP_EOL;
if (!$debug) {
return $msg;
}
return '-----' . PHP_EOL
. $msg
. '-----' . PHP_EOL
. $e . PHP_EOL;
}
}

View File

@@ -0,0 +1,39 @@
<?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 AuthException class
*
* @category Common
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg\Exception;
/**
* Represents an authentication error
*/
class AuthException extends SoftException
{
}

View File

@@ -0,0 +1,39 @@
<?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 DatabaseException class
*
* @category Common
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg\Exception;
/**
* Represents an error in the database
*/
class DatabaseException extends RuntimeException
{
}

View File

@@ -0,0 +1,66 @@
<?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 DatabaseExceptionFactory class
*
* @category Common
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg\Exception;
/**
* Factory class for DatabaseException
*/
class DatabaseExceptionFactory
{
/**
* Creates a DatabaseException instance with an appropriate message based on the passed class's name and error code.
*
* @param Exception $origin The original exception
*
* @return DatabaseException
*/
public static function fromException(\Throwable $origin)
{
$msg = null;
if (get_class($origin) === 'PDOException') {
switch ($origin->getCode()) {
case 1044:
case 1045:
$msg = 'Database access denied';
break;
case 2002:
case 2006:
$msg = 'Database connection error';
break;
}
}
if (!$msg) {
$msg = 'Database error';
}
return new DatabaseException($msg, -1, $origin);
}
}

View File

@@ -0,0 +1,39 @@
<?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 DatabaseFatalException class
*
* @category Common
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg\Exception;
/**
* Represents an error in the database
*/
class DatabaseFatalException extends LogicException
{
}

View File

@@ -0,0 +1,39 @@
<?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 DatabaseNotFoundException class
*
* @category Common
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg\Exception;
/**
* Represents the case where a row was not found in the database
*/
class DatabaseNotFoundException extends DatabaseException
{
}

View File

@@ -0,0 +1,39 @@
<?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 LogicException class
*
* @category Common
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg\Exception;
/**
* Base LogicException
*/
class LogicException extends \LogicException
{
}

View File

@@ -0,0 +1,39 @@
<?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 MailboxException class
*
* @category Common
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg\Exception;
/**
* Represents an error of a mailbox
*/
class MailboxException extends RuntimeException
{
}

View File

@@ -0,0 +1,43 @@
<?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 RuntimeException class
*
* @category Common
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg\Exception;
/**
* Base RuntimeException
*/
class RuntimeException extends \RuntimeException
{
public function __construct(string $message = '', int $code = -1, \Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
}

View File

@@ -0,0 +1,39 @@
<?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 SoftException class
*
* @category Common
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg\Exception;
/**
* This exception is ignored in the debug mode
*/
class SoftException extends RuntimeException
{
}

View File

@@ -0,0 +1,47 @@
<?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 class LogLevel
*
* @category API
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg\Log;
/**
* Describes log levels
*/
class LogLevel
{
const EMERGENCY = 'emergency';
const ALERT = 'alert';
const CRITICAL = 'critical';
const ERROR = 'error';
const WARNING = 'warning';
const NOTICE = 'notice';
const INFO = 'info';
const DEBUG = 'debug';
}

View File

@@ -0,0 +1,47 @@
<?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 interface LoggerAwareInterface
*
* @category API
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg\Log;
/**
* Describes a logger-aware instance
*/
interface LoggerAwareInterface
{
/**
* Sets a logger instance on the object
*
* @param LoggerInterface $logger
*
* @return void
*/
public function setLogger(LoggerInterface $logger): void;
}

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 interface LoggerInterface
*
* @category API
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg\Log;
/**
* Describes a logger instance
* The message MUST be a string or object implementing __toString().
*/
interface LoggerInterface
{
/**
* System is unusable
*
* @param string $message
* @param array $context
*
* @return void
*/
public function emergency($message, array $context = []): void;
/**
* Action must be taken immediately
*
* @param string $message
* @param array $context
*
* @return void
*/
public function alert($message, array $context = []): void;
/**
* Critical conditions
*
* @param string $message
* @param array $context
*
* @return void
*/
public function critical($message, array $context = []): void;
/**
* Runtime errors that do not require immediate action but should typically
* be logged and monitored
*
* @param string $message
* @param array $context
*
* @return void
*/
public function error($message, array $context = []): void;
/**
* Exceptional occurrences that are not errors
*
* @param string $message
* @param array $context
*
* @return void
*/
public function warning($message, array $context = []): void;
/**
* Normal but significant events
*
* @param string $message
* @param array $context
*
* @return void
*/
public function notice($message, array $context = []): void;
/**
* Interesting events
*
* @param string $message
* @param array $context
*
* @return void
*/
public function info($message, array $context = []): void;
/**
* Detailed debug information
*
* @param string $message
* @param array $context
*
* @return void
*/
public function debug($message, array $context = []): void;
/**
* Logs with an arbitrary level
*
* @param mixed $level
* @param string $message
* @param array $context
*
* @return void
*/
public function log($level, $message, array $context = []): void;
}

View File

@@ -0,0 +1,172 @@
<?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 class PhpSystemLogger;
*
* @category API
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg\Log;
/**
* Implements a logger to log messages via PHP's system logger,
* using the OS's system logging mechanism or a file, depending on
* what the error_log PHP configuration directive is set to.
*/
class PhpSystemLogger implements LoggerInterface
{
/**
* System is unusable
*
* @param string $message
* @param array $context
*
* @return void
*/
public function emergency($message, array $context = []): void
{
$this->log(LogLevel::EMERGENCY, $message, $context);
}
/**
* Action must be taken immediately
*
* @param string $message
* @param array $context
*
* @return void
*/
public function alert($message, array $context = []): void
{
$this->log(LogLevel::ALERT, $message, $context);
}
/**
* Critical conditions
*
* @param string $message
* @param array $context
*
* @return void
*/
public function critical($message, array $context = []): void
{
$this->log(LogLevel::CRITICAL, $message, $context);
}
/**
* Runtime errors that do not require immediate action but should typically
* be logged and monitored
*
* @param string $message
* @param array $context
*
* @return void
*/
public function error($message, array $context = []): void
{
$this->log(LogLevel::ERROR, $message, $context);
}
/**
* Exceptional occurrences that are not errors
*
* @param string $message
* @param array $context
*
* @return void
*/
public function warning($message, array $context = []): void
{
$this->log(LogLevel::WARNING, $message, $context);
}
/**
* Normal but significant events
*
* @param string $message
* @param array $context
*
* @return void
*/
public function notice($message, array $context = []): void
{
$this->log(LogLevel::NOTICE, $message, $context);
}
/**
* Interesting events
*
* @param string $message
* @param array $context
*
* @return void
*/
public function info($message, array $context = []): void
{
$this->log(LogLevel::INFO, $message, $context);
}
/**
* Detailed debug information
*
* @param string $message
* @param array $context
*
* @return void
*/
public function debug($message, array $context = []): void
{
$this->log(LogLevel::DEBUG, $message, $context);
}
/**
* Logs with an arbitrary level
*
* @param mixed $level
* @param string $message
* @param array $context
*
* @return void
*/
public function log($level, $message, array $context = []): void
{
switch ($level) {
case LogLevel::EMERGENCY:
case LogLevel::ALERT:
case LogLevel::CRITICAL:
case LogLevel::ERROR:
case LogLevel::WARNING:
case LogLevel::NOTICE:
case LogLevel::INFO:
case LogLevel::DEBUG:
break;
default:
throw new \InvalidArgumentException('Invalid log level argument');
}
\error_log("dmarc-srg [{$level}]: {$message}");
}
}

View File

@@ -0,0 +1,110 @@
<?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\Mail;
use Liuch\DmarcSrg\ReportFile\ReportFile;
use Liuch\DmarcSrg\Exception\SoftException;
class MailAttachment
{
private $conn;
private $filename;
private $bytes;
private $number;
private $mnumber;
private $encoding;
private $stream;
private $mime_type;
public function __construct($conn, $params)
{
$this->conn = $conn;
$this->filename = $params['filename'];
$this->bytes = $params['bytes'];
$this->number = $params['number'];
$this->mnumber = $params['mnumber'];
$this->encoding = $params['encoding'];
$this->stream = null;
$this->mime_type = null;
}
public function __destruct()
{
if (!is_null($this->stream) && get_resource_type($this->stream) == 'stream') {
fclose($this->stream);
}
}
public function mimeType()
{
if (is_null($this->mime_type)) {
$this->mime_type = ReportFile::getMimeType($this->filename, $this->datastream());
}
return $this->mime_type;
}
public function size()
{
return $this->bytes;
}
public function filename()
{
return $this->filename;
}
public function extension()
{
return pathinfo($this->filename, PATHINFO_EXTENSION);
}
public function datastream()
{
if (is_null($this->stream)) {
$this->stream = fopen('php://temp', 'r+');
fwrite($this->stream, $this->toString());
}
rewind($this->stream);
return $this->stream;
}
private function fetchBody()
{
return imap_fetchbody($this->conn, $this->mnumber, strval($this->number), FT_PEEK);
}
private function toString()
{
switch ($this->encoding) {
case ENC7BIT:
case ENC8BIT:
case ENCBINARY:
return $this->fetchBody();
case ENCBASE64:
return base64_decode($this->fetchBody());
case ENCQUOTEDPRINTABLE:
return imap_qprint($this->fetchBody());
}
throw new SoftException('Encoding failed: Unknown encoding');
}
}

View File

@@ -0,0 +1,142 @@
<?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\Mail;
/**
* The class is designed to easily create multipart/alternative message bodies.
*/
class MailBody
{
private $text = null;
private $html = null;
private $boundary = null;
/**
* Sets text content as a part of the message body
*
* @param array $text Text part of the message as an array of strings
*
* @return void
*/
public function setText(array &$text): void
{
$this->text = $text;
}
/**
* Sets html content as a part of the message body
*
* @param array $html Html part of the message as and array of strings
*
* @return void
*/
public function setHtml(array $html): void
{
$this->html = $html;
}
/**
* Return Content-Type header value for the whole message
*
* @return string
*/
public function contentType(): string
{
if ($this->boundary()) {
$ctype = 'multipart/alternative; boundary="' . $this->boundary() . '"';
} else {
if (!is_null($this->html)) {
$ctype = 'text/html';
} else {
$ctype = 'text/plain';
}
$ctype .= '; charset=utf-8';
}
return $ctype;
}
/**
* Returns all the message parts with required headers as an array of strings
*
* @return array
*/
public function content(): array
{
$content = [];
if ($this->text) {
$this->addBodyPart('text', $this->text, $content);
}
if ($this->html) {
$this->addBodyPart('html', $this->html, $content);
}
return $content;
}
/**
* Generates a boundary string of the message. If the body has only one part of the content
* it returns null
*
* @return string|null
*/
private function boundary()
{
if (!$this->boundary) {
if ($this->text && $this->html) {
$this->boundary = '==========' . sha1(uniqid()) . '=====';
}
}
return $this->boundary;
}
/**
* Adds the specified part of the content to the array passed as the third parameter
* with the required headers.
*
* @param string $type Type of the content to add
* @param array $part Part of the content to add
* @param array $content Where the data with headers should be added
*
* @return void
*/
private function addBodyPart(string $type, array &$part, array &$content): void
{
if ($this->boundary()) {
$content[] = '--' . $this->boundary();
switch ($type) {
case 'text':
$ctype = 'text/plain';
break;
case 'html':
$ctype = 'text/html';
break;
}
$content[] = 'Content-Type: ' . $ctype . '; charset=utf-8';
$content[] = 'Content-Transfer-Encoding: 7bit';
$content[] = '';
}
foreach ($part as $row) {
$content[] = $row;
}
unset($part);
}
}

View File

@@ -0,0 +1,468 @@
<?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\Mail;
use Liuch\DmarcSrg\Core;
use Liuch\DmarcSrg\ErrorHandler;
use Liuch\DmarcSrg\Exception\SoftException;
use Liuch\DmarcSrg\Exception\LogicException;
use Liuch\DmarcSrg\Exception\MailboxException;
class MailBox
{
private $conn;
private $server;
private $host;
private $mbox;
private $name;
private $uname;
private $passw;
private $delim;
private $attrs;
private $expunge;
private $options;
public function __construct($params)
{
if (!is_array($params)) {
throw new LogicException('Incorrect mailbox params');
}
$this->conn = null;
$this->uname = $params['username'];
$this->passw = $params['password'];
if (isset($params['name']) && is_string($params['name']) && strlen($params['name']) > 0) {
$this->name = $params['name'];
} else {
$name = $this->uname;
$pos = strpos($name, '@');
if ($pos !== false && $pos !== 0) {
$name = substr($name, 0, $pos);
}
$this->name = $name;
}
$this->mbox = $params['mailbox'];
$this->host = $params['host'];
$flags = $params['encryption'] ?? '';
switch ($flags) {
case 'ssl':
default:
$flags = '/ssl';
break;
case 'none':
$flags = '/notls';
break;
case 'starttls':
$flags = '/tls';
break;
}
if (isset($params['novalidate-cert']) && $params['novalidate-cert'] === true) {
$flags .= '/novalidate-cert';
}
$this->server = sprintf('{%s/imap%s}', $this->host, $flags);
$this->expunge = false;
$this->options = [];
if (isset($params['auth_exclude'])) {
$auth_exclude = $params['auth_exclude'];
switch (gettype($auth_exclude)) {
case 'string':
$auth_exclude = [ $auth_exclude ];
break;
case 'array':
break;
default:
$auth_exclude = null;
break;
}
if ($auth_exclude) {
$this->options['DISABLE_AUTHENTICATOR'] = $auth_exclude;
}
}
}
public function __destruct()
{
if (extension_loaded('imap')) {
$this->cleanup();
}
}
public function childMailbox(string $mailbox_name)
{
$this->ensureConnection();
try {
$mb_list = imap_list(
$this->conn,
self::utf8ToMutf7($this->server),
self::utf8ToMutf7($this->mbox) . $this->delim . self::utf8ToMutf7($mailbox_name)
);
} catch (\ErrorException $e) {
$mb_list = false;
}
$this->ensureErrorLog('imap_list');
if (!$mb_list) {
return null;
}
$child = clone $this;
$child->mbox .= $this->delim . $mailbox_name;
$child->conn = null;
$child->expunge = false;
return $child;
}
public function name()
{
return $this->name;
}
public function host()
{
return $this->host;
}
public function mailbox()
{
return $this->mbox;
}
public function check()
{
try {
$this->ensureConnection();
try {
$res = imap_status(
$this->conn,
self::utf8ToMutf7($this->server . $this->mbox),
SA_MESSAGES | SA_UNSEEN
);
} catch (\ErrorException $e) {
$res = false;
}
$error_message = $this->ensureErrorLog();
if (!$res) {
throw new MailboxException($error_message ?? 'Failed to get the mail box status');
}
if ($this->attrs & \LATT_NOSELECT) {
throw new MailboxException('The resource is not a mailbox');
}
$this->checkRights();
} catch (MailboxException $e) {
return ErrorHandler::exceptionResult($e);
}
return [
'error_code' => 0,
'message' => 'Successfully',
'status' => [
'messages' => $res->messages,
'unseen' => $res->unseen
]
];
}
public function search($criteria)
{
$this->ensureConnection();
try {
$res = imap_search($this->conn, $criteria);
} catch (\ErrorException $e) {
$res = false;
}
$error_message = $this->ensureErrorLog('imap_search');
if ($res === false) {
if (!$error_message) {
return [];
}
throw new MailboxException(
'Failed to search email messages',
-1,
new \ErrorException($error_message)
);
}
return $res;
}
public function sort($criteria, $search_criteria, $reverse)
{
$this->ensureConnection();
try {
$res = imap_sort($this->conn, $criteria, $reverse ? 1 : 0, SE_NOPREFETCH, $search_criteria);
} catch (\ErrorException $e) {
$res = false;
}
$error_message = $this->ensureErrorLog('imap_sort');
if ($res === false) {
if (!$error_message) {
return [];
}
throw new MailboxException(
'Failed to sort email messages',
-1,
new \ErrorException($error_message)
);
}
return $res;
}
public function message($number)
{
return new MailMessage($this->conn, $number);
}
public function ensureMailbox($mailbox_name)
{
$mbn = self::utf8ToMutf7($mailbox_name);
$srv = self::utf8ToMutf7($this->server);
$mbo = self::utf8ToMutf7($this->mbox);
$this->ensureConnection();
try {
$mb_list = imap_list($this->conn, $srv, $mbo . $this->delim . $mbn);
} catch (\ErrorException $e) {
$mb_list = false;
}
$error_message = $this->ensureErrorLog('imap_list');
if (empty($mb_list)) {
if ($error_message) {
throw new MailboxException(
'Failed to get the list of mailboxes',
-1,
new \ErrorException($error_message)
);
}
$new_mailbox = "{$srv}{$mbo}{$this->delim}{$mbn}";
try {
$res = imap_createmailbox($this->conn, $new_mailbox);
} catch (\ErrorException $e) {
$res = false;
}
$error_message = $this->ensureErrorLog('imap_createmailbox');
if (!$res) {
throw new MailboxException(
'Failed to create a new mailbox',
-1,
new \ErrorException($error_message ?? 'Unknown')
);
}
try {
imap_subscribe($this->conn, $new_mailbox);
} catch (\ErrorException $e) {
}
$this->ensureErrorLog('imap_subscribe');
}
}
public function moveMessage($number, $mailbox_name)
{
$this->ensureConnection();
$target = self::utf8ToMutf7($this->mbox) . $this->delim . self::utf8ToMutf7($mailbox_name);
try {
$res = imap_mail_move($this->conn, strval($number), $target);
} catch (\ErrorException $e) {
$res = false;
}
$error_message = $this->ensureErrorLog('imap_mail_move');
if (!$res) {
throw new MailboxException(
'Failed to move a message',
-1,
new \ErrorException($error_message ?? 'Unknown')
);
}
$this->expunge = true;
}
public function deleteMessage($number)
{
$this->ensureConnection();
try {
imap_delete($this->conn, strval($number));
} catch (\ErrorException $e) {
}
$this->ensureErrorLog('imap_delete');
$this->expunge = true;
}
public static function resetErrorStack()
{
imap_errors();
imap_alerts();
}
private function ensureConnection()
{
if (is_null($this->conn)) {
$error_message = null;
$srv = self::utf8ToMutf7($this->server);
try {
$this->conn = imap_open(
$srv,
$this->uname,
$this->passw,
OP_HALFOPEN,
0,
$this->options
);
} catch (\ErrorException $e) {
$this->conn = null;
}
if ($this->conn) {
$mbx = self::utf8ToMutf7($this->mbox);
try {
$mb_list = imap_getmailboxes($this->conn, $srv, $mbx);
} catch (\ErrorException $e) {
$mb_list = null;
}
if ($mb_list && count($mb_list) === 1) {
$this->delim = $mb_list[0]->delimiter ?? '/';
$this->attrs = $mb_list[0]->attributes ?? 0;
try {
if (imap_reopen($this->conn, $srv . $mbx)) {
return;
}
} catch (\ErrorException $e) {
}
} else {
$error_message = "Mailbox `{$this->mbox}` not found";
}
}
if (!$error_message) {
$error_message = imap_last_error();
if (!$error_message) {
$error_message = 'Cannot connect to the mail server';
}
}
Core::instance()->logger()->error("IMAP error: {$error_message}");
self::resetErrorStack();
if ($this->conn) {
try {
imap_close($this->conn);
} catch (\ErrorException $e) {
}
$this->ensureErrorLog('imap_close');
}
$this->conn = null;
throw new MailboxException($error_message);
}
}
private function ensureErrorLog(string $prefix = 'IMAP error')
{
if ($error_message = imap_last_error()) {
self::resetErrorStack();
$error_message = "{$prefix}: {$error_message}";
Core::instance()->logger()->error($error_message);
return $error_message;
}
return null;
}
private function checkRights(): void
{
if ($this->attrs & \LATT_NOINFERIORS) {
throw new SoftException('The mailbox may not have any children mailboxes');
}
if (!function_exists('imap_getacl')) {
return;
}
$mbox = self::utf8ToMutf7($this->mbox);
try {
$acls = imap_getacl($this->conn, $mbox);
} catch (\ErrorException $e) {
// It's not possible to get the ACLs information
$acls = false;
}
$this->ensureErrorLog('imap_getacl');
if ($acls !== false) {
$needed_rights_map = [
'l' => 'LOOKUP',
'r' => 'READ',
's' => 'WRITE-SEEN',
't' => 'WRITE-DELETE',
'k' => 'CREATE'
];
$result = [];
$needed_rights = array_keys($needed_rights_map);
foreach ([ "#{$this->uname}", '#authenticated', '#anyone' ] as $identifier) {
if (isset($acls[$identifier])) {
$rights = $acls[$identifier];
foreach ($needed_rights as $r) {
if (!str_contains($rights, $r)) {
$result[] = $needed_rights_map[$r];
}
}
break;
}
}
if (count($result) > 0) {
throw new SoftException(
'Not enough rights. Additionally, these rights are required: ' . implode(', ', $result)
);
}
}
}
/**
* Deletes messages marked for deletion, if any, and closes the connection
*
* @return void
*/
private function cleanup(): void
{
self::resetErrorStack();
if (!is_null($this->conn)) {
try {
if ($this->expunge) {
imap_expunge($this->conn);
}
} catch (\ErrorException $e) {
}
$this->ensureErrorLog('imap_expunge');
try {
imap_close($this->conn);
} catch (\ErrorException $e) {
}
$this->ensureErrorLog('imap_close');
}
}
/**
* It's a replacement for the standard function imap_utf8_to_mutf7
*
* @param string $s A UTF-8 encoded string
*
* @return string|false
*/
private static function utf8ToMutf7(string $s)
{
return mb_convert_encoding($s, 'UTF7-IMAP', 'UTF-8');
}
}

View File

@@ -0,0 +1,137 @@
<?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\Mail;
use Liuch\DmarcSrg\Core;
use Liuch\DmarcSrg\ErrorHandler;
use Liuch\DmarcSrg\Exception\LogicException;
use Liuch\DmarcSrg\Exception\RuntimeException;
use Liuch\DmarcSrg\Exception\MailboxException;
class MailBoxes implements \Iterator
{
private $box_list;
private $index = 0;
public function __construct()
{
$mailboxes = Core::instance()->config('mailboxes');
$this->box_list = [];
if (is_array($mailboxes)) {
$cnt = count($mailboxes);
if ($cnt > 0) {
if (isset($mailboxes[0])) {
for ($i = 0; $i < $cnt; ++$i) {
$this->box_list[] = new MailBox($mailboxes[$i]);
}
} else {
$this->box_list[] = new MailBox($mailboxes);
}
}
}
}
public function count()
{
return count($this->box_list);
}
public function list()
{
$id = 0;
$res = [];
foreach ($this->box_list as &$mbox) {
$id += 1;
$res[] = [
'id' => $id,
'name' => $mbox->name(),
'host' => $mbox->host(),
'mailbox' => $mbox->mailbox()
];
}
unset($mbox);
return $res;
}
public function mailbox($id)
{
if (!is_int($id) || $id <= 0 || $id > count($this->box_list)) {
throw new LogicException("Incorrect mailbox Id: {$i}");
}
return $this->box_list[$id - 1];
}
public function check($id)
{
if ($id !== 0) {
return $this->mailbox($id)->check();
}
$results = [];
$err_cnt = 0;
$box_cnt = count($this->box_list);
for ($i = 0; $i < $box_cnt; ++$i) {
$r = $this->box_list[$i]->check();
if ($r['error_code'] !== 0) {
++$err_cnt;
}
$results[] = $r;
}
$res = [];
if ($err_cnt == 0) {
$res['error_code'] = 0;
$res['message'] = 'Success';
} else {
$res['error_code'] = -1;
$res['message'] = sprintf('%d of the %d mailboxes failed the check', $err_cnt, $box_cnt);
}
$res['results'] = $results;
return $res;
}
public function current(): object
{
return $this->box_list[$this->index];
}
public function key(): int
{
return $this->index;
}
public function next(): void
{
++$this->index;
}
public function rewind(): void
{
$this->index = 0;
}
public function valid(): bool
{
return isset($this->box_list[$this->index]);
}
}

View File

@@ -0,0 +1,154 @@
<?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\Mail;
use Liuch\DmarcSrg\Core;
use Liuch\DmarcSrg\Exception\SoftException;
use Liuch\DmarcSrg\Exception\MailboxException;
class MailMessage
{
private $conn;
private $number;
private $attachment;
private $attachments_cnt;
public function __construct($conn, $number)
{
$this->conn = $conn;
$this->number = $number;
$this->attachment = null;
$this->attachments_cnt = -1;
}
public function overview()
{
$res = @imap_fetch_overview($this->conn, strval($this->number));
if (!isset($res[0])) {
if ($error_message = imap_last_error()) {
Core::instance()->logger()->error("imap_fetch_overview failed: {$error_message}");
}
MailBox::resetErrorStack();
return false;
}
return $res[0];
}
public function setSeen()
{
if (!@imap_setflag_full($this->conn, strval($this->number), '\\Seen')) {
if ($error_message = imap_last_error()) {
$error_message = '?';
}
MailBox::resetErrorStack();
Core::instance()->logger()->error("imap_setflag_full failed: {$error_message}");
throw new MailboxException("Failed to make a message seen: {$error_message}");
}
}
public function validate()
{
$this->ensureAttachment();
if ($this->attachments_cnt !== 1) {
throw new SoftException("Attachment count is not valid ({$this->attachments_cnt})");
}
$bytes = $this->attachment->size();
if ($bytes === -1) {
throw new SoftException("Failed to get attached file size. Wrong message format?");
}
if ($bytes < 50 || $bytes > 1 * 1024 * 1024) {
throw new SoftException("Attachment file size is not valid ({$bytes} bytes)");
}
$mime_type = $this->attachment->mimeType();
if (!in_array($mime_type, [ 'application/zip', 'application/gzip', 'application/x-gzip', 'text/xml' ])) {
throw new SoftException("Attachment file type is not valid ({$mime_type})");
}
}
public function attachment()
{
return $this->attachment;
}
private function ensureAttachment()
{
if ($this->attachments_cnt === -1) {
$structure = imap_fetchstructure($this->conn, $this->number);
if ($structure === false) {
throw new MailboxException('FetchStructure failed: ' . imap_last_error());
}
$this->attachments_cnt = 0;
$parts = isset($structure->parts) ? $structure->parts : [ $structure ];
foreach ($parts as $index => &$part) {
$att_part = $this->scanAttachmentPart($part, $index + 1);
if ($att_part) {
++$this->attachments_cnt;
if (!$this->attachment) {
$this->attachment = new MailAttachment($this->conn, $att_part);
}
}
}
unset($part);
}
}
private function scanAttachmentPart(&$part, $number)
{
$filename = null;
if ($part->ifdparameters) {
$filename = $this->getAttribute($part->dparameters, 'filename');
}
if (empty($filename) && $part->ifparameters) {
$filename = $this->getAttribute($part->parameters, 'name');
}
if (empty($filename)) {
return null;
}
return [
'filename' => imap_utf8($filename),
'bytes' => isset($part->bytes) ? $part->bytes : -1,
'number' => $number,
'mnumber' => $this->number,
'encoding' => $part->encoding
];
}
private function getAttribute(&$params, $name)
{
// need to check all objects as imap_fetchstructure
// returns multiple objects with the same attribute name,
// but first entry contains a truncated value
$value = null;
foreach ($params as &$obj) {
if (strcasecmp($obj->attribute, $name) === 0) {
$value = $obj->value;
}
}
return $value;
}
}

View File

@@ -0,0 +1,165 @@
<?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\Report;
use Liuch\DmarcSrg\Core;
use Liuch\DmarcSrg\Exception\SoftException;
use Liuch\DmarcSrg\Exception\DatabaseNotFoundException;
class Report
{
private $db = null;
private $data = null;
public function __construct($data, $db = null)
{
$this->data = $data;
$this->db = $db ?? Core::instance()->database();
}
public static function fromXmlFile($fd)
{
$data = ReportData::fromXmlFile($fd);
if (!self::checkData($data)) {
throw new SoftException('Incorrect or incomplete report data');
}
return new Report($data);
}
public function fetch()
{
$domain = $this->data['domain'];
$report_id = $this->data['report_id'];
if (empty($domain) || empty($report_id)) {
throw new SoftException('Not specified report\'s domain or id');
}
$this->data = [ 'domain' => $domain, 'report_id' => $report_id ];
try {
$this->db->getMapper('report')->fetch($this->data);
} catch (DatabaseNotFoundException $e) {
throw new SoftException('The report is not found');
}
}
public function save(string $real_fname)
{
$b_ts = $this->data['begin_time'];
$e_ts = $this->data['end_time'];
if (!$b_ts->getTimestamp() || !$e_ts->getTimestamp() || $b_ts > $e_ts) {
throw new SoftException('Failed to add an incoming report: wrong date range');
}
$this->db->getMapper('report')->save($this->data);
return [ 'message' => 'The report is loaded successfully' ];
}
public function get()
{
return $this->data;
}
public function set($name, $value)
{
if (empty($this->data['domain']) || empty($this->data['report_id'])) {
throw new SoftException('Not specified report\'s domain or id');
}
$this->db->getMapper('report')->setProperty($this->data, $name, $value);
return [ 'message' => 'Ok' ];
}
/**
* Checks report data for correctness and completeness
*
* @param array $data Report data
*
* @return bool
*/
private static function checkData(array $data): bool
{
static $fields = [
'domain' => [ 'required' => true, 'type' => 'string' ],
'begin_time' => [ 'required' => true, 'type' => 'object' ],
'end_time' => [ 'required' => true, 'type' => 'object' ],
'org' => [ 'required' => true, 'type' => 'string' ],
'external_id' => [ 'required' => true, 'type' => 'string' ],
'email' => [ 'required' => false, 'type' => 'string' ],
'extra_contact_info' => [ 'required' => false, 'type' => 'string' ],
'error_string' => [ 'required' => false, 'type' => 'array' ],
'policy_adkim' => [ 'required' => false, 'type' => 'string' ],
'policy_aspf' => [ 'required' => false, 'type' => 'string' ],
'policy_p' => [ 'required' => false, 'type' => 'string' ],
'policy_sp' => [ 'required' => false, 'type' => 'string' ],
'policy_np' => [ 'required' => false, 'type' => 'string' ],
'policy_pct' => [ 'required' => false, 'type' => 'string' ],
'policy_fo' => [ 'required' => false, 'type' => 'string' ],
'records' => [ 'required' => true, 'type' => 'array' ]
];
if (!self::checkRow($data, $fields) || count($data['records']) === 0) {
return false;
}
static $rfields = [
'ip' => [ 'required' => true, 'type' => 'string' ],
'rcount' => [ 'required' => true, 'type' => 'integer' ],
'disposition' => [ 'required' => true, 'type' => 'string' ],
'reason' => [ 'required' => false, 'type' => 'array' ],
'dkim_auth' => [ 'required' => false, 'type' => 'array' ],
'spf_auth' => [ 'required' => false, 'type' => 'array' ],
'dkim_align' => [ 'required' => true, 'type' => 'string' ],
'spf_align' => [ 'required' => true, 'type' => 'string' ],
'envelope_to' => [ 'required' => false, 'type' => 'string' ],
'envelope_from' => [ 'required' => false, 'type' => 'string' ],
'header_from' => [ 'required' => false, 'type' => 'string' ]
];
foreach ($data['records'] as &$rec) {
if (gettype($rec) !== 'array' || !self::checkRow($rec, $rfields)) {
return false;
}
}
return true;
}
/**
* Checks one row of report data
*
* @param array $row Data row
* @param array $def Row definition
*
* @return bool
*/
private static function checkRow(array &$row, array &$def): bool
{
foreach ($def as $key => &$dd) {
if (isset($row[$key])) {
if (gettype($row[$key]) !== $dd['type']) {
return false;
}
} elseif ($dd['required']) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,370 @@
<?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\Report;
use Liuch\DmarcSrg\DateTime;
use Liuch\DmarcSrg\Exception\RuntimeException;
class ReportData
{
public static $rep_data = null;
public static $tag_id = null;
public static function fromXmlFile($fd)
{
self::$tag_id = '<root>';
self::$rep_data = [ 'records' => [] ];
$parser = xml_parser_create();
xml_set_element_handler(
$parser,
'Liuch\DmarcSrg\Report\ReportData::xmlStartTag',
'Liuch\DmarcSrg\Report\ReportData::xmlEndTag'
);
xml_set_character_data_handler($parser, 'Liuch\DmarcSrg\Report\ReportData::xmlTagData');
xml_set_external_entity_ref_handler($parser, function () {
throw new RuntimeException('The XML document has an external entity!');
});
try {
while ($file_data = fread($fd, 4096)) {
if (!xml_parse($parser, $file_data, feof($fd))) {
throw new RuntimeException('XML error!');
}
}
} finally {
xml_parser_free($parser);
unset($parser);
}
return self::$rep_data;
}
public static function xmlStartTag($parser, $name, $attrs)
{
self::xmlEnterTag($name);
switch (self::$tag_id) {
case 'rec':
self::$rep_data['records'][] = [];
break;
case 'error_string':
if (!isset(self::$rep_data['error_string'])) {
self::$rep_data['error_string'] = [];
}
break;
case 'reason':
$idx = count(self::$rep_data['records']) - 1;
if (!isset(self::$rep_data['records'][$idx]['reason'])) {
self::$rep_data['records'][$idx]['reason'] = [];
}
self::$report_tags['reason']['tmp_data'] = [];
break;
case 'dkim_auth':
$idx = count(self::$rep_data['records']) - 1;
if (!isset(self::$rep_data['records'][$idx]['dkim_auth'])) {
self::$rep_data['records'][$idx]['dkim_auth'] = [];
}
self::$report_tags['dkim_auth']['tmp_data'] = [];
break;
case 'spf_auth':
$idx = count(self::$rep_data['records']) - 1;
if (!isset(self::$rep_data['records'][$idx]['spf_auth'])) {
self::$rep_data['records'][$idx]['spf_auth'] = [];
}
self::$report_tags['spf_auth']['tmp_data'] = [];
break;
}
}
public static function xmlEndTag($parser, $name)
{
switch (self::$tag_id) {
case 'reason':
$idx = count(self::$rep_data['records']) - 1;
self::$rep_data['records'][$idx]['reason'][] = self::$report_tags['reason']['tmp_data'];
unset(self::$report_tags['reason']['tmp_data']);
break;
case 'dkim_auth':
$idx = count(self::$rep_data['records']) - 1;
self::$rep_data['records'][$idx]['dkim_auth'][] = self::$report_tags['dkim_auth']['tmp_data'];
unset(self::$report_tags['dkim_auth']['tmp_data']);
break;
case 'spf_auth':
$idx = count(self::$rep_data['records']) - 1;
self::$rep_data['records'][$idx]['spf_auth'][] = self::$report_tags['spf_auth']['tmp_data'];
unset(self::$report_tags['spf_auth']['tmp_data']);
break;
case 'feedback':
// Set the default value if it's necessary and there is no data
foreach (self::$report_tags as $tag_id => &$tag_data) {
if (array_key_exists('default', $tag_data)) { // not isset() because of null values
if (isset($tag_data['header']) && $tag_data['header']) {
if (!isset(self::$rep_data[$tag_id])) {
self::$rep_data[$tag_id] = $tag_data['default'];
}
} else {
foreach (self::$rep_data['records'] as $idx => &$rec_val) {
if (!isset($rec_val[$tag_id])) {
$rec_val[$tag_id] = $tag_data['default'];
}
}
unset($rec_val);
}
}
}
unset($tag_data);
$b_ts = intval(self::$rep_data['begin_time']);
$e_ts = intval(self::$rep_data['end_time']);
self::$rep_data['begin_time'] = new DateTime('@' . ($b_ts < 0 ? 0 : $b_ts));
self::$rep_data['end_time'] = new DateTime('@' . ($e_ts < 0 ? 0 : $e_ts));
foreach (self::$rep_data['records'] as &$rec_data) {
$rec_data['rcount'] = intval($rec_data['rcount']);
}
unset($rec_data);
break;
}
self::xmlLeaveTag();
}
public static function xmlTagData($parser, $data)
{
switch (self::$tag_id) {
case 'error_string':
if (self::$tag_id === 'error_string') {
self::$rep_data['error_string'][] = $data;
}
break;
case 'reason_type':
self::$report_tags['reason']['tmp_data']['type'] = $data;
break;
case 'reason_comment':
self::$report_tags['reason']['tmp_data']['comment'] = $data;
break;
case 'dkim_domain':
self::$report_tags['dkim_auth']['tmp_data']['domain'] = $data;
break;
case 'dkim_selector':
self::$report_tags['dkim_auth']['tmp_data']['selector'] = $data;
break;
case 'dkim_result':
self::$report_tags['dkim_auth']['tmp_data']['result'] = $data;
break;
case 'dkim_human_result':
self::$report_tags['dkim_auth']['tmp_data']['human_result'] = $data;
break;
case 'spf_domain':
self::$report_tags['spf_auth']['tmp_data']['domain'] = $data;
break;
case 'spf_scope':
self::$report_tags['spf_auth']['tmp_data']['scope'] = $data;
break;
case 'spf_result':
self::$report_tags['spf_auth']['tmp_data']['result'] = $data;
break;
default:
if (!isset(self::$report_tags[self::$tag_id]['children'])) {
if (isset(self::$report_tags[self::$tag_id]['header']) &&
self::$report_tags[self::$tag_id]['header']
) {
if (!isset(self::$rep_data[self::$tag_id])) {
self::$rep_data[self::$tag_id] = $data;
} else {
self::$rep_data[self::$tag_id] .= $data;
}
} else {
$last_idx = count(self::$rep_data['records']) - 1;
$last_rec =& self::$rep_data['records'][$last_idx];
if (!isset($last_rec[self::$tag_id])) {
$last_rec[self::$tag_id] = $data;
} else {
$last_rec[self::$tag_id] .= $data;
}
unset($last_rec);
}
}
}
}
public static function xmlEnterTag($name)
{
if (!isset(self::$report_tags[self::$tag_id]['children']) ||
!isset(self::$report_tags[self::$tag_id]['children'][$name])
) {
throw new RuntimeException("Unknown tag: {$name}");
}
self::$tag_id = self::$report_tags[self::$tag_id]['children'][$name];
}
public static function xmlLeaveTag()
{
self::$tag_id = self::$report_tags[self::$tag_id]['parent'];
}
public static $report_tags = [
'<root>' => [
'children' => [
'FEEDBACK' => 'feedback'
]
],
'feedback' => [
'parent' => '<root>',
'children' => [
'VERSION' => 'ver',
'REPORT_METADATA' => 'rmd',
'POLICY_PUBLISHED' => 'p_p',
'RECORD' => 'rec'
]
],
'ver' => [ 'parent' => 'feedback', 'header' => true, 'default' => null ],
'rmd' => [
'parent' => 'feedback',
'children' => [
'ORG_NAME' => 'org',
'EMAIL' => 'email',
'EXTRA_CONTACT_INFO' => 'extra_contact_info',
'REPORT_ID' => 'external_id',
'DATE_RANGE' => 'd_range',
'ERROR' => 'error_string'
]
],
'p_p' => [
'parent' => 'feedback',
'children' => [
'DOMAIN' => 'domain',
'ADKIM' => 'policy_adkim',
'ASPF' => 'policy_aspf',
'P' => 'policy_p',
'SP' => 'policy_sp',
'NP' => 'policy_np',
'PCT' => 'policy_pct',
'FO' => 'policy_fo'
]
],
'rec' => [
'parent' => 'feedback',
'children' => [
'ROW' => 'row',
'IDENTIFIERS' => 'ident',
'AUTH_RESULTS' => 'au_res'
]
],
'org' => [ 'parent' => 'rmd', 'header' => true ],
'email' => [ 'parent' => 'rmd', 'header' => true, 'default' => null ],
'extra_contact_info' => [ 'parent' => 'rmd', 'header' => true, 'default' => null ],
'external_id' => [ 'parent' => 'rmd', 'header' => true ],
'd_range' => [
'parent' => 'rmd',
'children' => [
'BEGIN' => 'begin_time',
'END' => 'end_time'
]
],
'error_string' => [ 'parent' => 'rmd', 'header' => true, 'default' => null ],
'begin_time' => [ 'parent' => 'd_range', 'header' => true ],
'end_time' => [ 'parent' => 'd_range', 'header' => true ],
'domain' => [ 'parent' => 'p_p', 'header' => true ],
'policy_adkim' => [ 'parent' => 'p_p', 'header' => true, 'default' => null ],
'policy_aspf' => [ 'parent' => 'p_p', 'header' => true, 'default' => null ],
'policy_p' => [ 'parent' => 'p_p', 'header' => true, 'default' => null ],
'policy_sp' => [ 'parent' => 'p_p', 'header' => true, 'default' => null ],
'policy_np' => [ 'parent' => 'p_p', 'header' => true, 'default' => null ],
'policy_pct' => [ 'parent' => 'p_p', 'header' => true, 'default' => null ],
'policy_fo' => [ 'parent' => 'p_p', 'header' => true, 'default' => null ],
'row' => [
'parent' => 'rec',
'children' => [
'SOURCE_IP' => 'ip',
'COUNT' => 'rcount',
'POLICY_EVALUATED' => 'p_e'
]
],
'ident' => [
'parent' => 'rec',
'children' => [
'ENVELOPE_TO' => 'envelope_to',
'ENVELOPE_FROM' => 'envelope_from',
'HEADER_FROM' => 'header_from'
]
],
'au_res' => [
'parent' => 'rec',
'children' => [
'DKIM' => 'dkim_auth',
'SPF' => 'spf_auth'
]
],
'ip' => [ 'parent' => 'row' ],
'rcount' => [ 'parent' => 'row' ],
'p_e' => [
'parent' => 'row',
'children' => [
'DISPOSITION' => 'disposition',
'DKIM' => 'dkim_align',
'SPF' => 'spf_align',
'REASON' => 'reason'
]
],
'disposition' => [ 'parent' => 'p_e' ],
'dkim_align' => [ 'parent' => 'p_e' ],
'spf_align' => [ 'parent' => 'p_e' ],
'reason' => [
'parent' => 'p_e',
'default' => null,
'children' => [
'TYPE' => 'reason_type',
'COMMENT' => 'reason_comment'
]
],
'envelope_to' => [ 'parent' => 'ident', 'default' => null ],
'envelope_from' => [ 'parent' => 'ident', 'default' => null ],
'header_from' => [ 'parent' => 'ident', 'default' => null ],
'dkim_auth' => [
'parent' => 'au_res',
'default' => null,
'children' => [
'DOMAIN' => 'dkim_domain',
'SELECTOR' => 'dkim_selector',
'RESULT' => 'dkim_result',
'HUMAN_RESULT' => 'dkim_human_result'
]
],
'spf_auth' => [
'parent' => 'au_res',
'default' => null,
'children' => [
'DOMAIN' => 'spf_domain',
'SCOPE' => 'spf_scope',
'RESULT' => 'spf_result'
]
],
'reason_type' => [ 'parent' => 'reason' ],
'reason_comment' => [ 'parent' => 'reason' ],
'dkim_domain' => [ 'parent' => 'dkim_auth' ],
'dkim_selector' => [ 'parent' => 'dkim_auth' ],
'dkim_result' => [ 'parent' => 'dkim_auth' ],
'dkim_human_result' => [ 'parent' => 'dkim_auth' ],
'spf_domain' => [ 'parent' => 'spf_auth' ],
'spf_scope' => [ 'parent' => 'spf_auth' ],
'spf_result' => [ 'parent' => 'spf_auth' ]
];
}

View File

@@ -0,0 +1,242 @@
<?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/>.
*
* =========================
*
* This file contains the class ReportFetcher
*
* @category API
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg\Report;
use Liuch\DmarcSrg\Core;
use Liuch\DmarcSrg\ErrorHandler;
use Liuch\DmarcSrg\Report\Report;
use Liuch\DmarcSrg\Sources\Source;
use Liuch\DmarcSrg\ReportLog\ReportLogItem;
use Liuch\DmarcSrg\Exception\RuntimeException;
/**
* This class is designed to fetch report files from report sources and store them to the database.
*/
class ReportFetcher
{
private $source = null;
/**
* It's the constructor of the class.
*
* @param Source $sou Source for fetching report files.
*
* @return void
*/
public function __construct($sou)
{
$this->source = $sou;
}
/**
* Retrieves report files from the source and stores them in the database
* taking into account the limits from the configuration file.
*
* @return array Array of results.
*/
public function fetch(): array
{
try {
$this->source->rewind();
} catch (RuntimeException $e) {
return [[ 'source_error' => $e->getMessage() ]];
}
$core = Core::instance();
$limit = 0;
$stype = $this->source->type();
switch ($stype) {
case Source::SOURCE_MAILBOX:
$s_act = $core->config('fetcher/mailboxes/when_done', '');
$f_act = $core->config('fetcher/mailboxes/when_failed', '');
$limit = $core->config('fetcher/mailboxes/messages_maximum', 0);
break;
case Source::SOURCE_DIRECTORY:
$s_act = $core->config('fetcher/directories/when_done', '');
$f_act = $core->config('fetcher/directories/when_failed', '');
$limit = $core->config('fetcher/directories/files_maximum', 0);
break;
}
$limit = intval($limit);
if ($stype === Source::SOURCE_MAILBOX || $stype === Source::SOURCE_DIRECTORY) {
$this->source->setParams([
'when_done' => $s_act,
'when_failed' => $f_act
]);
}
$results = [];
while ($this->source->valid()) {
$result = null;
$fname = null;
$report = null;
$success = false;
$err_msg = null;
// Extracting and saving reports
try {
$rfile = $this->source->current();
$fname = $rfile->filename();
$report = Report::fromXmlFile($rfile->datastream());
$result = $report->save($fname);
$success = true;
} catch (RuntimeException $e) {
$err_msg = $e->getMessage();
$result = ErrorHandler::exceptionResult($e);
}
unset($rfile);
// Post processing
try {
if ($success) {
$this->source->accepted();
} else {
$this->source->rejected();
}
} catch (RuntimeException $e) {
$err_msg = $e->getMessage();
$result['post_processing_message'] = $err_msg;
}
// Adding a record to the log.
if (!$err_msg) {
$log = ReportLogItem::success($stype, $report, $fname, null)->save();
} else {
$log = ReportLogItem::failed($stype, $report, $fname, $err_msg)->save();
if ($this->source->type() === Source::SOURCE_MAILBOX) {
$msg = $this->source->mailMessage();
$ov = $msg->overview();
if ($ov) {
if (property_exists($ov, 'from')) {
$result['emailed_from'] = $ov->from;
}
if (property_exists($ov, 'date')) {
$result['emailed_date'] = $ov->date;
}
}
}
if ($report) {
$rd = $report->get();
if (isset($rd['external_id'])) {
$result['report_id'] = $rd['external_id'];
}
}
}
unset($report);
// Adding result to the results array.
$results[] = $result;
// Checking the fetcher limits
if ($limit > 0) {
if (--$limit === 0) {
break;
}
}
$this->source->next();
}
return $results;
}
/**
* Generates the final result based on the results of loading individual report files.
*
* @param array $results Array with results of loading report files.
*
* @return array Array of the final result to be sent to the client.
*/
public static function makeSummaryResult(array $results): array
{
$reps = [];
$others = [];
$r_count = 0;
$loaded = 0;
foreach ($results as &$r) {
if (isset($r['source_error'])) {
$others[] = $r['source_error'];
} else {
$reps[] = $r;
++$r_count;
if (!isset($r['error_code']) || $r['error_code'] === 0) {
++$loaded;
}
if (isset($r['post_processing_message'])) {
$others[] = $r['post_processing_message'];
}
}
}
unset($r);
$result = null;
$o_count = count($others);
if ($r_count + $o_count === 1) {
if ($r_count === 1) {
$result = $reps[0];
} else {
$result = [
'error_code' => -1,
'message' => $others[0]
];
}
} else {
$err_code = null;
$message = null;
if ($loaded === $r_count) {
$err_code = 0;
if ($r_count > 0) {
$message = strval($r_count) . ' report files have been loaded successfully';
} elseif ($o_count === 0) {
$message = 'There are no report files to load';
} else {
$err_code = -1;
}
} else {
$err_code = -1;
if ($loaded > 0) {
$message = "Only {$loaded} of the {$r_count} report files have been loaded";
} else {
$message = "None of the {$r_count} report files has been loaded";
}
}
$result['error_code'] = $err_code;
$result['message'] = $message;
if ($r_count > 0) {
$result['results'] = $reps;
}
if ($o_count > 0) {
$result['other_errors'] = $others;
}
}
return $result;
}
}

View File

@@ -0,0 +1,212 @@
<?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/>.
*
* =========================
*
* This file contains ReportList class
*
* @category API
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg\Report;
use Liuch\DmarcSrg\Core;
/**
* It's the main class for working with the incoming reports, such as:
* - getting a list of reports
* - deleting several reports at once
* - getting the number of reports stored in the database
*/
class ReportList
{
public const ORDER_NONE = 0;
public const ORDER_BEGIN_TIME = 1;
public const ORDER_ASCENT = 2;
public const ORDER_DESCENT = 3;
private $db = null;
private $limit = 0;
private $filter = [];
private $order = null;
/**
* The constructor
*
* @param DatabaseController $db The database controller
*/
public function __construct($db = null)
{
$this->db = $db ?? Core::instance()->database();
}
/**
* Returns a list of reports with specified parameters from position $pos
*
* This method returns a list of reports that depends on the filter and order.
* The filter, order, and limit for the list can be set using the setFilter, setOrder and setMaxCount methods.
*
* @param int $pos The starting position from which the list will be returned
*
* @return array An array with keys `reports` and `more`.
* `reports` is an array of incoming reports which contains maximum 25 records by default.
* Another value of the number of records can be specified by calling
* the method setMaxCount.
* `more` is true if there are more records in the database, false otherwise.
*/
public function getList(int $pos): array
{
$order = [
'field' => 'begin_time',
'direction' => ($this->order ?? self::ORDER_DESCENT) === self::ORDER_ASCENT ? 'ascent' : 'descent'
];
$max_rec = $this->limit > 0 ? $this->limit : 25;
$limit = [
'offset' => $pos,
'count' => $max_rec + 1
];
$list = $this->db->getMapper('report')->list($this->filter, $order, $limit);
if (count($list) > $max_rec) {
$more = true;
unset($list[$max_rec]);
} else {
$more = false;
}
return [
'reports' => $list,
'more' => $more
];
}
/**
* Sets the sort order for the list and for deleting several reports at once
*
* @param int $field The field to sort by. Currently only ORDER_BEGIN_TIME is available.
* @param int $direction The sorting direction. ORDER_ASCENT or ORDER_DESCENT must be used here.
*
* @return void
*/
public function setOrder(int $field, int $direction): void
{
$this->order = null;
if ($field > self::ORDER_NONE && $field < self::ORDER_ASCENT) {
if ($direction !== self::ORDER_ASCENT) {
$direction = self::ORDER_DESCENT;
}
$this->order = [
'field' => $field,
'direction' => $direction
];
}
}
/**
* Sets maximum numbers of records in the list and for deleting reports
*
* @param int $num Maximum number of records in the list
*
* @return void
*/
public function setMaxCount(int $num): void
{
if ($num > 0) {
$this->limit = $num;
} else {
$this->limit = 0;
}
}
/**
* Sets filter values for the list and for deleting reports
*
* @param array $filter Key-value array:
* 'before_time' => DateTime, timestamp
* 'dkim' => string, 'fail' or 'pass'
* 'domain' => string or instance of Domain class
* 'month' => string, yyyy-mm format
* 'organization' => string
* 'spf' => string, 'fail' or 'pass'
* 'status' => string, 'read' or 'unread'
* Note! 'dkim' and 'spf' do not affect the delete and count methods
*
* @return void
*/
public function setFilter(array $filter): void
{
$this->filter = $filter;
}
/**
* Returns the number of reports in the database
*
* It returns the number of reports in the database.
* The limit and some filter items (`dkim`, `spf`) do not affect this.
*
* @return int The number of reports in the database
*/
public function count(): int
{
$limit = [ 'offset' => 0, 'count' => $this->limit ];
return $this->db->getMapper('report')->count($this->filter, $limit);
}
/**
* Deletes reports from the database
*
* It deletes repors form the database. The filter items `dkim` and `spf` do not affect this.
*
* @return void
*/
public function delete(): void
{
$order = [
'field' => 'begin_time',
'direction' => ($this->order ?? self::ORDER_DESCENT) === self::ORDER_ASCENT ? 'ascent' : 'descent'
];
$limit = [ 'offset' => 0, 'count' => $this->limit ];
$this->db->getMapper('report')->delete($this->filter, $order, $limit);
}
/**
* Returns a list of values for each filter item except for `before_time`
*
* @return array An key-value array, where the key is the filter item name
* and the value is an array of possible values for the item
*/
public function getFilterList(): array
{
$domainMapper = $this->db->getMapper('domain');
$reportMapper = $this->db->getMapper('report');
return [
'domain' => $domainMapper->names(),
'month' => $reportMapper->months(),
'organization' => $reportMapper->organizations(),
'dkim' => [ 'pass', 'fail' ],
'spf' => [ 'pass', 'fail' ],
'status' => [ 'read', 'unread' ]
];
}
}

View File

@@ -0,0 +1,400 @@
<?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/>.
*
* =========================
*
* This file contains SummaryReport class
*
* @category API
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg\Report;
use Liuch\DmarcSrg\Statistics;
use Liuch\DmarcSrg\Exception\SoftException;
use Liuch\DmarcSrg\Exception\LogicException;
/**
* This class is for generating summary data for the specified period and domain
*/
class SummaryReport
{
private const LAST_WEEK = -1;
private const LAST_MONTH = -2;
private $period = 0;
private $domain = null;
private $stat = null;
private $subject = '';
/**
* Constructor
*
* @param string $period The period for which the report is created
* Must me one of the following values: `lastweek`, `lastmonth`, and `lastndays:N`
* where N is the number of days the report is created for
*/
public function __construct(string $period)
{
switch ($period) {
case 'lastweek':
$period = self::LAST_WEEK;
$subject = ' weekly';
break;
case 'lastmonth':
$period = self::LAST_MONTH;
$subject = ' monthly';
break;
default:
$ndays = 0;
$av = explode(':', $period);
if (count($av) === 2 && $av[0] === 'lastndays') {
$ndays = intval($av[1]);
if ($ndays <= 0) {
throw new SoftException('The parameter "days" has an incorrect value');
}
$subject = sprintf(' %d day%s', $ndays, ($ndays > 1 ? 's' : ''));
}
$period = $ndays;
break;
}
if (empty($subject)) {
throw new SoftException('The parameter "period" has an incorrect value');
}
$this->period = $period;
$this->subject = "DMARC{$subject} digest";
}
/**
* Binds a domain to the report
*
* @param Domain $domain The domain for which the report is created
*
* @return self
*/
public function setDomain($domain)
{
$this->domain = $domain;
$this->stat = null;
return $this;
}
/**
* Returns the report data as an array
*
* @return array
*/
public function toArray(): array
{
$this->ensureData();
$res = [];
$stat = $this->stat;
$range = $stat->range();
$res['date_range'] = [ 'begin' => $range[0], 'end' => $range[1] ];
$res['summary'] = $stat->summary();
$res['sources'] = $stat->ips();
$res['organizations'] = $stat->organizations();
return $res;
}
/**
* Returns the subject string. It is used in email messages.
*
* @return string
*/
public function subject(): string
{
return $this->subject;
}
/**
* Returns the report as an array of text strings
*
* @return array
*/
public function text(): array
{
$rdata = $this->reportData();
$res = [];
$res[] = '# Domain: ' . $this->domain->fqdn();
$res[] = ' Range: ' . $rdata['range'];
$res[] = '';
$res[] = '## Summary';
$total = $rdata['summary']['total'];
$res[] = sprintf(' Total: %d', $total);
$res[] = sprintf(' DKIM or SPF aligned: %s', self::num2percent($rdata['summary']['aligned'], $total));
$res[] = sprintf(' Not aligned: %s', self::num2percent($rdata['summary']['n_aligned'], $total));
$res[] = sprintf(' Organizations: %d', $rdata['summary']['organizations']);
$res[] = '';
if (count($rdata['sources']) > 0) {
$res[] = '## Sources';
$res[] = sprintf(
' %-25s %13s %13s %13s',
'',
'Total',
'SPF aligned',
'DKIM aligned'
);
foreach ($rdata['sources'] as &$it) {
$total = $it['emails'];
$spf_a = $it['spf_aligned'];
$dkim_a = $it['dkim_aligned'];
$spf_str = self::num2percent($spf_a, $total);
$dkim_str = self::num2percent($dkim_a, $total);
$res[] = sprintf(
' %-25s %13d %13s %13s',
$it['ip'],
$total,
$spf_str,
$dkim_str
);
}
unset($it);
$res[] = '';
}
if (count($rdata['organizations']) > 0) {
$res[] = '## Organizations';
$org_len = 15;
foreach ($rdata['organizations'] as &$org) {
$org_len = max($org_len, mb_strlen($org['name']));
}
unset($org);
$org_len = min($org_len, 55);
$res[] = sprintf(" %-{$org_len}s %8s %8s", '', 'emails', 'reports');
$frm_str = " %-{$org_len}s %8d %8d";
foreach ($rdata['organizations'] as &$org) {
$res[] = sprintf(
$frm_str,
trim($org['name']),
$org['emails'],
$org['reports']
);
}
unset($org);
$res[] = '';
}
return $res;
}
/**
* Returns the report as an array of html strings
*
* @return array
*/
public function html(): array
{
$h2a = 'style="margin:15px 0 5px;"';
$t2a = 'style="border-collapse:collapse;border-spacing:0;"';
$c1a = 'style="font-style:italic;"';
$d1s = 'padding-left:1em;';
$d2s = 'min-width:4em;';
$d3s = 'border:1px solid #888;';
$d4s = 'text-align:right;';
$d5s = 'padding:.3em;';
$add_red = function (int $num) {
return $num > 0 ? 'color:#f00;' : '';
};
$add_green = function (int $num) {
return $num > 0 ? 'color:#080;' : '';
};
$rdata = $this->reportData();
$res = [];
$res[] = "<h2 {$h2a}>Domain: " . htmlspecialchars($this->domain->fqdn()) . '</h2>';
$res[] = '<p style="margin:0;">Range: ' . htmlspecialchars($rdata['range']) . '</p>';
$res[] = "<h3 {$h2a}>Summary</h3>";
$res[] = '<table>';
$total = $rdata['summary']['total'];
$a_cnt = $rdata['summary']['aligned'];
$n_cnt = $rdata['summary']['n_aligned'];
$res[] = " <tr><td>Total: </td><td style=\"{$d1s}\">" . $total . '</td></tr>';
$color = $add_green($a_cnt);
$res[] = " <tr><td>DKIM or SPF aligned: </td><td style=\"{$d1s}{$color}\">{$a_cnt}</td></tr>";
$color = $add_red($n_cnt);
$res[] = " <tr><td>Not aligned: </td><td style=\"{$d1s}{$color}\">{$n_cnt}</td></tr>";
$res[] = " <tr><td>Organizations: </td><td style=\"{$d1s}\">" .
$rdata['summary']['organizations'] .
'</td></tr>';
$res[] = '</table>';
$rs2 = 'rowspan="2"';
$cs3 = 'colspan="3"';
$s_cnt = count($rdata['sources']);
if ($s_cnt > 0) {
$res[] = "<h3 {$h2a}>Sources</h3>";
$res[] = "<table {$t2a}>";
$res[] = " <caption {$c1a}>Total records: {$s_cnt}</caption>";
$res[] = ' <thead>';
$style = "style=\"{$d3s}{$d5s}\"";
$res[] = " <tr><th {$rs2} {$style}>IP address</th><th {$rs2} {$style}>Email volume</th>" .
"<th {$cs3} {$style}>SPF</th><th {$cs3} {$style}>DKIM</th></tr>";
$style = "style=\"{$d2s}{$d3s}{$d5s}\"";
$res[] = " <tr><th {$style}>pass</th><th {$style}>fail</th><th {$style}>rate</th>" .
"<th {$style}>pass</th><th {$style}>fail</th><th {$style}>rate</th></tr>";
$res[] = ' </thead>';
$res[] = ' <tbody>';
foreach ($rdata['sources'] as &$row) {
$ip = htmlspecialchars($row['ip']);
$total = $row['emails'];
$spf_a = $row['spf_aligned'];
$spf_n = $total - $spf_a;
$spf_p = sprintf('%.0f%%', $spf_a / $total * 100);
$dkim_a = $row['dkim_aligned'];
$dkim_n = $total - $dkim_a;
$dkim_p = sprintf('%.0f%%', $dkim_a / $total * 100);
$style = "style=\"{$d3s}{$d5s}";
$row_str = " <tr><td {$style}\">{$ip}</td><td {$style}{$d4s}\">{$total}</td>";
$row_str .= "<td {$style}{$d4s}{$add_green($spf_a)}\">{$spf_a}</td>";
$row_str .= "<td {$style}{$d4s}{$add_red($spf_n)}\">{$spf_n}</td>";
$row_str .= "<td {$style}{$d4s}\">{$spf_p}</td>";
$row_str .= "<td {$style}{$d4s}{$add_green($dkim_a)}\">{$dkim_a}</td>";
$row_str .= "<td {$style}{$d4s}{$add_red($dkim_n)}\">{$dkim_n}</td>";
$row_str .= "<td {$style}{$d4s}\">{$dkim_p}</td>";
$res[] = $row_str . '</tr>';
}
unset($row);
$res[] = ' </tbody>';
$res[] = '</table>';
}
$o_cnt = count($rdata['organizations']);
if ($o_cnt) {
$res[] = "<h3 {$h2a}>Organizations</h3>";
$res[] = "<table {$t2a}>";
$res[] = " <caption {$c1a}>Total records: {$o_cnt}</caption>";
$res[] = ' <thead>';
$style = "style=\"{$d3s}{$d5s}\"";
$res[] = " <tr><th {$style}>Name</th><th {$style}>Emails</th><th {$style}>Reports</th></tr>";
$res[] = ' </thead>';
$res[] = ' <tbody>';
foreach ($rdata['organizations'] as &$row) {
$name = htmlspecialchars($row['name']);
$style2 = "style=\"{$d3s}{$d4s}{$d5s}\"";
$res[] = " <tr><td {$style}>{$name}</td>" .
"<td {$style2}>{$row['emails']}</td>" .
"<td {$style2}>{$row['reports']}</td></tr>";
}
unset($row);
$res[] = ' </tbody>';
$res[] = '</table>';
}
return $res;
}
/**
* Returns the percentage with the original number. If $per is 0 then '0' is returned.
*
* @param int $per Value
* @param int $cent Divisor for percentage calculation
*
* @return string
*/
private static function num2percent(int $per, int $cent): string
{
if (!$per) {
return '0';
}
return sprintf('%.0f%%(%d)', $per / $cent * 100, $per);
}
/**
* Generates the report if it has not already been done
*
* @return void
*/
private function ensureData(): void
{
if (!$this->domain) {
throw new LogicException('No one domain was specified');
}
if (!$this->stat) {
switch ($this->period) {
case self::LAST_WEEK:
$this->stat = Statistics::lastWeek($this->domain);
break;
case self::LAST_MONTH:
$this->stat = Statistics::lastMonth($this->domain);
break;
default:
$this->stat = Statistics::lastNDays($this->domain, $this->period);
break;
}
}
}
/**
* Returns prepared data for the report
*
* @return array
*/
private function reportData(): array
{
$this->ensureData();
$stat = $this->stat;
$rdata = [];
$range = $stat->range();
$cyear = (new \Datetime())->format('Y');
$dform = ($range[0]->format('Y') !== $cyear || $range[1]->format('Y') !== $cyear) ? 'M d Y' : 'M d';
$rdata['range'] = $range[0]->format($dform) . ' - ' . $range[1]->format($dform);
$summ = $stat->summary();
$total = $summ['emails']['total'];
$aligned = $summ['emails']['dkim_spf_aligned'] +
$summ['emails']['dkim_aligned'] +
$summ['emails']['spf_aligned'];
$n_aligned = $total - $aligned;
$rdata['summary'] = [
'total' => $total,
'organizations' => $summ['organizations']
];
if ($total > 0) {
$rdata['summary']['aligned'] = $aligned;
$rdata['summary']['n_aligned'] = $n_aligned;
} else {
$rdata['summary']['aligned'] = $aligned;
$rdata['summary']['n_aligned'] = $aligned;
}
$rdata['sources'] = $stat->ips();
$rdata['organizations'] = $stat->organizations();
return $rdata;
}
}

View File

@@ -0,0 +1,194 @@
<?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\ReportFile;
use Liuch\DmarcSrg\Exception\SoftException;
class ReportFile
{
private $fd = null;
private $zip = null;
private $type = null;
private $remove = false;
private $filename = null;
private $filepath = null;
private $gzcutfilter = null;
private $gzinflatefilter = null;
private static $filters = [];
private static $ext_mime_map = [
'xml' => 'text/xml',
'gz' => 'application/gzip',
'zip' => 'application/zip'
];
private function __construct($filename, $type = null, $fd = null, $remove = false, $filepath = null)
{
$this->filename = $filename;
$this->type = $type ?? self::getMimeType($filename, $fd, $filepath);
$this->remove = $remove;
$this->filepath = $filepath;
switch ($this->type) {
case 'application/gzip':
case 'application/x-gzip':
$this->fd = $fd;
break;
case 'application/zip':
if ($fd) {
$tmpfname = tempnam(sys_get_temp_dir(), 'dmarc_');
if ($tmpfname === false) {
throw new SoftException('Failed to create a temporary file');
}
rewind($fd);
if (file_put_contents($tmpfname, $fd) === false) {
throw new SoftException('Failed to copy data to a temporary file');
}
$this->filepath = $tmpfname;
$this->remove = true;
}
break;
}
}
public function __destruct()
{
if ($this->fd) {
if ($this->isGzipStream()) {
$this->enableGzFilter(false);
}
gzclose($this->fd);
}
if ($this->zip) {
$this->zip->close();
}
if ($this->remove && $this->filepath) {
unlink($this->filepath);
}
}
public static function getMimeType($filename, $fd = null, $filepath = null)
{
if (function_exists('mime_content_type')) {
if ($fd && ($res = mime_content_type($fd))) {
return $res;
}
if ($filepath && ($res = mime_content_type($filepath))) {
return $res;
}
}
$ext = pathinfo(basename($filename), PATHINFO_EXTENSION);
return self::$ext_mime_map[$ext] ?? 'application/octet-stream';
}
public static function fromFile($filepath, $filename = null, $remove = false)
{
if (!is_file($filepath)) {
throw new SoftException('ReportFile: it is not a file');
}
return new ReportFile(
$filename ? basename($filename) : basename($filepath),
null,
null,
$remove,
$filepath
);
}
public static function fromStream($fd, $filename, $type)
{
return new ReportFile($filename, $type, $fd);
}
public function filename()
{
return $this->filename;
}
public function datastream()
{
if (!$this->fd) {
$fd = null;
switch ($this->type) {
case 'application/zip':
$this->zip = new \ZipArchive();
$this->zip->open($this->filepath);
if ($this->zip->count() !== 1) {
throw new SoftException('The archive must have only one file in it');
}
$zfn = $this->zip->getNameIndex(0);
if ($zfn !== pathinfo($zfn, PATHINFO_BASENAME)) {
throw new SoftException('There must not be any directories in the archive');
}
$fd = $this->zip->getStream($zfn);
break;
default:
// gzopen() can be used to read a file which is not in gzip format;
// in this case gzread() will directly read from the file without decompression.
$fd = gzopen($this->filepath, 'r');
break;
}
if (!$fd) {
throw new SoftException('Failed to open a report file');
}
$this->fd = $fd;
}
if ($this->isGzipStream()) {
ReportFile::ensureRegisterFilter(
'report_gzfile_cut_filter',
'Liuch\DmarcSrg\ReportFile\ReportGZFileCutFilter'
);
$this->enableGzFilter(true);
}
return $this->fd;
}
private static function ensureRegisterFilter($filtername, $classname)
{
if (!isset(ReportFile::$filters[$filtername])) {
stream_filter_register($filtername, $classname);
ReportFile::$filters[$filtername] = true;
}
}
private function enableGzFilter($enable)
{
if ($enable) {
if (!$this->gzcutfilter) {
$this->gzcutfilter = stream_filter_append($this->fd, 'report_gzfile_cut_filter', STREAM_FILTER_READ);
$this->gzinflatefilter = stream_filter_append($this->fd, 'zlib.inflate', STREAM_FILTER_READ);
}
} else {
if ($this->gzcutfilter) {
stream_filter_remove($this->gzinflatefilter);
stream_filter_remove($this->gzcutfilter);
}
}
}
private function isGzipStream(): bool
{
return (in_array($this->type, [ 'application/gzip', 'application/x-gzip' ]) && !$this->filepath);
}
}

View File

@@ -0,0 +1,114 @@
<?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\ReportFile;
use php_user_filter;
class ReportGZFileCutFilter extends php_user_filter
{
private $head = true;
private $header_data = '';
private $tail_data = '';
public function filter($in, $out, &$consumed, $closing): int
{
$b_cnt = 0;
while ($bucket = stream_bucket_make_writeable($in)) {
$consumed += $bucket->datalen;
$data = null;
if ($this->head) {
$data = $this->skipGzHeader($bucket->data);
} else {
$data = $bucket->data;
}
$data = $this->cutGzTail($data);
if (strlen($data) > 0) {
$bucket->data = $data;
stream_bucket_append($out, $bucket);
$b_cnt += 1;
}
}
return ($b_cnt > 0) ? PSFS_PASS_ON : PSFS_FEED_ME;
}
private function skipGzHeader($data)
{
// https://tools.ietf.org/html/rfc1952
$this->header_data .= $data;
$len = strlen($this->header_data);
if ($len < 10) { // minimal gz header
return '';
}
$pos = 10;
$flags = ord($this->header_data[3]);
if ($flags & 4) { // FLG.FEXTRA
$pos += (ord($this->header_data[$pos + 1]) | (ord($this->header_data[$pos + 2]) << 8)) + 2;
if ($pos > $len) {
return '';
}
}
if ($flags & 8) { // FLG.FNAME
$pos = $this->skipZeroTerminatedString($this->header_data, $len, $pos);
if ($pos > $len) {
return '';
}
}
if ($flags & 16) { // FLG.FCOMMENT
$pos = $this->skipZeroTerminatedString($this->header_data, $len, $pos);
if ($pos > $len) {
return '';
}
}
if ($flags & 2) { // FLG.FHCRC
$pos += 2;
if ($pos > $len) {
return '';
}
}
$res = substr($this->header_data, $pos);
$this->head = false;
$this->header_data = '';
return $res;
}
private function cutGzTail($data)
{
$res = $this->tail_data . $data;
$this->tail_data = substr($res, -8);
if (strlen($res) <= 8) {
return '';
}
return substr($res, 0, -8);
}
private function skipZeroTerminatedString($str, $len, $pos)
{
for ($i = $pos; $i < $len; ++$i) {
if ($str[$i] === "\0") {
return $i + 1;
}
}
return $len + 1;
}
}

View File

@@ -0,0 +1,96 @@
<?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\ReportLog;
use Liuch\DmarcSrg\Core;
class ReportLog
{
public const ORDER_ASCENT = 1;
public const ORDER_DESCENT = 2;
private $db = null;
private $filter = [
'from_time' => null,
'till_time' => null
];
private $order = [
'direction' => 'ascent'
];
private $rec_limit = 0;
private $position = 0;
public function __construct($from_time, $till_time, $db = null)
{
$this->filter['from_time'] = $from_time;
$this->filter['till_time'] = $till_time;
$this->db = $db ?? Core::instance()->database();
}
public function setOrder(int $dir)
{
$this->order['direction'] = ($dir === self::ORDER_DESCENT ? 'descent' : 'ascent');
}
public function setMaxCount(int $n)
{
$this->rec_limit = $n;
}
public function count()
{
$limit = [ 'offset' => 0, 'count' => $this->rec_limit ];
return $this->db->getMapper('report-log')->count($this->filter, $limit);
}
public function getList(int $pos)
{
$this->position = $pos;
$max_rec = $this->rec_limit > 0 ? $this->rec_limit : 25;
$limit = [ 'offset' => $pos, 'count' => $max_rec + 1 ];
$list = $this->db->getMapper('report-log')->list($this->filter, $this->order, $limit);
if (count($list) > $max_rec) {
$more = true;
unset($list[$max_rec]);
} else {
$more = false;
}
foreach ($list as &$it) {
$it['source'] = ReportLogItem::sourceToString($it['source']);
}
unset($it);
return [
'items' => $list,
'more' => $more
];
}
public function delete()
{
$limit = [ 'offset' => 0, 'count' => $this->rec_limit ];
$this->db->getMapper('report-log')->delete($this->filter, $this->order, $limit);
}
}

View File

@@ -0,0 +1,147 @@
<?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\ReportLog;
use Liuch\DmarcSrg\Core;
use Liuch\DmarcSrg\Sources\Source;
use Liuch\DmarcSrg\Exception\LogicException;
use Liuch\DmarcSrg\Exception\SoftException;
use Liuch\DmarcSrg\Exception\DatabaseNotFoundException;
class ReportLogItem
{
private $db = null;
private $data = [
'id' => null,
'domain' => null,
'external_id' => null,
'event_time' => null,
'filename' => null,
'source' => 0,
'success' => false,
'message' => null
];
private function __construct($source, $filename, $db)
{
if (!is_null($source)) {
if (gettype($source) !== 'integer' || $source <= 0) {
throw new LogicException('Invalid parameter passed');
}
}
$this->data['source'] = $source;
$this->data['filename'] = gettype($filename) == 'string' ? $filename : null;
$this->db = $db ?? Core::instance()->database();
}
public static function success(int $source, $report, $filename, $message, $db = null)
{
$li = new ReportLogItem($source, $filename, $db);
$li->data['success'] = true;
$rdata = $report->get();
$li->data['domain'] = $rdata['domain'];
$li->data['external_id'] = $rdata['external_id'];
$li->data['message'] = $message;
return $li;
}
public static function failed(int $source, $report, $filename, $message, $db = null)
{
$li = new ReportLogItem($source, $filename, $db);
$li->data['success'] = false;
if (!is_null($report)) {
$rdata = $report->get();
$li->data['domain'] = $rdata['domain'];
$li->data['external_id'] = $rdata['external_id'];
} else {
$li->data['domain'] = null;
$li->data['external_id'] = null;
}
$li->data['message'] = $message;
return $li;
}
/**
* Returns an instance of ReportLogItem with the passed Id
*
* @param int $id Item Id to return
* @param DatabaseController $db The database controller
*
* @return ReportLogItem an instance of ReportLogItem with the specified Id.
*/
public static function byId(int $id, $db = null)
{
$li = new ReportLogItem(null, null, $db);
$li->data['id'] = $id;
try {
$li->db->getMapper('report-log')->fetch($li->data);
} catch (DatabaseNotFoundException $e) {
throw new SoftException('The log item is not found');
}
return $li;
}
/**
* Converts an integer source value to a string representation
*
* Returns a string with the source name or an empty string if the integer value is incorrect.
*
* @param int $source - an integer value to convert
*
* @return string A string value of the passed source
*/
public static function sourceToString(int $source): string
{
switch ($source) {
case Source::SOURCE_UPLOADED_FILE:
return 'uploaded_file';
case Source::SOURCE_MAILBOX:
return 'email';
case Source::SOURCE_DIRECTORY:
return 'directory';
}
return '';
}
/**
* Returns an array with log item data
*
* @return array Log item data
*/
public function toArray(): array
{
$res = $this->data;
$res['source'] = static::sourceToString($this->data['source']);
return $res;
}
/**
* Saves the report log item to the database
*
* @return void
*/
public function save(): void
{
$this->db->getMapper('report-log')->save($this->data);
}
}

View File

@@ -0,0 +1,242 @@
<?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/>.
*
* =========================
*
* This file contains implementation of the class Setting
*
* @category API
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg\Settings;
use Liuch\DmarcSrg\Core;
use Liuch\DmarcSrg\Exception\SoftException;
use Liuch\DmarcSrg\Exception\DatabaseNotFoundException;
/**
* It's a class for accessing to settings item data
*
* This class is designed for storing and manipulating one item of settings data.
* All queries to the datatabase are made in lazy mode.
*/
abstract class Setting
{
public const TYPE_STRING = 1;
public const TYPE_INTEGER = 2;
public const TYPE_STRING_SELECT = 3;
protected $db = null;
protected $name = null;
protected $value = null;
protected $wignore = false;
/**
* Returns the type of the setting
*
* @return int Type of the setting
*/
abstract public function type(): int;
/**
* Checks if the value is correct
*
* @return bool True if the value is correct or false otherwise
*/
abstract protected function checkValue(): bool;
/**
* Converts a string to the value
*
* @param string $s String for conversion
*
* @return void
*/
abstract protected function stringToValue(string $s): void;
/**
* Returns a string representation of the value
*
* @return string The string value
*/
abstract protected function valueToString(): string;
/**
* It's a constructor of the class
*
* Some examples of using:
* (new Setting('some.setting'))->value(); - will return the value of the setting 'some.setting'.
* (new Setting([ 'name' => 'some.setting', 'value' => 'some string value' ])->save(); - will add
* this setting to the database if it does not exist in it or update the value of the setting.
*
* @param string|array $data Some setting data to identify it
* string value is treated as a name
* array has these fields: `name`, `value`
* and usually uses for creating a new setting item.
* @param boolean $wignore If true the wrong value is reset to the default
* or it throws an exception otherwise.
* @param DatabaseController $db The database controller
*
* @return void
*/
public function __construct($data, bool $wignore = false, $db = null)
{
$this->wignore = $wignore;
$this->db = $db ?? Core::instance()->database();
switch (gettype($data)) {
case 'string':
$this->name = $data;
SettingsList::checkName($this->name);
return;
case 'array':
if (!isset($data['name']) || gettype($data['name']) !== 'string') {
break;
}
$this->name = $data['name'];
SettingsList::checkName($this->name);
if (isset($data['value'])) {
$this->value = $data['value'];
if (!$this->checkValue()) {
if (!$wignore) {
break;
}
$this->resetToDefault();
}
}
return;
}
throw new SoftException('Wrong setting data');
}
/**
* Returns the name of the setting
*
* @return string The name of the setting
*/
public function name(): string
{
return $this->name;
}
/**
* Returns the value of the setting
*
* @return mixed The value of the setting
*/
public function value()
{
if (is_null($this->value)) {
$this->fetchData();
}
return $this->value;
}
/**
* Assigns the passed value to the setting
*
* @param mixed Value to assign
*
* @return void
*/
public function setValue($value): void
{
$this->value = $value;
if (!$this->checkValue()) {
if (!$this->wignore) {
throw new SoftException('Wrong setting value');
}
$this->resetToDefault();
}
}
/**
* Returns an array with setting data
*
* @return array Setting data
*/
public function toArray(): array
{
if (is_null($this->value)) {
$this->fetchData();
}
switch ($this->type()) {
case self::TYPE_STRING:
$type = 'string';
break;
case self::TYPE_INTEGER:
$type = 'integer';
break;
case self::TYPE_STRING_SELECT:
$type = 'select';
break;
}
return [
'type' => $type,
'name' => $this->name,
'value' => $this->value,
'default' => SettingsList::$schema[$this->name]['default']
];
}
/**
* 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.
*
* @return void
*/
public function save(): void
{
$this->db->getMapper('setting')->save($this->name, $this->valueToString());
}
/**
* Fetches the setting data from the database by its name
*
* @return void
*/
private function fetchData(): void
{
try {
$res = $this->db->getMapper('setting')->value($this->name);
} catch (DatabaseNotFoundException $e) {
$this->resetToDefault();
return;
}
$this->stringToValue($res);
if (!$this->checkValue()) {
$this->resetToDefault();
}
}
/**
* Resets the setting value to its default value
*
* @return void
*/
private function resetToDefault(): void
{
$this->value = SettingsList::$schema[$this->name]['default'];
}
}

View File

@@ -0,0 +1,109 @@
<?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/>.
*
* =========================
*
* This file contains implementation of the class SettingInteger
*
* @category API
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg\Settings;
/**
* It's a class for accessing to settings item data
*
* This class contains the implementation of the setting for integer values.
*/
class SettingInteger extends Setting
{
/**
* Returns the type of the setting
*
* @return int Type of the setting
*/
public function type(): int
{
return Setting::TYPE_INTEGER;
}
/**
* Checks if the value is correct
*
* @return bool True if the value is correct or false otherwise
*/
protected function checkValue(): bool
{
if (gettype($this->value) === 'integer') {
$sch = &SettingsList::$schema[$this->name];
if (!isset($sch['minimum']) || $this->value >= $sch['minimum']) {
if (!isset($sch['maximum']) || $this->value <= $sch['maximum']) {
return true;
}
}
}
return false;
}
/**
* Converts a string to the value
*
* @param string $s String for conversion
*
* @return void
*/
protected function stringToValue(string $s): void
{
$this->value = intval($s);
}
/**
* Returns a string representation of the value
*
* @return string The string value
*/
protected function valueToString(): string
{
return strval($this->value);
}
/**
* Returns an array with setting data
*
* @return array Setting data
*/
public function toArray(): array
{
$res = parent::toArray();
$sch = &SettingsList::$schema[$this->name];
if (isset($sch['minimum'])) {
$res['minimum'] = $sch['minimum'];
}
if (isset($sch['maximum'])) {
$res['maximum'] = $sch['maximum'];
}
return $res;
}
}

View File

@@ -0,0 +1,83 @@
<?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/>.
*
* =========================
*
* This file contains implementation of the class SettingString
*
* @category API
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg\Settings;
/**
* It's a class for accessing to settings item data
*
* This class contains the implementation of the setting for string values.
*/
class SettingString extends Setting
{
/**
* Returns the type of the setting
*
* @return int Type of the setting
*/
public function type(): int
{
return Setting::TYPE_STRING;
}
/**
* Checks if the value is correct
*
* @return bool True if the value is correct or false otherwise
*/
protected function checkValue(): bool
{
return (gettype($this->value) === 'string');
}
/**
* Converts a string to the value
*
* @param string $s String for conversion
*
* @return void
*/
protected function stringToValue(string $s): void
{
$this->value = $s;
}
/**
* Returns a string representation of the value
*
* @return string The string value
*/
protected function valueToString(): string
{
return $this->value;
}
}

View File

@@ -0,0 +1,78 @@
<?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/>.
*
* =========================
*
* This file contains implementation of the class SettingStringSelect
*
* @category API
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg\Settings;
/**
* It's a class for accessing to settings item data
*
* This class contains the implementation of the setting for string with a limited set of values.
*/
class SettingStringSelect extends SettingString
{
/**
* Returns the type of the setting
*
* @return int Type of the setting
*/
public function type(): int
{
return Setting::TYPE_STRING_SELECT;
}
/**
* Checks if the value is correct
*
* @return bool True if the value is correct or false otherwise
*/
protected function checkValue(): bool
{
if (parent::checkValue()) {
if (in_array($this->value, SettingsList::$schema[$this->name]['options'])) {
return true;
}
}
return false;
}
/**
* Returns an array with setting data
*
* @return array Setting data
*/
public function toArray(): array
{
$res = parent::toArray();
$res['options'] = SettingsList::$schema[$this->name]['options'];
return $res;
}
}

View File

@@ -0,0 +1,207 @@
<?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/>.
*
* =========================
*
* This file contains the class SettingsList
*
* @category API
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg\Settings;
use Liuch\DmarcSrg\Core;
use Liuch\DmarcSrg\Exception\SoftException;
/**
* This class is designed to work with the list of the settings
*/
class SettingsList
{
public const ORDER_ASCENT = 0;
public const ORDER_DESCENT = 1;
private $db = null;
private $order = self::ORDER_ASCENT;
/**
* The constructor
*
* @param DatabaseController $db Connector to the current database
*/
public function __construct($db = null)
{
$this->db = $db ?? Core::instance()->database();
}
/**
* Returns a list of the settings
*
* It returns a list of the settings that are marked public.
* The value is taken from the database, if any, or the default.
*
* @return array Array with instances of Setting class
*/
public function getList(): array
{
$db_map = $this->db->getMapper('setting')->list();
foreach (static::$schema as $name => &$sch_data) {
if ($sch_data['public'] ?? false) {
$value = $db_map[$name] ?? $sch_data['default'];
switch ($sch_data['type']) {
case 'select':
$list[] = new SettingStringSelect([
'name' => $name,
'value' => $value
], true, $this->db);
break;
case 'integer':
$list[] = new SettingInteger([
'name' => $name,
'value' => intval($value)
], true, $this->db);
break;
case 'string':
$list[] = new SettingString([
'name' => $name,
'value' => $value
], true, $this->db);
break;
}
}
}
unset($sch_data);
$dir = $this->order == self::ORDER_ASCENT ? 1 : -1;
usort($list, static function ($a, $b) use ($dir) {
return ($a->name() <=> $b->name()) * $dir;
});
return [
'list' => $list,
'more' => false
];
}
/**
* Sets the sorting direction for the list
*
* @param int $direction The sorting direction. ORDER_ASCENT or ORDER_DESCENT must be used here.
*
* @return SettingsList $this
*/
public function setOrder(int $direction)
{
if ($direction !== self::ORDER_DESCENT) {
$direction = self::ORDER_ASCENT;
}
$this->order = $direction;
return $this;
}
/**
* Throws an exception if there is no setting with name $name
*
* @param string $name Setting name to check
*
* @return void
*/
public static function checkName($name): void
{
if (!isset(self::$schema[$name])) {
throw new SoftException('Unknown setting name: ' . $name);
}
}
/**
* Returns an instance of the Setting class by its name
*
* It returns an instance of the Setting class but only if it is marked public.
*
* @param string $name Setting name
*
* @return Setting
*/
public static function getSettingByName(string $name)
{
self::checkName($name);
if (!(self::$schema[$name]['public'] ?? false)) {
throw new SoftException('Attempt to access an internal variable');
}
switch (self::$schema[$name]['type']) {
case 'string':
return new SettingString($name);
case 'select':
return new SettingStringSelect($name);
case 'integer':
return new SettingInteger($name);
}
}
/**
* List of the possible setting items that must be returned in getList method, their types and other data
*/
public static $schema = [
'version' => [
'type' => 'string',
'default' => ''
],
'status.emails-for-last-n-days' => [
'type' => 'integer',
'public' => true,
'minimum' => 1,
'maximum' => 365,
'default' => 30
],
'report-view.sort-records-by' => [
'type' => 'select',
'public' => true,
'options' => [ 'ip,ascent', 'ip,descent', 'message-count,ascent', 'message-count,descent' ],
'default' => 'message-count,descent'
],
'log-view.sort-list-by' => [
'type' => 'select',
'public' => true,
'options' => [ 'event-time,ascent', 'event-time,descent' ],
'default' => 'event-time,ascent'
],
'ui.datetime.offset' => [
'type' => 'select',
'public' => true,
'options' => [ 'auto', 'utc', 'local' ],
'default' => 'auto'
],
'ui.ipv4.url' => [
'type' => 'string',
'public' => true,
'default' => 'https://who.is/whois-ip/ip-address/{$ip}'
],
'ui.ipv6.url' => [
'type' => 'string',
'public' => true,
'default' => 'https://who.is/whois-ip/ip-address/{$ip}'
]
];
}

View File

@@ -0,0 +1,255 @@
<?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/>.
*
* =========================
*
* This file contains the class DirectorySource
*
* @category API
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg\Sources;
use Liuch\DmarcSrg\Core;
use Liuch\DmarcSrg\ReportFile\ReportFile;
use Liuch\DmarcSrg\Exception\SoftException;
use Liuch\DmarcSrg\Exception\RuntimeException;
/**
* This class is designed to process report files from local server directories.
*/
class DirectorySource extends Source
{
private $path = null;
private $list = null;
private $index = 0;
private $params = null;
/**
* Sets parameters that difine the behavior of the source
*
* @param $params Key-value array
* 'when_done' => one or more rules to be executed after successful report processing
* (array|string)
* 'when_failed' => one or more rules to be executed after report processing fails
* (array|string)
*
* @return void
*/
public function setParams(array $params): void
{
$this->params = [];
$this->params['when_done'] = SourceAction::fromSetting(
$params['when_done'] ?? [],
SourceAction::FLAG_BASENAME,
'delete'
);
$this->params['when_failed'] = SourceAction::fromSetting(
$params['when_failed'] ?? [],
SourceAction::FLAG_BASENAME,
'move_to:failed'
);
}
/**
* Returns an instance of the ReportFile class for the current file.
*
* @return ReportFile
*/
public function current(): object
{
return ReportFile::fromFile($this->list[$this->index]);
}
/**
* Returns the index of the currect file.
*
* @return int
*/
public function key(): int
{
return $this->index;
}
/**
* Moves forward to the next file.
*
* @return void
*/
public function next(): void
{
++$this->index;
}
/**
* Rewinds the position to the first file.
*
* @return void
*/
public function rewind(): void
{
if (is_null($this->list)) {
$this->path = $this->data->toArray()['location'];
if (!is_dir($this->path)) {
throw new SoftException("The {$this->path} directory does not exist!");
}
try {
$fs = new \FilesystemIterator($this->path);
} catch (\Exception $e) {
throw new RuntimeException("Error accessing directory {$this->path}", -1, $e);
}
$this->list = [];
foreach ($fs as $entry) {
if ($entry->isFile()) {
$this->list[] = $entry->getPathname();
}
}
}
if (is_null($this->params)) {
$this->setParams([]);
}
$this->index = 0;
}
/**
* Checks if the current postion is valid
*
* @return bool
*/
public function valid(): bool
{
return isset($this->list[$this->index]);
}
/**
* Processes the accepted report file according to the settings
*
* @return void
*/
public function accepted(): void
{
$this->processReportFileActions($this->params['when_done']);
}
/**
* Processes the rejected report file according to the settings
*
* @return void
*/
public function rejected(): void
{
$this->processReportFileActions($this->params['when_failed']);
}
/**
* Returns type of the source.
*
* @return int
*/
public function type(): int
{
return Source::SOURCE_DIRECTORY;
}
/**
* Logs an error message
*
* @param string $message
*/
private function logError(string $message): void
{
Core::instance()->logger()->error($message);
}
/**
* Processes the current report file according to settings
*
* @param array $actions List of actions to apply to the file
*
* @return void
*/
private function processReportFileActions(array &$actions): void
{
foreach ($actions as $sa) {
switch ($sa->type) {
case SourceAction::ACTION_DELETE:
$this->deleteReportFile();
break;
case SourceAction::ACTION_MOVE:
$this->moveReportFile($sa->param);
break;
}
}
}
/**
* Deletes the current report file
*
* @return void
*/
private function deleteReportFile(): void
{
try {
unlink($this->list[$this->index]);
} catch (\ErrorException $e) {
$error_message = "Error deleting file from directory {$this->path}";
$this->logError($error_message);
throw new RuntimeException($error_message, -1, $e);
}
}
/**
* Moves the current report file
*
* @param string $dir_name Directory name where to move the report file to
*
* @return void
*/
private function moveReportFile(string $dir_name): void
{
$fdir = $this->path . $dir_name;
if (!is_dir($fdir)) {
try {
mkdir($fdir);
} catch (\ErrorException $e) {
$e = new RuntimeException("Error creating directory {$fdir}/", -1, $e);
$this->logError(strval($e));
throw $e;
}
try {
chmod($fdir, 0700);
} catch (\ErrorException $e) {
$this->logError(strval($e));
}
}
$file = $this->list[$this->index];
try {
rename($file, $fdir . '/' . basename($file));
} catch (\ErrorException $e) {
$e = new RuntimeException("Error moving file to directory {$fdir}/", -1, $e);
$this->logError(strval($e));
throw $e;
}
}
}

View File

@@ -0,0 +1,238 @@
<?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/>.
*
* =========================
*
* This file contains the class MailboxSource
*
* @category API
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg\Sources;
use Liuch\DmarcSrg\ReportFile\ReportFile;
use Liuch\DmarcSrg\Exception\SoftException;
use Liuch\DmarcSrg\Exception\RuntimeException;
/**
* This class is designed to process report files from an mail box.
*/
class MailboxSource extends Source
{
private $list = null;
private $index = 0;
private $msg = null;
private $params = null;
/**
* Sets parameters that difine the behavior of the source
*
* @param $params Key-value array
* 'when_done' => one or more rules to be executed after successful report processing
* (array|string)
* 'when_failed' => one or more rules to be executed after report processing fails
* (array|string)
*
* @return void
*/
public function setParams(array $params): void
{
$this->params = [];
$this->params['when_done'] = SourceAction::fromSetting(
$params['when_done'] ?? [],
0,
'mark_seen'
);
$this->params['when_failed'] = SourceAction::fromSetting(
$params['when_failed'] ?? [],
0,
'move_to:failed'
);
}
/**
* Returns an instance of the ReportFile class for the current email message.
*
* @return ReportFile
*/
public function current(): object
{
$this->msg = $this->data->message($this->list[$this->index]);
try {
$this->msg->validate();
} catch (SoftException $e) {
throw new SoftException('Incorrect message: ' . $e->getMessage(), $e->getCode());
} catch (RuntimeException $e) {
throw new RuntimeException('Incorrect message', -1, $e);
}
$att = $this->msg->attachment();
return ReportFile::fromStream($att->datastream(), $att->filename(), $att->mimeType());
}
/**
* Returns the index of the currect email message.
*
* @return int
*/
public function key(): int
{
return $this->index;
}
/**
* Moves forward to the next email message
*
* @return void
*/
public function next(): void
{
$this->msg = null;
++$this->index;
}
/**
* Gets a list of unread messages and rewinds the position to the first email message.
*
* @return void
*/
public function rewind(): void
{
$this->msg = null;
$this->list = $this->data->sort(SORTDATE, 'UNSEEN', false);
$this->index = 0;
if (is_null($this->params)) {
$this->setParams([]);
}
}
/**
* Checks if the current postion is valid
*
* @return bool
*/
public function valid(): bool
{
return isset($this->list[$this->index]);
}
/**
* Processes the accepted email messages according to the settings
*
* @return void
*/
public function accepted(): void
{
if ($this->msg) {
$this->processMessageActions($this->params['when_done']);
}
}
/**
* Processes the rejected email messages according to the settings
*
* @return void
*/
public function rejected(): void
{
$this->processMessageActions($this->params['when_failed']);
}
/**
* Returns type of the source.
*
* @return int
*/
public function type(): int
{
return Source::SOURCE_MAILBOX;
}
/**
* Returns the current email message.
*
* @return MailMessage|null
*/
public function mailMessage()
{
return $this->msg;
}
/**
* Processes the current report message according to settings
*
* @param array $actions List of actions to apply to the message
*
* @return void
*/
private function processMessageActions(array &$actions): void
{
foreach ($actions as $sa) {
switch ($sa->type) {
case SourceAction::ACTION_SEEN:
$this->markMessageSeen();
break;
case SourceAction::ACTION_MOVE:
$this->moveMessage($sa->param);
break;
case SourceAction::ACTION_DELETE:
$this->deleteMessage();
break;
}
}
}
/**
* Marks the current report message as seen
*
* @return void
*/
public function markMessageSeen(): void
{
$this->msg->setSeen();
}
/**
* Moves the current report message
*
* @param string $mbox_name Child mailbox name where to move the current message to.
* If the target mailbox does not exists, it will be created.
*
* @return void
*/
private function moveMessage(string $mbox_name): void
{
$this->data->ensureMailbox($mbox_name);
$this->data->moveMessage($this->list[$this->index], $mbox_name);
}
/**
* Deletes the current report message
*
* @return void
*/
private function deleteMessage(): void
{
$this->data->deleteMessage($this->list[$this->index]);
}
}

View File

@@ -0,0 +1,115 @@
<?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/>.
*
* =========================
*
* This file contains the class Source
*
* @category API
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg\Sources;
/**
* It's an abstract class for easy access to reports of a report source
*/
abstract class Source implements \Iterator
{
public const SOURCE_UPLOADED_FILE = 1;
public const SOURCE_MAILBOX = 2;
public const SOURCE_DIRECTORY = 3;
protected $data = null;
/**
* Constructor
*
* @param mixed Data to reach report files
*
* @return void
*/
public function __construct($data)
{
$this->data = $data;
}
/**
* Sets parameters that difine the behavior of the source
*
* @param $params Key-value array
* 'when_done' => one or more rules to be executed after successful report processing
* (array|string)
* 'when_failed' => one or more rules to be executed after report processing fails
* (array|string)
*
* @return void
*/
public function setParams(array $params): void
{
}
/**
* Iterator interface methods
*/
abstract public function current(): object;
abstract public function key(): int;
abstract public function next(): void;
abstract public function rewind(): void;
abstract public function valid(): bool;
/**
* Called when the current report has been successfully processed.
*
* @return void
*/
public function accepted(): void
{
}
/**
* Called when the current report has been rejected.
*
* @return void
*/
public function rejected(): void
{
}
/**
* Returns type of source, i.e. one of Source::SOURCE_* values
*
* @return int
*/
abstract public function type(): int;
/**
* Returns the source itself that was passed to the constructor
*
* @return class
*/
public function container()
{
return $this->data;
}
}

View File

@@ -0,0 +1,156 @@
<?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/>.
*
* =========================
*
* This file contains the class SourceAction
*
* @category API
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg\Sources;
use Liuch\DmarcSrg\Exception\LogicException;
/**
* It's a class for describing one action of source
*/
class SourceAction
{
public const ACTION_SEEN = 1;
public const ACTION_MOVE = 2;
public const ACTION_DELETE = 3;
public const FLAG_BASENAME = 1;
private $valid = false;
private $type = 0;
private $param = null;
/**
* The constructor
*
* @param string $action Action name with parameter separated by colon
* Examples: 'move_to:failed', 'delete'
*
* @return void
*/
private function __construct(string $action)
{
if (($delim_offset = mb_strpos($action, ':')) === false) {
$name = $action;
$param = null;
} else {
$name = mb_substr($action, 0, $delim_offset);
$param = mb_substr($action, $delim_offset + 1);
}
switch ($name) {
case 'mark_seen':
$this->type = self::ACTION_SEEN;
if (!empty($param)) {
return;
}
break;
case 'move_to':
$this->type = self::ACTION_MOVE;
if (empty($param)) {
return;
}
break;
case 'delete':
$this->type = self::ACTION_DELETE;
if (!empty($param)) {
return;
}
break;
default:
return;
}
$this->param = $param;
$this->valid = true;
}
/**
* The getter
*
* @param string $name Property name. Must be one of the following: 'type', 'param'
*
* @return mixed
*/
public function __get(string $name)
{
if (in_array($name, [ 'type', 'param' ])) {
return $this->$name;
}
throw new LogicException('Undefined property: ' . $name);
}
/**
* Handles a setting, flags, and returns an array of SourceAction instances
*
* @param string|array $setting Setting from the conf.php
* @param int $flags Flags of extra checking the result
* @param string $default Action to add if the result array is empty
*
* @return array
*/
public static function fromSetting($setting, int $flags, string $default): array
{
if (gettype($setting) !== 'array') {
$setting = [ $setting ];
}
$tmap = [];
$list = [];
foreach ($setting as $it) {
if (gettype($it) === 'string') {
$sa = new self($it);
if ($sa->valid && !isset($tmap[$sa->type])) {
if (($flags & self::FLAG_BASENAME) && !self::checkBasename($sa)) {
continue;
}
$list[] = $sa;
$tmap[$sa->type] = true;
}
}
}
if (count($list) === 0) {
$sa = new self($default);
if ($sa->valid) {
$list[] = $sa;
}
}
return $list;
}
/**
* Checks if the param is just a directory name without a path
*
* @param self $sa
*
* @return bool
*/
private static function checkBasename($sa): bool
{
return ($sa->type !== self::ACTION_MOVE || basename($sa->param) === $sa->param);
}
}

View File

@@ -0,0 +1,113 @@
<?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/>.
*
* =========================
*
* This file contains the class UploadedFilesSource
*
* @category API
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg\Sources;
use Liuch\DmarcSrg\ReportFile\ReportFile;
use Liuch\DmarcSrg\Exception\SoftException;
/**
* This class is designed to process report files from uploaded files.
*/
class UploadedFilesSource extends Source
{
private $index = 0;
/**
* Returns an instance of the ReportFile class for the current file.
*
* @return ReportFile
*/
public function current(): object
{
if ($this->data['error'][$this->index] !== UPLOAD_ERR_OK) {
throw new SoftException('Failed to upload the report file');
}
$realfname = $this->data['name'][$this->index];
$tempfname = $this->data['tmp_name'][$this->index];
if (!is_uploaded_file($tempfname)) {
throw new SoftException('Possible file upload attack');
}
return ReportFile::fromFile($tempfname, $realfname, false);
}
/**
* Returns the index of the currect file.
*
* @return int
*/
public function key(): int
{
return $this->index;
}
/**
* Moves forward to the next file.
*
* @return void
*/
public function next(): void
{
++$this->index;
}
/**
* Rewinds the position to the first file.
*
* @return void
*/
public function rewind(): void
{
$this->index = 0;
}
/**
* Checks if the current postion is valid
*
* @return bool
*/
public function valid(): bool
{
return isset($this->data['name'][$this->index]);
}
/**
* Returns type of the source.
*
* @return int
*/
public function type(): int
{
return Source::SOURCE_UPLOADED_FILE;
}
}

View File

@@ -0,0 +1,169 @@
<?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/>.
*
* =========================
*
* This file contains the class Statistics
*
* @category API
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg;
use Liuch\DmarcSrg\DateTime;
/**
* This class is designed to get statistics on DMARC reports of a specified period
*/
class Statistics
{
private $db = null;
private $domain = null;
private $range = [
'date1' => null,
'date2' => null
];
/**
* The constructor of the class, it only uses in static methods of this class
*
* @param Domain|null $domain The domain for which you need to get statistics, null for all the domains.
* @param DatabaseController $db The database controller
*
* @return void
*/
private function __construct($domain, $db)
{
$this->domain = $domain;
$this->db = $db ?? Core::instance()->database();
}
/**
* Returns an instance of the class for the period from $date1 to $date2
*
* @param Domain|null $domain See the constructor for the details
* @param DateTime $date1 The date you need statistics from
* @param DateTime $date2 The date you need statistics to (not included)
* @param DatabaseController $db The database controller
*
* @return Statistics Instance of the class
*/
public static function fromTo($domain, $date1, $date2, $db = null)
{
$r = new Statistics($domain, $db);
$r->range['date1'] = $date1;
$r->range['date2'] = $date2;
return $r;
}
/**
* Returns an instance of the class for the last week
*
* @param Domain|null $domain See the constructor for the details
* @param DatabaseController $db The database controller
*
* @return Statistics Instance of the class
*/
public static function lastWeek($domain, $db = null)
{
$r = new Statistics($domain, $db);
$r->range['date1'] = new DateTime('monday last week');
$r->range['date2'] = (clone $r->range['date1'])->add(new \DateInterval('P7D'));
return $r;
}
/**
* Returns an instance of the class for the last month
*
* @param Domain|null $domain See the construct for the details
* @param DatabaseController $db The database controller
*
* @return Statistics Instance of the class
*/
public static function lastMonth($domain, $db = null)
{
$r = new Statistics($domain, $db);
$r->range['date1'] = new DateTime('midnight first day of last month');
$r->range['date2'] = new DateTime('midnight first day of this month');
return $r;
}
/**
* Returns an instance of the class for the last N days
*
* @param Domain|null $domain See the construct for the details
* @param int $ndays Number of days
* @param DatabaseController $db The database controller
*
* @return Statistics Instance of the class
*/
public static function lastNDays($domain, int $ndays, $db = null)
{
$r = new Statistics($domain, $db);
$r->range['date2'] = new DateTime('midnight');
$r->range['date1'] = (clone $r->range['date2'])->sub(new \DateInterval("P{$ndays}D"));
return $r;
}
/**
* Returns the date from and the date to in an array
*
* @return array - The range of the statistics
*/
public function range(): array
{
return [ (clone $this->range['date1']), (clone $this->range['date2'])->sub(new \DateInterval('PT1S')) ];
}
/**
* Returns summary information for e-mail messages as an array
*
* @return array Array with summary information
*/
public function summary(): array
{
return $this->db->getMapper('statistics')->summary($this->domain, $this->range);
}
/**
* Returns a list of ip-addresses from which the e-mail messages were received, with some statistics for each one
*
* @return array A list of ip-addresses with fields
* `ip`, `emails`, `dkim_aligned`, `spf_aligned`
*/
public function ips(): array
{
return $this->db->getMapper('statistics')->ips($this->domain, $this->range);
}
/**
* Returns a list of organizations that sent the reports with some statistics for each one
*
* @return array List of organizations with fields `name`, `reports`, `emails`
*/
public function organizations(): array
{
return $this->db->getMapper('statistics')->organizations($this->domain, $this->range);
}
}

View File

@@ -0,0 +1,109 @@
<?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/>.
*
* =========================
*
* This file contains the class Status
*
* @category API
* @package DmarcSrg
* @author Aleksey Andreev (liuch)
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
*/
namespace Liuch\DmarcSrg;
use Liuch\DmarcSrg\Settings\SettingsList;
/**
* This class is designed to get the general state of DmarcSrg
*/
class Status
{
private $core = null;
/**
* The constructor
*
* @param Core $core
*/
public function __construct(object $core)
{
$this->core = $core;
}
/**
* Returns general state of DmarcSrg
*
* This method returns an array with general state of the modules Admin, Auth
* and statistics for the last N days.
*
* @return array
*/
public function get(): array
{
$adm_res = $this->core->admin()->state();
$res = [
'state' => $adm_res['state']
];
if (isset($adm_res['error_code'])) {
$res['error_code'] = $adm_res['error_code'];
if (isset($adm_res['message'])) {
$res['message'] = $adm_res['message'];
}
if (isset($adm_res['debug_info'])) {
$res['debug_info'] = $adm_res['debug_info'];
}
} elseif (isset($adm_res['database']['error_code'])) {
$res['error_code'] = $adm_res['database']['error_code'];
if (isset($adm_res['database']['message'])) {
$res['message'] = $adm_res['database']['message'];
}
if (isset($adm_res['database']['debug_info'])) {
$res['debug_info'] = $adm_res['database']['debug_info'];
}
} elseif (isset($adm_res['message'])) {
$res['message'] = $adm_res['message'];
} elseif (isset($adm_res['database']['message'])) {
$res['message'] = $adm_res['database']['message'];
}
if (!isset($res['error_code']) || $res['error_code'] === 0) {
$days = SettingsList::getSettingByName('status.emails-for-last-n-days')->value();
$stat = Statistics::lastNDays(null, $days);
$res['emails'] = $stat->summary()['emails'];
$res['emails']['days'] = $days;
}
$auth = null;
if ($this->core->auth()->isEnabled()) {
$auth = $this->core->userId() !== false ? 'yes' : 'no';
} else {
$auth = 'disabled';
}
$res['authenticated'] = $auth;
$res['version'] = Core::APP_VERSION;
$res['php_version'] = phpversion();
return $res;
}
}