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,678 @@
/**
* 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/>.
*/
class Admin {
constructor(id) {
this._state = null;
this._element = null;
this._sources = null;
this._database = null;
}
display() {
let cn = document.getElementById("main-block");
if (!this._element) {
this._element = document.createElement("div");
this._element.setAttribute("class", "panel-container round-border");
this._element.appendChild(this._get_database_elements());
this._element.appendChild(this._get_sources_elements());
}
cn.appendChild(this._element);
}
update() {
this._get_admin_state();
}
title() {
return "Admin Panel";
}
_get_database_elements() {
let fr = document.createDocumentFragment();
let h = document.createElement("h4");
h.appendChild(document.createTextNode("Database"));
fr.appendChild(h);
if (!this._database) {
this._database = new DatabaseListBox(this._create_db_item_menu_element());
}
fr.appendChild(this._database.element());
return fr;
}
_get_sources_elements() {
let fr = document.createDocumentFragment();
let h = document.createElement("h4");
h.appendChild(document.createTextNode("Report sources"));
fr.appendChild(h);
if (!this._sources) {
this._sources = new SourceListBox();
}
fr.appendChild(this._sources.element());
return fr;
}
_get_admin_state() {
[ this._database, this._sources ].forEach(function(c) {
c.set_status("wait");
});
let t = this;
window.fetch("admin.php", {
method: "GET",
cache: "no-store",
headers: HTTP_HEADERS,
credentials: "same-origin"
}).then(function(resp) {
if (!resp.ok)
throw new Error("Failed to fetch the admin data");
return resp.json();
}).then(function(data) {
Common.checkResult(data);
t._state = data;
t._fill_data();
}).catch(function(err) {
Common.displayError(err);
t._fill_data(err.message);
});
}
_send_command(cmd) {
let t = this;
return window.fetch("admin.php", {
method: "POST",
cache: "no-store",
headers: Object.assign(HTTP_HEADERS, HTTP_HEADERS_POST),
credentials: "same-origin",
body: JSON.stringify(cmd)
}).then(function(resp) {
if (!resp.ok)
throw new Error("Failed");
return resp.json();
}).finally(function() {
t._get_admin_state();
Status.instance().update().catch(function(){});
});
}
_fill_data(err_msg) {
if (!err_msg) {
let d = this._state.database || [];
this._database.set_data({
root: {
name: d.name || "-",
type: d.type || "-",
correct: d.correct,
message: d.message || "-",
location: d.location || "-"
},
groups: [
{ name: "Tables", items: this._state.database.tables || [] }
]
});
this._sources.set_data({
groups: [
{ name: "Mailboxes", type: "mailbox", items: this._state.mailboxes || [] },
{ name: "Directories", type: "directory", items: this._state.directories || [] }
]
});
}
else {
this._database.set_status("error", err_msg);
this._sources.set_status("error", err_msg);
}
if (this._state && this._state.database && this._state.database.needs_upgrade) {
document.querySelector(".db-menu-button li[data-action=upgradedb]").classList.remove("hidden");
}
}
_create_db_item_menu_element() {
let el = document.createElement("div");
let span = document.createElement("span");
span.setAttribute("role", "button");
span.appendChild(document.createTextNode("..."));
el.appendChild(span);
//
let mn = document.createElement("div");
mn.setAttribute("class", "db-item-menu popup-menu round-border hidden");
let ul = document.createElement("ul");
Admin.db_actions.forEach(function(it) {
let li = document.createElement("li");
li.setAttribute("data-action", it.action);
li.setAttribute("data-title", it.title);
li.setAttribute("title", it.long_title);
let sp = document.createElement("span");
sp.appendChild(document.createTextNode(it.name));
li.appendChild(sp);
ul.appendChild(li);
if (it.action === "upgradedb")
li.classList.add("hidden");
}, this);
mn.appendChild(ul);
el.appendChild(mn);
let t = this;
el.addEventListener("click", function(event) {
let it = event.target.closest("li");
if (it || event.target.parentNode === this) {
event.stopPropagation();
this.querySelector(".popup-menu").classList.toggle("hidden");
}
if (it) {
let action = it.getAttribute("data-action");
let title = it.getAttribute("data-title");
t._do_db_action_password(action, title);
}
});
return el;
}
_do_db_action_password(action, title) {
let ld = new LoginDialog({
nofetch: true,
nousername: true
});
document.getElementById("main-block").appendChild(ld.element());
let that = this;
ld.show().then(function(d) {
if (d) {
that._do_db_action(action, title, { password: d.password });
}
}).catch(function(err) {
Common.displayError(err);
}).finally(function() {
ld.remove();
});
}
_do_db_action(action, title, data) {
let d = { cmd: action };
if (data) {
d = Object.assign(d, data);
}
this._send_command(d).then(function(data) {
Common.checkResult(data);
Notification.add({ text: title + ": " + (data.message || "Completed successfully!"), type: "info" });
}).catch(function(err) {
Common.displayError(err);
Notification.add({ text: title + ": " + err.message, type: "error", delay: 10000 });
});
}
}
Admin.db_actions = [
{
name: "Initiate",
action: "initdb",
title: "Initiate DB",
long_title: "Create all needed tables and indexes in the database"
},
{
name: "Drop",
action: "cleandb",
title: "Drop tables",
long_title: "Drop all the tables from the database"
},
{
name: "Upgrade",
action: "upgradedb",
title: "Upgrade DB",
long_title: "Update the structure of the database"
}
];
class DropdownListBox {
constructor() {
this._item_groups = [];
this._element = null;
this._root_item = null
this._list_element = null;
}
element() {
if (!this._element) {
let el = document.createElement("div");
el.setAttribute("class", "round-border");
let that = this;
el.addEventListener("click", function(event) {
if (event.target.closest(".root-list-block")) {
if (that._item_groups.length > 0) {
that._list_element.classList.toggle("hidden");
that._root_item.element().classList.toggle("bottom-border");
}
}
});
this._element = el;
this._update_element();
}
return this._element;
}
set_status(type, message) {
if (type === "wait") {
set_wait_status(this.element());
}
else if (type === "error") {
set_error_status(this.element(), message);
}
}
set_data(data) {
this._root_item = new ListBoxItem();
this._make_group_list(data);
this._make_root_columns(data);
if (this._element) {
this._update_element();
}
}
_update_element() {
if (this._element.children.length != 2) {
remove_all_children(this._element);
this._element.appendChild(this._content_container());
this._element.appendChild(this._list_container());
}
}
_content_container() {
if (!this._root_item) {
this._root_item = new ListBoxItem();
}
let c = this._root_item.element();
let cl = [];
for (let i = 0; i < c.classList.length; ++i) {
if (c.classList[i].startsWith("state-"))
cl.push(c.classList[i]);
}
c.setAttribute("class", "root-list-block" + (cl.length >0 && (" " + cl.join(" ")) || ""));
return c;
}
_list_container() {
let c = document.createElement("div");
c.setAttribute("class", "list-container hidden");
c.appendChild(this._make_list_item_elements());
this._list_element = c;
return c;
}
_make_root_columns(data) {
}
_make_group_list(data) {
this._item_groups = data.groups.map(function(gd) {
return this._make_group_item(gd);
}, this);
}
_make_group_item(gr_data) {
return new ListBoxItemGroup(gr_data);
}
_make_list_item_elements() {
let fr = document.createDocumentFragment();
this._item_groups.forEach(function(ig) {
fr.appendChild(ig.element());
});
return fr;
}
}
class DatabaseListBox extends DropdownListBox {
constructor(menu) {
super();
this._menu = menu;
this._name = null;
this._type = null;
this._correct = false;
this._message = null;
this._location = null;
}
set_data(data) {
this._name = data.root.name;
this._type = data.root.type;
this._correct = data.root.correct;
this._message = data.root.message;
this._location = data.root.location;
super.set_data(data);
}
_make_root_columns(data) {
this._root_item.state(this._correct && "green" || "red");
this._root_item.add_column(new StatusIndicator(this._name, this._message, "title-item-wrap"));
this._root_item.add_column(new ListBoxColumn(this._message, null, "message-item state-text"));
this._root_item.add_column(new ListBoxColumn(this._type, null, "db-type"));
this._root_item.add_column(new ListBoxColumn(this._location, null, "db-location"));
if (this._menu)
this._root_item.add_column(new ListBoxColumn(this._menu, null, "db-menu-button"));
}
_make_group_item(gr_data) {
return new DatabaseItemGroup(gr_data);
}
}
class SourceListBox extends DropdownListBox {
element() {
let _new = !this._element && true || false;
super.element();
if (_new) {
let that = this;
this._element.addEventListener("click", function(event) {
if (event.target.tagName == "BUTTON") {
let p = event.target.closest("div[data-id]")
if (p) {
let id = parseInt(p.getAttribute("data-id"));
let type = p.getAttribute("data-type");
that._check_button_clicked(id, type, event.target);
}
}
});
}
return this._element;
}
_make_root_columns(data) {
let count = this._item_groups.reduce(function(cnt, gr) {
return cnt + gr.count();
}, 0);
let enabled = (count > 0);
this._root_item.state(enabled && "green" || "gray");
this._root_item.add_column(new StatusIndicator("Total sources: " + count, enabled && "Enabled" || "Disabled"));
}
_make_group_item(gr_data) {
return new SourceItemGroup(gr_data);
}
_check_button_clicked(id, type, btn) {
let that = this;
let state = "yellow";
let btn_text = btn.textContent;
btn.textContent = "Checking...";
btn.disabled = true;
window.fetch("admin.php", {
method: "POST",
cache: "no-store",
headers: Object.assign(HTTP_HEADERS, HTTP_HEADERS_POST),
credentials: "same-origin",
body: JSON.stringify({ cmd: "checksource", id: id, type: type })
}).then(function(resp) {
if (!resp.ok)
throw new Error("Failed");
return resp.json();
}).then(function(data) {
Common.checkResult(data);
let msg = [ data.message ];
if (data.status) {
if (type === "mailbox") {
msg.push("Messages: " + data.status.messages);
msg.push("Unseen: " + data.status.unseen);
}
else if (type === "directory") {
msg.push("Files: " + data.status.files);
}
}
Notification.add({ text: msg, type: "info" });
state = "green";
}).catch(function(err) {
Common.displayError(err);
Notification.add({ text: err.message, type: "error" });
}).finally(function() {
btn.textContent = btn_text;
btn.disabled = false;
that._set_state(state, id, type);
});
}
_set_state(state, id, type) {
let flag = 0;
let gstate = "green";
for (let i = 0; flag !== 3 && i < this._item_groups.length; ++i) {
let gr = this._item_groups[i];
if (!(flag & 1) && gr.type() === type) {
gr.state(state, id);
flag |= 1;
}
if (!(flag & 2)) {
let s = gr.state();
if (s !== "green") {
gstate = s;
flag |= 2;
}
}
}
this._root_item.state(gstate);
}
}
class ListBoxItem {
constructor() {
this._state = null;
this._element = null;
this._columns = [];
}
add_column(col) {
this._columns.push(col);
}
element() {
if (!this._element) {
this._element = document.createElement("div");
let extra_class = "";
if (this._state) {
extra_class = " state-" + this._state;
}
this._element.setAttribute("class", "block-list-item round-border" + extra_class);
this._insert_column_elements();
}
return this._element;
}
state(state) {
if (!state) {
return this._state;
}
if (this._element) {
if (this._state) {
this._element.classList.remove("state-" + this._state);
}
this._element.classList.add("state-" + state);
}
this._state = state;
}
_insert_column_elements() {
this._columns.forEach(function(c) {
this._element.appendChild(c.element());
}, this);
}
}
class SourceListItem extends ListBoxItem {
constructor(data) {
super();
this._id = data.id;
this._type = data.type;
}
id() {
return this._id;
}
type() {
return this._type;
}
element() {
let el = super.element();
el.setAttribute("data-id", this._id);
el.setAttribute("data-type", this._type);
return el;
}
}
class ListBoxItemGroup {
constructor(data) {
this._name = data.name;
this._type = data.type;
this._element = null;
this._items = data.items.map(function(it) {
return this._make_item(it);
}, this);
}
type() {
return this._type;
}
count() {
return this._items.length;
}
element() {
if (!this._element) {
let fr = document.createDocumentFragment();
let h = document.createElement("h5");
h.appendChild(document.createTextNode(this._name + " (" + this._items.length + ")"));
fr.appendChild(h);
this._items.forEach(function(it) {
fr.appendChild(it.element());
});
this._element = fr;
}
return this._element;
}
_make_item(d) {
return new ListBoxItem();
}
}
class DatabaseItemGroup extends ListBoxItemGroup {
_make_item(d) {
let it = super._make_item(d);
let state = d.error_code && "red" || (d.message === "Ok" && "green" || "yellow");
it.state(state);
it.add_column(new StatusIndicator(d.name, d.message, "title-item-wrap"));
it.add_column(new ListBoxColumn(d.engine || d.message, null, "message-item state-text"));
it.add_column(new ListBoxColumn(d.rows || 0, "Records", "dbtable-records"));
it.add_column(new ListBoxColumn((d.data_length || 0) + (d.index_length || 0), "Size", "dbtable-size"));
return it;
}
}
class SourceItemGroup extends ListBoxItemGroup {
state(new_state, item_id) {
if (item_id !== undefined) {
this._items.find(function(item) {
if (item.id() == item_id) {
item.state(new_state);
return true;
}
return false;
});
return;
}
let gstate = "green";
for (let i = 0; i < this._items.length; ++i) {
let state = this._items[i].state();
if (state !== gstate) {
return state;
}
}
return gstate;
}
_make_item(d) {
let it = new SourceListItem({ id: d.id, type: this._type });
it.state("green");
it.add_column(new StatusIndicator(d.name, null, "title-item-wrap"));
if (this._type === "mailbox") {
it.add_column(new ListBoxColumn(d.mailbox, null, "mailbox-location"));
it.add_column(new ListBoxColumn(d.host, "Host", "mailbox-host"));
}
else {
it.add_column(new ListBoxColumn(d.location, null, "directory-location"));
}
it.add_column(new ListBoxColumn(this._make_check_button(), null, "source-check-button"));
return it;
}
_make_check_button() {
let btn = document.createElement("button");
btn.appendChild(document.createTextNode("Check accessibility"));
return btn;
}
}
class ListBoxColumn {
constructor(value, title, class_string) {
this._value = value;
this._title = title;
this._class = class_string;
this._element = null;
}
element() {
if (!this._element) {
this._element = document.createElement("div");
this._element.setAttribute("class", "block-item-column" + (this._class && (" " + this._class) || ""));
this._add_children();
}
return this._element;
}
_add_children() {
let val_el = this._element;
if (this._title) {
let sp = document.createElement("span");
sp.appendChild(document.createTextNode(this._title + ":"));
this._element.appendChild(sp);
val_el = document.createElement("span");
val_el.setAttribute("class", "value");
this._element.appendChild(val_el);
}
if (typeof(this._value) != "object")
val_el.appendChild(document.createTextNode(this._value));
else
val_el.appendChild(this._value);
}
}
class StatusIndicator extends ListBoxColumn {
_add_children() {
let div = document.createElement("div");
div.setAttribute("class", "state-background status-indicator");
if (this._title) {
div.setAttribute("title", this._title);
}
this._element.appendChild(div);
if (this._value) {
div = document.createElement("div");
div.appendChild(document.createTextNode(this._value));
this._element.appendChild(div);
}
}
}

View File

