Browse Source

Merge pull request #5524 from moisseev/fuzzy-config

[WebUI] Add fuzzy flag selectors to Scan/Learn tab
pull/5534/head
Vsevolod Stakhov 4 months ago
committed by GitHub
parent
commit
1bc61e5959
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 3
      eslint.config.mjs
  2. 9
      interface/css/rspamd.css
  3. 4
      interface/index.html
  4. 72
      interface/js/app/common.js
  5. 2
      interface/js/app/rspamd.js
  6. 94
      interface/js/app/upload.js
  7. 22
      rules/controller/fuzzy.lua

3
eslint.config.mjs

@ -13,6 +13,9 @@ export default [
...globals.browser,
define: false,
},
parserOptions: {
ecmaVersion: 2020,
},
sourceType: "script",
},
plugins: {

9
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%;
}

4
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">

72
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

2
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());

94
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;
});

22
rules/controller/fuzzy.lua

@ -37,10 +37,30 @@ local function handle_gen_fuzzy(task, conn, req_params)
end
end
local function handle_fuzzy_storages(_task, conn)
if type(rspamd_plugins.fuzzy_check) == 'table'
and type(rspamd_plugins.fuzzy_check.list_storages) == 'function' then
local ok, result = pcall(rspamd_plugins.fuzzy_check.list_storages, rspamd_config)
if ok then
conn:send_ucl({ success = true, storages = result })
else
conn:send_error(500, 'cannot list fuzzy storages')
end
else
conn:send_error(404, 'fuzzy_check is not enabled')
end
end
return {
hashes = {
handler = handle_gen_fuzzy,
need_task = true,
enable = false
},
}
storages = {
handler = handle_fuzzy_storages,
need_task = false,
enable = false
},
}
Loading…
Cancel
Save