From: moisseev Date: Wed, 27 Dec 2023 12:22:23 +0000 (+0300) Subject: [Minor] Move common stuff to separate files X-Git-Tag: 3.8.0~17^2~1 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=01a0df9f25cee9e56294aea179e6a4e5f1b9c09a;p=rspamd.git [Minor] Move common stuff to separate files --- 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 = $("
" + + "" + + "" + alertText + ""); + $(".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 = 'Read'; - if (!(item.editable === false || rspamd.read_only)) { + if (!(item.editable === false || common.read_only)) { $td = $($td).append(' Write'); } const $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("
\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 = $("
", { + class: "form-group d-inline-flex align-items-center" + }).append($("