@@ -0,0 +1,163 @@
/**
* 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/>.
*/
const HTTP_HEADERS = {
"Accept": "application/json"
};
const HTTP_HEADERS_POST = {
"Content-Type": "application/json"
};
function remove_all_children(el) {
while (el.children.length > 0)
el.removeChild(el.children[0]);
while (el.childNodes.length > 0)
el.removeChild(el.childNodes[0]);
}
function set_wait_status(el, text) {
let wait = document.createElement("div");
wait.setAttribute("class", "wait-message");
wait.appendChild(document.createTextNode(text || "Getting data..."));
if (el) {
remove_all_children(el);
el.appendChild(wait);
}
return wait;
}
function set_error_status(el, text) {
let err = document.createElement("div");
err.setAttribute("class", "error-message");
err.appendChild(document.createTextNode(text || "Error!"));
if (el) {
remove_all_children(el);
el.appendChild(err);
}
return err;
}
function date_range_to_string(d1, d2) {
let s1 = d1.toISOString().substr(0, 10);
let s2 = d2.toISOString().substr(0, 10);
if (s1 !== s2) {
let d3 = new Date(d2);
d3.setSeconds(d3.getSeconds() - 1);
if (s1 !== d3.toISOString().substr(0, 10))
s1 += " - " + s2;
}
return s1;
}
function create_report_result_element(name, value, long_rec, result) {
let span = document.createElement("span");
if (long_rec)
span.appendChild(document.createTextNode(name + ": " + value));
else
span.appendChild(document.createTextNode(name));
span.setAttribute("title", value);
let extra_class = "";
if (result === undefined || result !== "")
extra_class = " report-result-" + (result || value);
span.setAttribute("class", "report-result" + extra_class);
return span;
}
function scroll_to_element(element, container) { // because scrollIntoView is poorly supported by browsers
let diff = null;
let e_rect = element.getBoundingClientRect();
let c_rect = container.getBoundingClientRect();
let height = Math.min(e_rect.height, 64);
if (e_rect.top < c_rect.top + height * 2) {
diff = e_rect.top - c_rect.top - height * 2;
}
else if (e_rect.bottom > c_rect.bottom - height) {
diff = e_rect.bottom - c_rect.bottom + height;
}
if (diff) {
container.scrollBy(0, diff);
}
}
function bytes2size(bytes) {
if (!bytes) {
return "0 bytes";
}
const k = 1024;
const sizes = [ 'bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB' ];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}
class Common {
static tuneDateTimeOutput(mode) {
Date.prototype.outputMode = mode;
if (!Date.prototype.toUIString) {
Date.prototype.toUIString = function(prefer_utc) {
if (this.outputMode === "local" || (this.outputMode === "auto" && !prefer_utc)) {
return this.toLocaleString();
}
return this.toUTCString();
};
}
}
static makeIpElement(ip) {
let url = null;
let type = ip.includes(":") && 6 || 4;
switch (type) {
case 4:
url = Common.ipv4_url;
break;
case 6:
url = Common.ipv6_url;
break;
}
let tn = document.createTextNode(ip);
if (url) {
url = url.replace("{$ip}", ip).replace("{$eip}", encodeURIComponent(ip));
let el = document.createElement("a");
el.setAttribute("href", url);
el.setAttribute("target", "_blank");
el.setAttribute("title", "IP address information");
el.appendChild(tn);
return el;
}
return tn;
}
static checkResult(data) {
if (data.error_code !== undefined && data.error_code !== 0) {
throw data;
}
}
static displayError(obj) {
console.warn(obj.message || "Unknown error");
if (!(obj instanceof Error) && obj.debug_info) {
console.warn('Error code: ' + obj.debug_info.code);
console.warn('Error content: ' + obj.debug_info.content);
}
}
}
Common.tuneDateTimeOutput("auto");

View File

@@ -0,0 +1,486 @@
/**
* 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/>.
*/
class DomainList {
constructor() {
this._table = null;
this._scroll = null;
this._element = document.getElementById("main-block");
this._sort = { column: "fqdn", direction: "ascent" };
}
display() {
this._make_scroll_container();
this._make_table();
this._scroll.appendChild(this._table.element());
this._element.appendChild(this._scroll);
this._table.focus();
}
update() {
this._fetch_list();
}
title() {
return "Domain List";
}
_fetch_list() {
this._table.display_status("wait");
let that = this;
return window.fetch("domains.php", {
method: "GET",
cache: "no-store",
headers: HTTP_HEADERS,
credentials: "same-origin"
}).then(function(resp) {
if (!resp.ok)
throw new Error("Failed to fetch the domain list");
return resp.json();
}).then(function(data) {
that._table.display_status(null);
Common.checkResult(data);
let d = { more: data.more };
d.rows = data.domains.map(function(it) {
return that._make_row_data(it);
});
d.rows.push(new NewDomainRow(4));
let fr = new DomainFrame(d, that._table.last_row_index() + 1);
that._table.clear();
that._table.add_frame(fr);
if (that._sort.column) {
that._table.sort(that._sort.column, that._sort.direction);
}
that._table.focus();
}).catch(function(err) {
Common.displayError(err);
that._table.display_status("error");
});
}
_make_scroll_container() {
this._scroll = document.createElement("div");
this._scroll.setAttribute("class", "main-table-container");
}
_make_table() {
this._table = new ITable({
class: "main-table domains",
onclick: function(row) {
let data = row.userdata();
if (data) {
this._display_edit_dialog(data);
}
}.bind(this),
onsort: function(col) {
let dir = col.sorted() && "toggle" || "ascent";
this._table.set_sorted(col.name(), dir);
this._table.sort(col.name(), col.sorted());
this._sort.column = col.name();
this._sort.direction = col.sorted();
this._table.focus();
}.bind(this),
onfocus: function(el) {
scroll_to_element(el, this._scroll);
}.bind(this)
});
[
{ content: "", sortable: true, name: "status", class: "cell-status" },
{ content: "FQDN", sortable: true, name: "fqdn" },
{ content: "Updated", sortable: true, name: "date" },
{ content: "Description", class: "descr" }
].forEach(function(col) {
let c = this._table.add_column(col);
if (c.name() === this._sort.column) {
c.sort(this._sort.direction);
}
}, this);
}
_make_row_data(d) {
let rd = { cells: [], userdata: d.fqdn };
rd.cells.push(new DomainStatusCell(d.active));
rd.cells.push({ content: d.fqdn, class: "fqdn" });
rd.cells.push(new DomainTimeCell(new Date(d.updated_time)));
rd.cells.push({ content: d.description || "", class: "descr" });
return rd;
}
_display_edit_dialog(fqdn) {
let dlg = new DomainEditDialog(fqdn === "*new" && { "new": true } || { fqdn: fqdn });
this._element.appendChild(dlg.element());
let that = this;
dlg.show().then(function(d) {
if (d) {
that.update();
}
}).finally(function() {
dlg.element().remove();
that._table.focus();
});
}
}
class DomainStatusCell extends ITableCell {
constructor(is_active, props) {
props = props || {};
let ca = (props.class || "").split(" ");
ca.push(is_active && "state-green" || "state-gray");
props.class = ca.filter(function(s) { return s.length > 0; }).join(" ");
super(is_active, props);
}
value(target) {
if (target === "dom") {
let div = document.createElement("div");
div.setAttribute("class", "state-background status-indicator");
if (!this._title) {
div.setAttribute("title", this._content && "active" || "inactive");
}
return div;
}
return this._content;
}
}
class DomainTimeCell extends ITableCell {
value(target) {
if (target === "dom") {
return this._content && this._content.toUIString() || "";
}
if (target === "sort") {
return this._content && this._content.valueOf() || "";
}
super.value(target);
}
}
class NewDomainRow extends ITableRow {
constructor(col_cnt) {
super({
userdata: "*new",
cells: []
});
this._col_cnt = col_cnt;
}
element() {
if (!this._element) {
super.element();
this._element.classList.add("colspanned", "virtual-item");
for (let i = 0; i < this._col_cnt; ++i) {
let cell = document.createElement("div");
cell.setAttribute("class", "table-cell");
cell.appendChild(document.createTextNode(!i && "New domain" || "\u00A0"));
this._element.appendChild(cell);
}
}
return this._element;
}
}
class DomainFrame extends ITableFrame {
sort(col_idx, direction) {
this._sort_dir = (direction === "ascent" && 1) || (direction === "descent" && 2) || 0;
super.sort(col_idx, direction);
}
_compare_cells(c1, c2) {
if (!c1) {
return this._sort_dir === 2;
}
if (!c2) {
return this._sort_dir === 1;
}
return super._compare_cells(c1, c2);
}
}
class DomainEditDialog extends ModalDialog {
constructor(params) {
let tl = null;
let ba = [ "save", "close" ];
if (!params["new"]) {
tl = "Domain settings";
ba.splice(1, 0, "delete");
}
else {
tl = "New domain";
}
super({ title: tl, buttons: ba });
this._data = params || {};
this._content = null;
this._inputs = null;
this._fqdn_el = null;
this._actv_el = null;
this._desc_el = null;
this._c_tm_el = null;
this._u_tm_el = null;
this._fetched = false;
}
_gen_content() {
this._inputs = document.createElement("div");
this._inputs.setAttribute("class", "titled-input");
this._content.appendChild(this._inputs);
this._content.classList.add("vertical-content");
let fq = document.createElement("input");
fq.setAttribute("type", "text");
if (!this._data["new"]) {
fq.setAttribute("value", this._data.fqdn);
fq.disabled = true;
}
fq.required = true;
this._insert_row("FQDN", fq);
this._fqdn_el = fq;
{
let en = document.createElement("select");
let op1 = document.createElement("option");
op1.setAttribute("value", "yes");
op1.appendChild(document.createTextNode("Yes"));
en.appendChild(op1);
let op2 = document.createElement("option");
op2.setAttribute("value", "no");
op2.appendChild(document.createTextNode("No"));
en.appendChild(op2);
en.required = true;
this._insert_row("Active", en);
this._actv_el = en;
}
let tx = document.createElement("textarea");
this._insert_row("Description", tx).classList.add("description");
this._desc_el = tx;
let ct = document.createElement("input");
ct.setAttribute("type", "text");
ct.disabled = true;
ct.setAttribute("value","n/a");
this._insert_row("Created", ct);
this._c_tm_el = ct;
let ut = document.createElement("input");
ut.setAttribute("type", "text");
ut.setAttribute("value","n/a");
ut.disabled = true;
this._insert_row("Updated", ut);
this._u_tm_el = ut;
this._inputs.addEventListener("input", function(event) {
if (this._fetched || this._data["new"]) {
this._buttons[1].disabled = (
this._actv_el.dataset.server === this._actv_el.value &&
this._desc_el.defaultValue === this._desc_el.value &&
this._fqdn_el.defaultValue === this._fqdn_el.value
);
}
}.bind(this));
if (!this._data["new"] && !this._fetched) {
this._fetch_data();
}
}
_insert_row(text, v_el) {
let l_el = document.createElement("label");
let t_el = document.createElement("span");
t_el.appendChild(document.createTextNode(text + ": "));
l_el.appendChild(t_el);
l_el.appendChild(v_el);
this._inputs.appendChild(l_el);
return v_el;
}
_fetch_data() {
this._enable_ui(false);
this._content.appendChild(set_wait_status());
let uparams = new URLSearchParams();
uparams.set("domain", this._data.fqdn);
let that = this;
window.fetch("domains.php?" + uparams.toString(), {
method: "GET",
cache: "no-store",
headers: HTTP_HEADERS,
credentials: "same-origin"
}).then(function(resp) {
if (!resp.ok)
throw new Error("Failed to fetch the domain data");
return resp.json();
}).then(function(data) {
that._fetched = true;
Common.checkResult(data);
data.created_time = new Date(data.created_time);
data.updated_time = new Date(data.updated_time);
that._update_ui(data);
that._enable_ui(true);
}).catch(function(err) {
Common.displayError(err);
that._content.appendChild(set_error_status(null, err.message));
}).finally(function() {
that._content.querySelector(".wait-message").remove();
});
}
_update_ui(data) {
let val = "";
for (let i = 0; i < this._actv_el.options.length; ++i) {
let op = this._actv_el.options[i];
let ee = op.value === "yes";
if ((data.active && ee) || (!data.active && !ee)) {
op.setAttribute("selected", "selected");
val = op.value;
}
else {
op.removeAttribute("selected");
}
}
this._actv_el.value = val;
this._actv_el.dataset.server = val;
this._desc_el.appendChild(document.createTextNode(data.description || ""));
this._c_tm_el.setAttribute("value", data.created_time && data.created_time.toUIString() || "n/a");
this._u_tm_el.setAttribute("value", data.updated_time && data.updated_time.toUIString() || "n/a");
}
_add_button(container, text, type) {
let btn = null;
if (type === "save") {
text = "Save";
btn = document.createElement("button");
btn.disabled = true;
btn.addEventListener("click", this._save.bind(this));
}
else if (type === "delete") {
text = "Delete";
btn = document.createElement("button");
btn.addEventListener("click", this._confirm_delete.bind(this));
}
else {
super._add_button(container, text, type);
return;
}
btn.setAttribute("type", "button");
btn.appendChild(document.createTextNode(text));
container.appendChild(btn);
this._buttons.push(btn);
}
_enable_ui(en) {
this._fqdn_el.disabled = !en || !this._data["new"];
this._actv_el.disabled = !en;
this._desc_el.disabled = !en;
for (let i = 2; i < this._buttons.length - 1; ++i) {
this._buttons[i].disabled = !en;
}
this._update_first_last();
if (this._first) {
this._first.focus();
}
}
_save() {
this._enable_ui(false);
let em = this._content.querySelector(".error-message");
if (em) {
em.remove();
}
this._content.appendChild(set_wait_status());
let body = {};
body.fqdn = this._data["new"] && this._fqdn_el.value || this._data.fqdn;
body.action = this._data["new"] && "add" || "update";
body.active = this._actv_el.value === "yes";
body.description = this._desc_el.value;
let that = this;
window.fetch("domains.php", {
method: "POST",
cache: "no-store",
headers: Object.assign(HTTP_HEADERS, HTTP_HEADERS_POST),
credentials: "same-origin",
body: JSON.stringify(body)
}).then(function(resp) {
if (!resp.ok)
throw new Error("Failed to " + (body.new && "add" || "update") + " the domain data");
return resp.json();
}).then(function(data) {
Common.checkResult(data);
that._result = body;
that.hide();
Notification.add({
text: "The domain " + body.fqdn + " was " + (body.action === "add" && "added" || "updated")
});
}).catch(function(err) {
Common.displayError(err);
that._content.appendChild(set_error_status(null, err.message));
}).finally(function() {
that._content.querySelector(".wait-message").remove();
that._enable_ui(true);
});
}
_confirm_delete() {
if (confirm("Are sure you want to delete this domain?")) {
this._delete();
}
}
_delete() {
this._enable_ui(false);
let em = this._content.querySelector(".error-message");
if (em) {
em.remove();
}
this._content.appendChild(set_wait_status());
let body = {};
body.fqdn = this._data.fqdn;
body.action = "delete";
let that = this;
window.fetch("domains.php", {
method: "POST",
cache: "no-store",
headers: Object.assign(HTTP_HEADERS, HTTP_HEADERS_POST),
credentials: "same-origin",
body: JSON.stringify(body)
}).then(function(resp) {
if (!resp.ok)
throw new Error("Failed to delete the domain");
return resp.json();
}).then(function(data) {
Common.checkResult(data);
that._result = data;
that.hide();
Notification.add({ text: "The domain " + body.fqdn + " was removed" });
}).catch(function(err) {
Common.displayError(err);
that._content.appendChild(set_error_status(null, err.message));
}).finally(function() {
that._content.querySelector(".wait-message").remove();
that._enable_ui(true);
});
}
}

View File

