%PDF- %PDF-
Direktori : /www/varak.net/dmarc.varak.net/public/js/ |
Current File : //www/varak.net/dmarc.varak.net/public/js/report.js |
/** * dmarc-srg - A php parser, viewer and summary report generator for incoming DMARC reports. * Copyright (C) 2020-2025 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)) { document.getElementById("main-block").replaceChildren(); document.getElementById("detail-block").replaceChildren(); } } show_report(domain, report_time, org, report_id, filter) { this.element(); let that = this; return new Promise(function(resolve, reject) { if (!domain || !report_time || !org || !report_id) { let sp = (new URL(document.location)).searchParams; domain = sp.get("domain"); report_time = sp.get("time"); org = sp.get("org"); report_id = sp.get("report_id"); if (!domain || !report_time || !org || !report_id) { let err_msg = "Domain, report time, reporting organization, report ID must be specified"; set_error_status(that._cn_element, err_msg); reject(new Error(err_msg)); } } that._id_element.childNodes[0].nodeValue = report_id; set_wait_status(that._cn_element); that._rep_id = report_id + report_time; that._element.classList.remove("report-hidden"); that._close_btn.classList.add("active"); let rep = new Report(domain, report_time, org, report_id, filter); rep.fetch().then(function() { if (that._rep_id === report_id + report_time) { that._cn_element.replaceChildren(rep.element()); rep.set_value("seen", true); } 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_time, org, report_id, filter) { this._data = null; this._error = false; this._filter_btn = null; this._records_el = null; this._error_message = null; this._org = org; this._domain = domain; this._report_id = report_id; this._report_time = report_time; if (Common.rv_filter === "from-list") this._filter = filter; else this._filter = this._filter_storage(); this._filter ||= {}; } id() { return this._report_id; } error() { return this._error; } error_message() { return this._error_message; } fetch() { let url = new URL("report.php", document.location); let u_params = url.searchParams; u_params.set("org", this._org); u_params.set("time", this._report_time); u_params.set("domain", this._domain); u_params.set("report_id", this._report_id); let that = this; return window.fetch(url, { 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() { let el = this._create_element(); this._apply_filter(); this._update_filter_button(); return el; } 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 = new URL("report.php", document.location); let url_params = url.searchParams; url_params.set("action", "set"); url_params.set("org", this._org); url_params.set("time", this._report_time); url_params.set("domain", this._domain); url_params.set("report_id", this._report_id); return window.fetch(url, { 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, "fqdn")); 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 || "n/a", "fqdn")); 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"); rs.appendChild(this._get_filter_button()); let hd = document.createElement("h5"); hd.id = "records-title"; hd.appendChild(document.createTextNode("Records")); hd.appendChild(document.createElement("span")); rs.appendChild(hd); this._data.records.forEach(rec => { const tl = rs.appendChild(document.createElement("div")); tl.classList.add("report-record", "round-border"); const hd = tl.appendChild(document.createElement("div")); hd.classList.add("header"); hd.append( this._create_data_fragment("IP-address", rec.ip), (new HintButton({ data: rec.ip, content: this._make_ip_info_element.bind(this) })).element() ); tl.append( this._create_data_item("Message count", rec.count), this._create_data_item("Policy evaluated", this._create_ev_policy_fragment(rec)) ); if (rec.reason) tl.append(this._create_data_item("Evaluated reason", this._create_reason_fragment(rec.reason))); tl.append( this._create_data_item("Identifiers", this._create_identifiers_fragment(rec)), this._create_data_item("DKIM auth", this._create_dkim_auth_fragment(rec.dkim_auth)), this._create_data_item("SPF auth", this._create_spf_auth_fragment(rec.spf_auth)) ); }); let nd = document.createElement("div"); nd.classList.add("nodata", "hidden"); nd.textContent = "There are no records to display. Try changing the filter options."; rs.appendChild(nd); el.appendChild(rs); this._records_el = 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, cname) { let el = document.createElement("div"); el.setAttribute("class", "report-item"); el.appendChild(this._create_data_fragment(title, data, cname)); return el; } _create_data_fragment(title, data, cname) { 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"); if (cname) dt.classList.add(cname); fr.appendChild(dt); return fr; } _create_values_fragment(values, keys, clist) { if (!Array.isArray(values)) values = [ values ]; const params = keys.map(key => { let r = false; if (key.at(0) === "!") { r = true; key = key.substring(1); } const a = key.split(":"); return { name: a[0], title: a[1] || a[0], result: r }; }); const fr = document.createDocumentFragment(); const ca = Array.isArray(clist); const lcnt = values.length; values.forEach(val => { const elist = params.reduce((res, param, idx) => { let v = val[param.name]; if (v) { const r = param.result && v || null; const el = Common.createReportResultElement(param.title, r, v); const cl = ca ? clist[idx] : clist; if (cl) el.classList.add(cl); res.push(el); } return res; }, []); (lcnt < 2 ? fr : fr.appendChild(document.createElement("div"))).append(...elist); }); return fr; } _create_pub_policy_fragment(data) { if (!data) return "n/a"; return this._create_values_fragment(data, [ "adkim", "aspf", "p", "sp", "np", "pct", "fo" ]); } _create_ev_policy_fragment(data) { return this._create_values_fragment(data, [ "!dkim_align:DKIM", "!spf_align:SPF", "disposition" ]); } _create_reason_fragment(data) { return this._create_values_fragment(data, [ "type", "comment" ]); } _create_identifiers_fragment(data) { return this._create_values_fragment(data, [ "header_from", "envelope_from", "envelope_to" ], "fqdn"); } _create_dkim_auth_fragment(data) { if (!data) return "n/a"; return this._create_values_fragment(data, [ "domain", "selector", "!result" ], [ "fqdn" ]); } _create_spf_auth_fragment(data) { if (!data) return "n/a"; return this._create_values_fragment(data, [ "domain", "!result" ], [ "fqdn" ]); } _get_filter_button() { let btn = document.createElement("button"); btn.classList.add("toolbar-btn"); btn.textContent = "filter: "; btn.appendChild(document.createElement("span")); btn.addEventListener("click", function(event) { btn.disabled = true; let dlg = new ReportViewFilterDialog({ filter: this._filter }); document.getElementById("main-block").prepend(dlg.element()); dlg.show().then(function(res) { if (res && (res.dkim !== this._filter.dkim || res.spf !== this._filter.spf || res.disposition !== this._filter.disposition) ) { this._filter = res; this._apply_filter(); this._update_filter_button(); this._filter_storage(res); } }.bind(this)).finally(function() { dlg.element().remove(); btn.disabled = false; btn.focus(); }); }.bind(this)); this._filter_btn = btn; return btn; } _apply_filter() { if (!this._records_el) return; let rtitle = this._records_el.querySelector("#records-title"); if (!rtitle) return; let total = this._data.records.length; let displ = 0; let filter = this._filter; let e_list = this._records_el.querySelectorAll(".report-record"); for (let i = 0; i < total; ++i) { let rec = this._data.records[i]; if ((!filter.dkim || filter.dkim === rec.dkim_align) && (!filter.spf || filter.spf === rec.spf_align) && (!filter.disposition || filter.disposition === rec.disposition) ) { e_list[i].classList.remove("hidden"); ++displ; } else { e_list[i].classList.add("hidden"); } } let tstr = null; if (total === displ) tstr = total; else tstr = displ + "/" + total; rtitle.childNodes[1].textContent = " (" + tstr + ")"; let nd = this._records_el.querySelector(".nodata"); if (displ > 0) nd.classList.add("hidden"); else nd.classList.remove("hidden"); } _update_filter_button() { let ea = [ [ "dkim", this._filter.dkim ], [ "spf", this._filter.spf ] ].reduce(function(res, it) { if (it[1]) { let el = document.createElement("span"); el.classList.add("report-result-" + it[1]); el.textContent = it[0]; res.push(el); } return res; }, []); if (this._filter.disposition) { let el = document.createElement("span"); el.textContent = "disp=" + this._filter.disposition.substring(0, 1); ea.push(el); } let bt = this._filter_btn.childNodes[1]; bt.replaceChildren(); if (ea.length > 0) { for (let i = 0; i < ea.length; ++i) { if (i) bt.append(", "); bt.append(ea[i]); } } else { bt.textContent = "none"; } } _filter_storage(data) { let storage = null; switch (Common.rv_filter) { case "last-value": storage = "localStorage"; break; case "last-value-tab": storage = "sessionStorage"; break; default: return; } let res = {}; if (window[storage]) { let prefix = "ReportView.filter."; [ "dkim", "spf", "disposition" ].forEach(function(name) { if (data) window[storage].setItem(prefix + name, data[name] || ""); else res[name] = window[storage].getItem(prefix + name); }); } return res; } _make_ip_info_element(ip) { const el = document.createElement("div"); el.appendChild(document.createElement("h4")).textContent = "Host information"; this._make_ip_info_items(el, { id: "main" }, [ { title: "IP address", value: Common.makeIpElement(ip) } ]); this._update_ip_info(ip, el); return el; } _make_ip_info_items(el, group, items) { let ul = Array.from(el.querySelectorAll("ul[data-group]")).find(e => { return e.dataset.group === group.id; }); if (!ul) { if (group.id !== "main") { el.appendChild(document.createElement("h5")).textContent = group.title; } ul = el.appendChild(document.createElement("ul")); ul.dataset.group = group.id; } items.forEach(item => { const li = ul.appendChild(document.createElement("li")); li.appendChild(document.createElement("span")).textContent = item.title + ": "; const val = li.appendChild(document.createElement("span")); val.append(item.value); if (item.tvalue) val.title = item.tvalue; if (item.cvalue) val.classList.add(item.cvalue); }); } _update_ip_info(ip, el) { const cache_keys = [ "main.rdns", "main.rip" ]; const dict = { "main.ip": "IP address", "main.rdns": "rDNS name", "main.rip": "Reverse IP", "stats": "Statistics", "stats.reports": "Total reports", "stats.messages": "Total messages", "stats.last_report": "Last report" }; const that = this; function convertItem(name, item) { switch (name) { case "main.rip": if (typeof(item.value) == "boolean") { if (item.value) { item.value = "match"; item.cvalue = "report-result-pass"; } else { item.value = "not match"; item.cvalue = "report-result-fail"; } } break; case "stats.last_report": if (Array.isArray(item.value)) { try { if (item.value[0]) { const rt = new Date(that._data.date.begin); let lt = new Date(item.value[0]); if (lt.getTime() === rt.getTime() && item.value[1]) { lt = new Date(item.value[1]); } item.value = lt.toUIDateString(true); } } catch (err) { } } break; } switch (typeof(item.value)) { case "number": item.value = item.value.toLocaleString(); break; default: try { item.value = "" + item.value; } catch (err) { item.value = ""; } //break is not needed case "string": if (item.value.length >= 15) item.tvalue = item.value; break; } } function setData(result, el) { const m = new Map(); const groups = []; for (const d of result.data) { if (!Array.isArray(d) || typeof(d[0]) != "string") continue; const nm = d[0].split("."); if (nm.length != 2) continue; let gr_data = m.get(nm[0]); if (!gr_data) { gr_data = []; m.set(nm[0], gr_data); groups.push(nm[0]); } const item = { title: dict[d[0]] || nm[1], value: d[1] }; convertItem(d[0], item); gr_data.push(item); } for (let gr of groups) { gr = { id: gr, title: dict[gr] || gr }; that._make_ip_info_items(el, gr, m.get(gr.id)); } } const we = el.appendChild(document.createElement("div")); we.ariaLabel = "Wait please"; we.classList.add("spinner"); we.innerHTML = "<div></div><div></div><div></div><div></div><div></div>"; const excl_fields = {}; const cache_data = []; try { const s = window.sessionStorage && window.sessionStorage.getItem("ReportView.Cache.ip-" + ip); if (s) { const jdata = JSON.parse(s); const expire = jdata.expire || 0; if (Date.now() < expire) { const edata = []; cache_keys.forEach(n => { excl_fields[n] = true; cache_data.push([ n, jdata[n] ]); }); } } } catch (err) { } const url = new URL("hosts.php", document.location); url.searchParams.set("host", ip); url.searchParams.set( "fields", Object.keys(dict).filter(it => (it.includes(".") && it !== "main.ip" && !excl_fields[it])).join(",") ); return window.fetch(url, { method: "GET", cache: "no-store", headers: HTTP_HEADERS, credentials: "same-origin" }).then(resp => { if (!resp.ok) throw new Error("Failed to fetch host information"); return resp.json(); }).then(result => { Common.checkResult(result); // Extracting data for caching let cc = 0; const cache = {}; for (const it of result.data) { if (cache_keys.includes(it[0])) { cache[it[0]] = it[1]; ++cc; } } // Merging the received data with the local cache for (const it of cache_data) { if (cache[it[0]] === undefined) result.data.push(it); } // Updating the local cache if (cc) { const expire = new Date(); expire.setHours(expire.getHours() + 24); cache.expire = expire.valueOf(); window.sessionStorage && window.sessionStorage.setItem("ReportView.Cache.ip-" + ip, JSON.stringify(cache)); } // Merge dictionaries if (result.dictionary) Object.assign(dict, result.dictionary); // Set data setData(result, el); }).catch(err => { Common.displayError(err); Notification.add({ type: "error", text: err.message }); }).finally(() => { we.remove(); }); } } class ReportViewFilterDialog extends ReportFilterDialog { constructor(params) { params.title = "Records filtering"; params.item_list = [ "dkim", "spf", "disposition" ]; let pfa = [ "pass", "fail" ]; params.loaded_filters = { dkim: pfa, spf: pfa, disposition: [ "none", "reject", "quarantine" ] }; super(params); } }