/** * 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 ITable { constructor(params) { this._table = null; this._class = null; this._header = null; this._status = null; this._frames = []; this._columns = []; this._body = null; this._onsort = null; this._onclick = null; this._onfocus = null; if (params) { this._class = params.class || null; this._onsort = params.onsort || null; this._onclick = params.onclick || null; this._onfocus = params.onfocus || null; this._nodata_text = params.nodata_text || null; } this._focused = false; this._focused_row = null; this._selected_rows = []; } element() { if (!this._table) { let that = this; this._table = document.createElement("div"); if (this._class) this._table.setAttribute("class", this._class); this._table.classList.add("table"); this._table.setAttribute("tabindex", -1); this._table.addEventListener("focus", function(event) { that._focused = true; that._update_focus(); }, true); this._table.addEventListener("blur", function(event) { that._focused = false; that._update_focus(); }, true); let th = document.createElement("div"); th.setAttribute("class", "table-header"); this._table.appendChild(th); this._header = document.createElement("div"); this._header.setAttribute("class", "table-row"); this._header.addEventListener("click", function(event) { let col = that.get_column_by_element(event.target); if (col && col.is_sortable()) { if (that._onsort) { that._onsort(col); } } }); th.appendChild(this._header); this._fill_columns(); this._body = document.createElement("div"); this._body.setAttribute("class", "table-body"); this._body.addEventListener("click", function(event) { let row = that._get_row_by_element(event.target); if (row) { that._set_selected_rows([ row ]); if (that._onclick) that._onclick(row); } }); this._body.addEventListener("focus", function(event) { let row = that._get_row_by_element(event.target); if (row) { that._update_focused_row(row); if (that._onfocus) that._onfocus(row.element()); } }, true); this._body.addEventListener("blur", function(event) { let row = that._get_row_by_element(event.target); if (row) { row.onfocus(false); } }, true); this._body.addEventListener("keydown", function(event) { let row = null; switch (event.code) { case "ArrowDown": row = that._get_row(that._focused_row !== null && (that._focused_row.id() + 1) || 0); break; case "ArrowUp": if (that._focused_row) { let id = that._focused_row.id(); if (id >= 0) row = that._get_row(id - 1); } else { row = that._get_row(0); } break; case "PageUp": if (that._focused_row && that._frames.length > 0) { let c_id = that._focused_row.id(); let f_fr = that._frames[0]; let f_id = f_fr.first_index(); if (c_id == f_id) break; let s_el = that._get_scroll_element(); if (s_el) { let r_ht = that._focused_row.element().getBoundingClientRect().height; let s_ht = s_el.getBoundingClientRect().height; let n_id = Math.max(c_id - Math.floor(s_ht / r_ht) - 1, f_id); row = that._get_row(n_id); } else { row = f_fr.row(f_id); } } break; case "PageDown": if (that._focused_row && that._frames.length > 0) { let c_id = that._focused_row.id(); let l_fr = that._frames[that._frames.length - 1]; let l_id = l_fr.last_index(); if (c_id == l_id) break; let s_el = that._get_scroll_element(); if (s_el) { let r_ht = that._focused_row.element().getBoundingClientRect().height; let s_ht = s_el.getBoundingClientRect().height; let n_id = Math.min(c_id + Math.floor(s_ht / r_ht) - 1, l_id); row = that._get_row(n_id); } else { row = l_fr.row(l_id); } } break; case "Home": if (that._frames.length > 0) { let first_frame = that._frames[0]; row = first_frame.row(first_frame.first_index()); } break; case "End": if (that._frames.length > 0) { let last_frame = that._frames[that._frames.length - 1]; row = last_frame.row(last_frame.last_index()); } break; case "Enter": case "NumpadEnter": if (that._onclick && that._focused_row) that._onclick(that._focused_row); event.preventDefault(); return; } if (row) { row.element().focus(); that._set_selected_rows([ row ]); event.preventDefault(); } }); this._fill_frames(); this._table.appendChild(this._body); } return this._table; } more() { return this._frames.length > 0 && this._frames[this._frames.length - 1].more(); } frames_count() { return this._frames.length; } add_column(data) { let col = new ITableColumn(data.content, { name: data.name, class: data.class, sortable: data.sortable, sorted: data.sorted }); this._columns.push(col); if (this._header) this._header.appendChild(col.element()); return col; } get_column_by_element(el) { el = el && el.closest("div.table-cell"); if (el) { for (let i = 0; i < this._columns.length; ++i) { let col = this._columns[i]; if (el === col.element()) return col; } } } display_status(status, text) { if (this._status && !status) { this._status.remove(); this._status = null; return; } this.element(); this._status = document.createElement("div"); this._status.setAttribute("class", "table-row colspanned"); let el = document.createElement("div"); el.setAttribute("class", "table-cell"); this._status.appendChild(el); let el2 = document.createElement("div"); el2.setAttribute("class", "table-cell"); el2.appendChild(document.createTextNode("\u00A0")); // Non breaking space this._status.appendChild(el2); if (status === "wait") { set_wait_status(el); } else { remove_all_children(this._body); if (status === "nodata") { el.classList.add("nodata"); el.appendChild(document.createTextNode(text || "No data")); } else { set_error_status(el, text); } } this._body.appendChild(this._status); } last_row_index() { let idx = -1; if (this._frames.length > 0) { idx = this._frames[this._frames.length - 1].last_index(); } return idx; } add_frame(frame) { if (frame.count() === 0) { if (this._frames.length === 0) this.display_status("nodata", this._nodata_text); return } if (this._frames.length > 0 && this._frames[0].first_index() > frame.last_index()) { this._frames.unshift(frame); if (this._body) this._body.insertBefore(frame.element(), this._body.firstChild); } else { this._frames.push(frame); if (this._body) this._body.appendChild(frame.element()); } } clear() { this._frames = []; if (this._body) remove_all_children(this._body); this._focused_row = null; this._selected_rows = []; } focus() { if (!this._focused_row) { if (this._frames.length > 0) { let fr = this._frames[0]; this._focused_row = fr.row(fr.first_index()); } } if (this._focused_row) this._focused_row.element().focus(); } sort(col_name, direction) { if (this._frames.length == 1) { for (let i = 0; i < this._columns.length; ++i) { let col = this._columns[i]; if (col.is_sortable() && col.name() === col_name) { let fr = this._frames[0]; fr.sort(i, direction); if (this._body) { remove_all_children(this._body); this._body.appendChild(fr.element()); } return; } } } } set_sorted(col_name, direction) { this._columns.forEach(function(col) { if (col.is_sortable()) { if (col.name() !== col_name) { col.sort(null); } else { if (direction === "toggle") { direction = null; if (col.sorted() === "ascent") { direction = "descent"; } else if (col.sorted() === "descent") { direction = "ascent"; } } col.sort(direction); } } }); } _fill_columns() { this._columns.forEach(function(col) { this._header.appendChild(col.element()); }, this); } _fill_frames() { this._frames.forEach(function(fr) { this._body.appendChild(fr.element()); }, this); } _get_row(row_id) { for (let i = 0; i < this._frames.length; ++i) { let fr = this._frames[i]; if (fr.last_index() >= row_id) { if (fr.first_index() <= row_id) return fr.row(row_id); } } return null; } _get_row_by_element(el) { let row = null; if (el) { el = el.closest("div.table-row"); if (el) { let id = parseInt(el.getAttribute("data-id")); if (id !== NaN) row = this._get_row(id); } } return row; } _update_focus() { if (this._focused) this._table.classList.add("focused"); else this._table.classList.remove("focused"); } _update_focused_row(row) { if (this._focused_row && row !== this._focused_row) { this._focused_row.tabindex(-1); } this._focused_row = row; this._focused_row.tabindex(0); this._focused_row.onfocus(true); } _set_selected_rows(rows) { this._selected_rows.forEach(function(row) { row.select(false); }); rows.forEach(function(row) { row.select(true); }); this._selected_rows = rows; } _get_scroll_element() { let t_rect = this._table.getBoundingClientRect(); let p_elem = this._table.parentElement; while (p_elem) { let p_rect = p_elem.getBoundingClientRect(); if (t_rect.top < p_rect.top || t_rect.bottom > p_rect.bottom) { return p_elem; } p_elem = p_elem.paretnElement; } } } class ITableFrame { constructor(data, pos) { this._pos = pos; this._more = data.more && true || false; let id = pos; this._rows = data.rows.map(function(rd) { if (!(rd instanceof ITableRow)) { rd = new ITableRow(rd); } rd.id(id++); return rd; }); } count() { return this._rows.length; } first_index() { return this._pos; } last_index() { let cnt = this._rows.length; if (cnt > 0) { return this._pos + cnt - 1; } return null; } row(id) { let idx = id - this._pos; if (idx >= 0 && idx < this._rows.length) { return this._rows[idx]; } return null; } more() { return this._more; } element() { let fr = document.createDocumentFragment(); this._rows.forEach(function(row) { fr.appendChild(row.element()); }); return fr; } sort(col_idx, direction) { let dir = (direction === "ascent" && 1) || (direction === "descent" && 2) || 0; if (dir) { let that = this; this._rows.sort(function(a, b) { let c1 = a.cell(col_idx); let c2 = b.cell(col_idx); if (dir === 1) { return that._compare_cells(c2, c1); } return that._compare_cells(c1, c2); }); let id = this._pos; this._rows.forEach(function(row) { row.id(id++); }); } } _compare_cells(c1, c2) { return c1.value("sort") < c2.value("sort"); } } class ITableRow { constructor(data) { this._id = -1; this._focused = false; this._tabindex = -1; this._selected = false; this._element = null; this._class = data.class || null; this._userdata = data.userdata || null; this._cells = data.cells.map(function(col) { if (col instanceof ITableCell) { return col; } let props = null; if (col.title || col.class || col.label) { props = { title: col.title || null, class: col.class || null, label: col.label || null }; } return new ITableCell(col.content, props); }); } userdata() { return this._userdata; } element() { if (!this._element) { this._element = document.createElement("div"); this._element.setAttribute("data-id", this._id); if (this._class) this._element.setAttribute("class", this._class); this._element.classList.add("table-row"); this._cells.forEach(function(col) { this._element.appendChild(col.element()); }, this); this._update_focus(); this._update_tabindex(); this._update_select(); } return this._element; } onfocus(flag) { this._focused = flag; if (this._element) this._update_focus(); } tabindex(index) { if (this._tabindex !== index) { this._tabindex = index; this._update_tabindex(); } } select(flag) { this._selected = flag; if (this._element) this._update_select(); } id(new_id) { if (new_id !== undefined && new_id !== this._id) { this._id = new_id; if (this._element) { this._element.setAttribute("data-id", this._id); } } return this._id; } cell(index) { return this._cells[index] || null; } _update_focus() { if (this._focused) this._element.classList.add("focused"); else this._element.classList.remove("focused"); } _update_tabindex() { this._element.setAttribute("tabindex", this._tabindex); } _update_select() { if (this._selected) { this._element.classList.add("selected"); } else { this._element.classList.remove("selected"); } } } class ITableCell { constructor(content, props) { this._element = null; this._content = content; if (props) { this._title = props.title || null; this._class = props.class || null; this._label = props.label || null; } } element() { if (!this._element) { this._element = document.createElement("div"); if (this._title) { this._element.setAttribute("title", this._title); } if (this._class) { this._element.setAttribute("class", this._class); } if (this._label) { this._element.setAttribute("data-label", this._label); } this._element.classList.add("table-cell"); let content = this.value("dom"); if (content !== null) { if (typeof(content) === "object") { this._element.appendChild(content) } else { this._element.appendChild(document.createTextNode(content)); } } } return this._element; } value(target) { if (target === "dom" || typeof(this._content) !== "object") { return this._content; } return null; } } class ITableColumn extends ITableCell { constructor(content, props) { super(content, props); this._name = props.name; this._sortable = !!props.sortable; this._sorted = props.sorted || null; } element() { if (this._element !== super.element()) { this._update_sorted(); } return this._element; } is_sortable() { return this._sortable; } sort(dir) { if (this._sorted !== dir) { this._sorted = dir || null; if (this._element) { this._update_sorted(); } } } sorted() { return this._sorted; } name() { return this._name; } _update_sorted() { if (this._sortable) { this._element.classList.add("sortable"); let c_act = { asc: "remove", des: "remove" }; if (this._sorted) { this._element.classList.add("arrows"); if (this._sorted === "ascent") { c_act["asc"] = "add"; } else if (this._sorted === "descent") { c_act["des"] = "add"; } } else { this._element.classList.remove("arrows"); } for (let key in c_act) { this._element.classList[c_act[key]]("sorted-" + key); } } } } class ModalDialog { constructor(params) { this._params = params; this._element = null; this._title = null; this._buttons = []; this._content = null; this._first = null; this._last = null; this._result = null; this._callback = null; } element() { if (!this._element) { let ovl = document.createElement("div"); ovl.setAttribute("class", "dialog-overlay hidden"); let dlg = document.createElement("div"); dlg.setAttribute("class", "dialog"); let con = document.createElement("div"); con.setAttribute("class", "container"); this._title = document.createElement("div"); this._title.setAttribute("class", "title"); { let tt = document.createElement("div"); tt.setAttribute("class", "title-text"); tt.appendChild(document.createTextNode(this._params.title || "")); this._title.appendChild(tt); } let that = this; { let cbt = document.createElement("button"); cbt.setAttribute("type", "button"); cbt.setAttribute("class", "close-btn"); cbt.appendChild(document.createTextNode("x")); this._title.appendChild(cbt); this._buttons = [ cbt ]; cbt.addEventListener("click", function(event) { that.hide(); }); } con.appendChild(this._title); let frm = document.createElement("form"); this._content = document.createElement("div"); frm.appendChild(this._content); let bdv = document.createElement("div"); bdv.setAttribute("class", "dialog-buttons"); this._add_buttons(bdv); frm.appendChild(bdv); con.appendChild(frm); dlg.appendChild(con); ovl.appendChild(dlg); this._element = ovl; this._gen_content(); this._update_first_last(); this._element.addEventListener("click", function(event) { if (event.target === this && that._params.overlay_click !== "ignore") { that.hide(); } }); frm.addEventListener("keydown", function(event) { if (event.key == "Tab") { if (!event.shiftKey) { if (event.target == that._last) { that._first.focus(); event.preventDefault(); } } else { if (event.target == that._first) { that._last.focus(); event.preventDefault(); } } } }); frm.addEventListener("submit", function(event) { event.preventDefault(); that._submit(); }); frm.addEventListener("reset", function(event) { this._reset(); }.bind(this)); } return this._element; } show() { this.element(); this._result = null; this._title.querySelector("button.close-btn").classList.add("active"); this._element.classList.remove("hidden"); if (this._first) { this._first.focus(); } let that = this; return new Promise(function(resolve, reject) { that._callback = resolve; }); } hide() { if (this._element) { this._title.querySelector("button.close-btn").classList.remove("active"); this._element.classList.add("hidden"); } this._callback && this._callback(this._result); } _add_buttons(container) { let bl = this._params.buttons || []; bl.forEach(function(bt) { let name = null; let type = null; if (bt == "ok") { name = "Ok"; type = "submit"; } else if (bt == "apply") { name = "Apply"; type = "submit"; } else if (bt == "reset") { name = "Reset"; type = "reset"; } else if (bt == "login") { name = "Log in"; type = "submit"; } else if (bt == "cancel") { name = "Cancel"; type = "close"; } else if (bt == "close") { name = "Close"; type = "close"; } else { name = bt; type = bt; } this._add_button(container, name, type); }, this); } _add_button(container, text, type) { let btn = document.createElement("button"); if (type == "close") { btn.setAttribute("type", "button"); btn.addEventListener("click", this.hide.bind(this)); } else { btn.setAttribute("type", type); } btn.appendChild(document.createTextNode(text)); container.appendChild(btn); this._buttons.push(btn); } _gen_content() { } _update_first_last() { this._first = null; this._last = null; let list = this._element.querySelector("form").elements; for (let i = 0; i < list.length; ++i) { let el = list[i]; if (!el.elements && !el.disabled) { if (!this._first) this._first = el; this._last = el; } } } _submit() { } _reset() { } } class AboutDialog extends ModalDialog { constructor(params) { super({ title: "About", buttons: [ "ok" ] }); this._authors = params.authors; this._documentation = params.documentation; this._source_code = params.source_code; } element() { if (!this._element) { super.element(); this._element.children[0].classList.add("about"); this._content.classList.add("vertical-content"); this._content.parentElement.classList.add("vertical-content"); } return this._element; } _gen_content() { let header = document.createElement("h2"); header.appendChild(document.createTextNode(Router.app_name(true))); this._content.appendChild(header); let cblock = document.createElement("div"); this._authors.forEach(function(author) { let ablock = document.createElement("div"); ablock.appendChild(document.createTextNode("Copyright © " + author.years + ", ")); cblock.appendChild(ablock); let alink = document.createElement("a"); alink.setAttribute("href", author.url); alink.setAttribute("title", "The author's page"); alink.setAttribute("target", "_blank"); alink.appendChild(document.createTextNode(author.name)); ablock.appendChild(alink); }); this._content.appendChild(cblock); let oblock = document.createElement("div"); oblock.setAttribute("class", "left-titled"); let add_row = function(title, value) { let t_el = document.createElement("span"); t_el.appendChild(document.createTextNode(title + ": ")); oblock.appendChild(t_el); let v_el = document.createElement("div"); value.forEach(function(v) { if (v_el.children.length > 0) { v_el.appendChild(document.createTextNode(", ")); } let a_el = document.createElement("a"); a_el.setAttribute("href", v.url); a_el.setAttribute("title", v.title || v.ancor); a_el.setAttribute("target", "_blank"); a_el.appendChild(document.createTextNode(v.ancor)); v_el.appendChild(a_el); }); oblock.appendChild(v_el); }; this._content.appendChild(oblock); add_row("Documentation", this._documentation); add_row("Source code", this._source_code); { let tl = document.createElement("span"); tl.appendChild(document.createTextNode("PHP version: ")); oblock.appendChild(tl); let vl = document.createElement("span"); vl.appendChild(document.createTextNode(Router.php_version || "n/a")); oblock.appendChild(vl); } let lblock = document.createElement("div"); lblock.appendChild(document.createTextNode( "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._content.appendChild(lblock); } _submit() { this.hide(); } }