aboutsummaryrefslogtreecommitdiffstats
path: root/interface
diff options
context:
space:
mode:
authorVsevolod Stakhov <vsevolod@rspamd.com>2025-06-30 17:44:55 +0600
committerGitHub <noreply@github.com>2025-06-30 17:44:55 +0600
commit1bc61e5959fcf1d1cf4b57cf6496630e32a9ec95 (patch)
treee9dc22f383d730bfcce04c3f6e8e167cb378b7ba /interface
parent09756fbbc2d5901958b7099420dd648d627b40ee (diff)
parentce2dba2f58b8dacbf0569d31d40e2c001c1cc0ab (diff)
downloadrspamd-master.tar.gz
rspamd-master.zip
Merge pull request #5524 from moisseev/fuzzy-configHEADmaster
[WebUI] Add fuzzy flag selectors to Scan/Learn tab
Diffstat (limited to 'interface')
-rw-r--r--interface/css/rspamd.css9
-rw-r--r--interface/index.html4
-rw-r--r--interface/js/app/common.js72
-rw-r--r--interface/js/app/rspamd.js2
-rw-r--r--interface/js/app/upload.js94
5 files changed, 164 insertions, 17 deletions
diff --git a/interface/css/rspamd.css b/interface/css/rspamd.css
index a79fed28b..a4335c07b 100644
--- a/interface/css/rspamd.css
+++ b/interface/css/rspamd.css
@@ -95,6 +95,15 @@ fieldset[disabled] .btn {
pointer-events: auto;
cursor: not-allowed;
}
+.card.disabled,
+.input-group.disabled {
+ cursor: not-allowed;
+ opacity: 0.65;
+}
+.card.disabled *,
+.input-group.disabled * {
+ pointer-events: none;
+}
.w-1 {
width: 1%;
}
diff --git a/interface/index.html b/interface/index.html
index 61487ce29..30181e788 100644
--- a/interface/index.html
+++ b/interface/index.html
@@ -453,7 +453,8 @@
</div>
<div class="input-group d-inline-flex w-auto my-1">
<label for="fuzzy-flag" class="input-group-text">Flag</label>
- <input id="fuzzy-flag" class="form-control" value="1" min="1" type="number">
+ <select id="fuzzy-flag-picker" class="form-select"></select>
+ <input id="fuzzy-flag" class="form-control flex-grow-0" value="1" min="1" type="number">
<button class="btn btn-warning d-flex align-items-center" data-upload="compute-fuzzy"><i class="fas fa-hashtag me-2"></i>Compute fuzzy hashes</button>
</div>
<div class="float-end my-1">
@@ -495,6 +496,7 @@
<div class="row g-2 align-items-center">
<div class="col-auto d-flex align-items-center me-1">
<label for="fuzzyFlagText" class="me-1">Flag:</label>
+ <select id="fuzzyFlagText-picker" class="form-select"></select>
<input id="fuzzyFlagText" class="form-control" type="number" value="1"/>
</div>
<div class="col-auto d-flex align-items-center me-2">
diff --git a/interface/js/app/common.js b/interface/js/app/common.js
index 44e0dcf77..6f37d739e 100644
--- a/interface/js/app/common.js
+++ b/interface/js/app/common.js
@@ -57,6 +57,20 @@ define(["jquery", "nprogress"],
}, 5000);
}
+ /**
+ * Perform a request to a single Rspamd neighbour server.
+ *
+ * @param {Array.<Object>} neighbours_status
+ * Array of neighbour status objects.
+ * @param {number} ind
+ * Index of this neighbour in the `neighbours_status` array.
+ * @param {string} req_url
+ * Relative controller endpoint with optional query string.
+ * @param {Object} o
+ * The same `options` object passed into `ui.query`.
+ *
+ * @returns {void}
+ */
function queryServer(neighbours_status, ind, req_url, o) {
neighbours_status[ind].checked = false;
neighbours_status[ind].data = {};
@@ -152,23 +166,51 @@ define(["jquery", "nprogress"],
};
/**
- * @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.
+ * Perform an HTTP request to one or all Rspamd neighbours.
*
- * @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.
+ * @param {string} url
+ * Relative URL, including with optional query string (e.g. "plugins/selectors/check_selector?selector=from").
+ * @param {Object} [options]
+ * Ajax request configuration options.
+ * @param {Object|string|Array} [options.data]
+ * Request body for POST endpoints.
+ * @param {Object} [options.headers]
+ * Additional HTTP headers.
+ * @param {"GET"|"POST"} [options.method]
+ * HTTP method (defaults to "GET").
+ * @param {string} [options.server]
+ * Name or base-URL of the target server (defaults to the currently selected Rspamd neighbour).
+ * @param {Object} [options.params]
+ * Extra jQuery.ajax() settings (e.g. timeout, dataType).
+ * @param {string} [options.errorMessage]
+ * Text to show inside a Bootstrap alert on generic errors (e.g. network failure).
+ * @param {string} [options.errorOnceId]
+ * Prefix for an alert ID stored in session storage to ensure
+ * `errorMessage` is shown only once per server each session.
+ * @param {function(Array.<Object>, Object)} [options.success]
+ * Called on HTTP success. Receives:
+ * 1. results: Array of per-server status objects:
+ * {
+ * name: string,
+ * host: string,
+ * url: string, // full URL base for this neighbour
+ * checked: boolean, // whether this server was attempted
+ * status: boolean, // HTTP success (<400)
+ * data: any, // parsed JSON or raw text
+ * percentComplete: number
+ * }
+ * 2. jqXHR: jQuery XHR object with properties
+ * { readyState, status, statusText, responseText, responseJSON, … }
+ * @param {function(Object, Object, string, string)} [options.error]
+ * Called on HTTP error or network failure. Receives:
+ * 1. result: a per-server status object (status:false, data:{}).
+ * 2. jqXHR: jQuery XHR object (responseText, responseJSON, status, statusText).
+ * 3. textStatus: string describing error type ("error", "timeout", etc.).
+ * 4. errorThrown: exception message or HTTP statusText.
+ * @param {function()} [options.complete]
+ * Called once all servers have been tried; takes no arguments.
*
- * @returns {undefined}
+ * @returns {void}
*/
ui.query = function (url, options) {
// Force options to be an object
diff --git a/interface/js/app/rspamd.js b/interface/js/app/rspamd.js
index da4e9bccf..ceba6864b 100644
--- a/interface/js/app/rspamd.js
+++ b/interface/js/app/rspamd.js
@@ -176,7 +176,7 @@ define(["jquery", "app/common", "stickytabs", "visibility",
require(["app/symbols"], (module) => module.getSymbols());
break;
case "#scan_nav":
- require(["app/upload"]);
+ require(["app/upload"], (module) => module.getFuzzyStorages());
break;
case "#selectors_nav":
require(["app/selectors"], (module) => module.displayUI());
diff --git a/interface/js/app/upload.js b/interface/js/app/upload.js
index 0960ebf25..c82bb2306 100644
--- a/interface/js/app/upload.js
+++ b/interface/js/app/upload.js
@@ -312,5 +312,99 @@ define(["jquery", "app/common", "app/libft"],
};
ui.getClassifiers();
+
+ const fuzzyWidgets = [
+ {
+ picker: "#fuzzy-flag-picker",
+ input: "#fuzzy-flag",
+ container: ($picker) => $picker.parent()
+ },
+ {
+ picker: "#fuzzyFlagText-picker",
+ input: "#fuzzyFlagText",
+ container: ($picker) => $picker.closest("div.card")
+ }
+ ];
+
+ function toggleWidgets(showPicker, showInput) {
+ fuzzyWidgets.forEach(({picker, input}) => {
+ $(picker)[showPicker ? "show" : "hide"]();
+ $(input)[showInput ? "show" : "hide"]();
+ });
+ }
+
+ function setWidgetsDisabled(disable) {
+ fuzzyWidgets.forEach(({picker, container}) => {
+ container($(picker))[disable ? "addClass" : "removeClass"]("disabled");
+ });
+ }
+
+ let lastFuzzyStoragesReq = {config_id: null, server: null};
+
+ ui.getFuzzyStorages = function () {
+ const server = common.getServer();
+
+ const servers = JSON.parse(sessionStorage.getItem("Credentials") || "{}");
+ const config_id = servers[server]?.data?.config_id;
+
+ if ((config_id && config_id === lastFuzzyStoragesReq.config_id) ||
+ (!config_id && server === lastFuzzyStoragesReq.server)) {
+ return;
+ }
+ lastFuzzyStoragesReq = {config_id: config_id, server: server};
+
+ fuzzyWidgets.forEach(({picker, container}) => container($(picker)).removeAttr("title"));
+
+ common.query("plugins/fuzzy/storages", {
+ success: function (data) {
+ const storages = data[0].data.storages || {};
+ const hasWritableStorages = Object.keys(storages).some((name) => !storages[name].read_only);
+
+ toggleWidgets(true, false);
+ setWidgetsDisabled(!hasWritableStorages);
+
+ fuzzyWidgets.forEach(({picker, input}) => {
+ const $sel = $(picker);
+
+ $sel.empty();
+
+ if (hasWritableStorages) {
+ Object.entries(storages).forEach(([name, info]) => {
+ if (!info.read_only) {
+ Object.entries(info.flags).forEach(([symbol, val]) => {
+ $sel.append($("<option>", {value: val, text: `${name}:${symbol} (${val})`}));
+ });
+ }
+ });
+ $(input).val($sel.val());
+ $sel.off("change").on("change", () => $(input).val($sel.val()));
+ } else {
+ $sel.append($("<option>", {value: "", text: "No writable storages"}));
+ }
+ });
+ },
+ error: function (_result, _jqXHR, _textStatus, errorThrown) {
+ if (errorThrown === "fuzzy_check is not enabled") {
+ toggleWidgets(true, false);
+ setWidgetsDisabled(true);
+
+ fuzzyWidgets.forEach(({picker, container}) => {
+ const $picker = $(picker);
+ $picker
+ .empty()
+ .append($("<option>", {value: "", text: "fuzzy_check disabled"}))
+ .show();
+ container($picker)
+ .attr("title", "fuzzy_check module is not enabled in server configuration.");
+ });
+ } else {
+ toggleWidgets(false, true);
+ setWidgetsDisabled(false);
+ }
+ },
+ server: server
+ });
+ };
+
return ui;
});