@@ -0,0 +1,394 @@
/**
* 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/>.
*/
class Files {
constructor() {
this._container = null;
this._fieldset1 = null;
this._fieldset2 = null;
this._dir_table = null;
this._element = document.getElementById("main-block");
this._fcount_info = null;
this._fsize_info = null;
this._limits = {
upload_max_file_count: 0,
upload_max_file_size: 0
};
this._directories = [];
}
display() {
this._create_container();
this._create_local_file_uploading_element();
this._create_directory_loading_element();
this._container.appendChild(this._fieldset1);
this._container.appendChild(this._fieldset2);
this._element.appendChild(this._container);
this._fieldset1.focus();
}
update() {
if (!Status.instance().error()) {
this._fetch_data(true, true);
}
}
title() {
return "Report Files";
}
_create_container() {
this._container = document.createElement("div");
this._container.setAttribute("class", "panel-container round-border");
}
_create_local_file_uploading_element() {
this._fieldset1 = document.createElement("fieldset");
this._fieldset1.setAttribute("class", "round-border");
this._fieldset1.disabled = true;
let lg = document.createElement("legend");
lg.appendChild(document.createTextNode("Uploading local report files"));
this._fieldset1.appendChild(lg);
let fm = document.createElement("form");
fm.setAttribute("enctype", "multipart/form-data");
fm.setAttribute("method", "post");
fm.appendChild(this._create_input_element("hidden", "cmd", "upload-report"));
let fl = this._create_input_element("file", "report_file[]", null)
fl.required = true;
fl.multiple = true;
fm.appendChild(fl);
let dv = document.createElement("div");
dv.setAttribute("class", "buttons-block");
let sb = this._create_button_element("submit", "Upload reports");
sb.disabled = true;
dv.appendChild(sb);
dv.appendChild(this._create_button_element("reset", "Reset"));
fm.appendChild(dv);
let that = this;
fl.addEventListener("change", function(event) {
sb.disabled = !that._check_files(fl);
});
fm.addEventListener("reset", function(event) {
sb.disabled = true;
that._clear_warnings();
});
fm.addEventListener("submit", function(event) {
window.fetch("files.php", {
method: "POST",
credentials: "same-origin",
body: new FormData(fm)
}).then(function(resp) {
if (!resp.ok)
throw new Error("Failed to upload a report file");
return resp.json();
}).then(function(data) {
Common.checkResult(data);
Notification.add({ text: (data.message || "Uploaded successfully!"), type: "info" });
}).catch(function(err) {
Common.displayError(err);
Notification.add({ text: (err.message || "Error!"), type: "error" });
});
event.preventDefault();
fm.reset();
});
this._fieldset1.appendChild(fm);
}
_create_directory_loading_element() {
this._fieldset2 = document.createElement("fieldset");
this._fieldset2.setAttribute("class", "round-border");
this._fieldset2.disabled = true;
let lg = document.createElement("legend");
lg.appendChild(document.createTextNode("Loading report files from the server directory"));
this._fieldset2.appendChild(lg);
let fm = document.createElement("form");
fm.setAttribute("method", "post");
this._dir_table = new ITable({
class: "main-table subtable",
onclick: function(row) {
let userdata = row.userdata();
let checkbox = row.element().querySelector("input");
if (checkbox && !userdata.error) {
userdata.checked = !userdata.checked;
checkbox.checked = userdata.checked;
this._update_directory_button();
}
}.bind(this),
nodata_text: "No directories are configured."
});
[
{ content: "", class: "cell-status" },
{ content: "Name" },
{ content: "Files" },
{ content: "Location" }
].forEach(function(col) {
this._dir_table.add_column(col);
}, this);
fm.appendChild(this._dir_table.element());
let bb = document.createElement("div");
bb.setAttribute("class", "buttons-block");
fm.appendChild(bb);
let sb = this._create_button_element("submit", "Load reports");
sb.disabled = true;
bb.appendChild(sb);
fm.addEventListener("submit", function(event) {
sb.disabled = true;
let ids = this._directories.filter(function(it) {
return it.checked;
}).map(function(it) {
return it.id;
});
let that = this;
window.fetch("files.php", {
method: "POST",
headers: Object.assign(HTTP_HEADERS, HTTP_HEADERS_POST),
credentials: "same-origin",
body: JSON.stringify({ cmd: "load-directory", ids: ids })
}).then(function(resp) {
if (!resp.ok)
throw new Error("Failed to load report files");
return resp.json();
}).then(function(data) {
if (!data.error_code) {
Notification.add({ text: (data.message || "Loaded successfully!"), type: "info" });
}
if (data.other_errors) {
that._notify_other_errors(data.other_errors);
}
Common.checkResult(data);
}).catch(function(err) {
Common.displayError(err);
Notification.add({ text: (err.message || "Error!"), type: "error" });
}).finally(function() {
that._fetch_data(false, true);
});
event.preventDefault();
}.bind(this));
this._fieldset2.appendChild(fm);
}
_display_files_info() {
this._fcount_info = document.createElement("div");
this._fcount_info.setAttribute("class", "state-gray");
let dv = document.createElement("div");
dv.setAttribute("class", "state-text");
dv.appendChild(
document.createTextNode(
"You can upload not more than " + this._limits.upload_max_file_count + " files."
)
);
this._fcount_info.appendChild(dv);
this._fsize_info = document.createElement("div");
this._fsize_info.setAttribute("class", "state-gray");
dv = document.createElement("div");
dv.setAttribute("class", "state-text");
dv.appendChild(
document.createTextNode(
"You can upload a file with no more than " + bytes2size(this._limits.upload_max_file_size) + "."
)
);
this._fsize_info.appendChild(dv);
dv = document.createElement("div");
dv.setAttribute("class", "info-block");
dv.appendChild(this._fcount_info);
dv.appendChild(this._fsize_info);
this._fieldset1.appendChild(dv);
}
_update_directory_loading_element() {
this._dir_table.clear();
let d = {};
d.rows = this._directories.map(function(it) {
let files = it.files;
let chkbox = false;
it.checked = false;
let rd = { cells: [], userdata: it };
if (files < 0) {
chkbox = null;
files = "Error!";
rd.class = "state-red";
it.error = true;
}
rd.cells.push(new DirectoryCheckboxCell(chkbox));
rd.cells.push({ content: it.name });
rd.cells.push({ content: files, class: "state-text" });
rd.cells.push({ content: it.location });
return rd;
});
this._dir_table.add_frame(new ITableFrame(d, this._dir_table.last_row_index() + 1));
}
_update_directory_button() {
this._fieldset2.querySelector("button[type=submit]").disabled = !this._directories.some(function(it) {
return it.checked;
});
}
_clear_warnings() {
[ this._fcount_info, this._fsize_info ].forEach(function(el) {
if (el) {
el.classList.remove("state-red");
el.classList.add("state-gray");
}
});
}
_notify_other_errors(errors) {
let cut = null;
let length = errors.length;
if (length > 4) {
cut = errors.slice(0, 3);
cut.push("and " + (length - 3) + " more errors");
}
Notification.add({ text: cut || errors, type: "error" });
}
_set_warning(el) {
if (el) {
el.classList.remove("state-gray");
el.classList.add("state-red");
}
}
_check_files(fl_el) {
this._clear_warnings();
if (fl_el.files.length == 0) {
return false;
}
let res = true;
if (fl_el.files.length > this._limits.upload_max_file_count) {
res = false;
this._set_warning(this._fcount_info);
let message = "You can only upload " + this._limits.upload_max_file_count + " files.";
Notification.add({ type: "error", text: message, delay: 10000 });
}
let bf_cnt = 0;
for (let i = 0; i < fl_el.files.length; ++i) {
if (fl_el.files[i].size > this._limits.upload_max_file_size) {
++bf_cnt;
}
};
if (bf_cnt > 0) {
res = false;
this._set_warning(this._fsize_info);
Notification.add({
type: "error",
text: "" + bf_cnt + " file" + (bf_cnt > 1 && "s" || "") + " exceed the maximum allowed size.",
delay: 10000
});
}
return res;
}
_create_button_element(type, text) {
let el = document.createElement("button");
el.setAttribute("type", type);
el.appendChild(document.createTextNode(text));
return el;
}
_create_input_element(type, name, value) {
let el = document.createElement("input");
el.setAttribute("type", type);
if (name)
el.setAttribute("name", name);
if (value)
el.setAttribute("value", value);
return el;
}
_fetch_data(files, dirs) {
if (files) {
this._fieldset1.disabled = true;
this._fieldset1.insertBefore(set_wait_status(), this._fieldset1.children[0]);
}
if (dirs) {
this._fieldset2.disabled = true;
this._fieldset2.insertBefore(set_wait_status(), this._fieldset2.children[0]);
}
let that = this;
window.fetch("files.php", {
method: "GET",
headers: HTTP_HEADERS,
credentials: "same-origin"
}).then(function(resp) {
if (!resp.ok)
throw new Error("Failed to get loader data");
return resp.json();
}).then(function(data) {
Common.checkResult(data);
if (files) {
that._limits.upload_max_file_count = data.upload_max_file_count;
that._limits.upload_max_file_size = data.upload_max_file_size;
that._display_files_info();
that._fieldset1.disabled = false;
}
if (dirs) {
that._directories = data.directories || [];
that._update_directory_loading_element();
that._fieldset2.disabled = false;
}
}).catch(function(err) {
Common.displayError(err);
Notification.add({ type: "error", text: err.message });
if (files) {
that._fieldset1.insertBefore(set_error_status(null, err.message), that._fieldset1.children[0]);
}
if (dirs) {
that._fieldset2.insertBefore(set_error_status(null, err.message), that._fieldset2.children[0]);
}
}).finally(function() {
if (files) {
that._fieldset1.querySelector(".wait-message").remove();
}
if (dirs) {
that._fieldset2.querySelector(".wait-message").remove();
}
});
}
}
class DirectoryCheckboxCell extends ITableCell {
value(target) {
if (target === "dom") {
let cb = document.createElement("input");
cb.setAttribute("type", "checkbox");
if (this._content !== null) {
cb.checked = this._content;
}
else {
cb.disabled = true;
cb.checked = false;
}
return cb;
}
return this._content;
}
}

View File

@@ -0,0 +1,527 @@
/**
* 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/>.
*/
class ReportList {
constructor() {
this._table = null;
this._scroll = null;
this._filter = null;
this._sort = { column: "begin_time", direction: "descent" };
this._element = document.getElementById("main-block");
this._element2 = document.getElementById("detail-block");
this._fetching = false;
this._settings_btn = null;
this._settings_dlg = null;
}
display() {
this._gen_settings_button();
this._gen_content_container();
this._gen_table();
this._scroll.appendChild(this._table.element());
this._element.appendChild(this._scroll);
this._ensure_report_widget();
this._element2.appendChild(ReportWidget.instance().element());
this._ensure_settins_button();
ReportWidget.instance().hide();
this._table.focus();
}
update() {
this._handle_url_params();
this._update_table();
}
title() {
return "Report List";
}
onpopstate() {
if (!this._scroll) {
this.display();
this.update();
}
else {
if (!this._element.contains(this._scroll)) {
remove_all_children(this._element);
this._element.appendChild(this._scroll);
}
if (this._handle_url_params()) {
this._update_table();
}
}
this._ensure_settins_button();
this._ensure_report_widget();
if (this._table) {
this._table.focus();
}
}
_ensure_settins_button() {
let title_el = document.querySelector("h1");
if (!title_el.contains(this._settings_btn)) {
title_el.appendChild(this._settings_btn);
}
}
_ensure_report_widget() {
let wdg = ReportWidget.instance();
wdg.hide();
let el = wdg.element();
if (!this._element2.contains(el)) {
this._element2.appendChild(el);
}
}
/**
* Sets the _filter object from the document's location
* and updates the setting button if the filter changes
*
* @return bool True if the filter was changed, false otherwise
*/
_handle_url_params() {
let cnt = 0;
let filter = {};
(new URL(document.location.href)).searchParams.getAll("filter[]").forEach(function(it) {
let k = null;
let v = null;
let i = it.indexOf(":");
if (i != 0) {
if (i > 0) {
k = it.substr(0, i);
v = it.substr(i + 1);
}
else {
k = it;
v = "";
}
filter[k] = v;
++cnt;
}
});
let changed = !this._filter && cnt > 0;
if (this._filter) {
let cnt2 = 0;
changed = Object.keys(this._filter).some(function(k) {
++cnt2;
return cnt < cnt2 || this._filter[k] !== filter[k];
}, this) || cnt !== cnt2;
}
if (changed) {
this._filter = cnt && filter || null;
this._update_settings_button();
}
return changed;
}
_gen_settings_button() {
if (!this._settings_btn) {
let btn = document.createElement("span");
btn.setAttribute("class", "options-button");
btn.appendChild(document.createTextNode("\u{2699}"));
let that = this;
btn.addEventListener("click", function(event) {
that._display_settings_dialog();
event.preventDefault();
});
this._settings_btn = btn;
}
}
_update_settings_button() {
if (this._settings_btn) {
if (this._filter)
this._settings_btn.classList.add("active");
else {
this._settings_btn.classList.remove("active");
}
}
}
_gen_content_container() {
let that = this;
let el = document.createElement("div");
el.setAttribute("class", "main-table-container");
el.addEventListener("scroll", function() {
if (!that._fetching && el.scrollTop + el.clientHeight >= el.scrollHeight * 0.95) {
if (that._table.frames_count() === 0 || that._table.more()) {
that._fetch_list();
}
}
});
this._scroll = el;
}
_gen_table() {
this._table = new ReportTable({
class: "main-table report-list small-cards",
onclick: function(row) {
let data = row.userdata();
if (data)
this._display_report(data, row.id());
}.bind(this),
onsort: function(col) {
let dir = col.sorted() && "toggle" || "descent";
this._table.set_sorted(col.name(), dir);
this._sort.column = col.name();
this._sort.direction = col.sorted();
this.update();
}.bind(this),
onfocus: function(el) {
scroll_to_element(el, this._scroll);
}.bind(this)
});
[
{ content: "Domain" },
{ content: "Date", sortable: true, name: "begin_time" },
{ content: "Reporting Organization" },
{ content: "Report ID", class: "report-id" },
{ content: "Messages" },
{ content: "Result" }
].forEach(function(col) {
let c = this._table.add_column(col);
if (c.name() === this._sort.column) {
c.sort(this._sort.direction);
}
}, this);
}
_update_table() {
this._table.clear();
let that = this;
let frcnt = -1;
let again = function() {
if (frcnt < that._table.frames_count() && that._scroll.clientHeight * 1.5 >= that._scroll.scrollHeight) {
frcnt = that._table.frames_count();
that._fetch_list().then(function(frame) {
if (frame && frame.more())
again();
else
that._table.focus();
});
}
else
that._table.focus();
}
again();
}
_display_report(data, id) {
if (data.domain && data.report_id) {
let url = new URL("report.php", document.location.href);
url.searchParams.set("domain", data.domain);
url.searchParams.set("report_id", data.report_id);
window.history.pushState({ from: "list" }, "", url.toString());
let that = this;
ReportWidget.instance().show_report(data.domain, data.report_id).then(function() {
if (!that._table.seen(id)) {
that._table.seen(id, true);
}
}).catch(function(err) {
Common.displayError(err);
if (err.error_code && err.error_code === -2) {
LoginDialog.start({ nousername: true });
}
});
Router.update_title(ReportWidget.instance().title());
ReportWidget.instance().focus();
}
}
_fetch_list() {
this._table.display_status("wait");
this._fetching = true;
let pos = this._table.last_row_index() + 1;
let uparams = new URLSearchParams();
uparams.set("list", "reports");
uparams.set("position", pos);
uparams.set("order", this._sort.column);
uparams.set("direction", this._sort.direction);
if (this._filter) {
for (let nm in this._filter) {
uparams.append("filter[]", nm + ":" + this._filter[nm]);
}
}
let that = this;
return window.fetch("list.php?" + uparams.toString(), {
method: "GET",
cache: "no-store",
headers: HTTP_HEADERS,
credentials: "same-origin"
}).then(function(resp) {
if (!resp.ok)
throw new Error("Failed to fetch the report list");
return resp.json();
}).then(function(data) {
that._table.display_status(null);
Common.checkResult(data);
let d = { more: data.more };
d.rows = data.reports.map(function(it) {
return new ReportTableRow(that._make_row_data(it));
});
let fr = new ITableFrame(d, pos);
that._table.add_frame(fr);
return fr;
}).catch(function(err) {
Common.displayError(err);
that._table.display_status("error");
}).finally(function() {
that._fetching = false;
});
}
_make_row_data(d) {
let rd = { cells: [], userdata: { domain: d.domain, report_id: d.report_id }, seen: d.seen && true || false }
rd.cells.push({ content: d.domain, label: "Domain" });
let d1 = new Date(d.date.begin);
let d2 = new Date(d.date.end);
rd.cells.push({ content: date_range_to_string(d1, d2), title: d1.toUIString(true) + " - " + d2.toUIString(true), label: "Date" });
rd.cells.push({ content: d.org_name, label: "Reporting Organization" });
rd.cells.push({ content: d.report_id, class: "report-id" });
rd.cells.push({ content: d.messages, label: "Messages" });
rd.cells.push(new StatusColumn({ dkim_align: d.dkim_align, spf_align: d.spf_align }));
return rd;
}
_display_settings_dialog() {
let dlg = this._settings_dlg;
if (!this._settings_dlg) {
dlg = new ReportListSettingsDialog({ filter: this._filter });
this._settings_dlg = dlg;
}
this._element.appendChild(dlg.element());
dlg.show().then(function(d) {
if (d) {
let url = new URL(document.location.href);
url.searchParams.delete("filter[]");
for (let k in d) {
if (d[k]) {
url.searchParams.append("filter[]", k + ":" + d[k]);
}
}
window.history.replaceState(null, "", url.toString());
if (this._handle_url_params()) {
this._update_table();
}
}
}.bind(this)).finally(function() {
this._table.focus();
}.bind(this));
}
}
class ReportTable extends ITable {
seen(row_id, flag) {
let row = super._get_row(row_id);
if (row) {
if (flag === undefined)
return row.seen();
row.seen(flag);
}
}
}
class ReportTableRow extends ITableRow {
constructor(data) {
super(data);
this._seen = data.seen && true || false;
}
element() {
if (!this._element) {
super.element();
this._update_seen_element();
}
return this._element;
}
seen(flag) {
if (flag === undefined)
return this._seen;
this._seen = flag && true || false;
if (this._element)
this._update_seen_element();
}
_update_seen_element() {
if (this._seen)
this._element.classList.remove("unseen");
else
this._element.classList.add("unseen");
}
}
class StatusColumn extends ITableCell {
element() {
if (!this._element) {
super.element().setAttribute("data-label", "Result");
}
return this._element;
}
value(target) {
if (target === "dom") {
let d = this._content;
let fr = document.createDocumentFragment();
if (d.dkim_align) {
fr.appendChild(create_report_result_element("DKIM", d.dkim_align));
}
if (d.spf_align) {
fr.appendChild(create_report_result_element("SPF", d.spf_align));
}
return fr;
}
return super.value(target);
}
}
class ReportListSettingsDialog extends ModalDialog {
constructor(params) {
super({ title: "List display settings", buttons: [ "apply", "reset" ] });
this._data = params || {};
this._content = null;
this._ui_data = [
{ name: "domain", title: "Domain" },
{ name: "month", title: "Month" },
{ name: "organization", title: "Organization" },
{ name: "dkim", title: "DKIM result" },
{ name: "spf", title: "SPF result" },
{ name: "status", title: "Status" }
];
}
show() {
this._update_ui();
return super.show();
}
_gen_content() {
let fs = document.createElement("fieldset");
fs.setAttribute("class", "round-border titled-input");
let lg = document.createElement("legend");
lg.appendChild(document.createTextNode("Filter by"));
fs.appendChild(lg);
this._ui_data.forEach(function(ud) {
let el = this._create_select_label(ud.title, fs);
ud.element = el;
}, this);
this._content.appendChild(fs);
this._content.classList.add("vertical-content");
if (!this._data.loaded_filters)
this._fetch_data();
}
_create_select_label(text, c_el) {
let lb = document.createElement("label");
let sp = document.createElement("span");
sp.appendChild(document.createTextNode(text + ": "));
lb.appendChild(sp);
let sl = document.createElement("select");
lb.appendChild(sl);
c_el.appendChild(lb);
return sl;
}
_enable_ui(enable) {
let list = this._element.querySelector("form").elements;
for (let i = 0; i < list.length; ++i)
list[i].disabled = !enable;
}
_update_ui() {
this._update_filters();
}
_update_filters() {
let data = this._data.loaded_filters || {};
let vals = this._data.filter || {};
this._ui_data.forEach(function(ud) {
this._update_select_element(ud.element, data[ud.name], vals[ud.name]);
}, this);
}
_update_select_element(sl, d, v) {
remove_all_children(sl);
let ao = document.createElement("option");
ao.setAttribute("value", "");
ao.setAttribute("selected", "selected");
ao.appendChild(document.createTextNode("Any"));
sl.appendChild(ao);
let v2 = "";
if (d) {
let op = null;
d.forEach(function(fs) {
op = document.createElement("option");
op.setAttribute("value", fs);
op.appendChild(document.createTextNode(fs));
if (fs === v) {
v2 = v;
}
sl.appendChild(op);
}, this);
}
sl.value = v2;
}
_submit() {
let res = {};
let fdata = {};
this._ui_data.forEach(function(ud) {
let el = ud.element;
let val = el.options[el.selectedIndex].value;
res[ud.name] = val;
fdata[ud.name] = val;
});
this._data.filter = fdata;
this._result = res;
this.hide();
}
_fetch_data() {
let that = this;
this._enable_ui(false);
this._content.appendChild(set_wait_status());
window.fetch("list.php?list=filters", {
method: "GET",
cache: "no-store",
headers: HTTP_HEADERS,
credentials: "same-origin"
}).then(function(resp) {
if (!resp.ok)
throw new Error("Failed to fetch the filter list");
return resp.json();
}).then(function(data) {
Common.checkResult(data);
that._data.loaded_filters = data.filters;
that._update_ui();
that._enable_ui(true);
}).catch(function(err) {
Common.displayError(err);
that._content.appendChild(set_error_status());
}).finally(function() {
that._content.querySelector(".wait-message").remove();
});
}
}

