diff options
author | Vsevolod Stakhov <vsevolod@rspamd.com> | 2023-12-28 12:38:28 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-12-28 12:38:28 +0000 |
commit | 001a14d89343d99a7cc6524b1eb2be00c177e93b (patch) | |
tree | 319a3c51f9c5bca226d9f83c3bda95b75a92ef83 /interface | |
parent | 9686fcf0234256d0b16856899ac3f75b3572dde1 (diff) | |
parent | f0ecc1fe07c4c04a2920c0435a0211e912a81959 (diff) | |
download | rspamd-001a14d89343d99a7cc6524b1eb2be00c177e93b.tar.gz rspamd-001a14d89343d99a7cc6524b1eb2be00c177e93b.zip |
Merge pull request #4762 from moisseev/webui
[Minor] Move common stuff to separate files
Diffstat (limited to 'interface')
-rw-r--r-- | interface/js/app/common.js | 233 | ||||
-rw-r--r-- | interface/js/app/config.js | 34 | ||||
-rw-r--r-- | interface/js/app/graph.js | 24 | ||||
-rw-r--r-- | interface/js/app/history.js | 52 | ||||
-rw-r--r-- | interface/js/app/libft.js | 380 | ||||
-rw-r--r-- | interface/js/app/rspamd.js | 630 | ||||
-rw-r--r-- | interface/js/app/selectors.js | 20 | ||||
-rw-r--r-- | interface/js/app/stats.js | 12 | ||||
-rw-r--r-- | interface/js/app/symbols.js | 20 | ||||
-rw-r--r-- | interface/js/app/upload.js | 62 |
10 files changed, 744 insertions, 723 deletions
diff --git a/interface/js/app/common.js b/interface/js/app/common.js new file mode 100644 index 000000000..ea6102f60 --- /dev/null +++ b/interface/js/app/common.js @@ -0,0 +1,233 @@ +/* global jQuery */ + +define(["jquery", "nprogress"], + ($, NProgress) => { + "use strict"; + const ui = { + chartLegend: [ + {label: "reject", color: "#FF0000"}, + {label: "soft reject", color: "#BF8040"}, + {label: "rewrite subject", color: "#FF6600"}, + {label: "add header", color: "#FFAD00"}, + {label: "greylist", color: "#436EEE"}, + {label: "no action", color: "#66CC00"} + ], + locale: (localStorage.getItem("selected_locale") === "custom") ? localStorage.getItem("custom_locale") : null, + neighbours: [], + page_size: { + scan: 25, + errors: 25, + history: 25 + }, + symbols: { + scan: [], + history: [] + }, + tables: {} + }; + + + NProgress.configure({ + minimum: 0.01, + showSpinner: false, + }); + + function getPassword() { + return sessionStorage.getItem("Password"); + } + + function alertMessage(alertClass, alertText) { + const a = $("<div class=\"alert " + alertClass + " alert-dismissible fade in show\">" + + "<button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"alert\" title=\"Dismiss\"></button>" + + "<strong>" + alertText + "</strong>"); + $(".notification-area").append(a); + + setTimeout(() => { + $(a).fadeTo(500, 0).slideUp(500, function () { + $(this).alert("close"); + }); + }, 5000); + } + + function queryServer(neighbours_status, ind, req_url, o) { + neighbours_status[ind].checked = false; + neighbours_status[ind].data = {}; + neighbours_status[ind].status = false; + const req_params = { + jsonp: false, + data: o.data, + headers: $.extend({Password: getPassword()}, o.headers), + url: neighbours_status[ind].url + req_url, + xhr: function () { + const xhr = $.ajaxSettings.xhr(); + // Download progress + if (req_url !== "neighbours") { + xhr.addEventListener("progress", (e) => { + if (e.lengthComputable) { + neighbours_status[ind].percentComplete = e.loaded / e.total; + const percentComplete = neighbours_status + .reduce((prev, curr) => (curr.percentComplete ? curr.percentComplete + prev : prev), 0); + NProgress.set(percentComplete / neighbours_status.length); + } + }, false); + } + return xhr; + }, + success: function (json) { + neighbours_status[ind].checked = true; + neighbours_status[ind].status = true; + neighbours_status[ind].data = json; + }, + error: function (jqXHR, textStatus, errorThrown) { + neighbours_status[ind].checked = true; + function errorMessage() { + alertMessage("alert-error", neighbours_status[ind].name + " > " + + (o.errorMessage ? o.errorMessage : "Request failed") + + (errorThrown ? ": " + errorThrown : "")); + } + if (o.error) { + o.error(neighbours_status[ind], + jqXHR, textStatus, errorThrown); + } else if (o.errorOnceId) { + const alert_status = o.errorOnceId + neighbours_status[ind].name; + if (!(alert_status in sessionStorage)) { + sessionStorage.setItem(alert_status, true); + errorMessage(); + } + } else { + errorMessage(); + } + }, + complete: function (jqXHR) { + if (neighbours_status.every((elt) => elt.checked)) { + if (neighbours_status.some((elt) => elt.status)) { + if (o.success) { + o.success(neighbours_status, jqXHR); + } else { + alertMessage("alert-success", "Request completed"); + } + } else { + alertMessage("alert-error", "Request failed"); + } + if (o.complete) o.complete(); + NProgress.done(); + } + }, + statusCode: o.statusCode + }; + if (o.method) { + req_params.method = o.method; + } + if (o.params) { + $.each(o.params, (k, v) => { + req_params[k] = v; + }); + } + $.ajax(req_params); + } + + + // Public functions + + ui.alertMessage = alertMessage; + ui.getPassword = getPassword; + + // Get selectors' current state + ui.getSelector = function (id) { + const e = document.getElementById(id); + return e.options[e.selectedIndex].value; + }; + + /** + * @param {string} url - A string containing the URL to which the request is sent + * @param {Object} [options] - A set of key/value pairs that configure the Ajax request. All settings are optional. + * + * @param {Function} [options.complete] - A function to be called when the requests to all neighbours complete. + * @param {Object|string|Array} [options.data] - Data to be sent to the server. + * @param {Function} [options.error] - A function to be called if the request fails. + * @param {string} [options.errorMessage] - Text to display in the alert message if the request fails. + * @param {string} [options.errorOnceId] - A prefix of the alert ID to be added to the session storage. If the + * parameter is set, the error for each server will be displayed only once per session. + * @param {Object} [options.headers] - An object of additional header key/value pairs to send along with requests + * using the XMLHttpRequest transport. + * @param {string} [options.method] - The HTTP method to use for the request. + * @param {Object} [options.params] - An object of additional jQuery.ajax() settings key/value pairs. + * @param {string} [options.server] - A server to which send the request. + * @param {Function} [options.success] - A function to be called if the request succeeds. + * + * @returns {undefined} + */ + ui.query = function (url, options) { + // Force options to be an object + const o = options || {}; + Object.keys(o).forEach((option) => { + if (["complete", "data", "error", "errorMessage", "errorOnceId", "headers", "method", "params", "server", + "statusCode", "success"] + .indexOf(option) < 0) { + throw new Error("Unknown option: " + option); + } + }); + + let neighbours_status = [{ + name: "local", + host: "local", + url: "", + }]; + o.server = o.server || ui.getSelector("selSrv"); + if (o.server === "All SERVERS") { + queryServer(neighbours_status, 0, "neighbours", { + success: function (json) { + const [{data}] = json; + if (jQuery.isEmptyObject(data)) { + ui.neighbours = { + local: { + host: window.location.host, + url: window.location.origin + window.location.pathname + } + }; + } else { + ui.neighbours = data; + } + neighbours_status = []; + $.each(ui.neighbours, (ind) => { + neighbours_status.push({ + name: ind, + host: ui.neighbours[ind].host, + url: ui.neighbours[ind].url, + }); + }); + $.each(neighbours_status, (ind) => { + queryServer(neighbours_status, ind, url, o); + }); + }, + errorMessage: "Cannot receive neighbours data" + }); + } else { + if (o.server !== "local") { + neighbours_status = [{ + name: o.server, + host: ui.neighbours[o.server].host, + url: ui.neighbours[o.server].url, + }]; + } + queryServer(neighbours_status, 0, url, o); + } + }; + + ui.escapeHTML = function (string) { + const htmlEscaper = /[&<>"'/`=]/g; + const htmlEscapes = { + "&": "&", + "<": "<", + ">": ">", + "\"": """, + "'": "'", + "/": "/", + "`": "`", + "=": "=" + }; + return String(string).replace(htmlEscaper, (match) => htmlEscapes[match]); + }; + + return ui; + }); diff --git a/interface/js/app/config.js b/interface/js/app/config.js index 1aaf71289..6be107555 100644 --- a/interface/js/app/config.js +++ b/interface/js/app/config.js @@ -24,13 +24,13 @@ /* global require */ -define(["jquery", "app/rspamd"], - ($, rspamd) => { +define(["jquery", "app/common"], + ($, common) => { "use strict"; const ui = {}; ui.getActions = function getActions(checked_server) { - rspamd.query("actions", { + common.query("actions", { success: function (data) { $("#actionsFormField").empty(); const items = []; @@ -88,15 +88,15 @@ define(["jquery", "app/rspamd"], // String to array for comparison const eltsArray = JSON.parse(elts); if (eltsArray[0] < 0) { - rspamd.alertMessage("alert-modal alert-error", "Spam can not be negative"); + common.alertMessage("alert-modal alert-error", "Spam can not be negative"); } else if (eltsArray[1] < 0) { - rspamd.alertMessage("alert-modal alert-error", "Rewrite subject can not be negative"); + common.alertMessage("alert-modal alert-error", "Rewrite subject can not be negative"); } else if (eltsArray[2] < 0) { - rspamd.alertMessage("alert-modal alert-error", "Probable spam can not be negative"); + common.alertMessage("alert-modal alert-error", "Probable spam can not be negative"); } else if (eltsArray[3] < 0) { - rspamd.alertMessage("alert-modal alert-error", "Greylist can not be negative"); + common.alertMessage("alert-modal alert-error", "Greylist can not be negative"); } else if (descending(eltsArray)) { - rspamd.query("saveactions", { + common.query("saveactions", { method: "POST", params: { data: elts, @@ -105,14 +105,14 @@ define(["jquery", "app/rspamd"], server: server }); } else { - rspamd.alertMessage("alert-modal alert-error", "Incorrect order of actions thresholds"); + common.alertMessage("alert-modal alert-error", "Incorrect order of actions thresholds"); } }; ui.getMaps = function (checked_server) { const $listmaps = $("#listMaps"); $listmaps.closest(".card").hide(); - rspamd.query("maps", { + common.query("maps", { success: function (json) { const [{data}] = json; $listmaps.empty(); @@ -121,7 +121,7 @@ define(["jquery", "app/rspamd"], $.each(data, (i, item) => { let $td = '<td><span class="badge text-bg-secondary">Read</span></td>'; - if (!(item.editable === false || rspamd.read_only)) { + if (!(item.editable === false || common.read_only)) { $td = $($td).append(' <span class="badge text-bg-success">Write</span>'); } const $tr = $("<tr>").append($td); @@ -158,9 +158,9 @@ define(["jquery", "app/rspamd"], // Modal form for maps $(document).on("click", "[data-bs-toggle=\"modal\"]", function () { - const checked_server = rspamd.getSelector("selSrv"); + const checked_server = common.getSelector("selSrv"); const item = $(this).data("item"); - rspamd.query("getmap", { + common.query("getmap", { headers: { Map: item.map }, @@ -180,11 +180,11 @@ define(["jquery", "app/rspamd"], jar.updateCode(data[0].data); }); } else { - document.querySelector("#editor").innerHTML = rspamd.escapeHTML(data[0].data); + document.querySelector("#editor").innerHTML = common.escapeHTML(data[0].data); } let icon = "fa-edit"; - if (item.editable === false || rspamd.read_only) { + if (item.editable === false || common.read_only) { $("#editor").attr(editor[mode].readonly_attr); icon = "fa-eye"; $("#modalSaveGroup").hide(); @@ -218,9 +218,9 @@ define(["jquery", "app/rspamd"], }); function saveMap(server) { - rspamd.query("savemap", { + common.query("savemap", { success: function () { - rspamd.alertMessage("alert-success", "Map data successfully saved"); + common.alertMessage("alert-success", "Map data successfully saved"); $("#modalDialog").modal("hide"); }, errorMessage: "Save map error", diff --git a/interface/js/app/graph.js b/interface/js/app/graph.js index 2fc00a457..71306f457 100644 --- a/interface/js/app/graph.js +++ b/interface/js/app/graph.js @@ -25,8 +25,8 @@ /* global FooTable */ -define(["jquery", "app/rspamd", "d3evolution", "d3pie", "d3", "footable"], - ($, rspamd, D3Evolution, D3Pie, d3) => { +define(["jquery", "app/common", "d3evolution", "d3pie", "d3", "footable"], + ($, common, D3Evolution, D3Pie, d3) => { "use strict"; const rrd_pie_config = { @@ -68,16 +68,16 @@ define(["jquery", "app/rspamd", "d3evolution", "d3pie", "d3", "footable"], legend: { space: 140, - entries: rspamd.chartLegend + entries: common.chartLegend } }; function initGraph() { const graph = new D3Evolution("graph", $.extend({}, graph_options, { - yScale: rspamd.getSelector("selYScale"), - type: rspamd.getSelector("selType"), - interpolate: rspamd.getSelector("selInterpolate"), - convert: rspamd.getSelector("selConvert"), + yScale: common.getSelector("selYScale"), + type: common.getSelector("selType"), + interpolate: common.getSelector("selInterpolate"), + convert: common.getSelector("selConvert"), })); $("#selYScale").change(function () { graph.yScale(this.value); @@ -127,7 +127,7 @@ define(["jquery", "app/rspamd", "d3evolution", "d3pie", "d3", "footable"], } function initSummaryTable(rows, unit) { - rspamd.tables.rrd_summary = FooTable.init("#rrd-table", { + common.tables.rrd_summary = FooTable.init("#rrd-table", { sorting: { enabled: true }, @@ -151,8 +151,8 @@ define(["jquery", "app/rspamd", "d3evolution", "d3pie", "d3", "footable"], } function drawRrdTable(rows, unit) { - if (Object.prototype.hasOwnProperty.call(rspamd.tables, "rrd_summary")) { - $.each(rspamd.tables.rrd_summary.rows.all, (i, row) => { + if (Object.prototype.hasOwnProperty.call(common.tables, "rrd_summary")) { + $.each(common.tables.rrd_summary.rows.all, (i, row) => { row.val(rows[i], false, true); }); } else { @@ -199,7 +199,7 @@ define(["jquery", "app/rspamd", "d3evolution", "d3pie", "d3", "footable"], } - rspamd.query("graph", { + common.query("graph", { success: function (req_data) { let data = null; const neighbours_data = req_data @@ -214,7 +214,7 @@ define(["jquery", "app/rspamd", "d3evolution", "d3pie", "d3", "footable"], if ((curr[0][0].x !== res[0][0].x) || (curr[0][curr[0].length - 1].x !== res[0][res[0].length - 1].x)) { time_match = false; - rspamd.alertMessage("alert-error", + common.alertMessage("alert-error", "Neighbours time extents do not match. Check if time is synchronized on all servers."); arr.splice(1); // Break out of .reduce() by mutating the source array } diff --git a/interface/js/app/history.js b/interface/js/app/history.js index a4d027da0..58d835fba 100644 --- a/interface/js/app/history.js +++ b/interface/js/app/history.js @@ -24,8 +24,8 @@ /* global FooTable */ -define(["jquery", "app/rspamd", "d3", "footable"], - ($, rspamd, d3) => { +define(["jquery", "app/common", "app/libft", "d3", "footable"], + ($, common, libft, d3) => { "use strict"; const ui = {}; let prevVersion = null; @@ -38,15 +38,15 @@ define(["jquery", "app/rspamd", "d3", "footable"], $("#selSymOrder_history, label[for='selSymOrder_history']").hide(); $.each(data, (i, item) => { - item.time = rspamd.unix_time_format(item.unix_time); - rspamd.preprocess_item(item); + item.time = libft.unix_time_format(item.unix_time); + libft.preprocess_item(item); item.symbols = Object.keys(item.symbols) .map((key) => item.symbols[key]) .sort(compare) .map((e) => e.name) .join(", "); item.time = { - value: rspamd.unix_time_format(item.unix_time), + value: libft.unix_time_format(item.unix_time), options: { sortValue: item.unix_time } @@ -234,7 +234,7 @@ define(["jquery", "app/rspamd", "d3", "footable"], function process_history_data(data) { const process_functions = { - 2: rspamd.process_history_v2, + 2: libft.process_history_v2, legacy: process_history_legacy }; let pf = process_functions.legacy; @@ -263,12 +263,12 @@ define(["jquery", "app/rspamd", "d3", "footable"], } ui.getHistory = function () { - rspamd.query("history", { + common.query("history", { success: function (req_data) { function differentVersions(neighbours_data) { const dv = neighbours_data.some((e) => e.version !== neighbours_data[0].version); if (dv) { - rspamd.alertMessage("alert-error", + common.alertMessage("alert-error", "Neighbours history backend versions do not match. Cannot display history."); return true; } @@ -293,21 +293,21 @@ define(["jquery", "app/rspamd", "d3", "footable"], } const o = process_history_data(data); const {items} = o; - rspamd.symbols.history = o.symbols; + common.symbols.history = o.symbols; - if (Object.prototype.hasOwnProperty.call(rspamd.tables, "history") && + if (Object.prototype.hasOwnProperty.call(common.tables, "history") && version === prevVersion) { - rspamd.tables.history.rows.load(items); + common.tables.history.rows.load(items); } else { - rspamd.destroyTable("history"); + libft.destroyTable("history"); // Is there a way to get an event when the table is destroyed? setTimeout(() => { - rspamd.initHistoryTable(data, items, "history", get_history_columns(data), false); + libft.initHistoryTable(data, items, "history", get_history_columns(data), false); }, 200); } prevVersion = version; } else { - rspamd.destroyTable("history"); + libft.destroyTable("history"); } }, complete: function () { $("#refresh").removeAttr("disabled").removeClass("disabled"); }, @@ -316,7 +316,7 @@ define(["jquery", "app/rspamd", "d3", "footable"], }; function initErrorsTable(rows) { - rspamd.tables.errors = FooTable.init("#errorsLog", { + common.tables.errors = FooTable.init("#errorsLog", { columns: [ {sorted: true, direction: "DESC", @@ -340,7 +340,7 @@ define(["jquery", "app/rspamd", "d3", "footable"], paging: { enabled: true, limit: 5, - size: rspamd.page_size.errors + size: common.page_size.errors }, filtering: { enabled: true, @@ -354,9 +354,9 @@ define(["jquery", "app/rspamd", "d3", "footable"], } ui.getErrors = function () { - if (rspamd.read_only) return; + if (common.read_only) return; - rspamd.query("errors", { + common.query("errors", { success: function (data) { const neighbours_data = data .filter((d) => d.status) // filter out unavailable neighbours @@ -364,14 +364,14 @@ define(["jquery", "app/rspamd", "d3", "footable"], const rows = [].concat.apply([], neighbours_data); $.each(rows, (i, item) => { item.ts = { - value: rspamd.unix_time_format(item.ts), + value: libft.unix_time_format(item.ts), options: { sortValue: item.ts } }; }); - if (Object.prototype.hasOwnProperty.call(rspamd.tables, "errors")) { - rspamd.tables.errors.rows.load(rows); + if (Object.prototype.hasOwnProperty.call(common.tables, "errors")) { + common.tables.errors.rows.load(rows); } else { initErrorsTable(rows); } @@ -386,8 +386,8 @@ define(["jquery", "app/rspamd", "d3", "footable"], }; - rspamd.set_page_size("history", $("#history_page_size").val()); - rspamd.bindHistoryTableEventHandlers("history", 8); + libft.set_page_size("history", $("#history_page_size").val()); + libft.bindHistoryTableEventHandlers("history", 8); $("#updateHistory").off("click"); $("#updateHistory").on("click", (e) => { @@ -402,10 +402,10 @@ define(["jquery", "app/rspamd", "d3", "footable"], if (!confirm("Are you sure you want to reset history log?")) { // eslint-disable-line no-alert return; } - rspamd.destroyTable("history"); - rspamd.destroyTable("errors"); + libft.destroyTable("history"); + libft.destroyTable("errors"); - rspamd.query("historyreset", { + common.query("historyreset", { success: function () { ui.getHistory(); ui.getErrors(); diff --git a/interface/js/app/libft.js b/interface/js/app/libft.js new file mode 100644 index 000000000..58262d22f --- /dev/null +++ b/interface/js/app/libft.js @@ -0,0 +1,380 @@ +/* global FooTable */ + +define(["jquery", "app/common", "footable"], + ($, common) => { + "use strict"; + const ui = {}; + + let pageSizeTimerId = null; + let pageSizeInvocationCounter = 0; + + function get_compare_function(table) { + const compare_functions = { + magnitude: function (e1, e2) { + return Math.abs(e2.score) - Math.abs(e1.score); + }, + name: function (e1, e2) { + return e1.name.localeCompare(e2.name); + }, + score: function (e1, e2) { + return e2.score - e1.score; + } + }; + + return compare_functions[common.getSelector("selSymOrder_" + table)]; + } + + function sort_symbols(o, compare_function) { + return Object.keys(o) + .map((key) => o[key]) + .sort(compare_function) + .map((e) => e.str) + .join("<br>\n"); + } + + + // Public functions + + ui.set_page_size = function (table, page_size, changeTablePageSize) { + const n = parseInt(page_size, 10); // HTML Input elements return string representing a number + if (n > 0) { + common.page_size[table] = n; + + if (changeTablePageSize && + $("#historyTable_" + table + " tbody").is(":parent")) { // Table is not empty + clearTimeout(pageSizeTimerId); + const t = FooTable.get("#historyTable_" + table); + if (t) { + pageSizeInvocationCounter = 0; + // Wait for input finish + pageSizeTimerId = setTimeout(() => t.pageSize(n), 1000); + } else if (++pageSizeInvocationCounter < 10) { + // Wait for FooTable instance ready + pageSizeTimerId = setTimeout(() => ui.set_page_size(table, n, true), 1000); + } + } + } + }; + + ui.bindHistoryTableEventHandlers = function (table, symbolsCol) { + function change_symbols_order(order) { + $(".btn-sym-" + table + "-" + order).addClass("active").siblings().removeClass("active"); + const compare_function = get_compare_function(table); + $.each(common.tables[table].rows.all, (i, row) => { + const cell_val = sort_symbols(common.symbols[table][i], compare_function); + row.cells[symbolsCol].val(cell_val, false, true); + }); + } + + $("#selSymOrder_" + table).unbind().change(function () { + const order = this.value; + change_symbols_order(order); + }); + $("#" + table + "_page_size").change((e) => ui.set_page_size(table, e.target.value, true)); + $(document).on("click", ".btn-sym-order-" + table + " input", function () { + const order = this.value; + $("#selSymOrder_" + table).val(order); + change_symbols_order(order); + }); + }; + + ui.destroyTable = function (table) { + if (common.tables[table]) { + common.tables[table].destroy(); + delete common.tables[table]; + } + }; + + ui.initHistoryTable = function (data, items, table, columns, expandFirst) { + /* eslint-disable no-underscore-dangle */ + FooTable.Cell.extend("collapse", function () { + // call the original method + this._super(); + // Copy cell classes to detail row tr element + this._setClasses(this.$detail); + }); + /* eslint-enable no-underscore-dangle */ + + /* eslint-disable consistent-this, no-underscore-dangle, one-var-declaration-per-line */ + FooTable.actionFilter = FooTable.Filtering.extend({ + construct: function (instance) { + this._super(instance); + this.actions = ["reject", "add header", "greylist", + "no action", "soft reject", "rewrite subject"]; + this.def = "Any action"; + this.$action = null; + }, + $create: function () { + this._super(); + const self = this; + const $form_grp = $("<div/>", { + class: "form-group d-inline-flex align-items-center" + }).append($("<label/>", { + class: "sr-only", + text: "Action" + })).prependTo(self.$form); + + $("<div/>", { + class: "form-check form-check-inline", + title: "Invert action match." + }).append( + self.$not = $("<input/>", { + type: "checkbox", + class: "form-check-input", + id: "not_" + table + }).on("change", {self: self}, self._onStatusDropdownChanged), + $("<label/>", { + class: "form-check-label", + for: "not_" + table, + text: "not" + }) + ).appendTo($form_grp); + + self.$action = $("<select/>", { + class: "form-select" + }).on("change", { + self: self + }, self._onStatusDropdownChanged).append( + $("<option/>", { + text: self.def + })).appendTo($form_grp); + + $.each(self.actions, (i, action) => { + self.$action.append($("<option/>").text(action)); + }); + }, + _onStatusDropdownChanged: function (e) { + const {self} = e.data; + const selected = self.$action.val(); + if (selected !== self.def) { + const not = self.$not.is(":checked"); + let query = null; + + if (selected === "reject") { + query = not ? "-reject OR soft" : "reject -soft"; + } else { + query = not ? selected.replace(/(\b\w+\b)/g, "-$1") : selected; + } + + self.addFilter("action", query, ["action"]); + } else { + self.removeFilter("action"); + } + self.filter(); + } + }); + /* eslint-enable consistent-this, no-underscore-dangle, one-var-declaration-per-line */ + + common.tables[table] = FooTable.init("#historyTable_" + table, { + columns: columns, + rows: items, + expandFirst: expandFirst, + paging: { + enabled: true, + limit: 5, + size: common.page_size[table] + }, + filtering: { + enabled: true, + position: "left", + connectors: false + }, + sorting: { + enabled: true + }, + components: { + filtering: FooTable.actionFilter + }, + on: { + "expand.ft.row": function (e, ft, row) { + setTimeout(() => { + const detail_row = row.$el.next(); + const order = common.getSelector("selSymOrder_" + table); + detail_row.find(".btn-sym-" + table + "-" + order) + .addClass("active").siblings().removeClass("active"); + }, 5); + } + } + }); + }; + + ui.preprocess_item = function (item) { + function escape_HTML_array(arr) { + arr.forEach((d, i) => { arr[i] = common.escapeHTML(d); }); + } + + for (const prop in item) { + if (!{}.hasOwnProperty.call(item, prop)) continue; + switch (prop) { + case "rcpt_mime": + case "rcpt_smtp": + escape_HTML_array(item[prop]); + break; + case "symbols": + Object.keys(item.symbols).forEach((key) => { + const sym = item.symbols[key]; + if (!sym.name) { + sym.name = key; + } + sym.name = common.escapeHTML(sym.name); + if (sym.description) { + sym.description = common.escapeHTML(sym.description); + } + + if (sym.options) { + escape_HTML_array(sym.options); + } + }); + break; + default: + if (typeof item[prop] === "string") { + item[prop] = common.escapeHTML(item[prop]); + } + } + } + + if (item.action === "clean" || item.action === "no action") { + item.action = "<div style='font-size:11px' class='badge text-bg-success'>" + item.action + "</div>"; + } else if (item.action === "rewrite subject" || item.action === "add header" || item.action === "probable spam") { + item.action = "<div style='font-size:11px' class='badge text-bg-warning'>" + item.action + "</div>"; + } else if (item.action === "spam" || item.action === "reject") { + item.action = "<div style='font-size:11px' class='badge text-bg-danger'>" + item.action + "</div>"; + } else { + item.action = "<div style='font-size:11px' class='badge text-bg-info'>" + item.action + "</div>"; + } + + const score_content = (item.score < item.required_score) + ? "<span class='text-success'>" + item.score.toFixed(2) + " / " + item.required_score + "</span>" + : "<span class='text-danger'>" + item.score.toFixed(2) + " / " + item.required_score + "</span>"; + + item.score = { + options: { + sortValue: item.score + }, + value: score_content + }; + }; + + ui.unix_time_format = function (tm) { + const date = new Date(tm ? tm * 1000 : 0); + return (common.locale) + ? date.toLocaleString(common.locale) + : date.toLocaleString(); + }; + + ui.process_history_v2 = function (data, table) { + // Display no more than rcpt_lim recipients + const rcpt_lim = 3; + const items = []; + const unsorted_symbols = []; + const compare_function = get_compare_function(table); + + $("#selSymOrder_" + table + ", label[for='selSymOrder_" + table + "']").show(); + + $.each(data.rows, + (i, item) => { + function more(p) { + const l = item[p].length; + return (l > rcpt_lim) ? " … (" + l + ")" : ""; + } + function format_rcpt(smtp, mime) { + let full = ""; + let shrt = ""; + if (smtp) { + full = "[" + item.rcpt_smtp.join(", ") + "] "; + shrt = "[" + item.rcpt_smtp.slice(0, rcpt_lim).join(",​") + more("rcpt_smtp") + "]"; + if (mime) { + full += " "; + shrt += " "; + } + } + if (mime) { + full += item.rcpt_mime.join(", "); + shrt += item.rcpt_mime.slice(0, rcpt_lim).join(",​") + more("rcpt_mime"); + } + return {full: full, shrt: shrt}; + } + + function get_symbol_class(name, score) { + if (name.match(/^GREYLIST$/)) { + return "symbol-special"; + } + + if (score < 0) { + return "symbol-negative"; + } else if (score > 0) { + return "symbol-positive"; + } + return null; + } + + ui.preprocess_item(item); + Object.values(item.symbols).forEach((sym) => { + sym.str = '<span class="symbol-default ' + get_symbol_class(sym.name, sym.score) + '"><strong>'; + + if (sym.description) { + sym.str += '<abbr title="' + sym.description + '">' + sym.name + "</abbr>"; + } else { + sym.str += sym.name; + } + sym.str += "</strong> (" + sym.score + ")</span>"; + + if (sym.options) { + sym.str += " [" + sym.options.join(",") + "]"; + } + }); + unsorted_symbols.push(item.symbols); + item.symbols = sort_symbols(item.symbols, compare_function); + if (table === "scan") { + item.unix_time = (new Date()).getTime() / 1000; + } + item.time = { + value: ui.unix_time_format(item.unix_time), + options: { + sortValue: item.unix_time + } + }; + item.time_real = item.time_real.toFixed(3); + item.id = item["message-id"]; + + if (table === "history") { + let rcpt = {}; + if (!item.rcpt_mime.length) { + rcpt = format_rcpt(true, false); + } else if ( + $(item.rcpt_mime).not(item.rcpt_smtp).length !== 0 || + $(item.rcpt_smtp).not(item.rcpt_mime).length !== 0 + ) { + rcpt = format_rcpt(true, true); + } else { + rcpt = format_rcpt(false, true); + } + item.rcpt_mime_short = rcpt.shrt; + item.rcpt_mime = rcpt.full; + + if (item.sender_mime !== item.sender_smtp) { + item.sender_mime = "[" + item.sender_smtp + "] " + item.sender_mime; + } + } + items.push(item); + }); + + return {items: items, symbols: unsorted_symbols}; + }; + + ui.waitForRowsDisplayed = function (table, rows_total, callback, iteration) { + let i = (typeof iteration === "undefined") ? 10 : iteration; + const num_rows = $("#historyTable_" + table + " > tbody > tr:not(.footable-detail-row)").length; + if (num_rows === common.page_size[table] || + num_rows === rows_total) { + return callback(); + } else if (--i) { + setTimeout(() => { + ui.waitForRowsDisplayed(table, rows_total, callback, i); + }, 500); + } + return null; + }; + + return ui; + }); diff --git a/interface/js/app/rspamd.js b/interface/js/app/rspamd.js index b1d71a5c9..938f048e7 100644 --- a/interface/js/app/rspamd.js +++ b/interface/js/app/rspamd.js @@ -23,48 +23,20 @@ THE SOFTWARE. */ -/* global jQuery, FooTable, require, Visibility */ +/* global require, Visibility */ -define(["jquery", "nprogress", "stickytabs", "visibility", +define(["jquery", "app/common", "stickytabs", "visibility", "bootstrap", "fontawesome"], -($, NProgress) => { +($, common) => { "use strict"; - const ui = { - chartLegend: [ - {label: "reject", color: "#FF0000"}, - {label: "soft reject", color: "#BF8040"}, - {label: "rewrite subject", color: "#FF6600"}, - {label: "add header", color: "#FFAD00"}, - {label: "greylist", color: "#436EEE"}, - {label: "no action", color: "#66CC00"} - ], - page_size: { - scan: 25, - errors: 25, - history: 25 - }, - symbols: { - scan: [], - history: [] - } - }; + const ui = {}; const defaultAjaxTimeout = 20000; const ajaxTimeoutBox = ".popover #settings-popover #ajax-timeout"; const graphs = {}; - const tables = {}; - let neighbours = []; // list of clusters let checked_server = "All SERVERS"; const timer_id = []; - let pageSizeTimerId = null; - let pageSizeInvocationCounter = 0; - let locale = (localStorage.getItem("selected_locale") === "custom") ? localStorage.getItem("custom_locale") : null; - - NProgress.configure({ - minimum: 0.01, - showSpinner: false, - }); function ajaxSetup(ajax_timeout, setFieldValue, saveToLocalStorage) { const timeout = (ajax_timeout && ajax_timeout >= 0) ? ajax_timeout : defaultAjaxTimeout; @@ -92,7 +64,7 @@ define(["jquery", "nprogress", "stickytabs", "visibility", } function disconnect() { - [graphs, tables].forEach((o) => { + [graphs, common.tables].forEach((o) => { Object.keys(o).forEach((key) => { o[key].destroy(); delete o[key]; @@ -108,12 +80,6 @@ define(["jquery", "nprogress", "stickytabs", "visibility", ui.connect(); } - // Get selectors' current state - function getSelector(id) { - const e = document.getElementById(id); - return e.options[e.selectedIndex].value; - } - function tabClick(id) { let tab_id = id; if ($(id).attr("disabled")) return; @@ -180,7 +146,7 @@ define(["jquery", "nprogress", "stickytabs", "visibility", break; case "#throughput_nav": require(["app/graph"], (module) => { - const selData = getSelector("selData"); // Graph's dataset selector state + const selData = common.getSelector("selData"); // Graph's dataset selector state const step = { day: 60000, week: 300000 @@ -192,8 +158,8 @@ define(["jquery", "nprogress", "stickytabs", "visibility", refreshInterval = null; } setAutoRefresh(refreshInterval, "throughput", - () => module.draw(graphs, neighbours, checked_server, selData)); - if (id !== "#autoRefresh") module.draw(graphs, neighbours, checked_server, selData); + () => module.draw(graphs, common.neighbours, checked_server, selData)); + if (id !== "#autoRefresh") module.draw(graphs, common.neighbours, checked_server, selData); $(".preset").hide(); $(".history").hide(); @@ -245,72 +211,16 @@ define(["jquery", "nprogress", "stickytabs", "visibility", }, (id === "#autoRefresh") ? 0 : 1000); } - function getPassword() { - return sessionStorage.getItem("Password"); - } - - function get_compare_function(table) { - const compare_functions = { - magnitude: function (e1, e2) { - return Math.abs(e2.score) - Math.abs(e1.score); - }, - name: function (e1, e2) { - return e1.name.localeCompare(e2.name); - }, - score: function (e1, e2) { - return e2.score - e1.score; - } - }; - - return compare_functions[getSelector("selSymOrder_" + table)]; - } - function saveCredentials(password) { sessionStorage.setItem("Password", password); } - function set_page_size(table, page_size, changeTablePageSize) { - const n = parseInt(page_size, 10); // HTML Input elements return string representing a number - if (n > 0) { - ui.page_size[table] = n; - - if (changeTablePageSize && - $("#historyTable_" + table + " tbody").is(":parent")) { // Table is not empty - clearTimeout(pageSizeTimerId); - const t = FooTable.get("#historyTable_" + table); - if (t) { - pageSizeInvocationCounter = 0; - // Wait for input finish - pageSizeTimerId = setTimeout(() => t.pageSize(n), 1000); - } else if (++pageSizeInvocationCounter < 10) { - // Wait for FooTable instance ready - pageSizeTimerId = setTimeout(() => set_page_size(table, n, true), 1000); - } - } - } - } - - function sort_symbols(o, compare_function) { - return Object.keys(o) - .map((key) => o[key]) - .sort(compare_function) - .map((e) => e.str) - .join("<br>\n"); - } - - function unix_time_format(tm) { - const date = new Date(tm ? tm * 1000 : 0); - return (locale) - ? date.toLocaleString(locale) - : date.toLocaleString(); - } - function displayUI() { // In many browsers local storage can only store string. // So when we store the boolean true or false, it actually stores the strings "true" or "false". - ui.read_only = sessionStorage.getItem("read_only") === "true"; + common.read_only = sessionStorage.getItem("read_only") === "true"; - ui.query("auth", { + common.query("auth", { success: function (neighbours_status) { $("#selSrv").empty(); $("#selSrv").append($('<option value="All SERVERS">All SERVERS</option>')); @@ -326,7 +236,7 @@ define(["jquery", "nprogress", "stickytabs", "visibility", complete: function () { ajaxSetup(localStorage.getItem("ajax_timeout")); - if (ui.read_only) { + if (common.read_only) { $(".ro-disable").attr("disabled", true); $(".ro-hide").hide(); } else { @@ -343,98 +253,8 @@ define(["jquery", "nprogress", "stickytabs", "visibility", }); } - function alertMessage(alertClass, alertText) { - const a = $("<div class=\"alert " + alertClass + " alert-dismissible fade in show\">" + - "<button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"alert\" title=\"Dismiss\"></button>" + - "<strong>" + alertText + "</strong>"); - $(".notification-area").append(a); - - setTimeout(() => { - $(a).fadeTo(500, 0).slideUp(500, function () { - $(this).alert("close"); - }); - }, 5000); - } - - function queryServer(neighbours_status, ind, req_url, o) { - neighbours_status[ind].checked = false; - neighbours_status[ind].data = {}; - neighbours_status[ind].status = false; - const req_params = { - jsonp: false, - data: o.data, - headers: $.extend({Password: getPassword()}, o.headers), - url: neighbours_status[ind].url + req_url, - xhr: function () { - const xhr = $.ajaxSettings.xhr(); - // Download progress - if (req_url !== "neighbours") { - xhr.addEventListener("progress", (e) => { - if (e.lengthComputable) { - neighbours_status[ind].percentComplete = e.loaded / e.total; - const percentComplete = neighbours_status - .reduce((prev, curr) => (curr.percentComplete ? curr.percentComplete + prev : prev), 0); - NProgress.set(percentComplete / neighbours_status.length); - } - }, false); - } - return xhr; - }, - success: function (json) { - neighbours_status[ind].checked = true; - neighbours_status[ind].status = true; - neighbours_status[ind].data = json; - }, - error: function (jqXHR, textStatus, errorThrown) { - neighbours_status[ind].checked = true; - function errorMessage() { - alertMessage("alert-error", neighbours_status[ind].name + " > " + - (o.errorMessage ? o.errorMessage : "Request failed") + - (errorThrown ? ": " + errorThrown : "")); - } - if (o.error) { - o.error(neighbours_status[ind], - jqXHR, textStatus, errorThrown); - } else if (o.errorOnceId) { - const alert_status = o.errorOnceId + neighbours_status[ind].name; - if (!(alert_status in sessionStorage)) { - sessionStorage.setItem(alert_status, true); - errorMessage(); - } - } else { - errorMessage(); - } - }, - complete: function (jqXHR) { - if (neighbours_status.every((elt) => elt.checked)) { - if (neighbours_status.some((elt) => elt.status)) { - if (o.success) { - o.success(neighbours_status, jqXHR); - } else { - alertMessage("alert-success", "Request completed"); - } - } else { - alertMessage("alert-error", "Request failed"); - } - if (o.complete) o.complete(); - NProgress.done(); - } - }, - statusCode: o.statusCode - }; - if (o.method) { - req_params.method = o.method; - } - if (o.params) { - $.each(o.params, (k, v) => { - req_params[k] = v; - }); - } - $.ajax(req_params); - } // Public functions - ui.alertMessage = alertMessage; ui.connect = function () { // Prevent locking out of the WebUI if timeout is too low. @@ -484,7 +304,7 @@ define(["jquery", "nprogress", "stickytabs", "visibility", return; } - ui.query("auth", { + common.query("auth", { headers: { Password: password }, @@ -503,7 +323,7 @@ define(["jquery", "nprogress", "stickytabs", "visibility", if (textStatus.statusText === "Unauthorized") { invalidFeedback("#authUnauthorizedFeedback"); } else { - ui.alertMessage("alert-modal alert-error", textStatus.statusText); + common.alertMessage("alert-modal alert-error", textStatus.statusText); } $("#connectPassword").val(""); $("#connectPassword").focus(); @@ -518,420 +338,6 @@ define(["jquery", "nprogress", "stickytabs", "visibility", }); }; - ui.getPassword = getPassword; - ui.getSelector = getSelector; - - /** - * @param {string} url - A string containing the URL to which the request is sent - * @param {Object} [options] - A set of key/value pairs that configure the Ajax request. All settings are optional. - * - * @param {Function} [options.complete] - A function to be called when the requests to all neighbours complete. - * @param {Object|string|Array} [options.data] - Data to be sent to the server. - * @param {Function} [options.error] - A function to be called if the request fails. - * @param {string} [options.errorMessage] - Text to display in the alert message if the request fails. - * @param {string} [options.errorOnceId] - A prefix of the alert ID to be added to the session storage. If the - * parameter is set, the error for each server will be displayed only once per session. - * @param {Object} [options.headers] - An object of additional header key/value pairs to send along with requests - * using the XMLHttpRequest transport. - * @param {string} [options.method] - The HTTP method to use for the request. - * @param {Object} [options.params] - An object of additional jQuery.ajax() settings key/value pairs. - * @param {string} [options.server] - A server to which send the request. - * @param {Function} [options.success] - A function to be called if the request succeeds. - * - * @returns {undefined} - */ - ui.query = function (url, options) { - // Force options to be an object - const o = options || {}; - Object.keys(o).forEach((option) => { - if (["complete", "data", "error", "errorMessage", "errorOnceId", "headers", "method", "params", "server", - "statusCode", "success"] - .indexOf(option) < 0) { - throw new Error("Unknown option: " + option); - } - }); - - let neighbours_status = [{ - name: "local", - host: "local", - url: "", - }]; - o.server = o.server || checked_server; - if (o.server === "All SERVERS") { - queryServer(neighbours_status, 0, "neighbours", { - success: function (json) { - const [{data}] = json; - if (jQuery.isEmptyObject(data)) { - neighbours = { - local: { - host: window.location.host, - url: window.location.origin + window.location.pathname - } - }; - } else { - neighbours = data; - } - neighbours_status = []; - $.each(neighbours, (ind) => { - neighbours_status.push({ - name: ind, - host: neighbours[ind].host, - url: neighbours[ind].url, - }); - }); - $.each(neighbours_status, (ind) => { - queryServer(neighbours_status, ind, url, o); - }); - }, - errorMessage: "Cannot receive neighbours data" - }); - } else { - if (o.server !== "local") { - neighbours_status = [{ - name: o.server, - host: neighbours[o.server].host, - url: neighbours[o.server].url, - }]; - } - queryServer(neighbours_status, 0, url, o); - } - }; - - // Scan and History shared functions - - ui.tables = tables; - ui.unix_time_format = unix_time_format; - ui.set_page_size = set_page_size; - - ui.bindHistoryTableEventHandlers = function (table, symbolsCol) { - function change_symbols_order(order) { - $(".btn-sym-" + table + "-" + order).addClass("active").siblings().removeClass("active"); - const compare_function = get_compare_function(table); - $.each(tables[table].rows.all, (i, row) => { - const cell_val = sort_symbols(ui.symbols[table][i], compare_function); - row.cells[symbolsCol].val(cell_val, false, true); - }); - } - - $("#selSymOrder_" + table).unbind().change(function () { - const order = this.value; - change_symbols_order(order); - }); - $("#" + table + "_page_size").change((e) => set_page_size(table, e.target.value, true)); - $(document).on("click", ".btn-sym-order-" + table + " input", function () { - const order = this.value; - $("#selSymOrder_" + table).val(order); - change_symbols_order(order); - }); - }; - - ui.destroyTable = function (table) { - if (tables[table]) { - tables[table].destroy(); - delete tables[table]; - } - }; - - - ui.initHistoryTable = function (data, items, table, columns, expandFirst) { - /* eslint-disable no-underscore-dangle */ - FooTable.Cell.extend("collapse", function () { - // call the original method - this._super(); - // Copy cell classes to detail row tr element - this._setClasses(this.$detail); - }); - /* eslint-enable no-underscore-dangle */ - - /* eslint-disable consistent-this, no-underscore-dangle, one-var-declaration-per-line */ - FooTable.actionFilter = FooTable.Filtering.extend({ - construct: function (instance) { - this._super(instance); - this.actions = ["reject", "add header", "greylist", - "no action", "soft reject", "rewrite subject"]; - this.def = "Any action"; - this.$action = null; - }, - $create: function () { - this._super(); - const self = this; - const $form_grp = $("<div/>", { - class: "form-group d-inline-flex align-items-center" - }).append($("<label/>", { - class: "sr-only", - text: "Action" - })).prependTo(self.$form); - - $("<div/>", { - class: "form-check form-check-inline", - title: "Invert action match." - }).append( - self.$not = $("<input/>", { - type: "checkbox", - class: "form-check-input", - id: "not_" + table - }).on("change", {self: self}, self._onStatusDropdownChanged), - $("<label/>", { - class: "form-check-label", - for: "not_" + table, - text: "not" - }) - ).appendTo($form_grp); - - self.$action = $("<select/>", { - class: "form-select" - }).on("change", { - self: self - }, self._onStatusDropdownChanged).append( - $("<option/>", { - text: self.def - })).appendTo($form_grp); - - $.each(self.actions, (i, action) => { - self.$action.append($("<option/>").text(action)); - }); - }, - _onStatusDropdownChanged: function (e) { - const {self} = e.data; - const selected = self.$action.val(); - if (selected !== self.def) { - const not = self.$not.is(":checked"); - let query = null; - - if (selected === "reject") { - query = not ? "-reject OR soft" : "reject -soft"; - } else { - query = not ? selected.replace(/(\b\w+\b)/g, "-$1") : selected; - } - - self.addFilter("action", query, ["action"]); - } else { - self.removeFilter("action"); - } - self.filter(); - } - }); - /* eslint-enable consistent-this, no-underscore-dangle, one-var-declaration-per-line */ - - tables[table] = FooTable.init("#historyTable_" + table, { - columns: columns, - rows: items, - expandFirst: expandFirst, - paging: { - enabled: true, - limit: 5, - size: ui.page_size[table] - }, - filtering: { - enabled: true, - position: "left", - connectors: false - }, - sorting: { - enabled: true - }, - components: { - filtering: FooTable.actionFilter - }, - on: { - "expand.ft.row": function (e, ft, row) { - setTimeout(() => { - const detail_row = row.$el.next(); - const order = getSelector("selSymOrder_" + table); - detail_row.find(".btn-sym-" + table + "-" + order) - .addClass("active").siblings().removeClass("active"); - }, 5); - } - } - }); - }; - - ui.escapeHTML = function (string) { - const htmlEscaper = /[&<>"'/`=]/g; - const htmlEscapes = { - "&": "&", - "<": "<", - ">": ">", - "\"": """, - "'": "'", - "/": "/", - "`": "`", - "=": "=" - }; - return String(string).replace(htmlEscaper, (match) => htmlEscapes[match]); - }; - - ui.preprocess_item = function (item) { - function escape_HTML_array(arr) { - arr.forEach((d, i) => { arr[i] = ui.escapeHTML(d); }); - } - - for (const prop in item) { - if (!{}.hasOwnProperty.call(item, prop)) continue; - switch (prop) { - case "rcpt_mime": - case "rcpt_smtp": - escape_HTML_array(item[prop]); - break; - case "symbols": - Object.keys(item.symbols).forEach((key) => { - const sym = item.symbols[key]; - if (!sym.name) { - sym.name = key; - } - sym.name = ui.escapeHTML(sym.name); - if (sym.description) { - sym.description = ui.escapeHTML(sym.description); - } - - if (sym.options) { - escape_HTML_array(sym.options); - } - }); - break; - default: - if (typeof item[prop] === "string") { - item[prop] = ui.escapeHTML(item[prop]); - } - } - } - - if (item.action === "clean" || item.action === "no action") { - item.action = "<div style='font-size:11px' class='badge text-bg-success'>" + item.action + "</div>"; - } else if (item.action === "rewrite subject" || item.action === "add header" || item.action === "probable spam") { - item.action = "<div style='font-size:11px' class='badge text-bg-warning'>" + item.action + "</div>"; - } else if (item.action === "spam" || item.action === "reject") { - item.action = "<div style='font-size:11px' class='badge text-bg-danger'>" + item.action + "</div>"; - } else { - item.action = "<div style='font-size:11px' class='badge text-bg-info'>" + item.action + "</div>"; - } - - const score_content = (item.score < item.required_score) - ? "<span class='text-success'>" + item.score.toFixed(2) + " / " + item.required_score + "</span>" - : "<span class='text-danger'>" + item.score.toFixed(2) + " / " + item.required_score + "</span>"; - - item.score = { - options: { - sortValue: item.score - }, - value: score_content - }; - }; - - ui.process_history_v2 = function (data, table) { - // Display no more than rcpt_lim recipients - const rcpt_lim = 3; - const items = []; - const unsorted_symbols = []; - const compare_function = get_compare_function(table); - - $("#selSymOrder_" + table + ", label[for='selSymOrder_" + table + "']").show(); - - $.each(data.rows, - (i, item) => { - function more(p) { - const l = item[p].length; - return (l > rcpt_lim) ? " … (" + l + ")" : ""; - } - function format_rcpt(smtp, mime) { - let full = ""; - let shrt = ""; - if (smtp) { - full = "[" + item.rcpt_smtp.join(", ") + "] "; - shrt = "[" + item.rcpt_smtp.slice(0, rcpt_lim).join(",​") + more("rcpt_smtp") + "]"; - if (mime) { - full += " "; - shrt += " "; - } - } - if (mime) { - full += item.rcpt_mime.join(", "); - shrt += item.rcpt_mime.slice(0, rcpt_lim).join(",​") + more("rcpt_mime"); - } - return {full: full, shrt: shrt}; - } - - function get_symbol_class(name, score) { - if (name.match(/^GREYLIST$/)) { - return "symbol-special"; - } - - if (score < 0) { - return "symbol-negative"; - } else if (score > 0) { - return "symbol-positive"; - } - return null; - } - - ui.preprocess_item(item); - Object.values(item.symbols).forEach((sym) => { - sym.str = '<span class="symbol-default ' + get_symbol_class(sym.name, sym.score) + '"><strong>'; - - if (sym.description) { - sym.str += '<abbr title="' + sym.description + '">' + sym.name + "</abbr>"; - } else { - sym.str += sym.name; - } - sym.str += "</strong> (" + sym.score + ")</span>"; - - if (sym.options) { - sym.str += " [" + sym.options.join(",") + "]"; - } - }); - unsorted_symbols.push(item.symbols); - item.symbols = sort_symbols(item.symbols, compare_function); - if (table === "scan") { - item.unix_time = (new Date()).getTime() / 1000; - } - item.time = { - value: unix_time_format(item.unix_time), - options: { - sortValue: item.unix_time - } - }; - item.time_real = item.time_real.toFixed(3); - item.id = item["message-id"]; - - if (table === "history") { - let rcpt = {}; - if (!item.rcpt_mime.length) { - rcpt = format_rcpt(true, false); - } else if ( - $(item.rcpt_mime).not(item.rcpt_smtp).length !== 0 || - $(item.rcpt_smtp).not(item.rcpt_mime).length !== 0 - ) { - rcpt = format_rcpt(true, true); - } else { - rcpt = format_rcpt(false, true); - } - item.rcpt_mime_short = rcpt.shrt; - item.rcpt_mime = rcpt.full; - - if (item.sender_mime !== item.sender_smtp) { - item.sender_mime = "[" + item.sender_smtp + "] " + item.sender_mime; - } - } - items.push(item); - }); - - return {items: items, symbols: unsorted_symbols}; - }; - - ui.waitForRowsDisplayed = function (table, rows_total, callback, iteration) { - let i = (typeof iteration === "undefined") ? 10 : iteration; - const num_rows = $("#historyTable_" + table + " > tbody > tr:not(.footable-detail-row)").length; - if (num_rows === ui.page_size[table] || - num_rows === rows_total) { - return callback(); - } else if (--i) { - setTimeout(() => { - ui.waitForRowsDisplayed(table, rows_total, callback, i); - }, 500); - } - return null; - }; - (function initSettings() { let selected_locale = null; @@ -950,22 +356,22 @@ define(["jquery", "nprogress", "stickytabs", "visibility", now.toLocaleString(custom_locale); if (saveToLocalStorage) localStorage.setItem("custom_locale", custom_locale); - locale = (selected_locale === "custom") ? custom_locale : null; + common.locale = (selected_locale === "custom") ? custom_locale : null; toggle_form_group_class("invalid", "valid"); } catch (err) { - locale = null; + common.locale = null; toggle_form_group_class("valid", "invalid"); } } else { if (saveToLocalStorage) localStorage.setItem("custom_locale", null); - locale = null; + common.locale = null; $(localeTextbox).removeClass("is-valid is-invalid"); } // Display date example $(".popover #settings-popover #date-example").text( - (locale) - ? now.toLocaleString(locale) + (common.locale) + ? now.toLocaleString(common.locale) : now.toLocaleString() ); } diff --git a/interface/js/app/selectors.js b/interface/js/app/selectors.js index 2a0097e79..53240d838 100644 --- a/interface/js/app/selectors.js +++ b/interface/js/app/selectors.js @@ -1,5 +1,5 @@ -define(["jquery", "app/rspamd"], - ($, rspamd) => { +define(["jquery", "app/common"], + ($, common) => { "use strict"; const ui = {}; @@ -11,23 +11,23 @@ define(["jquery", "app/rspamd"], } function get_server() { - const checked_server = rspamd.getSelector("selSrv"); + const checked_server = common.getSelector("selSrv"); return (checked_server === "All SERVERS") ? "local" : checked_server; } function checkMsg(data) { const selector = $("#selectorsSelArea").val(); - rspamd.query("plugins/selectors/check_message?selector=" + encodeURIComponent(selector), { + common.query("plugins/selectors/check_message?selector=" + encodeURIComponent(selector), { data: data, method: "POST", success: function (neighbours_status) { const json = neighbours_status[0].data; if (json.success) { - rspamd.alertMessage("alert-success", "Message successfully processed"); + common.alertMessage("alert-success", "Message successfully processed"); $("#selectorsResArea") .val(Object.prototype.hasOwnProperty.call(json, "data") ? json.data.toString() : ""); } else { - rspamd.alertMessage("alert-error", "Unexpected error processing message"); + common.alertMessage("alert-error", "Unexpected error processing message"); } }, server: get_server() @@ -40,8 +40,8 @@ define(["jquery", "app/rspamd"], enable_disable_check_btn(); } const selector = $("#selectorsSelArea").val(); - if (selector.length && !rspamd.read_only) { - rspamd.query("plugins/selectors/check_selector?selector=" + encodeURIComponent(selector), { + if (selector.length && !common.read_only) { + common.query("plugins/selectors/check_selector?selector=" + encodeURIComponent(selector), { method: "GET", success: function (json) { if (json[0].data.success) { @@ -70,7 +70,7 @@ define(["jquery", "app/rspamd"], } function getList(list) { - rspamd.query("plugins/selectors/list_" + list, { + common.query("plugins/selectors/list_" + list, { method: "GET", success: function (neighbours_status) { const json = neighbours_status[0].data; @@ -85,7 +85,7 @@ define(["jquery", "app/rspamd"], } ui.displayUI = function () { - if (!rspamd.read_only && + if (!common.read_only && !$("#selectorsTable-extractors>tbody>tr").length && !$("#selectorsTable-transforms>tbody>tr").length) buildLists(); if (!$("#selectorsSelArea").is(".is-valid, .is-invalid")) checkSelectors(); diff --git a/interface/js/app/stats.js b/interface/js/app/stats.js index ddf641d69..04b4a75c5 100644 --- a/interface/js/app/stats.js +++ b/interface/js/app/stats.js @@ -22,8 +22,8 @@ THE SOFTWARE. */ -define(["jquery", "app/rspamd", "d3pie", "d3"], - ($, rspamd, D3Pie, d3) => { +define(["jquery", "app/common", "d3pie", "d3"], + ($, common, D3Pie, d3) => { "use strict"; // @ ms to date function msToTime(seconds) { @@ -254,7 +254,7 @@ define(["jquery", "app/rspamd", "d3pie", "d3"], ["no action", "soft reject", "add header", "rewrite subject", "greylist", "reject"] .forEach((action) => { data.push({ - color: rspamd.chartLegend.find((item) => item.label === action).color, + color: common.chartLegend.find((item) => item.label === action).color, label: action, value: actions[action] }); @@ -266,7 +266,7 @@ define(["jquery", "app/rspamd", "d3pie", "d3"], // Public API const ui = { statWidgets: function (graphs, checked_server) { - rspamd.query("stat", { + common.query("stat", { success: function (neighbours_status) { const neighbours_sum = { version: neighbours_status[0].data.version, @@ -315,7 +315,7 @@ define(["jquery", "app/rspamd", "d3pie", "d3"], const alerted = "alerted_stats_legacy_" + neighbours_status[e].name; promises.push($.ajax({ url: neighbours_status[e].url + "auth", - headers: {Password: rspamd.getPassword()}, + headers: {Password: common.getPassword()}, success: function (data) { sessionStorage.removeItem(alerted); ["config_id", "version", "uptime"].forEach((p) => { @@ -326,7 +326,7 @@ define(["jquery", "app/rspamd", "d3pie", "d3"], error: function (jqXHR, textStatus, errorThrown) { if (!(alerted in sessionStorage)) { sessionStorage.setItem(alerted, true); - rspamd.alertMessage("alert-error", neighbours_status[e].name + " > " + + common.alertMessage("alert-error", neighbours_status[e].name + " > " + "Cannot receive legacy stats data" + (errorThrown ? ": " + errorThrown : "")); } process_node_stat(e); diff --git a/interface/js/app/symbols.js b/interface/js/app/symbols.js index b00fc990f..1e3fb5de7 100644 --- a/interface/js/app/symbols.js +++ b/interface/js/app/symbols.js @@ -24,8 +24,8 @@ /* global FooTable */ -define(["jquery", "app/rspamd", "footable"], - ($, rspamd) => { +define(["jquery", "app/common", "footable"], + ($, common) => { "use strict"; const ui = {}; let altered = {}; @@ -41,10 +41,10 @@ define(["jquery", "app/rspamd", "footable"], const values = []; Object.entries(altered).forEach(([key, value]) => values.push({name: key, value: value})); - rspamd.query("./savesymbols", { + common.query("./savesymbols", { success: function () { clear_altered(); - rspamd.alertMessage("alert-modal alert-success", "Symbols successfully saved"); + common.alertMessage("alert-modal alert-success", "Symbols successfully saved"); }, complete: () => $("#save-alert button").removeAttr("disabled", true), errorMessage: "Save symbols error", @@ -124,7 +124,7 @@ define(["jquery", "app/rspamd", "footable"], // @get symbols into modal form ui.getSymbols = function (checked_server) { clear_altered(); - rspamd.query("symbols", { + common.query("symbols", { success: function (json) { const [{data}] = json; const items = process_symbols_data(data); @@ -182,7 +182,7 @@ define(["jquery", "app/rspamd", "footable"], }); /* eslint-enable consistent-this, no-underscore-dangle, one-var-declaration-per-line */ - rspamd.tables.symbols = FooTable.init("#symbolsTable", { + common.tables.symbols = FooTable.init("#symbolsTable", { columns: [ {sorted: true, direction: "ASC", name: "group", title: "Group"}, {name: "symbol", title: "Symbol"}, @@ -213,7 +213,7 @@ define(["jquery", "app/rspamd", "footable"], }, on: { "ready.ft.table": function () { - if (rspamd.read_only) { + if (common.read_only) { $(".mb-disabled").attr("disabled", true); } } @@ -228,11 +228,11 @@ define(["jquery", "app/rspamd", "footable"], $("#updateSymbols").on("click", (e) => { e.preventDefault(); clear_altered(); - const checked_server = rspamd.getSelector("selSrv"); - rspamd.query("symbols", { + const checked_server = common.getSelector("selSrv"); + common.query("symbols", { success: function (data) { const [items] = process_symbols_data(data[0].data); - rspamd.tables.symbols.rows.load(items); + common.tables.symbols.rows.load(items); }, server: (checked_server === "All SERVERS") ? "local" : checked_server }); diff --git a/interface/js/app/upload.js b/interface/js/app/upload.js index c474396ae..e5ddb1c84 100644 --- a/interface/js/app/upload.js +++ b/interface/js/app/upload.js @@ -24,8 +24,8 @@ /* global require */ -define(["jquery", "app/rspamd"], - ($, rspamd) => { +define(["jquery", "app/common", "app/libft"], + ($, common, libft) => { "use strict"; const ui = {}; @@ -47,15 +47,15 @@ define(["jquery", "app/rspamd"], } function server() { - if (rspamd.getSelector("selSrv") === "All SERVERS" && - rspamd.getSelector("selLearnServers") === "random") { + if (common.getSelector("selSrv") === "All SERVERS" && + common.getSelector("selLearnServers") === "random") { const servers = $("#selSrv option").slice(1).map((_, o) => o.value); return servers[Math.floor(Math.random() * servers.length)]; } return null; } - rspamd.query(url, { + common.query(url, { data: data, params: { processData: false, @@ -64,9 +64,9 @@ define(["jquery", "app/rspamd"], headers: headers, success: function (json, jqXHR) { cleanTextUpload(source); - rspamd.alertMessage("alert-success", "Data successfully uploaded"); + common.alertMessage("alert-success", "Data successfully uploaded"); if (jqXHR.status !== 200) { - rspamd.alertMessage("alert-info", jqXHR.statusText); + common.alertMessage("alert-info", jqXHR.statusText); } }, server: server() @@ -90,7 +90,8 @@ define(["jquery", "app/rspamd"], style: {minwidth: 82} }, { name: "passthrough_module", - title: '<div title="The module that has set the pre-result">Pass-through module</div>' + title: '<div title="The module that has set the pre-result">Pass-through module</div>', + breakpoints: "xs sm md" }, { name: "score", title: "Score", @@ -119,6 +120,7 @@ define(["jquery", "app/rspamd"], style: {maxWidth: 72}, sortValue: function (val) { return Number(val); } }, { + classes: "history-col-time", sorted: true, direction: "DESC", name: "time", @@ -128,13 +130,13 @@ define(["jquery", "app/rspamd"], } function get_server() { - const checked_server = rspamd.getSelector("selSrv"); + const checked_server = common.getSelector("selSrv"); return (checked_server === "All SERVERS") ? "local" : checked_server; } // @upload text function scanText(data, headers) { - rspamd.query("checkv2", { + common.query("checkv2", { data: data, params: { processData: false, @@ -144,7 +146,7 @@ define(["jquery", "app/rspamd"], success: function (neighbours_status) { function scrollTop(rows_total) { // Is there a way to get an event when all rows are loaded? - rspamd.waitForRowsDisplayed("scan", rows_total, () => { + libft.waitForRowsDisplayed("scan", rows_total, () => { $("#cleanScanHistory").removeAttr("disabled", true); $("html, body").animate({ scrollTop: $("#scanResult").offset().top @@ -154,40 +156,40 @@ define(["jquery", "app/rspamd"], const json = neighbours_status[0].data; if (json.action) { - rspamd.alertMessage("alert-success", "Data successfully scanned"); + common.alertMessage("alert-success", "Data successfully scanned"); const rows_total = $("#historyTable_scan > tbody > tr:not(.footable-detail-row)").length + 1; - const o = rspamd.process_history_v2({rows: [json]}, "scan"); + const o = libft.process_history_v2({rows: [json]}, "scan"); const {items} = o; - rspamd.symbols.scan.push(o.symbols[0]); + common.symbols.scan.push(o.symbols[0]); - if (Object.prototype.hasOwnProperty.call(rspamd.tables, "scan")) { - rspamd.tables.scan.rows.load(items, true); + if (Object.prototype.hasOwnProperty.call(common.tables, "scan")) { + common.tables.scan.rows.load(items, true); scrollTop(rows_total); } else { - rspamd.destroyTable("scan"); + libft.destroyTable("scan"); require(["footable"], () => { // Is there a way to get an event when the table is destroyed? setTimeout(() => { - rspamd.initHistoryTable(data, items, "scan", columns_v2(), true); + libft.initHistoryTable(data, items, "scan", columns_v2(), true); scrollTop(rows_total); }, 200); }); } } else { - rspamd.alertMessage("alert-error", "Cannot scan data"); + common.alertMessage("alert-error", "Cannot scan data"); } }, errorMessage: "Cannot upload data", statusCode: { 404: function () { - rspamd.alertMessage("alert-error", "Cannot upload data, no server found"); + common.alertMessage("alert-error", "Cannot upload data, no server found"); }, 500: function () { - rspamd.alertMessage("alert-error", "Cannot tokenize message: no text data"); + common.alertMessage("alert-error", "Cannot tokenize message: no text data"); }, 503: function () { - rspamd.alertMessage("alert-error", "Cannot tokenize message: no text data"); + common.alertMessage("alert-error", "Cannot tokenize message: no text data"); } }, server: get_server() @@ -207,7 +209,7 @@ define(["jquery", "app/rspamd"], $("#hash-card").slideDown(); } - rspamd.query("plugins/fuzzy/hashes?flag=" + $("#fuzzy-flag").val(), { + common.query("plugins/fuzzy/hashes?flag=" + $("#fuzzy-flag").val(), { data: data, params: { processData: false, @@ -216,10 +218,10 @@ define(["jquery", "app/rspamd"], success: function (neighbours_status) { const json = neighbours_status[0].data; if (json.success) { - rspamd.alertMessage("alert-success", "Message successfully processed"); + common.alertMessage("alert-success", "Message successfully processed"); fillHashTable(json.hashes); } else { - rspamd.alertMessage("alert-error", "Unexpected error processing message"); + common.alertMessage("alert-error", "Unexpected error processing message"); } }, server: get_server() @@ -227,8 +229,8 @@ define(["jquery", "app/rspamd"], } - rspamd.set_page_size("scan", $("#scan_page_size").val()); - rspamd.bindHistoryTableEventHandlers("scan", 3); + libft.set_page_size("scan", $("#scan_page_size").val()); + libft.bindHistoryTableEventHandlers("scan", 3); $("#cleanScanHistory").off("click"); $("#cleanScanHistory").on("click", (e) => { @@ -236,8 +238,8 @@ define(["jquery", "app/rspamd"], if (!confirm("Are you sure you want to clean scan history?")) { // eslint-disable-line no-alert return; } - rspamd.destroyTable("scan"); - rspamd.symbols.scan.length = 0; + libft.destroyTable("scan"); + common.symbols.scan.length = 0; $("#cleanScanHistory").attr("disabled", true); }); @@ -288,7 +290,7 @@ define(["jquery", "app/rspamd"], uploadText(data, source, headers); } } else { - rspamd.alertMessage("alert-error", "Message source field cannot be blank"); + common.alertMessage("alert-error", "Message source field cannot be blank"); } return false; }); |