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