View File

@@ -0,0 +1,141 @@
/**
* 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/>.
*/
class LoginDialog extends ModalDialog {
constructor(params) {
super();
this._params = params || {};
this._params.buttons = [ "ok", "cancel" ];
this._params.title = "Authentication";
this._params.overlay_click = "ignore";
this._user = null;
this._pass = null;
this._msg_el = null;
}
remove() {
if (this._element) {
this._element.remove();
this._element = null;
}
}
_gen_content() {
let tdiv = document.createElement("div");
tdiv.setAttribute("class", "titled-input");
if (!this._params.nousername) {
this._user = this._insert_row(tdiv, "User name", "text", "Enter your user name");
}
this._pass = this._insert_row(tdiv, "Password", "password", "Enter your password");
this._msg_el = set_wait_status(null, "Enter your credentials");
this._content.setAttribute("class", "vertical-content");
this._content.appendChild(tdiv);
this._content.appendChild(this._msg_el);
}
_insert_row(c_el, text, type, placeholder) {
let l_el = document.createElement("label");
c_el.appendChild(l_el);
let t_el = document.createElement("span");
t_el.appendChild(document.createTextNode(text + ": "));
l_el.appendChild(t_el);
let inp = document.createElement("input");
inp.required = true;
inp.setAttribute("type", type);
if (placeholder) {
inp.setAttribute("placeholder", placeholder);
}
l_el.appendChild(inp);
return inp;
}
_enable_elements(enable) {
this._buttons[0].disabled = !enable;
let elements = this._element.querySelector("form").elements;
for (let i = 0; i < elements.length; ++i) {
elements[i].disabled = !enable;
}
}
_submit() {
this._buttons[1].focus();
this._enable_elements(false);
let body = {};
if (!this._params.nousername) {
body.username = this._user.value;
}
body.password = this._pass.value;
if (this._params.nofetch) {
this._result = body;
this.hide();
return;
}
let that = this;
let hide = false;
this._set_message("Sending credentials to the server...", false);
window.fetch("login.php", {
method: "POST",
cache: "no-store",
headers: Object.assign(HTTP_HEADERS, HTTP_HEADERS_POST),
credentials: "same-origin",
body: JSON.stringify(body)
}).then(function(resp) {
if (!resp.ok)
throw new Error("Failed to log in");
return resp.json();
}).then(function(data) {
Common.checkResult(data);
that._result = data;
Notification.add({ type: "info", text: data.message || "Successfully!" });
hide = true;
}).catch(function(err) {
that._pass.value = "";
Common.displayError(err);
that._set_message(err.message, true);
}).finally(function() {
that._enable_elements(true);
that._first.focus();
if (hide)
that.hide();
});
}
_set_message(text, error) {
let el = error && set_error_status(null, text) || set_wait_status(null, text);
this._msg_el.replaceWith(el);
this._msg_el = el;
}
}
LoginDialog.start = function (params) {
let login = new LoginDialog(params);
document.getElementById("main-block").appendChild(login.element());
login.show().then(function(d) {
if (d) {
Router.go();
}
}).catch(function(err) {
Common.displayError(err);
}).finally(function() {
login.remove();
login = null;
});
};

View File

@@ -0,0 +1,304 @@
/**
* 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/>.
*/
class Logs {
constructor() {
this._table = null;
this._scroll = null;
this._element = document.getElementById("main-block");
this._fetching = false;
this._sort = { column: "", direction: "" };
}
display() {
this._make_scroll_container();
this._make_table();
this._scroll.appendChild(this._table.element());
this._element.appendChild(this._scroll);
this._table.focus();
}
update() {
this._table.clear();
let that = this;
let fr_cnt = -1;
let again = function() {
let fc = that._table.frames_count()
if (fr_cnt < fc && that._scroll.scrollHeight <= that._scroll.clientHeight * 1.5) {
fr_cnt = fc;
that._fetch_list().then(function(frame) {
if (frame && frame.more()) {
again();
}
else {
that._table.focus();
}
});
}
else {
that._table.focus();
}
};
again();
}
title() {
return "Logs";
}
_fetch_list() {
this._table.display_status("wait");
this._fetching = true;
let pos = this._table.last_row_index() + 1;
let uparams = new URLSearchParams();
uparams.set("position", pos);
if (this._sort.column && this._sort.direction) {
uparams.set("order", this._sort.column);
uparams.set("direction", this._sort.direction);
}
let that = this;
return window.fetch("logs.php?" + uparams.toString(), {
method: "GET",
cache: "no-store",
headers: HTTP_HEADERS,
credentials: "same-origin"
}).then(function(resp) {
if (!resp.ok)
throw new Error("Failed to fetch the logs");
return resp.json();
}).then(function(data) {
that._table.display_status(null);
Common.checkResult(data);
if (data.sorted_by) {
let cname = data.sorted_by.column;
let dir = data.sorted_by.direction;
if (that._sort.column !== cname || that._sort.direction !== dir) {
that._sort.column = cname;
that._sort.direction = dir;
that._table.set_sorted(cname, dir);
}
}
let d = { more: data.more };
d.rows = data.items.map(function(it) {
return new ITableRow(that._make_row_data(it));
});
let fr = new ITableFrame(d, pos);
that._table.add_frame(fr);
return fr;
}).catch(function(err) {
Common.displayError(err);
that._table.display_status("error");
}).finally(function() {
that._fetching = false;
});
}
_make_scroll_container() {
let that = this;
let el = document.createElement("div");
el.setAttribute("class", "main-table-container");
el.addEventListener("scroll", function() {
if (!that._fetching && el.scrollTop + el.clientHeight >= el.scrollHeight * 0.95) {
if (that._table.frames_count() === 0 || that._table.more()) {
that._fetch_list();
}
}
});
this._scroll = el;
}
_make_table() {
this._table = new ITable({
class: "main-table small-cards",
onclick: function(row) {
let data = row.userdata();
if (data) {
this._display_item_dialog(data);
}
}.bind(this),
onsort: function(col) {
let dir = col.sorted() && "toggle" || "descent";
this._table.set_sorted(col.name(), dir);
this._sort.column = col.name();
this._sort.direction = col.sorted();
this.update();
}.bind(this),
onfocus: function(el) {
scroll_to_element(el, this._scroll);
}.bind(this)
});
[
{ content: "", class: "cell-status" },
{ content: "Domain", name: "domain" },
{ content: "Source" },
{ content: "Event time", sortable: true, name: "event_time" },
{ content: "Message" }
].forEach(function(col) {
let c = this._table.add_column(col);
if (c.name() === this._sort.column) {
c.sort(this._sort.direction);
}
}, this);
}
_make_row_data(d) {
let rd = { cells: [], userdata: { id: d.id } };
rd.cells.push(new LogsResultCell(d.success));
rd.cells.push({ content: d.domain, label: "Domain" });
rd.cells.push({ content: d.source, label: "Source" });
rd.cells.push({ content: (new Date(d.event_time)).toUIString(), label: "Event time" });
rd.cells.push({ content: d.message, label: "Message" });
return rd;
}
_display_item_dialog(data) {
let dlg = new LogItemDialog(data);
this._element.appendChild(dlg.element());
let that = this;
dlg.show().finally(function() {
dlg.element().remove();
that._table.focus();
});
}
}
class LogsResultCell extends ITableCell {
constructor(success, props) {
props = props || {};
let ca = (props.class || "").split(" ");
ca.push(success && "state-green" || "state-red");
props.class = ca.filter(function(s) { return s.length > 0; }).join(" ");
super(success, props);
}
element() {
if (!this._element) {
super.element().setAttribute("data-label", "Result");
}
return this._element;
}
value(target) {
if (target === "dom") {
let div = document.createElement("div");
div.setAttribute("class", "state-background status-indicator");
if (!this.title) {
div.setAttribute("title", this._content && "Ok" || "Failed");
}
return div;
}
return this._content;
}
}
class LogItemDialog extends ModalDialog {
constructor(data) {
super({ title: "Log record", buttons: [ "close" ] });
this._data = data;
this._table = null;
this._res_el = null;
this._dom_el = null;
this._time_el = null; // event_time
this._rid_el = null; // external_id
this._file_el = null; // filename
this._sou_el = null; // source
this._msg_el = null; // message
}
_gen_content() {
this._table = document.createElement("div");
this._table.setAttribute("class", "left-titled");
this._content.appendChild(this._table);
this._time_el = this._insert_row("Event time");
this._res_el = this._insert_row("Result");
this._res_el.setAttribute("class", "state-text");
this._dom_el = this._insert_row("Domain");
this._rid_el = this._insert_row("Report Id");
this._file_el = this._insert_row("File name");
this._sou_el = this._insert_row("Source");
this._msg_el = this._insert_row("Message");
this._fetch_data();
}
_insert_row(text) {
let t_el = document.createElement("span");
t_el.appendChild(document.createTextNode(text + ": "));
this._table.appendChild(t_el);
let v_el = document.createElement("span");
this._table.appendChild(v_el);
return v_el;
}
_fetch_data() {
this._content.appendChild(set_wait_status());
let uparams = new URLSearchParams();
uparams.set("id", this._data.id);
let that = this;
window.fetch("logs.php?" + uparams.toString(), {
method: "GET",
cache: "no-store",
headers: HTTP_HEADERS,
credentials: "same-origin"
}).then(function(resp) {
if (!resp.ok)
throw new Error("Failed to fetch the log item");
return resp.json();
}).then(function(data) {
Common.checkResult(data);
that._data.domain = data.domain;
that._data.report_id = data.report_id;
that._data.event_time = new Date(data.event_time);
that._data.filename = data.filename;
that._data.source = data.source;
that._data.success = data.success;
that._data.message = data.message;
that._update_ui();
}).catch(function(err) {
Common.displayError(err);
that._content.appendChild(set_error_status(null, err.message));
}).finally(function() {
that._content.querySelector(".wait-message").remove();
});
}
_update_ui() {
this._time_el.textContent = this._data.event_time.toUIString();
if (this._data.success) {
this._res_el.textContent = "Ok";
this._res_el.parentElement.classList.add("state-green");
}
else {
this._res_el.textContent = "Failed";
this._res_el.parentElement.classList.add("state-red");
}
this._dom_el.textContent = this._data.domain || "n/a";
this._rid_el.textContent = this._data.report_id || "n/a";
this._file_el.textContent = this._data.filename || "n/a";
this._sou_el.textContent = this._data.source;
this._msg_el.textContent = this._data.message || "n/a";
}
}

View File

