aboutsummaryrefslogtreecommitdiffstats
path: root/interface
diff options
context:
space:
mode:
Diffstat (limited to 'interface')
-rw-r--r--interface/css/rspamd.css26
-rw-r--r--interface/index.html70
-rw-r--r--interface/js/app/common.js130
-rw-r--r--interface/js/app/rspamd.js7
-rw-r--r--interface/js/app/selectors.js10
-rw-r--r--interface/js/app/symbols.js1
-rw-r--r--interface/js/app/upload.js284
7 files changed, 407 insertions, 121 deletions
diff --git a/interface/css/rspamd.css b/interface/css/rspamd.css
index 896f92008..9f97a668b 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%;
}
@@ -418,19 +427,24 @@ table#symbolsTable input[type="number"] {
.outline-dashed-primary { outline: 2px dashed var(--bs-primary); }
-#scanMsgSource:placeholder-shown {
+#scanMsgSource:placeholder-shown,
+#selectorsMsgArea:placeholder-shown {
background-image: url("../img/drop-area.svg");
background-repeat: no-repeat;
background-position: center;
opacity: 0.8;
}
-#scanMsgSource:not(:placeholder-shown) { background-image: none;}
+
+#scanMsgSource:not(:placeholder-shown),
+#selectorsMsgArea:not(:placeholder-shown) {
+ background-image: none;
+}
.scorebar-spam {
- background-color: rgba(240 0 0 / 0.1) !important;
+ background-color: rgb(240 0 0 / 0.1) !important;
}
.scorebar-ham {
- background: rgba(100 230 80 / 0.1) !important;
+ background: rgb(100 230 80 / 0.1) !important;
}
.danger .icon {
@@ -588,10 +602,10 @@ table#symbolsTable input[type="number"] {
bottom: unset;
}
.codejar-linenumbers {
- background: rgba(255 255 255 / 0.07) !important;
+ background: rgb(255 255 255 / 0.07) !important;
}
.codejar-linenumber {
- color: rgba(120 120 120 / 1) !important;
+ color: rgb(120 120 120 / 1) !important;
text-align: right;
}
.editor {
diff --git a/interface/index.html b/interface/index.html
index a759ac48f..30181e788 100644
--- a/interface/index.html
+++ b/interface/index.html
@@ -448,12 +448,13 @@
</div>
<div class="card-footer d-md-flex justify-content-between py-1">
<div class="input-group d-inline-flex w-auto my-1">
- <button type="submit" class="btn btn-primary d-flex align-items-center" data-upload="scan"><i class="fas fa-search me-2"></i>Scan message</button>
+ <button type="submit" class="btn btn-primary d-flex align-items-center" data-upload="checkv2"><i class="fas fa-search me-2"></i>Scan message</button>
<button class="btn btn-secondary d-flex align-items-center" id="scanOptionsToggle" data-bs-toggle="collapse" data-bs-target="#scanOptions"><i class="fas fa-bars me-2"></i>Options</button>
</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">
@@ -474,32 +475,55 @@
</div>
</div>
<div class="card-body">
- <div class="row">
- <div class="col-lg-6">
+ <div class="row g-3">
+ <div class="col-lg-auto d-flex">
<div class="card bg-light shadow card-body card p-2">
<p>Learn Bayesian classifier:</p>
<form>
- <div class="btn-group">
- <button class="btn btn-success d-flex align-items-center" type="button" data-upload="ham" disabled><i class="fas fa-thumbs-up me-2"></i>Upload HAM</button>
- <button class="btn btn-danger d-flex align-items-center" type="button" data-upload="spam" disabled><i class="fas fa-thumbs-down me-2"></i>Upload SPAM</button>
+ <div class="d-flex flex-wrap flex-lg-column align-items-start align-items-lg-stretch gap-2">
+ <select id="classifier" class="form-select w-auto"></select>
+ <div class="btn-group">
+ <button class="btn btn-success d-flex align-items-center" type="button" data-upload="learnham" disabled><i class="fas fa-thumbs-up me-2"></i>Upload HAM</button>
+ <button class="btn btn-danger d-flex align-items-center" type="button" data-upload="learnspam" disabled><i class="fas fa-thumbs-down me-2"></i>Upload SPAM</button>
+ </div>
</div>
</form>
</div>
</div>
- <div class="col-lg-6">
- <div class="card bg-light shadow card-body card p-2">
- <p>Learn Fuzzy storage:</p>
- <form class="d-flex">
- <div class="d-flex align-items-center">
- <label for="fuzzyFlagText">Flag:</label>
- <input name="fuzzyFlagText" id="fuzzyFlagText" class="form-control ms-1" type="number" value="1"/>
+ <div class="col-lg d-flex">
+ <div class="card bg-light shadow card-body p-2">
+ <p>Fuzzy hash storage management:</p>
+ <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="d-flex align-items-center ms-2">
- <label for="fuzzyWeightText">Weight:</label>
- <input name="fuzzyWeightText" id="fuzzyWeightText" class="form-control ms-1" type="number" value="1"/>
+ <div class="col-auto d-flex align-items-center me-2">
+ <label for="fuzzyWeightText" class="me-1">Weight:</label>
+ <input id="fuzzyWeightText" class="form-control" type="number" value="1"/>
</div>
- <button class="btn btn-warning ms-2 d-flex align-items-center" data-upload="fuzzy" disabled><i class="fas fa-upload me-2"></i>Upload FUZZY</button>
- </form>
+ <div class="col-auto">
+ <button class="btn btn-warning me-1" data-upload="fuzzyadd" disabled><i class="fas fa-circle-plus me-2"></i>Add to storage</button>
+ <button class="btn btn-danger" data-upload="fuzzydel" disabled><i class="fas fa-trash-can me-2"></i>Delete from storage</button>
+ </div>
+ </div>
+ <div class="row mt-3">
+ <div class="col">
+ <label for="fuzzyDelList" class="form-label">Hashes to delete</label>
+ <textarea class="form-control" id="fuzzyDelList" rows="3" placeholder="Enter one hash per line, or separate with commas, semicolons, or spaces."></textarea>
+ </div>
+ </div>
+ <div class="row mt-2">
+ <div class="col d-flex justify-content-end">
+ <button class="btn btn-danger me-2" id="deleteHashesBtn" disabled>
+ <i class="fas fa-trash-can me-2"></i><span class="btn-label">Delete hashes</span>
+ </button>
+ <button class="btn btn-secondary" id="clearHashesBtn" disabled>
+ <i class="fas fa-eraser me-2"></i>Clear
+ </button>
+ </div>
+ </div>
</div>
</div>
</div>
@@ -510,7 +534,7 @@
<div class="card-header text-secondary py-2 d-flex align-items-center">
<span class="icon me-3"><i class="fas fa-hashtag"></i></span>
<span class="h6 fw-bolder my-auto">Fuzzy hashes</span>
- <button type="button" class="card-close-btn btn-close float-end" aria-label="Close"></button>
+ <button type="button" class="card-close-btn btn-close ms-auto" aria-label="Close"></button>
</div>
<div class="card-body p-0 table-responsive">
<table class="table status-table table-sm table-bordered text-nowrap mb-0" id="hashTable">
@@ -561,6 +585,10 @@
<div class="card-header text-secondary py-2 d-flex align-items-center">
<span class="icon me-3"><i class="fas fa-envelope"></i></span>
<span class="h6 fw-bolder my-auto">Test Rspamd selectors</span>
+ <div class="d-flex input-group-sm align-items-center ms-auto">
+ <label for="formFile" class="col-auto col-form-label-sm me-1">Choose a file:</label>
+ <input class="form-control form-control-sm btn btn-secondary" id="selectorsFile" type="file">
+ </div>
</div>
<div class="card-body p-0">
<div class="row h-100 m-0" id="row-main">
@@ -589,7 +617,7 @@
<div class="col">
<div class="form-group">
<label class="form-label" for="selectorsMsgArea">Message source:</label>
- <textarea class="form-control" id="selectorsMsgArea" rows="9" placeholder="Paste raw message source"></textarea>
+ <textarea class="form-control" id="selectorsMsgArea" rows="9" placeholder='Paste raw message source, drag and drop files here or use "Browse..." button.'></textarea>
</div>
<button class="btn btn-secondary d-flex align-items-center float-end" id="selectorsMsgClean"><i class="fas fa-trash-alt me-2"></i>Clean form</button>
</div>
diff --git a/interface/js/app/common.js b/interface/js/app/common.js
index ace4bbba1..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
@@ -261,5 +303,63 @@ define(["jquery", "nprogress"],
).appendTo(ftFilter.$dropdown);
};
+ ui.fileUtils = {
+ readFile(files, callback, index = 0) {
+ const file = files[index];
+ const reader = new FileReader();
+ reader.onerror = () => alertMessage("alert-error", `Error reading file: ${file.name}`);
+ reader.onloadend = () => callback(reader.result);
+ reader.readAsText(file);
+ },
+
+ setFileInputFiles(fileInput, files, i) {
+ const dt = new DataTransfer();
+ if (arguments.length > 2) dt.items.add(files[i]);
+ $(fileInput).prop("files", dt.files);
+ },
+
+ setupFileHandling(textArea, fileInput, fileSet, enable_btn_cb, multiple_files_cb) {
+ const dragoverClassList = "outline-dashed-primary bg-primary-subtle";
+ const {readFile, setFileInputFiles} = ui.fileUtils;
+
+ function handleFileInput(fileSource) {
+ fileSet.files = fileSource.files;
+ fileSet.index = 0;
+ const {files} = fileSet;
+
+ if (files.length === 1) {
+ setFileInputFiles(fileInput, files, 0);
+ enable_btn_cb();
+ readFile(files, (result) => {
+ $(textArea).val(result);
+ enable_btn_cb();
+ });
+ } else if (multiple_files_cb) {
+ multiple_files_cb(files);
+ } else {
+ alertMessage("alert-warning", "Multiple files processing is not supported.");
+ }
+ }
+
+ $(textArea)
+ .on("dragenter dragover dragleave drop", (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ })
+ .on("dragenter dragover", () => $(textArea).addClass(dragoverClassList))
+ .on("dragleave drop", () => $(textArea).removeClass(dragoverClassList))
+ .on("drop", (e) => handleFileInput(e.originalEvent.dataTransfer))
+ .on("input", () => {
+ enable_btn_cb();
+ if (fileSet.files) {
+ fileSet.files = null;
+ setFileInputFiles(fileInput, fileSet.files);
+ }
+ });
+
+ $(fileInput).on("change", (e) => handleFileInput(e.target));
+ }
+ };
+
return ui;
});
diff --git a/interface/js/app/rspamd.js b/interface/js/app/rspamd.js
index 6d047d6f6..cb7fb8ace 100644
--- a/interface/js/app/rspamd.js
+++ b/interface/js/app/rspamd.js
@@ -176,7 +176,10 @@ define(["jquery", "app/common", "stickytabs", "visibility",
require(["app/symbols"], (module) => module.getSymbols());
break;
case "#scan_nav":
- require(["app/upload"]);
+ require(["app/upload"], (module) => {
+ module.getClassifiers();
+ module.getFuzzyStorages();
+ });
break;
case "#selectors_nav":
require(["app/selectors"], (module) => module.displayUI());
@@ -236,6 +239,8 @@ define(["jquery", "app/common", "stickytabs", "visibility",
complete: function () {
ajaxSetup(localStorage.getItem("ajax_timeout"));
+ if (require.defined("app/upload")) require(["app/upload"], (module) => module.getClassifiers());
+
if (common.read_only) {
$(".ro-disable").attr("disabled", true);
$(".ro-hide").hide();
diff --git a/interface/js/app/selectors.js b/interface/js/app/selectors.js
index c2b8b27e5..4a1c6d0d0 100644
--- a/interface/js/app/selectors.js
+++ b/interface/js/app/selectors.js
@@ -2,6 +2,7 @@ define(["jquery", "app/common"],
($, common) => {
"use strict";
const ui = {};
+ const fileSet = {files: null, index: null};
function enable_disable_check_btn() {
$("#selectorsChkMsgBtn").prop("disabled", (
@@ -129,12 +130,15 @@ define(["jquery", "app/common"],
return false;
});
- $("#selectorsMsgArea").on("input", () => {
- enable_disable_check_btn();
- });
$("#selectorsSelArea").on("input", () => {
checkSelectors();
});
+ $("#selectorsMsgClean").on("click", () => {
+ $("#selectorsMsgArea").val("");
+ $("#selectorsFile").val("");
+ });
+
+ common.fileUtils.setupFileHandling("#selectorsMsgArea", "#selectorsFile", fileSet, enable_disable_check_btn);
return ui;
});
diff --git a/interface/js/app/symbols.js b/interface/js/app/symbols.js
index 3ff5d5a4b..21711a1e5 100644
--- a/interface/js/app/symbols.js
+++ b/interface/js/app/symbols.js
@@ -135,6 +135,7 @@ define(["jquery", "app/common", "footable"],
construct: function (instance) {
this._super(instance);
[,this.groups] = items;
+ this.groups.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
this.def = "Any group";
this.$group = null;
},
diff --git a/interface/js/app/upload.js b/interface/js/app/upload.js
index a484a41aa..f95d5cc90 100644
--- a/interface/js/app/upload.js
+++ b/interface/js/app/upload.js
@@ -28,25 +28,15 @@ define(["jquery", "app/common", "app/libft"],
($, common, libft) => {
"use strict";
const ui = {};
- let files = null;
- let filesIdx = null;
+ const fileSet = {files: null, index: null};
+ const lastReqContext = {
+ classifiers: {config_id: null, server: null},
+ storages: {config_id: null, server: null}
+ };
let scanTextHeaders = {};
- function cleanTextUpload(source) {
- $("#" + source + "TextSource").val("");
- }
-
- function uploadText(data, source, headers) {
- let url = null;
- if (source === "spam") {
- url = "learnspam";
- } else if (source === "ham") {
- url = "learnham";
- } else if (source === "fuzzy") {
- url = "fuzzyadd";
- } else if (source === "scan") {
- url = "checkv2";
- }
+ function uploadText(data, url, headers, method = "POST") {
+ const deferred = new $.Deferred();
function server() {
if (common.getSelector("selSrv") === "All SERVERS" &&
@@ -62,34 +52,25 @@ define(["jquery", "app/common", "app/libft"],
params: {
processData: false,
},
- method: "POST",
+ method: method,
headers: headers,
success: function (json, jqXHR) {
- cleanTextUpload(source);
common.alertMessage("alert-success", "Data successfully uploaded");
if (jqXHR.status !== 200) {
common.alertMessage("alert-info", jqXHR.statusText);
}
+ deferred.resolve();
},
+ complete: () => deferred.resolve(),
server: server()
});
- }
-
- function enable_disable_scan_btn(disable) {
- $("#scan button:not(#cleanScanHistory, #scanOptionsToggle, .ft-columns-btn)")
- .prop("disabled", (disable || $.trim($("textarea").val()).length === 0));
- }
- function setFileInputFiles(i) {
- const dt = new DataTransfer();
- if (arguments.length) dt.items.add(files[i]);
- $("#formFile").prop("files", dt.files);
+ return deferred.promise();
}
- function readFile(callback, i) {
- const reader = new FileReader();
- reader.readAsText(files[(arguments.length === 1) ? 0 : i]);
- reader.onload = () => callback(reader.result);
+ function enable_disable_scan_btn(disable) {
+ $("#scan button:not(#cleanScanHistory, #deleteHashesBtn, #scanOptionsToggle, .ft-columns-btn)")
+ .prop("disabled", (disable || $.trim($("#scanMsgSource").val()).length === 0));
}
function scanText(data) {
@@ -110,7 +91,7 @@ define(["jquery", "app/common", "app/libft"],
const {items} = o;
common.symbols.scan.push(o.symbols[0]);
- if (files) items[0].file = files[filesIdx].name;
+ if (fileSet.files) items[0].file = fileSet.files[fileSet.index].name;
if (Object.prototype.hasOwnProperty.call(common.tables, "scan")) {
common.tables.scan.rows.load(items, true);
@@ -118,14 +99,16 @@ define(["jquery", "app/common", "app/libft"],
require(["footable"], () => {
libft.initHistoryTable(data, items, "scan", libft.columns_v2("scan"), true,
() => {
- if (files && filesIdx < files.length - 1) {
- readFile((result) => {
- if (filesIdx === files.length - 1) {
+ const {files} = fileSet;
+ if (files && fileSet.index < files.length - 1) {
+ common.fileUtils.readFile(files, (result) => {
+ const {index} = fileSet;
+ if (index === files.length - 1) {
$("#scanMsgSource").val(result);
- setFileInputFiles(filesIdx);
+ common.fileUtils.setFileInputFiles("#formFile", files, index);
}
scanText(result);
- }, ++filesIdx);
+ }, ++fileSet.index);
} else {
enable_disable_scan_btn();
$("#cleanScanHistory, #scan .ft-columns-dropdown .btn-dropdown-apply")
@@ -206,13 +189,6 @@ define(["jquery", "app/common", "app/libft"],
});
enable_disable_scan_btn();
- $("textarea").on("input", () => {
- enable_disable_scan_btn();
- if (files) {
- files = null;
- setFileInputFiles();
- }
- });
$("#scanClean").on("click", () => {
enable_disable_scan_btn(true);
@@ -238,18 +214,25 @@ define(["jquery", "app/common", "app/libft"],
const source = $(this).data("upload");
const data = $("#scanMsgSource").val();
if ($.trim(data).length > 0) {
- if (source === "scan") {
+ if (source === "checkv2") {
getScanTextHeaders();
scanText(data);
} else if (source === "compute-fuzzy") {
getFuzzyHashes(data);
} else {
let headers = {};
- if (source === "fuzzy") {
+ if (source === "learnham" || source === "learnspam") {
+ const classifier = $("#classifier").val();
+ if (classifier) headers = {classifier: classifier};
+ } else if (source === "fuzzyadd") {
headers = {
flag: $("#fuzzyFlagText").val(),
weight: $("#fuzzyWeightText").val()
};
+ } else if (source === "fuzzydel") {
+ headers = {
+ flag: $("#fuzzyFlagText").val(),
+ };
}
uploadText(data, source, headers);
}
@@ -259,39 +242,190 @@ define(["jquery", "app/common", "app/libft"],
return false;
});
- function fileInputHandler(obj) {
- ({files} = obj);
- filesIdx = 0;
- if (files.length === 1) {
- setFileInputFiles(0);
- enable_disable_scan_btn();
- readFile((result) => {
- $("#scanMsgSource").val(result);
- enable_disable_scan_btn();
- });
+ function setDelhashButtonsDisabled(disabled = true) {
+ ["#deleteHashesBtn", "#clearHashesBtn"].forEach((s) => $(s).prop("disabled", disabled));
+ }
+
+ /**
+ * Parse a textarea (or any input) value into an array of non-empty tokens.
+ * Splits on commas, semicolons or any whitespace (space, tab, newline).
+ *
+ * @param {string} selector - jQuery selector for the input element.
+ * @returns {string[]} - Trimmed, non-empty tokens.
+ */
+ function parseHashes(selector) {
+ return $(selector).val()
+ .split(/[,\s;]+/)
+ .map((t) => t.trim())
+ .filter((t) => t.length > 0);
+ }
+
+ $("#fuzzyDelList").on("input", () => {
+ const hasTokens = parseHashes("#fuzzyDelList").length > 0;
+ setDelhashButtonsDisabled(!hasTokens);
+ });
+
+ $("#deleteHashesBtn").on("click", () => {
+ $("#fuzzyDelList").prop("disabled", true);
+ setDelhashButtonsDisabled();
+ $("#deleteHashesBtn").find(".btn-label").text("Deleting…");
+
+ const hashes = parseHashes("#fuzzyDelList");
+ const promises = hashes.map((h) => {
+ const headers = {
+ flag: $("#fuzzyFlagText").val(),
+ Hash: h
+ };
+ return uploadText(null, "fuzzydelhash", headers, "GET");
+ });
+
+ $.when.apply($, promises).always(() => {
+ $("#fuzzyDelList").prop("disabled", false);
+ setDelhashButtonsDisabled(false);
+ $("#deleteHashesBtn").find(".btn-label").text("Delete hashes");
+ });
+ });
+
+ $("#clearHashesBtn").on("click", () => {
+ $("#fuzzyDelList").val("").focus();
+ setDelhashButtonsDisabled();
+ });
+
+
+ function multiple_files_cb(files) {
// eslint-disable-next-line no-alert
- } else if (files.length < 10 || confirm("Are you sure you want to scan " + files.length + " files?")) {
+ if (files.length < 10 || confirm("Are you sure you want to scan " + files.length + " files?")) {
getScanTextHeaders();
- readFile((result) => scanText(result));
+ common.fileUtils.readFile(files, (result) => scanText(result));
+ }
+ }
+
+ common.fileUtils.setupFileHandling("#scanMsgSource", "#formFile", fileSet, enable_disable_scan_btn, multiple_files_cb);
+
+
+ /**
+ * Returns `true` if we should skip the request as configuration is not changed,
+ * otherwise bumps the request context cache and returns `false`.
+ *
+ * @param {string} server
+ * Name of the currently selected Rspamd neighbour.
+ * @param {"classifiers"|"storages"} key
+ * Which endpoint’s cache to check.
+ * @returns {boolean}
+ */
+ function shouldSkipRequest(server, key) {
+ const servers = JSON.parse(sessionStorage.getItem("Credentials") || "{}");
+ const config_id = servers[server]?.data?.config_id;
+ const last = lastReqContext[key];
+
+ if ((config_id && config_id === last.config_id) ||
+ (!config_id && server === last.server)) {
+ return true;
+ }
+
+ lastReqContext[key] = {config_id, server};
+ return false;
+ }
+
+ ui.getClassifiers = function () {
+ const server = common.getServer();
+ if (shouldSkipRequest(server, "classifiers")) return;
+
+ if (!common.read_only) {
+ const sel = $("#classifier").empty().append($("<option>", {value: "", text: "All classifiers"}));
+ common.query("bayes/classifiers", {
+ success: function (data) {
+ data[0].data.forEach((c) => sel.append($("<option>", {value: c, text: c})));
+ },
+ server: server
+ });
+ }
+ };
+
+
+ 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"]();
+ });
}
- const dragoverClassList = "outline-dashed-primary bg-primary-subtle";
- $("#scanMsgSource")
- .on("dragenter dragover dragleave drop", (e) => {
- e.preventDefault();
- e.stopPropagation();
- })
- .on("dragenter dragover", () => {
- $("#scanMsgSource").addClass(dragoverClassList);
- })
- .on("dragleave drop", () => {
- $("#scanMsgSource").removeClass(dragoverClassList);
- })
- .on("drop", (e) => fileInputHandler(e.originalEvent.dataTransfer));
-
- $("#formFile").on("change", (e) => fileInputHandler(e.target));
+ function setWidgetsDisabled(disable) {
+ fuzzyWidgets.forEach(({picker, container}) => {
+ container($(picker))[disable ? "addClass" : "removeClass"]("disabled");
+ });
+ }
+
+ ui.getFuzzyStorages = function () {
+ const server = common.getServer();
+ if (shouldSkipRequest(server, "storages")) return;
+
+ 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;
});