487 lines
13 KiB
JavaScript
487 lines
13 KiB
JavaScript
|
/**
|
||
|
* 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);
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|