@@ -0,0 +1,335 @@
/**
* 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/>.
*/
class Router {
}
Router.start = function() {
Router._initial_header = document.querySelector("h1").textContent;
document.getElementsByTagName("body")[0].addEventListener("keydown", function(event) {
if (event.code == "Escape" && !event.shiftKey && !event.ctrlKey && !event.altKey) {
let cbtn = document.querySelector(".close-btn.active");
if (cbtn) {
cbtn.click();
event.preventDefault();
}
document.querySelectorAll("div.popup-menu:not(.hidden)").forEach(function(m) {
m.classList.add("hidden");
});
}
});
window.addEventListener("click", function(event) {
if (!event.target.closest("div.popup-menu")) {
document.querySelectorAll("div.popup-menu:not(.hidden)").forEach(function(m) {
m.classList.add("hidden");
});
}
let mm_toggle = document.getElementById("main-menu-toggle");
if (mm_toggle.checked) {
if (event.target.tagName == "A" || !event.target.closest("#main-menu-button")) {
mm_toggle.checked = false;
}
}
});
document.getElementById("main-menu-button").addEventListener("click", function(event) {
let el = event.target;
if (el.tagName === "A") {
let href = el.getAttribute("href");
if (href !== "") {
event.preventDefault();
window.history.pushState(null, "", href);
Router.go();
}
}
});
window.addEventListener("popstate", function(event) {
let m = Router._url2module();
if (m) {
let p = m.pointer;
if (p && p.onpopstate) {
if (p.title) {
Router.update_title(p.title());
}
p.onpopstate(event.state);
} else {
Router.go();
}
}
});
document.getElementById("main-menu").addEventListener("click", function(event) {
let el = event.target.closest("ul>li");
if (el) {
el.classList.toggle("closed");
}
});
document.querySelector(".menu-box .about a").addEventListener("click", function(event) {
event.preventDefault();
setTimeout(function() {
let dlg = new AboutDialog({
authors: [
{ name: "Aleksey Andreev", url: "https://github.com/liuch", years: "2021-2022" }
],
documentation: [
{ ancor: "README on GitHub", url: "https://github.com/liuch/dmarc-srg/blob/master/README.md" }
],
source_code: [
{ ancor: "DmarcSrg on GitHub", url: "https://github.com/liuch/dmarc-srg" }
]
});
document.getElementById("main-block").appendChild(dlg.element());
dlg.show().finally(function() {
dlg.element().remove();
});
}, 0);
});
Router.go();
};
Router.go = function(url) {
Status.instance().update({ settings: [ "ui.datetime.offset", "ui.ipv4.url", "ui.ipv6.url" ] }).then(function(d) {
if (d) {
Router._update_menu(d.authenticated);
if (d.settings) {
if (d.settings["ui.datetime.offset"]) {
Common.tuneDateTimeOutput(d.settings["ui.datetime.offset"]);
}
Common.ipv4_url = d.settings["ui.ipv4.url"] || '';
Common.ipv6_url = d.settings["ui.ipv6.url"] || '';
}
if (d.error_code !== -2) {
try {
Common.checkResult(d);
} catch (err) {
Common.displayError(err);
}
let module = Router._url2module(url);
if (module) {
if (!module.pointer)
module.start(module);
let p = module.pointer;
if (p.oncleardata)
p.oncleardata();
else
Router._clear_data();
if (p.title)
Router.update_title(p.title());
if (p.display)
p.display();
if (p.update)
p.update();
}
}
if (d.state && d.state !== "Ok" && !d.error_code && d.message) {
Notification.add({ type: "warn", text: d.message, delay: 20000 });
}
if (d.version !== Router._app_ver) {
Router._app_ver = d.version;
Router.update_title();
}
if (d.php_version) {
Router.php_version = d.php_version;
}
}
});
};
Router.app_name = function(version) {
let name = "DmarcSrg";
if (version && Router._app_ver) {
name += " " + Router._app_ver;
}
return name;
}
Router.update_title = function(str) {
let title1 = Router.app_name(false);
let title2 = str || Router._title || null;
if (str) {
Router._title = str;
}
document.title = title1 + (title2 && (": " + title2) || "");
let h1 = document.querySelector("h1");
if (str === "") {
h1.textContent = Router._initial_header || "";
} else if (str) {
h1.textContent = title2 || "";
}
};
Router._update_menu = function(authenticated) {
let m_el = document.getElementById("main-menu");
let l_el = m_el.querySelector("#auth-action");
if (l_el) {
l_el.remove();
}
{
let subs = m_el.querySelectorAll(".submenu .selected")
for (let i = 0; i < subs.length; ++i) {
subs[i].classList.remove("selected");
}
let href = document.location.origin + document.location.pathname;
let f1 = false;
for (let i = 0; i < m_el.children.length; ++i) {
let smenu = m_el.children[i];
if (smenu !== l_el) {
let f2 = false;
if (!f1) {
let a_ls = smenu.querySelectorAll("ul>li>a");
for (let k = 0; k < a_ls.length; ++k) {
let a = a_ls[k];
if (a.href === href) {
f1 = true;
f2 = true;
a.parentElement.classList.add("selected")
break;
}
}
}
if (f2) {
smenu.classList.remove("closed");
}
else {
smenu.classList.add("closed");
}
}
}
}
if (authenticated !== "disabled") {
l_el = document.createElement("li");
l_el.setAttribute("id", "auth-action");
let a_el = document.createElement("a");
a_el.setAttribute("href", "");
if (authenticated == "yes") {
a_el.appendChild(document.createTextNode("Log out"));
a_el.addEventListener("click", function(event) {
event.preventDefault();
if (!this.classList.contains("disabled")) {
let m_el = this;
m_el.classList.add("disabled");
window.fetch("logout.php", {
method: "POST",
cache: "no-store",
headers: Object.assign(HTTP_HEADERS, HTTP_HEADERS_POST),
credentials: "same-origin",
body: JSON.stringify({})
}).then(function(resp) {
if (!resp.ok)
throw new Error("Failed to log out");
return resp.json();
}).then(function(data) {
Common.checkResult(data);
Status.instance().reset();
Router._clear_data();
Router._update_menu("no");
Router.update_title("");
}).catch(function(err) {
Common.displayError(err);
m_el.classList.remove("disabled");
Notification.add({ type: "error", text: err.message });
});
}
});
}
else if (authenticated == "no") {
a_el.appendChild(document.createTextNode("Log in"));
a_el.addEventListener("click", function(event) {
event.preventDefault();
LoginDialog.start({ nousername: true });
});
}
l_el.appendChild(a_el);
m_el.appendChild(l_el);
}
};
Router._clear_data = function() {
remove_all_children(document.getElementById("main-block"));
remove_all_children(document.getElementById("detail-block"));
};
Router._modules = {
list: {
start: function(m) {
m.pointer = new ReportList();
}
},
report: {
start: function(m) {
m.pointer = ReportWidget.instance();
}
},
admin: {
start: function(m) {
m.pointer = new Admin();
}
},
files: {
start: function(m) {
m.pointer = new Files();
}
},
domains: {
start: function(m) {
m.pointer = new DomainList();
}
},
logs: {
start: function(m) {
m.pointer = new Logs();
}
},
summary: {
start: function(m) {
m.pointer = new Summary();
}
},
settings: {
start: function(m) {
m.pointer = new Settings();
}
}
};
Router._url2module = function(url) {
let rr = /([^\/]*)$/.exec(url || document.location.pathname);
return rr && Router._modules[Router._routes[rr[1]]] || null;
};
Router._routes = {
"": "list",
"list.php": "list",
"logs.php": "logs",
"admin.php": "admin",
"files.php": "files",
"report.php": "report",
"domains.php": "domains",
"summary.php": "summary",
"settings.php": "settings"
};
window.onload = Router.start;

View File

@@ -0,0 +1,86 @@
/**
* 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/>.
*/
class Notification {
constructor(params) {
this._params = params;
this._element = this._create_element();
}
element() {
return this._element;
}
_create_element() {
let el = document.createElement("div");
el.setAttribute("class", "notification");
if (this._params.type === "error")
el.classList.add("notif-error");
else if (this._params.type === "warn")
el.classList.add("notif-warn");
else
el.classList.add("notif-info");
{
let text = this._params.text;
if (typeof(text) !== "object")
text = [ text ];
for (let i = 0; ; ) {
el.appendChild(document.createTextNode(text[i]));
++i;
if (i == text.length)
break;
el.appendChild(document.createElement("br"));
}
}
let btn = document.createElement("button");
btn.setAttribute("type", "button");
btn.setAttribute("class", "notif-close");
btn.appendChild(document.createTextNode("x"));
el.appendChild(btn);
el.addEventListener("click", function(event) {
if (event.target.classList.contains("notif-close"))
this.remove();
});
if (this._params.delay > 0) {
setTimeout(function() {
el.style.transition = "opacity 2s ease-in-out";
el.style.opacity = 0;
setTimeout(function() { el.remove(); }, 2000);
}, this._params.delay);
}
return el;
}
}
Notification.add = function(params) {
for (let key in Notification.defaults) {
if (params[key] === undefined)
params[key] = Notification.defaults[key];
}
let notif = new Notification(params);
document.getElementById("notifications-block").appendChild(notif.element());
return notif;
}
Notification.defaults = {
type: "info",
delay: 5000
};

View File

@@ -0,0 +1,427 @@
/**
* 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/>.
*/
class ReportWidget {
constructor() {
this._rep_id = null;
this._element = null;
this._close_btn = null;
this._id_element = null;
this._cn_element = null;
this._onclose_act = null;
}
display() {
if (!this._element || !document.contains(this._element)) {
let cn = document.getElementById("main-block");
cn.appendChild(this.element());
}
}
update() {
this.show_report().catch(function(err) {
Common.displayError(err);
});
}
onpopstate() {
this.display();
this.update();
}
oncleardata() {
if (!this._element || !document.contains(this._element)) {
remove_all_children(document.getElementById("main-block"));
remove_all_children(document.getElementById("detail-block"));
}
}
show_report(domain, report_id) {
this.element();
let that = this;
return new Promise(function(resolve, reject) {
if (!domain || !report_id) {
let sp = (new URL(document.location.href)).searchParams;
domain = sp.get("domain");
report_id = sp.get("report_id");
if (!domain || !report_id) {
set_error_status(this._cn_element, err.message);
reject(new Error("Domain and report_id must be specified"));
}
}
that._id_element.childNodes[0].nodeValue = report_id;
set_wait_status(that._cn_element);
that._rep_id = report_id;
that._element.classList.remove("report-hidden");
that._close_btn.classList.add("active");
let rep = new Report(domain, report_id);
rep.fetch().then(function() {
if (that._rep_id === report_id) {
remove_all_children(that._cn_element);
that._cn_element.appendChild(rep.element());
rep.set_value("seen", true).then(function(data) {
Common.checkResult(data);
}).catch(function(err) {
Common.displayError(err);
});
}
resolve();
}).catch(function(err) {
let err_str = rep.error_message() || "Failed to get the report data";
set_error_status(that._cn_element, err_str);
reject(err);
});
});
}
element() {
if (!this._element) {
this._gen_element();
}
return this._element;
}
title() {
return "Report Detail";
}
focus() {
let el = this._element;
if (el)
el.focus();
}
hide() {
if (this._element && !this._element.classList.contains("report-hidden")) {
this._element.classList.add("report-hidden");
this._close_btn.classList.remove("active");
return true;
}
return false;
}
close() {
if (this.hide() && this._onclose_act)
this._onclose_act();
}
onclose(fn) {
this._onclose_act = typeof(fn) == "function" && fn || null;
}
_gen_element() {
let el = document.createElement("div");
el.setAttribute("class", "report-modal report-hidden");
el.setAttribute("tabindex", -1);
el.addEventListener("click", function(event) {
if (event.target.classList.contains("close-btn") || event.target.classList.contains("report-header")) {
if (window.history.state && window.history.state.from === "list")
this.close();
else
window.history.go(-1);
}
}.bind(this));
let hd = document.createElement("div");
hd.setAttribute("class", "report-header");
{
let ht = document.createElement("span");
ht.setAttribute("class", "header-text");
ht.appendChild(document.createTextNode("DMARC Report (Id: "));
let id = document.createElement("span");
id.setAttribute("id", "report-modal-id");
id.appendChild(document.createTextNode("?"));
this._id_element = id;
ht.appendChild(id);
ht.appendChild(document.createTextNode(")"));
hd.appendChild(ht);
}
el.appendChild(hd);
let bd = document.createElement("div");
bd.setAttribute("class", "body");
let cn = document.createElement("div");
cn.setAttribute("class", "content");
this._cn_element = cn;
bd.appendChild(cn);
let cb = document.createElement("button");
cb.setAttribute("class", "btn close-btn");
cb.appendChild(document.createTextNode("Close"));
this._close_btn = cb;
bd.appendChild(cb);
el.appendChild(bd);
this._element = el;
}
}
ReportWidget.instance = function() {
if (!ReportWidget._instance) {
ReportWidget._instance = new ReportWidget();
ReportWidget._instance.onclose(function() {
window.history.go(-1);
});
}
return ReportWidget._instance;
}
class Report {
constructor(domain, report_id) {
this._data = null;
this._error = false;
this._error_message = null;
this._domain = domain;
this._report_id = report_id;
}
id() {
return this._report_id;
}
error() {
return this._error;
}
error_message() {
return this._error_message;
}
fetch() {
let u_params = new URLSearchParams();
u_params.set("domain", this._domain);
u_params.set("report_id", this._report_id);
let that = this;
return window.fetch("report.php?" + u_params.toString(), {
method: "GET",
cache: "no-store",
headers: HTTP_HEADERS,
credentials: "same-origin"
}).then(function(resp) {
if (!resp.ok)
throw new Error("Failed to fetch report data");
return resp.json();
}).then(function(data) {
Common.checkResult(data);
that._data = data.report;
that._error = false;
that._error_message = null;
}).catch(function(err) {
that._data = null;
that._error = true;
that._error_message = err.message;
throw err;
});
}
element() {
return this._create_element();
}
set_value(name, value) {
let definitions = {
"seen": "boolean"
};
if (value === undefined || definitions[name] !== typeof(value)) {
console.warn("Set report value: Incorrect value");
return Promise.resolve({});
}
let url_params = new URLSearchParams();
url_params.set("action", "set");
url_params.set("domain", this._domain);
url_params.set("report_id", this._report_id);
return window.fetch("report.php?" + url_params.toString(), {
method: "POST",
cache: "no-store",
headers: Object.assign(HTTP_HEADERS, HTTP_HEADERS_POST),
credentials: "same-origin",
body: JSON.stringify({ name: name, value: value })
}).then(function(resp) {
if (!resp.ok)
throw new Error("Failed to set report value");
return resp.json();
}).catch(function(err) {
Common.displayError(err);
});
}
_create_element() {
let el = document.createDocumentFragment();
let md = document.createElement("div");
md.setAttribute("class", "report-metadata");
md.appendChild(this._create_data_item("Report Id", this._data.report_id));
md.appendChild(this._create_data_item("Reporting organization", this._data.org_name));
md.appendChild(this._create_data_item("Domain", this._data.domain));
let d1 = new Date(this._data.date.begin);
let d2 = new Date(this._data.date.end);
md.appendChild(this._create_data_item("Date range", d1.toUIString(true) + " - " + d2.toUIString(true)));
md.appendChild(this._create_data_item("Email", this._data.email));
if (this._data.extra_contact_info)
md.appendChild(this._create_data_item("Extra contact info", this._data.extra_contact_info));
md.appendChild(this._create_data_item("Published policy", this._create_pub_policy_fragment(this._data.policy)));
if (this._data.error_string)
md.appendChild(this._create_data_item("Error string", "???"));
md.appendChild(this._create_data_item("Loaded time", (new Date(this._data.loaded_time)).toUIString()));
el.appendChild(md);
// Records
let rs = document.createElement("div");
rs.setAttribute("class", "report-records");
let hd = document.createElement("h5");
hd.appendChild(document.createTextNode("Records"));
rs.appendChild(hd);
this._data.records.forEach(function(rec) {
let tl = document.createElement("div");
tl.setAttribute("class", "report-record round-border");
let hd = document.createElement("div");
hd.setAttribute("class", "header");
hd.appendChild(this._create_data_fragment("IP-address", Common.makeIpElement(rec.ip)));
tl.appendChild(hd);
tl.appendChild(this._create_data_item("Message count", rec.count));
tl.appendChild(this._create_data_item("Policy evaluated", this._create_ev_policy_fragment(rec)));
if (rec.reason)
tl.appendChild(this._create_data_item("Evaluated reason", this._create_reason_fragment(rec.reason)));
tl.appendChild(this._create_data_item("Identifiers", this._create_identifiers_fragment(rec)));
tl.appendChild(this._create_data_item("DKIM auth", this._create_dkim_auth_fragment(rec.dkim_auth)));
tl.appendChild(this._create_data_item("SPF auth", this._create_spf_auth_fragment(rec.spf_auth)));
rs.appendChild(tl);
}, this);
el.appendChild(rs);
return el;
}
_get_row_container(ctn, data) {
if (data.length < 2)
return ctn;
let div = document.createElement("div")
ctn.appendChild(div);
return div;
}
_create_data_item(title, data) {
let el = document.createElement("div");
el.setAttribute("class", "report-item");
el.appendChild(this._create_data_fragment(title, data));
return el;
}
_create_data_fragment(title, data) {
let fr = document.createDocumentFragment();
let tl = document.createElement("span");
tl.appendChild(document.createTextNode(title + ": "));
tl.setAttribute("class", "title");
fr.appendChild(tl);
if (typeof(data) !== "object")
data = document.createTextNode(data);
let dt = document.createElement(data.childNodes.length > 1 ? "div" : "span");
dt.setAttribute("class", "value");
dt.appendChild(data);
if (Array.from(dt.children).find(function(ch) {
return ch.tagName === "DIV";
})) dt.classList.add("rows");
fr.appendChild(dt);
return fr;
}
_create_ev_policy_fragment(data) {
let fr = document.createDocumentFragment();
if (data.dkim_align)
fr.appendChild(create_report_result_element("DKIM", data.dkim_align, true));
if (data.spf_align)
fr.appendChild(create_report_result_element("SPF", data.spf_align, true));
if (data.disposition)
fr.appendChild(create_report_result_element("disposition", data.disposition, true, ""));
return fr;
}
_create_reason_fragment(data) {
let fr = document.createDocumentFragment();
data.forEach(function(rec) {
let ctn = this._get_row_container(fr, data);
if (rec.type)
ctn.appendChild(create_report_result_element("type", rec.type, true, ""));
if (rec.comment)
ctn.appendChild(create_report_result_element("comment", rec.comment, true, ""));
}.bind(this));
return fr;
}
_create_identifiers_fragment(data) {
let fr = document.createDocumentFragment();
if (data.header_from)
fr.appendChild(create_report_result_element("header_from", data.header_from, true, ""));
if (data.envelope_from)
fr.appendChild(create_report_result_element("envelope_from", data.envelope_from, true, ""));
if (data.envelope_to)
fr.appendChild(create_report_result_element("envelope_to", data.envelope_to, true, ""));
return fr;
}
_create_dkim_auth_fragment(data) {
if (!data)
return "n/a";
let fr = document.createDocumentFragment();
data.forEach(function(rec) {
let ctn = this._get_row_container(fr, data);
if (rec.domain)
ctn.appendChild(create_report_result_element("domain", rec.domain, true, ""));
if (rec.selector)
ctn.appendChild(create_report_result_element("selector", rec.selector, true, ""));
if (rec.result)
ctn.appendChild(create_report_result_element("result", rec.result, true));
}.bind(this));
return fr;
}
_create_spf_auth_fragment(data) {
if (!data)
return "n/a";
let fr = document.createDocumentFragment();
data.forEach(function(rec) {
let ctn = this._get_row_container(fr, data);
if (rec.domain)
ctn.appendChild(create_report_result_element("domain", rec.domain, true, ""));
if (rec.result)
ctn.appendChild(create_report_result_element("result", rec.result, true));
}.bind(this));
return fr;
}
_create_pub_policy_fragment(data) {
if (!data)
return "n/a";
let fr = document.createDocumentFragment();
[
[ "adkim", data.adkim ], [ "aspf", data.aspf ], [ "p", data.p ], [ "sp", data.sp ],
[ "np", data.np ], [ "pct", data.pct ], [ "fo", data.fo ]
].forEach(function(pol) {
if (pol[1]) fr.appendChild(create_report_result_element(pol[0], pol[1], true, ""));
});
return fr;
}
}

