%PDF- %PDF-
| Direktori : /www/varak.net/dmarc.varak.net/public/js/ |
| Current File : /www/varak.net/dmarc.varak.net/public/js/list.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 ReportList {
constructor() {
this._page = null;
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._filter_btn = null;
this._rep_counter = null;
this._cnt_updated = 0;
this._settings_dlg = null;
this._column_list = [
{ name: "domain", content: "Domain", class: "fqdn" },
{ name: "begin_time", content: "Date", sortable: true },
{ name: "organization", content: "Reporting Organization", class: "orgname" },
{ name: "report_id", content: "Report ID", class: "report-id" },
{ name: "messages", content: "Messages" },
{ name: "result", content: "Result" },
{ name: "disposition", content: "Disposition" },
{ name: "quarantined", content: "Quarantined" },
{ name: "rejected", content: "Rejected" }
];
}
display() {
this._make_page_container();
this._make_scroll_container();
this._make_table();
const t_el = this._table.element();
const b_el = this._make_toolbar().element();
b_el.setAriaControls(t_el);
this._scroll.append(t_el);
this._page.append(b_el, this._scroll);
this._element.append(this._page);
this._ensure_report_widget();
this._element2.appendChild(ReportWidget.instance().element());
ReportWidget.instance().hide();
this._table.focus();
}
update() {
this._filter = Common.getFilterFromURL(new URL(document.location));
this._update_table();
this._update_settings_button();
}
title() {
return "Report List";
}
onpopstate() {
if (!this._page) {
this.display();
this.update();
}
else {
if (!this._element.contains(this._page)) this._element.replaceChildren(this._page);
const f = Common.getFilterFromURL(new URL(document.location), this._filter);
if (f !== undefined) {
this._filter = f;
Status.instance().reset();
Status.instance().update({ page: "list" });
this._update_table();
this._update_settings_button();
}
}
this._ensure_report_widget();
if (this._table) {
this._table.focus();
}
}
_ensure_report_widget() {
let wdg = ReportWidget.instance();
wdg.hide();
let el = wdg.element();
if (!this._element2.contains(el)) {
this._element2.appendChild(el);
}
}
_update_settings_button() {
if (this._filter_btn) {
this._filter_btn.element().classList[ this._filter ? "add" : "remove" ]("active");
}
}
_make_page_container() {
const el = document.createElement("div");
el.classList.add("page-container");
this._page = el;
}
_make_scroll_container() {
const el = document.createElement("div");
el.tabIndex = -1;
el.classList.add("table-wrapper");
el.addEventListener("scroll", event => {
if (!this._fetching && el.scrollTop + el.clientHeight >= el.scrollHeight * 0.95) {
if (this._table.frames_count() === 0 || this._table.more()) {
this._fetch_list();
}
}
});
this._scroll = el;
}
_make_toolbar() {
const tb = new Toolbar("Report list toolbar");
this._filter_btn = new ToolbarButton({
title: "Filter",
content: "filter_icon",
onclick: () => this._display_settings_dialog()
});
const cb = new ToolbarButton({
title: "Columns",
content: "columns_icon",
onclick: () => this._display_columns_dialog()
});
this._rep_counter = new ReportCounter();
tb.appendItem(this._filter_btn).appendItem(cb).appendSpacer().appendItem(this._rep_counter.element());
return tb;
}
_make_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)
});
this._column_list.forEach(col => {
let c = this._table.add_column(col);
if (c.name() === this._sort.column) {
c.sort(this._sort.direction);
}
});
this._get_visible_columns(true);
}
_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.time && data.org && data.report_id) {
let url = new URL("report.php", document.location);
url.searchParams.set("org", data.org);
url.searchParams.set("time", data.time);
url.searchParams.set("domain", data.domain);
url.searchParams.set("report_id", data.report_id);
window.history.pushState({ from: "list" }, "", url);
let filter = this._filter && {
dkim: this._filter.dkim || "",
spf: this._filter.spf || "",
disposition: this._filter.disposition || ""
} || null;
ReportWidget.instance().show_report(data.domain, data.time, data.org, data.report_id, filter).then(() => {
if (!this._table.seen(id)) {
this._table.seen(id, true);
this._rep_counter.decrease();
}
}).catch(err => {
Common.displayError(err);
if (err.error_code && err.error_code === -2) {
LoginDialog.start();
}
});
Router.update_title(ReportWidget.instance().title());
ReportWidget.instance().focus();
}
}
_fetch_list() {
this._table.display_status("wait");
this._fetching = true;
const qlist = [ "reports" ];
const pos = this._table.last_row_index() + 1;
const now = Date.now();
if (!pos || now - this._cnt_updated >= 60000) {
qlist.push("count");
this._cnt_updated = now;
}
const uparams = new URLSearchParams();
uparams.set("list", qlist.join(","));
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]);
}
}
return window.fetch("list.php?" + uparams, {
method: "GET",
cache: "no-store",
headers: HTTP_HEADERS,
credentials: "same-origin"
}).then(resp => {
if (!resp.ok)
throw new Error("Failed to fetch the report list");
return resp.json();
}).then(data => {
this._table.display_status(null);
Common.checkResult(data);
let d = { more: data.more };
d.rows = data.reports.map(it => {
return new ReportTableRow(this._make_row_data(it));
});
let fr = new ITableFrame(d, pos);
this._table.add_frame(fr);
if (data.count) this._rep_counter.set(data.count);
return fr;
}).catch(err => {
Common.displayError(err);
this._table.display_status("error");
}).finally(() => {
this._fetching = false;
});
}
_make_row_data(d) {
let rd = {
cells: [],
userdata: { domain: d.domain, time: d.date.begin, org: d.org_name, report_id: d.report_id },
seen: d.seen && true || false
};
rd.cells.push({ content: d.domain, label: "Domain", class: "fqdn" });
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", class: "orgname" });
rd.cells.push({ content: d.report_id, label: "Report ID", class: "report-id" });
rd.cells.push({ content: Common.abbrNumber(d.messages, 1e6), label: "Messages" });
rd.cells.push(new ResultColumn({ dkim_align: d.dkim_align, spf_align: d.spf_align }, { label: "Result" }));
rd.cells.push(new DispositionColumn({
none: d.messages - d.rejected - d.quarantined,
rejected: d.rejected,
quarantined: d.quarantined
}, { label: "Disposition" }));
rd.cells.push(new FailsColumn(d.quarantined, { label: "Quarantined" }));
rd.cells.push(new FailsColumn(d.rejected, { label: "Rejected" }));
return rd;
}
_display_settings_dialog() {
let dlg = this._settings_dlg;
if (!this._settings_dlg) {
dlg = new ReportListFilterDialog({ filter: this._filter });
this._settings_dlg = dlg;
}
this._element.append(dlg.element());
dlg.show().then(d => {
if (!d) return;
const url = new URL(document.location);
url.searchParams.delete("filter[]");
for (const k in d) {
if (d[k]) url.searchParams.append("filter[]", `${k}:${d[k]}`);
}
window.history.replaceState(null, "", url);
const f = Common.getFilterFromURL(url, this._filter);
if (f !== undefined) {
this._filter = f;
Status.instance().reset();
Status.instance().update({ page: "list" });
this._update_table();
this._update_settings_button();
}
}).finally(() => {
this._table.focus();
});
}
_get_visible_columns(update) {
let names = null;
try {
names = JSON.parse(window.localStorage.getItem("reportListColumns"));
} catch (err) {
}
let res = null;
if (Array.isArray(names)) {
res = names.filter(name => this._column_list.find(c => (c.name === name)));
}
if (!res || !res.length) res = [ "domain", "begin_time", "organization", "messages", "result", "disposition" ];
if (update) {
this._table.set_columns_visible(this._column_list.reduce((list, col, idx) => {
if (res.includes(col.name)) list.push(idx);
return list;
}, []));
}
return res;
}
_save_visible_columns (columns) {
try {
window.localStorage.setItem("reportListColumns", JSON.stringify(columns));
} catch (err) {
Notification.add({ text: "Unable to save the column set", type: "error" });
}
}
_display_columns_dialog() {
const dlg = new ReportListColumnsDialog({ columns: this._column_list, checked: this._get_visible_columns(false) });
this._element.append(dlg.element())
dlg.show().then(d => {
if (!d) return;
this._save_visible_columns(d);
this._get_visible_columns(true);
}).finally(() => {
dlg.element().remove();
this._table.focus();
});
}
}
class ReportCounter {
constructor() {
this._total = null;
this._unread = null;
this._element = null;
this._total_el = null;
this._unread_el = null;
}
set(data) {
this._total = data.total;
this._unread = data.unread;
if (this._element) this._update_element();
}
decrease() {
--this._unread;
if (this._element) this._update_element();
}
element() {
if (!this._element) {
this._element = document.createElement("span");
this._total_el = document.createElement("strong");
this._unread_el = document.createElement("strong");
this._update_element();
this._element.append("Unread: ", this._unread_el, " of ", this._total_el);
}
return this._element;
}
_update_element() {
this._total_el.textContent = this._total !== null ? this._total : '-';
this._unread_el.textContent = this._unread !== null ? this._unread : '-';
}
}
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 ResultColumn extends ITableCell {
value(target) {
if (target === "dom") {
let d = this._content;
let fr = document.createDocumentFragment();
[ [ "dkim_align", "DKIM" ], [ "spf_align", "SPF" ] ].forEach(ai => {
const align = d[ai[0]];
if (align) {
if (![ "fail", "unknown" ].reduce((cnt, ares) => {
if (align[ares]) {
const val = Common.abbrNumber(align[ares]);
fr.append(Common.createReportResultElement(ai[1], ares, val))
++cnt;
}
return cnt;
}, 0)) {
fr.append(Common.createReportResultElement(ai[1], "pass"));
}
}
});
return fr;
}
return super.value(target);
}
}
class DispositionColumn extends ITableCell {
value(target) {
if (target === "dom") {
const fr = document.createDocumentFragment();
const d = this._content;
[ [ "none", "None", "pass" ], [ "quarantined", "Quar", "fail" ], [ "rejected", "Rej", "fail" ] ].forEach(it => {
if (d[it[0]]) fr.append(Common.createReportResultElement(it[1], it[2], Common.abbrNumber(d[it[0]])));
});
return fr;
}
return super.value(target);
}
}
class ColoredIntColumn extends ITableCell {
_make_colored_int_element(value, factor) {
const el = document.createElement("span");
factor *= value;
if (factor) el.classList.add("report-result-" + (factor > 0 ? "pass" : "fail"));
el.append(Common.abbrNumber(value, 1e6));
return el;
}
}
class FailsColumn extends ColoredIntColumn {
value(target) {
if (target !== "dom") return super.value(target);
return this._make_colored_int_element(this._content, -1);
}
}
class ReportListFilterDialog extends ReportFilterDialog {
constructor(params) {
params.title = "Report list filter";
params.item_list = [ "domain", "month", "organization", "dkim", "spf", "disposition", "status" ];
super(params);
}
_fetch_data() {
this._enable_ui(false);
this.display_status("wait", "Getting data...");
window.fetch("list.php?list=filters", {
method: "GET",
cache: "no-store",
headers: HTTP_HEADERS,
credentials: "same-origin"
}).then(resp => {
if (!resp.ok) throw new Error("Failed to fetch the filter list");
return resp.json();
}).then(data => {
Common.checkResult(data);
this._data.loaded_filters = data.filters;
this._update_ui();
this._enable_ui(true);
}).catch(err => {
Common.displayError(err);
this.display_status("error", err.message);
}).finally(() => {
this.display_status("wait", null);
});
}
}
class ReportListColumnsDialog extends ModalDialog {
constructor(params) {
super({ title: "Columns", buttons: [ "apply", "close" ] });
this._columns = params.columns;
this._checked = params.checked;
}
element() {
if (!this._element) {
super.element();
const fs = document.createElement("fieldset");
this._content.replaceWith(fs);
fs.classList.add("vertical-content", "round-border");
fs.appendChild(document.createElement("legend")).textContent = "Check visible columns";
this._columns.forEach(ci => {
fs.append(this._make_column_checkbox(ci.name, ci.content, this._checked.includes(ci.name)));
});
fs.addEventListener("change", event => {
this._buttons[1].disabled = !fs.querySelector("input[type=checkbox]:checked");
});
fs.dispatchEvent(new Event("change"));
this._content = fs;
}
return this._element;
}
_submit() {
this._result = [];
for (const chb of this._content.querySelectorAll("input[type=checkbox]")) {
if (chb.checked) this._result.push(chb.name);
}
this.hide();
}
_make_column_checkbox(name, title, checked) {
const lb = document.createElement("label");
const cb = lb.appendChild(document.createElement("input"));
cb.type = "checkbox";
cb.name = name;
if (checked) cb.checked = true;
lb.append(title);
return lb;
}
}