smeserver-dmarc-srg/root/opt/dmarc-srg/js/summary.js

538 lines
16 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 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 + ")";
}
}