View File

@@ -0,0 +1,355 @@
/**
* 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/>.
*/
class Settings {
constructor() {
this._table = null;
this._scrool = null;
this._sort = "ascent";
this._element = document.getElementById("main-block");
}
display() {
this._make_scroll_container();
this._make_table();
this._scroll.appendChild(this._table.element());
this._element.appendChild(this._scroll);
this._table.focus();
}
update() {
this._fetch_settings();
}
title() {
return "Advanced Settings";
}
_fetch_settings() {
this._table.display_status("wait");
let that = this;
let uparams = new URLSearchParams();
uparams.set("direction", this._sort);
return window.fetch("settings.php?" + uparams.toString(), {
method: "GET",
cache: "no-store",
headers: HTTP_HEADERS,
credentials: "same-origin"
}).then(function(resp) {
if (!resp.ok)
throw new Error("Failed to fetch the settings");
return resp.json();
}).then(function(data) {
that._table.display_status(null);
Common.checkResult(data);
let d = { more: data.more };
d.rows = data.settings.map(function(it) {
return that._make_row_data(it);
});
that._table.clear();
let fr = new ITableFrame(d, 0);
that._table.add_frame(fr);
that._table.focus();
}).catch(function(err) {
Common.displayError(err);
that._table.display_status("error", err.message);
});
}
_make_scroll_container() {
this._scroll = document.createElement("div");
this._scroll.setAttribute("class", "main-table-container");
}
_make_table() {
this._table = new ITable({
class: "main-table small-cards",
onclick: function(row) {
let data = row.userdata();
if (data) {
this._display_edit_dialog(data);
}
}.bind(this),
onsort: function(col) {
let dir = col.sorted() && "toggle" || "ascent";
this._table.set_sorted(col.name(), dir);
this._sort = col.sorted();
this.update();
}.bind(this),
onfocus: function(el) {
scroll_to_element(el, this._scroll);
}.bind(this)
});
[
{ content: "Name", name: "name", sortable: true },
{ content: "Value", name: "value" },
{ content: "Description", name: "descr" }
].forEach(function(col) {
let c = this._table.add_column(col);
if (c.name() === "name") {
c.sort(this._sort);
}
}, this);
}
_make_row_data(d) {
let rd = { cells: [], userdata: d.name };
rd.cells.push({ content: d.name, class: "setting-name", label: "Name " });
rd.cells.push({ content: d.value, class: "setting-value", label: "Value " });
rd.cells.push({ content: Settings._descriptions_short[d.name] || Settings._descriptions[d.name] || "No description", label: "Description " });
if (d.value !== d.default) {
rd.class = "custom-value";
}
return rd;
}
_display_edit_dialog(name) {
let dlg = new SettingEditDialog({
name: name,
description: Settings._descriptions[name]
});
this._element.appendChild(dlg.element());
let that = this;
dlg.show().then(function(d) {
if (d) {
that.update();
}
}).finally(function() {
dlg.element().remove();
that._table.focus();
});
}
static _descriptions = {
"status.emails-for-last-n-days": "The period in days for which statistics are displayed in the status block.",
"report-view.sort-records-by": "How records are sorted in the report view dialog.",
"log-view.sort-list-by": "How report log items are sorted by default in the log view dialog.",
"ui.datetime.offset": "Time zone offset of displayed dates in UI. Auto means that the report range is in UTC and all other dates are in local.",
"ui.ipv4.url": "The URL that will be used as a link when clicking on the IPv4 address. For example: https://somewhoisservice.net/ip/{$ip}, where {$ip} is IP address from the UI. Use {$eip} if you want to insert url encoded IP address. Use an empty string to disable.",
"ui.ipv6.url": "The URL that will be used as a link when clicking on the IPv6 address. For example: https://somewhoisservice.net/ip/{$ip}, where {$ip} is IP address from the UI. Use {$eip} if you want to insert url encoded IP address. Use an empty string to disable."
};
static _descriptions_short = {
"ui.datetime.offset": "Time zone offset of displayed dates in UI.",
"ui.ipv4.url": "The URL that will be used as a link when clicking on the IPv4 address.",
"ui.ipv6.url": "The URL that will be used as a link when clicking on the IPv6 address."
};
}
class SettingEditDialog extends ModalDialog {
constructor(param) {
super({ title: "Setting dialog", buttons: [ "ok", "close" ] });
this._data = param || {};
this._content = null;
this._table = null;
this._val_el = null;
this._val_tp = null;
this._desc_el = null;
this._save_bt = null;
this._fetched = false;
}
_gen_content() {
this._table = document.createElement("div");
this._table.setAttribute("class", "titled-input");
this._content.appendChild(this._table);
this._content.classList.add("vertical-content");
let nm = document.createElement("input");
nm.setAttribute("type", "text");
nm.setAttribute("disabled", "disabled");
nm.setAttribute("value", this._data.name);
this._insert_row("Name", nm);
let val = document.createElement("input");
val.setAttribute("type", "text");
val.disabled = true;
this._insert_row("Value", val);
this._val_el = val;
this._val_tp = "string";
let desc = document.createElement("textarea");
desc.setAttribute("disabled", "disabled");
if (this._data.description) {
desc.appendChild(document.createTextNode(this._data.description));
}
desc.classList.add("description");
this._insert_row("Description", desc);
this._desc_el = desc;
this._save_bt = this._buttons[1];
this._save_bt.disabled = true;
this._table.addEventListener("input", function(event) {
if (this._fetched && event.target == this._val_el) {
let e_val = null;
switch (this._val_tp) {
case "select":
e_val = this._val_el.value;
break;
case "integer":
e_val = this._val_el.valueAsNumber;
break;
}
this._save_bt.disabled = (e_val === this._data.value);
}
}.bind(this));
this._fetch_data();
}
_add_button(container, text, type) {
if (type == "submit") {
text = "Save";
}
super._add_button(container, text, type);
}
_insert_row(text, val_el) {
let lb = document.createElement("label");
let sp = document.createElement("span");
sp.appendChild(document.createTextNode(text + ": "));
lb.appendChild(sp);
lb.appendChild(val_el);
this._table.appendChild(lb);
}
_fetch_data() {
this._enable_ui(false);
this._content.appendChild(set_wait_status());
let uparams = new URLSearchParams();
uparams.set("name", this._data.name);
let that = this;
window.fetch("settings.php?" + uparams.toString(), {
method: "GET",
cache: "no-store",
headers: HTTP_HEADERS,
credentials: "same-origin"
}).then(function(resp) {
if (!resp.ok)
throw new Error("Failed to fetch setting data for " + that._data.name);
return resp.json();
}).then(function(data) {
Common.checkResult(data);
that._data.value = data.value;
that._update_ui(data);
that._enable_ui(true);
that._fetched = true;
}).catch(function(err) {
Common.displayError(err);
that._content.appendChild(set_error_status(null, err.message));
}).finally(function() {
that._content.querySelector(".wait-message").remove();
});
}
_enable_ui(en) {
this._val_el.disabled = !en;
this._update_first_last();
if (this._first) {
this._first.focus();
}
}
_update_ui(data) {
if (data.type !== this._val_tp) {
let new_el = null;
if (data.type == "integer") {
new_el = document.createElement("input");
new_el.setAttribute("type", "number");
if (typeof(data.minimum) == "number") {
new_el.setAttribute("min", data.minimum);
}
if (typeof(data.maximum) == "number") {
new_el.setAttribute("max", data.maximum);
}
} else if (data.type == "select") {
new_el = document.createElement("select");
data.options.forEach(function(op) {
let opt_el = document.createElement("option");
opt_el.setAttribute("value", op);
opt_el.appendChild(document.createTextNode(op));
if (op === data.value) {
opt_el.setAttribute("selected", "selected");
}
new_el.appendChild(opt_el);
});
}
if (new_el) {
new_el.setAttribute("required", "required");
this._val_el.replaceWith(new_el);
this._val_el = new_el;
}
this._val_tp = data.type;
}
this._val_el.value = data.value;
}
_submit() {
this._save_bt.disabled = true;
this._enable_ui(false);
let em = this._content.querySelector(".error-message");
if (em) {
em.remove();
}
this._content.appendChild(set_wait_status());
let body = {};
body.name = this._data.name;
if (this._val_tp == "integer") {
body.value = this._val_el.valueAsNumber;
}
else {
body.value = this._val_el.value;
}
body.action = "update";
let that = this;
window.fetch("settings.php", {
method: "POST",
cache: "no-store",
headers: Object.assign(HTTP_HEADERS, HTTP_HEADERS_POST),
credentials: "same-origin",
body: JSON.stringify(body)
}).then(function(resp) {
if (!resp.ok)
throw new Error("Failed to update the setting");
return resp.json();
}).then(function(data) {
Common.checkResult(data);
that._data.value = that._val_el.value;
that._result = body;
that.hide();
Notification.add({ type: "info", text: (data.message || "Updated successfully!") });
}).catch(function(err) {
Common.displayError(err);
that._content.appendChild(set_error_status(null, err.message));
Notification.add({ type: "error", text: err.message });
}).finally(function() {
that._content.querySelector(".wait-message").remove();
that._save_bt.disabled = false;
that._enable_ui(true);
});
}
}

View File

@@ -0,0 +1,206 @@
/**
* 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/>.
*/
class Status {
constructor() {
this._data = {};
}
update(params) {
return this._fetch(params || {}).then(function(data) {
return data;
}).catch(function(err) {
Common.displayError(err);
});
}
reset() {
this._data.emails = null;
this._data.error_code = 0;
this._update_block();
}
error() {
return this._data.error_code && this._data.error_code !== 0 || false;
}
_fetch(params) {
let p_string = '';
if (params.settings && params.settings.length) {
let uparams = new URLSearchParams();
uparams.set("settings", params.settings.join(','));
p_string = '?' + uparams.toString();
}
let that = this;
return new Promise(function(resolve, reject) {
window.fetch("status.php" + p_string, {
method: "GET",
cache: "no-store",
headers: HTTP_HEADERS,
credentials: "same-origin"
}).then(function(resp) {
if (!resp.ok)
throw new Error("Failed to fetch the status");
return resp.json();
}).then(function(data) {
that._data = {
state: data.state,
error_code: data.error_code,
message: data.message,
emails: data.emails
};
if (data.exeption)
that._data.exeption = data.exeption;
that._update_block();
if (data.error_code === -2) {
LoginDialog.start({ nousername: true });
}
resolve(data);
}).catch(function(err) {
that._data = {
state: "Err",
error_code: -100,
message: err.message
};
that._update_block();
reject(err);
});
});
}
_update_block() {
this._ensure_element_created();
if (this._data.error_code) {
Notification.add({ text: "[" + this._data.error_code + "] " + this._data.message, type: "error" });
}
if (!this._data.emails) {
this._data.emails = {
days: 0,
total: -1,
spf_aligned: 0,
dkim_aligned: 0,
dkim_spf_aligned: 0
};
}
let days = this._data.emails.days;
let total = this._data.emails.total;
let passed = this._data.emails.dkim_spf_aligned;
let forwarded = this._data.emails.dkim_aligned + this._data.emails.spf_aligned;
let failed = total - passed - forwarded;
this._set_element_data(
"processed",
(total === -1 || total === undefined) && "?" || total,
total !== -1 && "state-blue" || null
);
this._set_element_data(
"passed",
this._formatted_statistic(passed, total),
total !== -1 && "state-green" || null
);
this._set_element_data(
"forwarded",
this._formatted_statistic(forwarded, total),
total !== -1 && "state-green" || null
);
this._set_element_data(
"failed",
this._formatted_statistic(failed, total),
total !== -1 && "state-red" || null
);
{
let el = document.getElementById("stat-block");
if (days > 0) {
el.setAttribute("title", "Statistics for the last " + days + " days");
}
else {
el.removeAttribute("title");
}
}
}
_formatted_statistic(val, total) {
if (total === -1)
return "?";
if (!total)
return "-";
if (val === 0)
return "0";
let rval = Math.round(val / total * 100);
return (val > 0 && rval === 0 && "+" || "" ) + rval + "%";
}
_set_element_data(id, data, c_name) {
let el1 = document.getElementById("stat-" + id);
if (c_name)
el1.setAttribute("class", c_name);
else
el1.removeAttribute("class");
let el2 = el1.querySelector(".stat-val")
el2.childNodes[0].nodeValue = data;
}
_ensure_element_created() {
let block = document.getElementById("stat-block");
if (block && block.children.length === 0) {
let ul = document.createElement("ul");
Status._element_list.forEach(function(id) {
let li = document.createElement("li");
let div = document.createElement("div");
div.setAttribute("id", "stat-" + id);
let val = document.createElement("span");
val.setAttribute("class", "stat-val state-text");
val.appendChild(document.createTextNode("?"));
let msg = document.createElement("span");
msg.setAttribute("class", "stat-msg");
msg.appendChild(document.createTextNode(Status._element_data[id].text));
div.appendChild(val);
div.appendChild(msg);
li.appendChild(div);
ul.appendChild(li);
});
block.appendChild(ul);
}
}
}
Status.instance = function() {
if (!this._instance)
this._instance = new Status();
return this._instance;
}
Status._element_list = [ "processed", "passed", "forwarded", "failed" ];
Status._element_data = {
processed: {
text: "Emails processed"
},
passed: {
text: "Fully aligned"
},
forwarded: {
text: "Partially aligned"
},
failed: {
text: "Not aligned"
}
};

View File

