/** * 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 . */ 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; } }