generated from smedev/Template-for-SMEServer-Contribs-Package
Add in software files and templates
This commit is contained in:
678
root/opt/dmarc-srg/js/admin.js
Normal file
678
root/opt/dmarc-srg/js/admin.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
163
root/opt/dmarc-srg/js/common.js
Normal file
163
root/opt/dmarc-srg/js/common.js
Normal 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");
|
||||
|
486
root/opt/dmarc-srg/js/domains.js
Normal file
486
root/opt/dmarc-srg/js/domains.js
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
394
root/opt/dmarc-srg/js/files.js
Normal file
394
root/opt/dmarc-srg/js/files.js
Normal 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;
|
||||
}
|
||||
}
|
||||
|
527
root/opt/dmarc-srg/js/list.js
Normal file
527
root/opt/dmarc-srg/js/list.js
Normal 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
141
root/opt/dmarc-srg/js/login.js
Normal file
141
root/opt/dmarc-srg/js/login.js
Normal 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;
|
||||
});
|
||||
};
|
||||
|
304
root/opt/dmarc-srg/js/logs.js
Normal file
304
root/opt/dmarc-srg/js/logs.js
Normal 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";
|
||||
}
|
||||
}
|
||||
|
335
root/opt/dmarc-srg/js/main.js
Normal file
335
root/opt/dmarc-srg/js/main.js
Normal 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;
|
||||
|
86
root/opt/dmarc-srg/js/notification.js
Normal file
86
root/opt/dmarc-srg/js/notification.js
Normal 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
|
||||
};
|
||||
|
427
root/opt/dmarc-srg/js/report.js
Normal file
427
root/opt/dmarc-srg/js/report.js
Normal 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;
|
||||
}
|
||||
}
|
||||
|
355
root/opt/dmarc-srg/js/settings.js
Normal file
355
root/opt/dmarc-srg/js/settings.js
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
206
root/opt/dmarc-srg/js/status.js
Normal file
206
root/opt/dmarc-srg/js/status.js
Normal 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"
|
||||
}
|
||||
};
|
||||
|
537
root/opt/dmarc-srg/js/summary.js
Normal file
537
root/opt/dmarc-srg/js/summary.js
Normal 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 + ")";
|
||||
}
|
||||
}
|
||||
|
961
root/opt/dmarc-srg/js/widgets.js
Normal file
961
root/opt/dmarc-srg/js/widgets.js
Normal 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();
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user