@@ -0,0 +1,537 @@
/**
* 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/>.
*/
class Summary {
constructor(id) {
this._report = null;
this._element = document.getElementById("main-block");
this._container = null;
this._options_data = null;
this._options_block = null;
this._report_block = null;
}
display() {
this._create_container();
this._element.appendChild(this._container);
this._create_options_block();
this._create_report_block();
this._container.appendChild(this._options_block);
this._container.appendChild(document.createElement("hr"));
this._container.appendChild(this._report_block);
}
update() {
this._handle_url_params();
this._update_options_block();
this._fetch_report();
}
title() {
return "Summary Reports";
}
_handle_url_params() {
let url_params = new URL(document.location.href).searchParams;
let domain = url_params.get("domain");
let period = url_params.get("period");
let format = url_params.get("format");
if (domain && period) {
this._options_data = { domain: domain, period: period, format: format || "text" };
} else {
this._options_data = null;
}
}
_create_container() {
this._container = document.createElement("div");
this._container.setAttribute("class", "panel-container round-border");
}
_create_options_block() {
let opts = document.createElement("div");
opts.setAttribute("class", "options-block");
opts.appendChild(document.createTextNode("Report options: "));
opts.appendChild(document.createTextNode("none"));
let btn = document.createElement("button");
btn.setAttribute("class", "options-button");
btn.appendChild(document.createTextNode("Change"));
btn.addEventListener("click", function(event) {
this._display_dialog();
}.bind(this));
opts.appendChild(btn);
this._options_block = opts;
}
_update_options_block() {
let text = "none";
if (this._options_data) {
text = "domain=" + this._options_data.domain + " period=" + this._options_data.period;
}
this._options_block.childNodes[1].textContent = text;
}
_create_report_block() {
this._report_block = document.createElement("div");
this._report_block.setAttribute("class", "summary-report");
}
_display_dialog() {
let dlg = new OptionsDialog(this._options_data);
document.getElementById("main-block").appendChild(dlg.element());
dlg.show().then(function(d) {
if (!d) {
return;
}
let url = new URL(document.location.href);
url.searchParams.set("domain", d.domain);
let period = d.period;
if (period === "lastndays") {
period += ":" + d.days;
}
url.searchParams.set("period", period);
url.searchParams.set("format", d.format);
window.history.replaceState(null, "", url.toString());
remove_all_children(this._element);
this.display();
this.update();
}.bind(this)).finally(function() {
this._options_block.lastChild.focus();
}.bind(this));
}
_fetch_report() {
remove_all_children(this._report_block);
if (!this._options_data) {
this._report_block.appendChild(document.createTextNode("Report options are not selected"));
return;
}
this._report_block.appendChild(set_wait_status());
let uparams = new URLSearchParams();
let domain = this._options_data.domain;
uparams.set("domain", domain);
uparams.set("period", this._options_data.period);
uparams.set("format", this._options_data.format === "html" ? "raw" : "text");
window.fetch("summary.php?mode=report&" + uparams.toString(), {
method: "GET",
cache: "no-store",
headers: HTTP_HEADERS,
credentials: "same-origin"
}).then(function(resp) {
if (!resp.ok) {
throw new Error("Failed to fetch the report");
}
return resp.json();
}).then(function(report) {
Common.checkResult(report);
report.domain = domain;
this._report = new SummaryReport(report);
this._display_report();
}.bind(this)).catch(function(err) {
Common.displayError(err);
set_error_status(this._report_block, 'Error: ' + err.message);
}.bind(this)).finally(function() {
let wm = this._report_block.querySelector(".wait-message");
if (wm) {
wm.remove();
}
}.bind(this));
}
_display_report() {
let el = null;
let text = this._report.text();
if (text) {
el = document.createElement("pre");
el.appendChild(document.createTextNode(this._report.text()));
} else {
el = this._report.html();
if (!el) {
el = document.createElement("p");
el.appendChild(document.createTextNode("No data"));
}
}
this._report_block.appendChild(el);
}
}
class OptionsDialog extends ModalDialog {
constructor(params) {
super({ title: "Report options", buttons: [ "apply", "reset" ] });
this._data = params || {};
this._content = null;
this._domains = null;
this._ui_data = [
{ name: "domain", title: "Domain" },
{ name: "period", title: "Period" },
{ name: "days", title: "Days", type: "input" },
{ name: "format", title: "Format" }
];
}
_gen_content() {
let container = document.createElement("div");
container.setAttribute("class", "titled-input");
this._content.appendChild(container);
this._content.classList.add("vertical-content");
this._ui_data.forEach(function(row) {
let i_el = this._add_option_row(row.name, row.title, container, row.type);
if (row.name === "days") {
i_el.setAttribute("type", "number");
i_el.setAttribute("min", "1");
i_el.setAttribute("max", "9999");
i_el.setAttribute("value", "");
}
row.element = i_el;
}, this);
this._ui_data[1].element.addEventListener("change", function(event) {
let days_el = this._ui_data[2].element;
if (event.target.value === "lastndays") {
days_el.disabled = false;
delete days_el.dataset.disabled;
days_el.value = days_el.dataset.value || "1";
} else {
days_el.disabled = true;
days_el.dataset.value = days_el.value || "1";
days_el.dataset.disabled = true;
days_el.value = "";
}
}.bind(this));
this._update_period_element();
this._update_format_element();
if (!this._domains) {
this._fetch_data();
}
}
_submit() {
let res = {
domain: this._ui_data[0].element.value,
period: this._ui_data[1].element.value,
format: this._ui_data[3].element.value
};
if (res.period === "lastndays") {
res.days = parseInt(this._ui_data[2].element.value) || 1;
}
this._result = res;
this.hide();
}
_add_option_row(name, title, p_el, type) {
let l_el = document.createElement("label");
p_el.appendChild(l_el);
let t_el = document.createElement("span");
t_el.appendChild(document.createTextNode(title + ": "));
l_el.appendChild(t_el);
let n_el = document.createElement(type || "select");
n_el.setAttribute("name", name);
l_el.appendChild(n_el);
return n_el;
}
_update_domain_element() {
let el = this._ui_data[0].element;
remove_all_children(el);
let c_val = this._data.domain || "";
if (this._domains) {
this._domains.forEach(function(name) {
let opt = document.createElement("option");
opt.setAttribute("value", name);
if (name === c_val) {
opt.setAttribute("selected", "");
}
opt.appendChild(document.createTextNode(name));
el.appendChild(opt);
});
}
}
_update_period_element() {
let el = this._ui_data[1].element;
let c_val = this._data.period && this._data.period.split(":") || [ "lastweek" ];
[
[ "lastweek", "Last week"],
[ "lastmonth", "Last month" ],
[ "lastndays", "Last N days" ]
].forEach(function(it) {
let opt = document.createElement("option");
opt.setAttribute("value", it[0]);
if (it[0] === c_val[0]) {
opt.setAttribute("selected", "");
}
opt.appendChild(document.createTextNode(it[1]));
el.appendChild(opt);
});
if (c_val[1]) {
let val = parseInt(c_val[1]);
let i_el = this._ui_data[2].element;
i_el.setAttribute("value", val);
i_el.dataset.value = val;
}
el.dispatchEvent(new Event("change"));
}
_update_format_element() {
let el = this._ui_data[3].element;
let cv = this._data.format || "text";
[
[ "text", "Plain text" ],
[ "html", "HTML" ]
].forEach(function(it) {
let opt = document.createElement("option");
opt.setAttribute("value", it[0]);
if (it[0] === cv) {
opt.setAttribute("selected", "");
}
opt.appendChild(document.createTextNode(it[1]));
el.appendChild(opt);
});
}
_enable_ui(enable) {
let list = this._element.querySelector("form").elements;
for (let i = 0; i < list.length; ++i) {
let el = list[i];
el.disabled = !enable || el.dataset.disabled;
}
}
_fetch_data() {
this._enable_ui(false);
this._content.appendChild(set_wait_status());
window.fetch("summary.php?mode=options", {
method: "GET",
cache: "no-store",
headers: HTTP_HEADERS,
credentials: "same-origin"
}).then(function(resp) {
if (!resp.ok) {
throw new Error("Failed to fetch the report options list");
}
return resp.json();
}).then(function(data) {
Common.checkResult(data);
this._domains = data.domains;
this._update_domain_element();
this._enable_ui(true);
}.bind(this)).catch(function(err) {
Common.displayError(err);
this._content.appendChild(set_error_status());
}.bind(this)).finally(function() {
this._content.querySelector(".wait-message").remove();
}.bind(this));
}
_reset() {
window.setTimeout(function() {
this._ui_data[1].element.dispatchEvent(new Event("change"));
}.bind(this), 0);
}
}
class SummaryReport {
constructor(data) {
this._report = data;
}
text() {
let lines = this._report.text || [];
if (lines.length > 0) {
return lines.join("\n");
}
}
html() {
let data = this._report.data;
let html = document.createDocumentFragment();
let header = document.createElement("h2");
header.appendChild(document.createTextNode("Domain: " + this._report.domain));
html.appendChild(header);
{
let range = document.createElement("div");
let d1 = (new Date(data.date_range.begin)).toLocaleDateString();
let d2 = (new Date(data.date_range.end)).toLocaleDateString();
range.appendChild(document.createTextNode("Range: " + d1 + " - " + d2));
html.appendChild(range);
}
{
let header = document.createElement("h3");
header.appendChild(document.createTextNode("Summary"));
html.appendChild(header);
let cont = document.createElement("div");
cont.setAttribute("class", "left-titled");
html.appendChild(cont);
function add_row(title, value, cname) {
let te = document.createElement("span");
te.appendChild(document.createTextNode(title + ": "));
cont.appendChild(te);
let ve = document.createElement("span");
if (cname) {
ve.setAttribute("class", cname);
}
ve.appendChild(document.createTextNode(value));
cont.appendChild(ve);
}
let emails = data.summary.emails;
let total = emails.total;
add_row("Total", total);
let aligned = emails.dkim_spf_aligned + emails.dkim_aligned + emails.spf_aligned;
let n_aligned = total - aligned;
add_row(
"DKIM or SPF aligned",
SummaryReport.num2percent(aligned, total),
aligned && "report-result-pass" || null
);
add_row(
"Not aligned",
SummaryReport.num2percent(n_aligned, total),
n_aligned && "report-result-fail" || null
);
add_row("Organizations", data.summary.organizations);
}
if (data.sources && data.sources.length) {
let header = document.createElement("h3");
header.appendChild(document.createTextNode("Sources"));
html.appendChild(header);
let table = document.createElement("table");
table.setAttribute("class", "report-table");
html.appendChild(table);
let caption = document.createElement("caption");
caption.appendChild(document.createTextNode("Total records: " + data.sources.length));
table.appendChild(caption);
let thead = document.createElement("thead");
table.appendChild(thead);
[
[
[ "IP address", 0, 2 ], [ "Email volume", 0, 2 ], [ "SPF", 3, 0 ], [ "DKIM", 3, 0 ]
],
[
[ "pass" ], [ "fail" ], [ "rate" ], [ "pass" ], [ "fail" ], [ "rate" ]
]
].forEach(function(row) {
let tr = document.createElement("tr");
thead.appendChild(tr);
row.forEach(function(col) {
let th = document.createElement("th");
th.appendChild(document.createTextNode(col[0]));
if (col[1]) {
th.setAttribute("colspan", col[1]);
}
if (col[2]) {
th.setAttribute("rowspan", col[2]);
}
tr.appendChild(th);
});
});
let tbody = document.createElement("tbody");
table.appendChild(tbody);
data.sources.forEach(function(sou) {
let tr = document.createElement("tr");
tbody.appendChild(tr);
let va = [];
va.push([ Common.makeIpElement(sou.ip), 0 ]);
let ett = sou.emails;
let spf = sou.spf_aligned;
let dkm = sou.dkim_aligned;
va.push([ ett, 1 ]);
va.push([ spf, 3 ]);
va.push([ ett - spf, 5 ]);
va.push([ spf / ett, 8 ]);
va.push([ dkm, 3 ]);
va.push([ ett - dkm, 5 ]);
va.push([ dkm / ett, 8 ]);
va.forEach(function(it) {
let val = it[0];
let mode = it[1];
let td = document.createElement("td");
if (val && (mode & 2)) {
td.setAttribute("class", "report-result-pass");
}
if (val && (mode & 4)) {
td.setAttribute("class", "report-result-fail");
}
if (mode & 8) {
val = (val * 100).toFixed(0) + "%";
} else if (mode & 1) {
val = val.toLocaleString();
}
if (typeof(val) === "object") {
td.appendChild(val);
} else {
td.appendChild(document.createTextNode(val));
}
tr.appendChild(td);
});
});
}
if (data.organizations && data.organizations.length) {
let header = document.createElement("h3");
header.appendChild(document.createTextNode("Organizations"));
html.appendChild(header);
let table = document.createElement("table");
table.setAttribute("class", "report-table");
html.appendChild(table);
let caption = document.createElement("caption");
caption.appendChild(document.createTextNode("Total records: " + data.organizations.length));
table.appendChild(caption);
let thead = document.createElement("thead");
table.appendChild(thead);
let tr = document.createElement("tr");
thead.appendChild(tr);
[ "Name", "Emails", "Reports" ].forEach(function(org) {
let th = document.createElement("th");
th.appendChild(document.createTextNode(org));
tr.appendChild(th);
});
let tbody = document.createElement("tbody");
table.appendChild(tbody);
data.organizations.forEach(function(org) {
let tr = document.createElement("tr");
tbody.appendChild(tr);
let va = [];
va.push(org.name);
va.push(org.emails.toLocaleString());
va.push(org.reports.toLocaleString());
va.forEach(function(v) {
let td = document.createElement("td");
td.appendChild(document.createTextNode(v));
tr.appendChild(td);
});
});
}
return html;
}
static num2percent(per, cent) {
if (!per) {
return "0";
}
return "" + Math.round(per / cent * 100, per) + "% (" + per + ")";
}
}

View File

