From 32f0677dbca9e3347b931c1d0105eb37aa57e90d Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Fri, 25 Jun 2021 16:21:17 +0200 Subject: [eventlog] Add event filtering and notification system --- modules-available/eventlog/pages/log.inc.php | 57 +++++++ .../eventlog/pages/mailconfigs.inc.php | 98 ++++++++++++ modules-available/eventlog/pages/rules.inc.php | 172 ++++++++++++++++++++ .../eventlog/pages/transports.inc.php | 176 +++++++++++++++++++++ 4 files changed, 503 insertions(+) create mode 100644 modules-available/eventlog/pages/log.inc.php create mode 100644 modules-available/eventlog/pages/mailconfigs.inc.php create mode 100644 modules-available/eventlog/pages/rules.inc.php create mode 100644 modules-available/eventlog/pages/transports.inc.php (limited to 'modules-available/eventlog/pages') diff --git a/modules-available/eventlog/pages/log.inc.php b/modules-available/eventlog/pages/log.inc.php new file mode 100644 index 00000000..a48b4a95 --- /dev/null +++ b/modules-available/eventlog/pages/log.inc.php @@ -0,0 +1,57 @@ +exec(); + foreach ($res as $row) { + $row['date'] = Util::prettyTime($row['dateline']); + $row['icon'] = self::typeToIcon($row['logtypeid']); + $row['color'] = self::typeToColor($row['logtypeid']); + $lines[] = $row; + } + + $paginate->render('_page', array( + 'list' => $lines + )); + } + + private static function typeToIcon($type) + { + switch ($type) { + case 'info': + return 'ok'; + case 'warning': + return 'exclamation-sign'; + case 'failure': + return 'remove'; + default: + return 'question-sign'; + } + } + + private static function typeToColor($type) + { + switch ($type) { + case 'info': + return ''; + case 'warning': + return 'orange'; + case 'failure': + return 'red'; + default: + return ''; + } + } + +} \ No newline at end of file diff --git a/modules-available/eventlog/pages/mailconfigs.inc.php b/modules-available/eventlog/pages/mailconfigs.inc.php new file mode 100644 index 00000000..6d5d20b6 --- /dev/null +++ b/modules-available/eventlog/pages/mailconfigs.inc.php @@ -0,0 +1,98 @@ +', '>=', 'regex']; + + public static function doPreprocess() + { + if (Request::isPost()) { + $action = Request::post('action'); + if ($action === 'save-mailconfig') { + self::saveMailconfig(); + } elseif ($action === 'delete-mailconfig') { + self::deleteMailconfig(); + } else { + Message::addError('main.invalid-action', $action); + } + Util::redirect('?do=eventlog&show=mailconfigs'); + } + } + + private static function saveMailconfig() + { + User::assertPermission('filter.mailconfigs.edit'); + $id = Request::post('id', Request::REQUIRED, 'int'); + $data = [ + 'host' => Request::post('host', Request::REQUIRED, 'string'), + 'port' => Request::post('port', Request::REQUIRED, 'int'), + 'ssl' => Request::post('ssl', Request::REQUIRED, 'string'), + 'senderaddress' => Request::post('senderaddress', Request::REQUIRED, 'string'), + 'replyto' => Request::post('replyto', '', 'string'), + 'username' => Request::post('username', '', 'string'), + 'password' => Request::post('password', '', 'string'), + ]; + if ($id === 0) { + // NEW + Database::exec("INSERT INTO mail_config (host, port, `ssl`, senderaddress, replyto, username, password) + VALUES (:host, :port, :ssl, :senderaddress, :replyto, :username, :password)", $data); + } else { + // UPDATE + $data['configid'] = $id; + Database::exec("UPDATE mail_config SET host = :host, port = :port, `ssl` = :ssl, + senderaddress = :senderaddress, replyto = :replyto, username = :username, password = :password + WHERE configid = :configid", $data); + } + Message::addSuccess("event-mailconfig-saved", $id); + Util::redirect('?do=eventlog&show=mailconfigs'); + } + + private static function deleteMailconfig() + { + User::assertPermission('filter.mailconfigs.edit'); + $id = Request::post('id', Request::REQUIRED, 'int'); + Database::exec("DELETE FROM mail_config WHERE configid = :id", ['id' => $id]); + } + + /* + * + */ + + public static function doRender() + { + $id = Request::get('id', null, 'int'); + if ($id !== null) { + self::showMailconfigEditor($id); + } else { + // LIST + $data = []; + $data['configs'] = Database::queryAll('SELECT configid, host, port, `ssl`, senderaddress, replyto + FROM mail_config + ORDER BY host'); + Render::addTemplate('page-filters-mailconfigs', $data); + } + } + + /** + * @param int $id Config to edit. If id is 0, a new config will be created. + */ + private static function showMailconfigEditor(int $id) + { + if ($id !== 0) { + // EDIT + $data = Database::queryFirst('SELECT configid, host, port, `ssl`, senderaddress, replyto, + username, password + FROM mail_config + WHERE configid = :id', ['id' => $id]); + if ($data === false) { + Message::addError('invalid-mailconfig-id', $id); + Util::redirect('?do=eventlog&show=mailconfigs'); + } + } else { + $data = ['configid' => 0]; + } + Render::addTemplate('page-filters-edit-mailconfig', $data); + } + +} \ No newline at end of file diff --git a/modules-available/eventlog/pages/rules.inc.php b/modules-available/eventlog/pages/rules.inc.php new file mode 100644 index 00000000..131c4eb6 --- /dev/null +++ b/modules-available/eventlog/pages/rules.inc.php @@ -0,0 +1,172 @@ +', '>=', 'regex']; + + public static function doPreprocess() + { + if (Request::isPost()) { + $action = Request::post('action'); + if ($action === 'save-filter') { + self::saveRule(); + } elseif ($action === 'delete-filter') { + self::deleteRule(); + } else { + Message::addError('main.invalid-action', $action); + } + Util::redirect('?do=eventlog&show=rules'); + } + } + + private static function saveRule() + { + User::assertPermission('filter.rules.edit'); + $id = Request::post('id', Request::REQUIRED, 'int'); + $type = Request::post('type', Request::REQUIRED, 'string'); + $title = Request::post('title', Request::REQUIRED, 'string'); + $message = Request::post('message', Request::REQUIRED, 'string'); + $transports = Request::post('transports', [], 'array'); + $filters = Request::post('filter', Request::REQUIRED, 'array'); + $filters = array_filter($filters, function ($item) { + return is_array($item) && !empty($item['path']) && !empty($item['op']); + }); + foreach ($filters as $index => &$item) { + $item['index'] = $index; + } + unset($item); + if (empty($filters)) { + Message::addError('no-valid-filters'); + Util::redirect('?do=eventlog&show=rules'); + } + if ($id === 0) { + $id = null; + } + $data = [ + 'id' => $id, + 'type' => $type, + 'title' => $title, + 'data' => json_encode(['list' => array_values($filters)]), + 'subject' => Request::post('subject', '', 'string'), + 'message' => $message, + ]; + if ($id === null) { + // NEW + Database::exec("INSERT INTO notification_rule (ruleid, type, title, datafilter, subject, message) + VALUES (:id, :type, :title, :data, :subject, :message)", $data); + $id = Database::lastInsertId(); + } else { + Database::exec("UPDATE notification_rule SET type = :type, title = :title, datafilter = :data, + subject = :subject, message = :message + WHERE ruleid = :id", $data); + } + if (empty($transports)) { + Database::exec("DELETE FROM notification_rule_x_transport WHERE ruleid = :id", ['id' => $id]); + } else { + Database::exec("DELETE FROM notification_rule_x_transport + WHERE ruleid = :id AND transportid NOT IN (:transports)", + ['id' => $id, 'transports' => $transports]); + Database::exec("INSERT IGNORE INTO notification_rule_x_transport (ruleid, transportid) + VALUES :list", ['list' => array_map(function ($i) use ($id) { return [$id, $i]; }, $transports)]); + } + Message::addSuccess("event-rule-saved", $id); + Util::redirect('?do=eventlog&show=rules'); + } + + private static function deleteRule() + { + User::assertPermission('filter.rules.edit'); + $id = Request::post('id', Request::REQUIRED, 'int'); + Database::exec("DELETE FROM notification_rule WHERE ruleid = :id", ['id' => $id]); + } + + /* + * + */ + + public static function doRender() + { + $id = Request::get('id', null, 'int'); + if ($id !== null) { + self::showRuleEditor($id); + } else { + // LIST + $data = []; + $data['filters'] = Database::queryAll('SELECT ruleid, type, title, datafilter, + Count(transportid) AS useCount + FROM notification_rule + LEFT JOIN notification_rule_x_transport sfxb USING (ruleid) + GROUP BY ruleid, title + ORDER BY title, ruleid'); + Render::addTemplate('page-filters-rules', $data); + } + } + + /** + * @param int $id Rule to edit. If id is 0, a new rule will be created. + */ + private static function showRuleEditor(int $id) + { + // EDIT + $index = 0; + $existing = []; + if ($id !== 0) { + $data = Database::queryFirst('SELECT ruleid, title, type, datafilter, subject, message + FROM notification_rule WHERE ruleid = :id', ['id' => $id]); + if ($data === false) { + Message::addError('invalid-rule-id', $id); + Util::redirect('?do=eventlog&show=rules'); + } + $list = json_decode($data['datafilter'], true); + if (!is_array($list['list'])) { + $list['list'] = []; + } + foreach ($list['list'] as $item) { + if (isset($item['index'])) { + $existing[] = $item['index']; + } + } + foreach ($list['list'] as &$item) { + if (!isset($item['index'])) { + while (in_array($index, $existing)) { + $index++; + } + $item['index'] = $index++; + } + $item['operators'] = []; + foreach (self::OP_LIST as $op) { + $item['operators'][] = [ + 'name' => $op, + 'selected' => ($op === $item['op']) ? 'selected' : '', + ]; + } + } + $data['filter'] = $list['list']; + } else { + $data = ['filter' => [], 'ruleid' => 0]; + } + for ($i = 0; $i < 2; ++$i) { + while (in_array($index, $existing)) { + $index++; + } + $data['filter'][] = [ + 'index' => $index++, + 'operators' => array_map(function ($item) { return ['name' => $item]; }, self::OP_LIST), + ]; + } + // Add suggestions for type + $data['types'] = Database::queryColumnArray("SELECT DISTINCT type + FROM notification_sample + ORDER BY type"); + // + Module::isAvailable('bootstrap_multiselect'); + $data['transports'] = Database::queryAll("SELECT nb.transportid, nb.title, + IF(sfxb.ruleid IS NULL, '', 'selected') AS selected + FROM notification_backend nb + LEFT JOIN notification_rule_x_transport sfxb ON (sfxb.transportid = nb.transportid AND sfxb.ruleid = :id)", + ['id' => $id]); + Render::addTemplate('page-filters-edit-rule', $data); + } + +} \ No newline at end of file diff --git a/modules-available/eventlog/pages/transports.inc.php b/modules-available/eventlog/pages/transports.inc.php new file mode 100644 index 00000000..b72f36f9 --- /dev/null +++ b/modules-available/eventlog/pages/transports.inc.php @@ -0,0 +1,176 @@ + [Request::REQUIRED, 'string', ['mail', 'irc', 'http', 'group']], + 'mail-config-id' => [0, 'int'], + 'mail-users' => [[], 'int[]'], + 'mail-extra-mails' => ['', 'string'], + 'irc-server' => ['', 'string'], + 'irc-server-password' => ['', 'string'], + 'irc-target' => ['', 'string'], + 'irc-nickname' => ['', 'string'], + 'http-uri' => ['', 'string'], + 'http-method' => ['', 'string', ['GET', 'POST']], + 'http-post-field' => ['', 'string'], + 'http-post-format' => ['', 'string', ['FORM', 'JSON', 'JSON_AUTO']], + 'group-list' => [[], 'int[]'], + ]; + $data = []; + foreach ($types as $key => $def) { + if (substr($def[1], -1) === ']') { + $type = substr($def[1], 0, -2); + $array = true; + } else { + $type = $def[1]; + $array = false; + } + if ($array) { + $value = Request::post($key, [], 'array'); + foreach ($value as &$v) { + settype($v, $type); + if (isset($def[2]) && !in_array($v, $def[2])) { + Message::addWarning('main.value-invalid', $key, $v); + } + } + } else { + $value = Request::post($key, $def[0], $type); + if (isset($def[2]) && !in_array($value, $def[2])) { + Message::addWarning('main.value-invalid', $key, $value); + } + } + $data[$key] = $value; + } + //die(print_r($data)); + $params = [ + 'title' => Request::post('title', 'Backend', 'string'), + 'description' => Request::post('description', '', 'string'), + 'data' => json_encode($data), + ]; + if ($id === 0) { + $res = Database::exec("INSERT INTO notification_backend (title, description, data) + VALUES (:title, :description, :data)", $params); + $id = Database::lastInsertId(); + } else { + $params['transportid'] = $id; + $res = Database::exec("UPDATE notification_backend + SET title = :title, description = :description, data = :data + WHERE transportid = :transportid", $params); + } + if (empty($rules)) { + Database::exec("DELETE FROM notification_rule_x_transport WHERE transportid = :id", ['id' => $id]); + } else { + Database::exec("DELETE FROM notification_rule_x_transport + WHERE transportid = :id AND ruleid NOT IN (:rules)", + ['id' => $id, 'rules' => $rules]); + Database::exec("INSERT IGNORE INTO notification_rule_x_transport (ruleid, transportid) + VALUES :list", ['list' => array_map(function ($i) use ($id) { return [$i, $id]; }, $rules)]); + } + if ($res > 0) { + Message::addSuccess('transport-saved', $id); + } + Util::redirect('?do=eventlog&show=transports§ion=transports'); + } + + private static function deleteTransport() + { + User::assertPermission('filter.transports.edit'); + $id = Request::post('id', Request::REQUIRED, 'int'); + Database::exec("DELETE FROM notification_backend WHERE transportid = :id", ['id' => $id]); + } + + /* + * + */ + + public static function doRender() + { + $id = Request::get('id', null, 'int'); + if ($id !== null) { + self::showTransportEditor($id); + } else { + // LIST + $data = []; + $data['transports'] = []; + foreach (Database::queryAll('SELECT transportid, title, data, + Count(ruleid) AS useCount + FROM notification_backend nb + LEFT JOIN notification_rule_x_transport sfxb USING (transportid) + GROUP BY transportid, title + ORDER BY title, transportid') as $transport) { + $json = json_decode($transport['data'], true); + $transport['type'] = $json['type']; + $transport['details'] = NotificationTransport::getInstance($json); + $data['transports'][] = $transport; + } + Render::addTemplate('page-filters-transports', $data); + } + } + + /** + * @param int $id Transport to edit, 0 to create a new one + */ + private static function showTransportEditor(int $id) + { + if ($id !== 0) { + $entry = Database::queryFirst('SELECT transportid, title, description, data + FROM notification_backend + WHERE transportid = :id', ['id' => $id]); + if ($entry === false) { + Message::addError('invalid-transport-id', $id); + Util::redirect('?do=eventlog&show=transports§ion=transports'); + } + $entry['data'] = json_decode($entry['data'], true); + $entry[($entry['data']['type'] ?? '') . '_selected'] = 'selected'; + $entry[($entry['data']['http-method'] ?? '') . '_selected'] = 'selected'; + $entry[($entry['data']['http-post-format'] ?? '') . '_selected'] = 'selected'; + } else { + $entry = ['transportid' => $id]; + } + $entry['users'] = []; + foreach (Database::queryAll("SELECT userid, login, fullname, email FROM user ORDER BY login") as $row) { + $row['disabled'] = strpos($row['email'], '@') ? '' : 'disabled'; + $row['selected'] = in_array($row['userid'], $entry['data']['mail-users'] ?? []) ? 'selected' : ''; + $entry['users'][] = $row; + } + $entry['mailconfigs'] = []; + foreach (Database::queryAll("SELECT configid, host, port, senderaddress FROM mail_config") as $row) { + $row['selected'] = $row['configid'] == $entry['data']['mail-config-id'] ? 'selected' : ''; + $entry['mailconfigs'][] = $row; + } + foreach (Database::queryAll("SELECT transportid, title FROM notification_backend") as $row) { + $row['selected'] = in_array($row['transportid'], ($entry['data']['group-list'] ?? [])) ? 'selected' : ''; + $entry['backends'][] = $row; + } + Module::isAvailable('bootstrap_multiselect'); + $entry['rules'] = Database::queryAll("SELECT sf.ruleid, sf.title, + IF(sfxb.transportid IS NULL, '', 'selected') AS selected + FROM notification_rule sf + LEFT JOIN notification_rule_x_transport sfxb ON (sf.ruleid = sfxb.ruleid AND sfxb.transportid = :id)", + ['id' => $id]); + Render::addTemplate('page-filters-edit-transport', $entry); + } + +} \ No newline at end of file -- cgit v1.2.3-55-g7522