%PDF- %PDF-
| Direktori : /www/varak.net/dmarc.varak.net/public/js/ |
| Current File : /www/varak.net/dmarc.varak.net/public/js/domains.js |
/**
* dmarc-srg - A php parser, viewer and summary report generator for incoming DMARC reports.
* Copyright (C) 2021-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 DomainList {
constructor() {
this._page = null;
this._table = null;
this._scroll = null;
this._element = document.getElementById("main-block");
this._sort = { column: "fqdn", direction: "ascent" };
}
display() {
this._make_page_container();
this._make_scroll_container();
this._make_table();
this._scroll.append(this._table.element());
this._page.append(this._scroll);
this._element.append(this._page);
this._table.focus();
}
update() {
this._fetch_list();
}
title() {
return "Domain List";
}
_fetch_list() {
this._table.display_status("wait");
let that = this;
return window.fetch("domains.php", {
method: "GET",
cache: "no-store",
headers: HTTP_HEADERS,
credentials: "same-origin"
}).then(function(resp) {
if (!resp.ok)
throw new Error("Failed to fetch the domain list");
return resp.json();
}).then(function(data) {
that._table.display_status(null);
Common.checkResult(data);
let d = { more: data.more };
d.rows = data.domains.map(function(it) {
return that._make_row_data(it);
});
if (User.level === "admin" || User.level === "manager") {
d.rows.push(new NewDomainRow(4));
}
let fr = new DomainFrame(d, that._table.last_row_index() + 1);
that._table.clear();
that._table.add_frame(fr);
if (that._sort.column) {
that._table.sort(that._sort.column, that._sort.direction);
}
that._table.focus();
}).catch(function(err) {
Common.displayError(err);
that._table.display_status("error");
});
}
_make_page_container() {
this._page = document.createElement("div");
this._page.classList.add("page-container");
}
_make_scroll_container() {
this._scroll = document.createElement("div");
this._scroll.classList.add("table-wrapper");
}
_make_table() {
this._table = new ITable({
class: "main-table domains",
onclick: function(row) {
let data = row.userdata();
if (data) {
this._display_edit_dialog(data);
}
}.bind(this),
onsort: function(col) {
let dir = col.sorted() && "toggle" || "ascent";
this._table.set_sorted(col.name(), dir);
this._table.sort(col.name(), col.sorted());
this._sort.column = col.name();
this._sort.direction = col.sorted();
this._table.focus();
}.bind(this),
onfocus: function(el) {
scroll_to_element(el, this._scroll);
}.bind(this)
});
[
{ content: "", sortable: true, name: "status", class: "cell-status" },
{ content: "FQDN", sortable: true, name: "fqdn" },
{ content: "Updated", sortable: true, name: "date" },
{ content: "Description", class: "descr" }
].forEach(function(col) {
let c = this._table.add_column(col);
if (c.name() === this._sort.column) {
c.sort(this._sort.direction);
}
}, this);
}
_make_row_data(d) {
let rd = { cells: [], userdata: d.fqdn };
rd.cells.push(new DomainStatusCell(d.active));
rd.cells.push({ content: d.fqdn, class: "fqdn" });
rd.cells.push(new DomainTimeCell(new Date(d.updated_time)));
rd.cells.push({ content: d.description || "", class: "descr" });
return rd;
}
_display_edit_dialog(fqdn) {
let dlg_par = {};
if (fqdn === "*new") {
dlg_par["new"] = true;
dlg_par.disable = User.level !== "admin";
}
else {
dlg_par.fqdn = fqdn;
dlg_par.disable = User.level === "user";
}
dlg_par.level = User.level;
let dlg = new DomainEditDialog(dlg_par);
this._element.appendChild(dlg.element());
let that = this;
dlg.show().then(function(d) {
if (d) {
that.update();
}
}).finally(function() {
dlg.element().remove();
that._table.focus();
});
}
}
class DomainStatusCell extends ITableCell {
constructor(is_active, props) {
props = props || {};
let ca = (props.class || "").split(" ");
ca.push(is_active && "state-green" || "state-gray");
props.class = ca.filter(function(s) { return s.length > 0; }).join(" ");
super(is_active, props);
}
value(target) {
if (target === "dom") {
let div = document.createElement("div");
div.setAttribute("class", "state-background status-indicator");
if (!this._title) {
div.setAttribute("title", this._content && "active" || "inactive");
}
return div;
}
return this._content;
}
}
class DomainTimeCell extends ITableCell {
value(target) {
if (target === "dom") {
return this._content && this._content.toUIString() || "";
}
if (target === "sort") {
return this._content && this._content.valueOf() || "";
}
super.value(target);
}
}
class NewDomainRow extends ITableRow {
constructor(col_cnt) {
super({
userdata: "*new",
cells: []
});
this._col_cnt = col_cnt;
}
element() {
if (!this._element) {
super.element();
this._element.classList.add("colspanned", "virtual-item");
for (let i = 0; i < this._col_cnt; ++i) {
let cell = document.createElement("div");
cell.setAttribute("class", "table-cell");
cell.appendChild(document.createTextNode(!i && "New domain" || "\u00A0"));
this._element.appendChild(cell);
}
}
return this._element;
}
}
class DomainFrame extends ITableFrame {
sort(col_idx, direction) {
this._sort_dir = (direction === "ascent" && 1) || (direction === "descent" && 2) || 0;
super.sort(col_idx, direction);
}
_compare_cells(c1, c2) {
if (!c1) {
return this._sort_dir === 2;
}
if (!c2) {
return this._sort_dir === 1;
}
return super._compare_cells(c1, c2);
}
}
class DomainEditDialog extends VerticalDialog {
constructor(params) {
let tl = null;
let ba = [];
if (params.level !== "user") ba.push("save");
if (params["new"]) {
tl = "New domain";
}
else {
tl = "Domain settings";
if (params.level !== "user") ba.push("delete");
}
ba.push("close");
super({ title: tl, buttons: ba });
this._data = params || {};
this._note_el = null;
this._fqdn_el = null;
this._actv_el = null;
this._desc_el = null;
this._c_tm_el = null;
this._u_tm_el = null;
this._fetched = false;
}
_gen_content() {
let dis = this._data.disable;
this._note_el = this._content.appendChild(document.createElement("div"));
this._note_el.classList.add("hidden", "warn-block");
let fq = document.createElement("input");
fq.setAttribute("type", "text");
if (!this._data["new"]) {
fq.setAttribute("value", this._data.fqdn);
fq.disabled = true;
}
fq.required = true;
this._insert_input_row("FQDN", fq);
this._fqdn_el = fq;
{
let en = document.createElement("select");
let op1 = document.createElement("option");
op1.setAttribute("value", "yes");
op1.appendChild(document.createTextNode("Yes"));
en.appendChild(op1);
let op2 = document.createElement("option");
op2.setAttribute("value", "no");
op2.appendChild(document.createTextNode("No"));
en.appendChild(op2);
en.required = true;
en.disabled = dis;
this._insert_input_row("Active", en);
this._actv_el = en;
}
let tx = document.createElement("textarea");
tx.classList.add("description")
tx.disabled = dis;
this._insert_input_row("Description", tx);
this._desc_el = tx;
let ct = document.createElement("input");
ct.setAttribute("type", "text");
ct.disabled = true;
ct.setAttribute("value","n/a");
this._insert_input_row("Created", ct);
this._c_tm_el = ct;
let ut = document.createElement("input");
ut.setAttribute("type", "text");
ut.setAttribute("value","n/a");
ut.disabled = true;
this._insert_input_row("Updated", ut);
this._u_tm_el = ut;
this._inputs.addEventListener("input", function(event) {
if (this._fetched || this._data["new"]) {
this._buttons[1].disabled = (
this._fqdn_el.value.trim() === "" || (
this._actv_el.dataset.server === this._actv_el.value &&
this._desc_el.defaultValue === this._desc_el.value &&
this._fqdn_el.defaultValue === this._fqdn_el.value)
);
}
}.bind(this));
if (!this._fetched) this._fetch_data();
}
_fetch_data() {
this._enable_ui(false);
this.display_status("wait", "Getting data...");
const url = new URL("domains.php", document.location);
url.searchParams.set("domain", this._data.fqdn || "");
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 the domain data");
return resp.json();
}).then(data => {
this._fetched = true;
Common.checkResult(data);
if (data.created_time) data.created_time = new Date(data.created_time);
if (data.updated_time) data.updated_time = new Date(data.updated_time);
this._update_ui(data);
this._enable_ui(true);
}).catch(err => {
Common.displayError(err);
this.display_status("error", err.message);
}).finally(() => {
this.display_status("wait", null);
});
}
_update_ui(data) {
if (data.verification === "dns") {
this._note_el.classList.remove("hidden");
let lm = document.createElement("a");
lm.href = "";
lm.textContent = "Learn more";
this._note_el.append("Domain verification is required! ", lm);
lm.addEventListener("click", event => {
event.preventDefault();
this._display_verification_dialog(data.verification_data, lm);
});
}
if (this._data["new"]) return;
let val = "";
for (let i = 0; i < this._actv_el.options.length; ++i) {
let op = this._actv_el.options[i];
if (data.active === (op.value === "yes")) {
op.setAttribute("selected", "selected");
val = op.value;
}
else {
op.removeAttribute("selected");
}
}
this._actv_el.value = val;
this._actv_el.dataset.server = val;
this._desc_el.appendChild(document.createTextNode(data.description || ""));
this._c_tm_el.setAttribute("value", data.created_time && data.created_time.toUIString() || "n/a");
this._u_tm_el.setAttribute("value", data.updated_time && data.updated_time.toUIString() || "n/a");
}
_add_button(container, text, type) {
let btn = null;
if (type === "save") {
text = "Save";
btn = document.createElement("button");
btn.disabled = true;
btn.addEventListener("click", this._save.bind(this));
}
else if (type === "delete") {
text = "Delete";
btn = document.createElement("button");
btn.addEventListener("click", () => this._confirm_delete(false));
}
else {
super._add_button(container, text, type);
return;
}
btn.setAttribute("type", "button");
btn.appendChild(document.createTextNode(text));
container.appendChild(btn);
this._buttons.push(btn);
}
_enable_ui(en) {
let dis = this._data.disable;
this._fqdn_el.disabled = !en || !this._data["new"];
this._actv_el.disabled = !en || dis;
this._desc_el.disabled = !en || dis;
for (let i = 2; i < this._buttons.length - 1; ++i) {
this._buttons[i].disabled = !en;
}
this.focus();
}
_save() {
this._enable_ui(false);
this.display_status("wait", "Sending data to the server...");
const body = {};
body.fqdn = this._data["new"] && this._fqdn_el.value || this._data.fqdn;
body.action = this._data["new"] && "add" || "update";
body.active = this._actv_el.value === "yes";
body.description = this._desc_el.value;
window.fetch("domains.php", {
method: "POST",
cache: "no-store",
headers: Object.assign(HTTP_HEADERS, HTTP_HEADERS_POST),
credentials: "same-origin",
body: JSON.stringify(body)
}).then(resp => {
if (!resp.ok)
throw new Error("Failed to " + (body.new && "add" || "update") + " the domain data");
return resp.json();
}).then(data => {
Common.checkResult(data);
this._result = body;
this.hide();
Notification.add({
text: "The domain " + body.fqdn + " was " + (body.action === "add" && "added" || "updated")
});
}).catch(err => {
Common.displayError(err);
this.display_status("error", err.message);
}).finally(() => {
this.display_status("wait", null);
this._enable_ui(true);
});
}
_confirm_delete(with_reports) {
let msg;
if (with_reports) {
msg = "There are incoming reports for the domain. Do you want to delete the domain with its reports?";
} else {
msg = "Are sure you want to delete this domain?";
}
if (confirm(msg)) {
this._delete(with_reports);
return true;
}
}
_delete(with_reports) {
this._enable_ui(false);
this.display_status("wait", "Sending a request to the server...");
const body = {
fqdn: this._data.fqdn,
force: with_reports,
action: "delete"
};
window.fetch("domains.php", {
method: "POST",
cache: "no-store",
headers: Object.assign(HTTP_HEADERS, HTTP_HEADERS_POST),
credentials: "same-origin",
body: JSON.stringify(body)
}).then(resp => {
if (!resp.ok) throw new Error("Failed to delete the domain");
return resp.json();
}).then(data => {
Common.checkResult(data);
this._result = data;
this.hide();
Notification.add({ text: "The domain " + body.fqdn + " was removed" });
}).catch(err => {
if (err.error_code === -10 && this._confirm_delete(true)) return;
Common.displayError(err);
this.display_status("error", err.message);
}).finally(() => {
this.display_status("wait", null);
this._enable_ui(true);
});
}
_display_verification_dialog(data, source_el) {
const dlg = new VerificationDialog(data);
this._element.classList.add("hidden");
document.getElementById("main-block").append(dlg.element());
dlg.show().finally(() => {
dlg.element().remove();
this._element.classList.remove("hidden");
source_el.focus();
});
}
}
class VerificationDialog extends ModalDialog {
constructor(data) {
super({ title: "Domain verification information", buttons: [ "close" ] });
this._data = data;
}
_gen_content() {
this._element.children[0].classList.add("verification");
this._content.classList.add("vertical-content");
const t1 = document.createElement("div");
t1.textContent = "To ensure that you are the owner of the domain, add a TXT record to the DNS settings at your domain registrar with the following content:";
const t2 = document.createElement("textarea");
t2.textContent = this._data;
t2.readOnly = true;
const t3 = document.createElement("div");
t3.textContent = "Once the domain has been successfully added, the TXT record can be deleted.";
const t4 = document.createElement("div");
t4.textContent = "Important! Some registrars may require additional time to publish your verification code. If the tool can't find your new TXT record, wait an hour before you try again."
this._content.append(t1, t2, t3, t4);
}
}