@@ -0,0 +1,961 @@
/**
* 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/>.
*/
class ITable {
constructor(params) {
this._table = null;
this._class = null;
this._header = null;
this._status = null;
this._frames = [];
this._columns = [];
this._body = null;
this._onsort = null;
this._onclick = null;
this._onfocus = null;
if (params) {
this._class = params.class || null;
this._onsort = params.onsort || null;
this._onclick = params.onclick || null;
this._onfocus = params.onfocus || null;
this._nodata_text = params.nodata_text || null;
}
this._focused = false;
this._focused_row = null;
this._selected_rows = [];
}
element() {
if (!this._table) {
let that = this;
this._table = document.createElement("div");
if (this._class)
this._table.setAttribute("class", this._class);
this._table.classList.add("table");
this._table.setAttribute("tabindex", -1);
this._table.addEventListener("focus", function(event) {
that._focused = true;
that._update_focus();
}, true);
this._table.addEventListener("blur", function(event) {
that._focused = false;
that._update_focus();
}, true);
let th = document.createElement("div");
th.setAttribute("class", "table-header");
this._table.appendChild(th);
this._header = document.createElement("div");
this._header.setAttribute("class", "table-row");
this._header.addEventListener("click", function(event) {
let col = that.get_column_by_element(event.target);
if (col && col.is_sortable()) {
if (that._onsort) {
that._onsort(col);
}
}
});
th.appendChild(this._header);
this._fill_columns();
this._body = document.createElement("div");
this._body.setAttribute("class", "table-body");
this._body.addEventListener("click", function(event) {
let row = that._get_row_by_element(event.target);
if (row) {
that._set_selected_rows([ row ]);
if (that._onclick)
that._onclick(row);
}
});
this._body.addEventListener("focus", function(event) {
let row = that._get_row_by_element(event.target);
if (row) {
that._update_focused_row(row);
if (that._onfocus)
that._onfocus(row.element());
}
}, true);
this._body.addEventListener("blur", function(event) {
let row = that._get_row_by_element(event.target);
if (row) {
row.onfocus(false);
}
}, true);
this._body.addEventListener("keydown", function(event) {
let row = null;
switch (event.code) {
case "ArrowDown":
row = that._get_row(that._focused_row !== null && (that._focused_row.id() + 1) || 0);
break;
case "ArrowUp":
if (that._focused_row) {
let id = that._focused_row.id();
if (id >= 0)
row = that._get_row(id - 1);
}
else {
row = that._get_row(0);
}
break;
case "PageUp":
if (that._focused_row && that._frames.length > 0) {
let c_id = that._focused_row.id();
let f_fr = that._frames[0];
let f_id = f_fr.first_index();
if (c_id == f_id)
break;
let s_el = that._get_scroll_element();
if (s_el) {
let r_ht = that._focused_row.element().getBoundingClientRect().height;
let s_ht = s_el.getBoundingClientRect().height;
let n_id = Math.max(c_id - Math.floor(s_ht / r_ht) - 1, f_id);
row = that._get_row(n_id);
}
else {
row = f_fr.row(f_id);
}
}
break;
case "PageDown":
if (that._focused_row && that._frames.length > 0) {
let c_id = that._focused_row.id();
let l_fr = that._frames[that._frames.length - 1];
let l_id = l_fr.last_index();
if (c_id == l_id)
break;
let s_el = that._get_scroll_element();
if (s_el) {
let r_ht = that._focused_row.element().getBoundingClientRect().height;
let s_ht = s_el.getBoundingClientRect().height;
let n_id = Math.min(c_id + Math.floor(s_ht / r_ht) - 1, l_id);
row = that._get_row(n_id);
}
else {
row = l_fr.row(l_id);
}
}
break;
case "Home":
if (that._frames.length > 0) {
let first_frame = that._frames[0];
row = first_frame.row(first_frame.first_index());
}
break;
case "End":
if (that._frames.length > 0) {
let last_frame = that._frames[that._frames.length - 1];
row = last_frame.row(last_frame.last_index());
}
break;
case "Enter":
case "NumpadEnter":
if (that._onclick && that._focused_row)
that._onclick(that._focused_row);
event.preventDefault();
return;
}
if (row) {
row.element().focus();
that._set_selected_rows([ row ]);
event.preventDefault();
}
});
this._fill_frames();
this._table.appendChild(this._body);
}
return this._table;
}
more() {
return this._frames.length > 0 && this._frames[this._frames.length - 1].more();
}
frames_count() {
return this._frames.length;
}
add_column(data) {
let col = new ITableColumn(data.content, {
name: data.name,
class: data.class,
sortable: data.sortable,
sorted: data.sorted
});
this._columns.push(col);
if (this._header)
this._header.appendChild(col.element());
return col;
}
get_column_by_element(el) {
el = el && el.closest("div.table-cell");
if (el) {
for (let i = 0; i < this._columns.length; ++i) {
let col = this._columns[i];
if (el === col.element())
return col;
}
}
}
display_status(status, text) {
if (this._status && !status) {
this._status.remove();
this._status = null;
return;
}
this.element();
this._status = document.createElement("div");
this._status.setAttribute("class", "table-row colspanned");
let el = document.createElement("div");
el.setAttribute("class", "table-cell");
this._status.appendChild(el);
let el2 = document.createElement("div");
el2.setAttribute("class", "table-cell");
el2.appendChild(document.createTextNode("\u00A0")); // Non breaking space
this._status.appendChild(el2);
if (status === "wait") {
set_wait_status(el);
}
else {
remove_all_children(this._body);
if (status === "nodata") {
el.classList.add("nodata");
el.appendChild(document.createTextNode(text || "No data"));
}
else {
set_error_status(el, text);
}
}
this._body.appendChild(this._status);
}
last_row_index() {
let idx = -1;
if (this._frames.length > 0) {
idx = this._frames[this._frames.length - 1].last_index();
}
return idx;
}
add_frame(frame) {
if (frame.count() === 0) {
if (this._frames.length === 0)
this.display_status("nodata", this._nodata_text);
return
}
if (this._frames.length > 0 && this._frames[0].first_index() > frame.last_index()) {
this._frames.unshift(frame);
if (this._body)
this._body.insertBefore(frame.element(), this._body.firstChild);
}
else {
this._frames.push(frame);
if (this._body)
this._body.appendChild(frame.element());
}
}
clear() {
this._frames = [];
if (this._body)
remove_all_children(this._body);
this._focused_row = null;
this._selected_rows = [];
}
focus() {
if (!this._focused_row) {
if (this._frames.length > 0) {
let fr = this._frames[0];
this._focused_row = fr.row(fr.first_index());
}
}
if (this._focused_row)
this._focused_row.element().focus();
}
sort(col_name, direction) {
if (this._frames.length == 1) {
for (let i = 0; i < this._columns.length; ++i) {
let col = this._columns[i];
if (col.is_sortable() && col.name() === col_name) {
let fr = this._frames[0];
fr.sort(i, direction);
if (this._body) {
remove_all_children(this._body);
this._body.appendChild(fr.element());
}
return;
}
}
}
}
set_sorted(col_name, direction) {
this._columns.forEach(function(col) {
if (col.is_sortable()) {
if (col.name() !== col_name) {
col.sort(null);
}
else {
if (direction === "toggle") {
direction = null;
if (col.sorted() === "ascent") {
direction = "descent";
}
else if (col.sorted() === "descent") {
direction = "ascent";
}
}
col.sort(direction);
}
}
});
}
_fill_columns() {
this._columns.forEach(function(col) {
this._header.appendChild(col.element());
}, this);
}
_fill_frames() {
this._frames.forEach(function(fr) {
this._body.appendChild(fr.element());
}, this);
}
_get_row(row_id) {
for (let i = 0; i < this._frames.length; ++i) {
let fr = this._frames[i];
if (fr.last_index() >= row_id) {
if (fr.first_index() <= row_id)
return fr.row(row_id);
}
}
return null;
}
_get_row_by_element(el) {
let row = null;
if (el) {
el = el.closest("div.table-row");
if (el) {
let id = parseInt(el.getAttribute("data-id"));
if (id !== NaN)
row = this._get_row(id);
}
}
return row;
}
_update_focus() {
if (this._focused)
this._table.classList.add("focused");
else
this._table.classList.remove("focused");
}
_update_focused_row(row) {
if (this._focused_row && row !== this._focused_row) {
this._focused_row.tabindex(-1);
}
this._focused_row = row;
this._focused_row.tabindex(0);
this._focused_row.onfocus(true);
}
_set_selected_rows(rows) {
this._selected_rows.forEach(function(row) {
row.select(false);
});
rows.forEach(function(row) {
row.select(true);
});
this._selected_rows = rows;
}
_get_scroll_element() {
let t_rect = this._table.getBoundingClientRect();
let p_elem = this._table.parentElement;
while (p_elem) {
let p_rect = p_elem.getBoundingClientRect();
if (t_rect.top < p_rect.top || t_rect.bottom > p_rect.bottom) {
return p_elem;
}
p_elem = p_elem.paretnElement;
}
}
}
class ITableFrame {
constructor(data, pos) {
this._pos = pos;
this._more = data.more && true || false;
let id = pos;
this._rows = data.rows.map(function(rd) {
if (!(rd instanceof ITableRow)) {
rd = new ITableRow(rd);
}
rd.id(id++);
return rd;
});
}
count() {
return this._rows.length;
}
first_index() {
return this._pos;
}
last_index() {
let cnt = this._rows.length;
if (cnt > 0) {
return this._pos + cnt - 1;
}
return null;
}
row(id) {
let idx = id - this._pos;
if (idx >= 0 && idx < this._rows.length) {
return this._rows[idx];
}
return null;
}
more() {
return this._more;
}
element() {
let fr = document.createDocumentFragment();
this._rows.forEach(function(row) {
fr.appendChild(row.element());
});
return fr;
}
sort(col_idx, direction) {
let dir = (direction === "ascent" && 1) || (direction === "descent" && 2) || 0;
if (dir) {
let that = this;
this._rows.sort(function(a, b) {
let c1 = a.cell(col_idx);
let c2 = b.cell(col_idx);
if (dir === 1) {
return that._compare_cells(c2, c1);
}
return that._compare_cells(c1, c2);
});
let id = this._pos;
this._rows.forEach(function(row) {
row.id(id++);
});
}
}
_compare_cells(c1, c2) {
return c1.value("sort") < c2.value("sort");
}
}
class ITableRow {
constructor(data) {
this._id = -1;
this._focused = false;
this._tabindex = -1;
this._selected = false;
this._element = null;
this._class = data.class || null;
this._userdata = data.userdata || null;
this._cells = data.cells.map(function(col) {
if (col instanceof ITableCell) {
return col;
}
let props = null;
if (col.title || col.class || col.label) {
props = {
title: col.title || null,
class: col.class || null,
label: col.label || null
};
}
return new ITableCell(col.content, props);
});
}
userdata() {
return this._userdata;
}
element() {
if (!this._element) {
this._element = document.createElement("div");
this._element.setAttribute("data-id", this._id);
if (this._class)
this._element.setAttribute("class", this._class);
this._element.classList.add("table-row");
this._cells.forEach(function(col) {
this._element.appendChild(col.element());
}, this);
this._update_focus();
this._update_tabindex();
this._update_select();
}
return this._element;
}
onfocus(flag) {
this._focused = flag;
if (this._element)
this._update_focus();
}
tabindex(index) {
if (this._tabindex !== index) {
this._tabindex = index;
this._update_tabindex();
}
}
select(flag) {
this._selected = flag;
if (this._element)
this._update_select();
}
id(new_id) {
if (new_id !== undefined && new_id !== this._id) {
this._id = new_id;
if (this._element) {
this._element.setAttribute("data-id", this._id);
}
}
return this._id;
}
cell(index) {
return this._cells[index] || null;
}
_update_focus() {
if (this._focused)
this._element.classList.add("focused");
else
this._element.classList.remove("focused");
}
_update_tabindex() {
this._element.setAttribute("tabindex", this._tabindex);
}
_update_select() {
if (this._selected) {
this._element.classList.add("selected");
}
else {
this._element.classList.remove("selected");
}
}
}
class ITableCell {
constructor(content, props) {
this._element = null;
this._content = content;
if (props) {
this._title = props.title || null;
this._class = props.class || null;
this._label = props.label || null;
}
}
element() {
if (!this._element) {
this._element = document.createElement("div");
if (this._title) {
this._element.setAttribute("title", this._title);
}
if (this._class) {
this._element.setAttribute("class", this._class);
}
if (this._label) {
this._element.setAttribute("data-label", this._label);
}
this._element.classList.add("table-cell");
let content = this.value("dom");
if (content !== null) {
if (typeof(content) === "object") {
this._element.appendChild(content)
}
else {
this._element.appendChild(document.createTextNode(content));
}
}
}
return this._element;
}
value(target) {
if (target === "dom" || typeof(this._content) !== "object") {
return this._content;
}
return null;
}
}
class ITableColumn extends ITableCell {
constructor(content, props) {
super(content, props);
this._name = props.name;
this._sortable = !!props.sortable;
this._sorted = props.sorted || null;
}
element() {
if (this._element !== super.element()) {
this._update_sorted();
}
return this._element;
}
is_sortable() {
return this._sortable;
}
sort(dir) {
if (this._sorted !== dir) {
this._sorted = dir || null;
if (this._element) {
this._update_sorted();
}
}
}
sorted() {
return this._sorted;
}
name() {
return this._name;
}
_update_sorted() {
if (this._sortable) {
this._element.classList.add("sortable");
let c_act = {
asc: "remove",
des: "remove"
};
if (this._sorted) {
this._element.classList.add("arrows");
if (this._sorted === "ascent") {
c_act["asc"] = "add";
}
else if (this._sorted === "descent") {
c_act["des"] = "add";
}
}
else {
this._element.classList.remove("arrows");
}
for (let key in c_act) {
this._element.classList[c_act[key]]("sorted-" + key);
}
}
}
}
class ModalDialog {
constructor(params) {
this._params = params;
this._element = null;
this._title = null;
this._buttons = [];
this._content = null;
this._first = null;
this._last = null;
this._result = null;
this._callback = null;
}
element() {
if (!this._element) {
let ovl = document.createElement("div");
ovl.setAttribute("class", "dialog-overlay hidden");
let dlg = document.createElement("div");
dlg.setAttribute("class", "dialog");
let con = document.createElement("div");
con.setAttribute("class", "container");
this._title = document.createElement("div");
this._title.setAttribute("class", "title");
{
let tt = document.createElement("div");
tt.setAttribute("class", "title-text");
tt.appendChild(document.createTextNode(this._params.title || ""));
this._title.appendChild(tt);
}
let that = this;
{
let cbt = document.createElement("button");
cbt.setAttribute("type", "button");
cbt.setAttribute("class", "close-btn");
cbt.appendChild(document.createTextNode("x"));
this._title.appendChild(cbt);
this._buttons = [ cbt ];
cbt.addEventListener("click", function(event) {
that.hide();
});
}
con.appendChild(this._title);
let frm = document.createElement("form");
this._content = document.createElement("div");
frm.appendChild(this._content);
let bdv = document.createElement("div");
bdv.setAttribute("class", "dialog-buttons");
this._add_buttons(bdv);
frm.appendChild(bdv);
con.appendChild(frm);
dlg.appendChild(con);
ovl.appendChild(dlg);
this._element = ovl;
this._gen_content();
this._update_first_last();
this._element.addEventListener("click", function(event) {
if (event.target === this && that._params.overlay_click !== "ignore") {
that.hide();
}
});
frm.addEventListener("keydown", function(event) {
if (event.key == "Tab") {
if (!event.shiftKey) {
if (event.target == that._last) {
that._first.focus();
event.preventDefault();
}
}
else {
if (event.target == that._first) {
that._last.focus();
event.preventDefault();
}
}
}
});
frm.addEventListener("submit", function(event) {
event.preventDefault();
that._submit();
});
frm.addEventListener("reset", function(event) {
this._reset();
}.bind(this));
}
return this._element;
}
show() {
this.element();
this._result = null;
this._title.querySelector("button.close-btn").classList.add("active");
this._element.classList.remove("hidden");
if (this._first) {
this._first.focus();
}
let that = this;
return new Promise(function(resolve, reject) {
that._callback = resolve;
});
}
hide() {
if (this._element) {
this._title.querySelector("button.close-btn").classList.remove("active");
this._element.classList.add("hidden");
}
this._callback && this._callback(this._result);
}
_add_buttons(container) {
let bl = this._params.buttons || [];
bl.forEach(function(bt) {
let name = null;
let type = null;
if (bt == "ok") {
name = "Ok";
type = "submit";
}
else if (bt == "apply") {
name = "Apply";
type = "submit";
}
else if (bt == "reset") {
name = "Reset";
type = "reset";
}
else if (bt == "login") {
name = "Log in";
type = "submit";
}
else if (bt == "cancel") {
name = "Cancel";
type = "close";
}
else if (bt == "close") {
name = "Close";
type = "close";
}
else {
name = bt;
type = bt;
}
this._add_button(container, name, type);
}, this);
}
_add_button(container, text, type) {
let btn = document.createElement("button");
if (type == "close") {
btn.setAttribute("type", "button");
btn.addEventListener("click", this.hide.bind(this));
}
else {
btn.setAttribute("type", type);
}
btn.appendChild(document.createTextNode(text));
container.appendChild(btn);
this._buttons.push(btn);
}
_gen_content() {
}
_update_first_last() {
this._first = null;
this._last = null;
let list = this._element.querySelector("form").elements;
for (let i = 0; i < list.length; ++i) {
let el = list[i];
if (!el.elements && !el.disabled) {
if (!this._first)
this._first = el;
this._last = el;
}
}
}
_submit() {
}
_reset() {
}
}
class AboutDialog extends ModalDialog {
constructor(params) {
super({
title: "About",
buttons: [ "ok" ]
});
this._authors = params.authors;
this._documentation = params.documentation;
this._source_code = params.source_code;
}
element() {
if (!this._element) {
super.element();
this._element.children[0].classList.add("about");
this._content.classList.add("vertical-content");
this._content.parentElement.classList.add("vertical-content");
}
return this._element;
}
_gen_content() {
let header = document.createElement("h2");
header.appendChild(document.createTextNode(Router.app_name(true)));
this._content.appendChild(header);
let cblock = document.createElement("div");
this._authors.forEach(function(author) {
let ablock = document.createElement("div");
ablock.appendChild(document.createTextNode("Copyright © " + author.years + ", "));
cblock.appendChild(ablock);
let alink = document.createElement("a");
alink.setAttribute("href", author.url);
alink.setAttribute("title", "The author's page");
alink.setAttribute("target", "_blank");
alink.appendChild(document.createTextNode(author.name));
ablock.appendChild(alink);
});
this._content.appendChild(cblock);
let oblock = document.createElement("div");
oblock.setAttribute("class", "left-titled");
let add_row = function(title, value) {
let t_el = document.createElement("span");
t_el.appendChild(document.createTextNode(title + ": "));
oblock.appendChild(t_el);
let v_el = document.createElement("div");
value.forEach(function(v) {
if (v_el.children.length > 0) {
v_el.appendChild(document.createTextNode(", "));
}
let a_el = document.createElement("a");
a_el.setAttribute("href", v.url);
a_el.setAttribute("title", v.title || v.ancor);
a_el.setAttribute("target", "_blank");
a_el.appendChild(document.createTextNode(v.ancor));
v_el.appendChild(a_el);
});
oblock.appendChild(v_el);
};
this._content.appendChild(oblock);
add_row("Documentation", this._documentation);
add_row("Source code", this._source_code);
{
let tl = document.createElement("span");
tl.appendChild(document.createTextNode("PHP version: "));
oblock.appendChild(tl);
let vl = document.createElement("span");
vl.appendChild(document.createTextNode(Router.php_version || "n/a"));
oblock.appendChild(vl);
}
let lblock = document.createElement("div");
lblock.appendChild(document.createTextNode(
"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._content.appendChild(lblock);
}
_submit() {
this.hide();
}
}