File: //proc/1/cwd/usr/share/rspamd/www/js/app/symbols.js
/*
* Copyright (C) 2017 Vsevolod Stakhov <vsevolod@highsecure.ru>
*/
define(["app/common", "app/tab-utils", "tabulator"],
(common, tabUtils, Tabulator) => {
"use strict";
const ui = {};
let altered = {};
let groupSelectEl = null;
function setSymbolsControlsDisabled(disabled) {
document.querySelectorAll("#refresh, #updateSymbols").forEach((el) => {
el.disabled = disabled;
});
}
function setSaveButtonsDisabled(disabled) {
document.querySelectorAll("#save-alert button").forEach((el) => {
el.disabled = disabled;
});
}
function clear_altered() {
document.getElementById("save-alert").classList.add("d-none");
altered = {};
}
function saveSymbols(server) {
setSaveButtonsDisabled(true);
const values = [];
Object.entries(altered).forEach(([key, value]) => values.push({name: key, value: value}));
common.query("./savesymbols", {
success: function () {
clear_altered();
common.alertMessage("alert-modal alert-success", "Symbols successfully saved");
},
complete: () => setSaveButtonsDisabled(false),
errorMessage: "Save symbols error",
method: "POST",
params: {
data: JSON.stringify(values),
dataType: "json",
},
server: server
});
}
function process_symbols_data(data) {
const items = [];
const lookup = {};
const freqs = [];
const stddevs = [];
const distinct_groups = [];
data.forEach((group) => {
group.rules.forEach((item) => {
const formatter = new Intl.NumberFormat("en", {
minimumFractionDigits: 2,
maximumFractionDigits: 6,
useGrouping: false
});
item.group = group.group;
let label_class = "";
if (item.weight < 0) {
label_class = "scorebar-ham";
} else if (item.weight > 0) {
label_class = "scorebar-spam";
}
item.weight = '<input class="form-control input-sm mb-disabled scorebar ' + label_class +
'" autocomplete="off" type="number" step="0.01" tabindex="1" ' +
'value="' + formatter.format(item.weight) + '" id="_sym_' + item.symbol + '"></input>';
if (!item.time) {
item.time = 0;
}
item.time = Number(item.time).toFixed(2);
// Normalize frequency values for scaling
["frequency", "frequency_stddev"].forEach((p) => (item[p] = Number(item[p] || 0)));
freqs.push(item.frequency);
stddevs.push(item.frequency_stddev);
if (!(item.group in lookup)) {
lookup[item.group] = 1;
distinct_groups.push(item.group);
}
items.push(item);
});
});
// For better mean calculations - use only non-zero values
const nonzero_freqs = freqs.filter((f) => Number(f) > 0.0);
const avg_freq = nonzero_freqs.length > 0
? nonzero_freqs.reduce((acc, f) => acc + Number(f), 0.0) / nonzero_freqs.length
: 0.0;
let mult = 1.0;
let exp = 0.0;
if (avg_freq > 0.0) {
while (mult * avg_freq < 1.0) {
mult *= 10;
exp++;
}
}
return [items, distinct_groups, {mult, exp}];
}
// Populate a native <select> with distinct sorted groups from the
// table data, plus an empty option to clear the filter.
function formatFreq(value, params) {
return (value * params.mult).toFixed(2) + ((params.exp > 0) ? "e-" + params.exp : "");
}
function populateGroupSelect(select, rows) {
const current = select.value;
select.innerHTML = "";
select.appendChild(new Option("Any group", ""));
const data = rows || (common.tables.symbols && common.tables.symbols.getData()) || [];
[...new Set(data.map((r) => r.group))]
.sort((a, b) => a.localeCompare(b))
.forEach((g) => select.appendChild(new Option(g, g)));
select.value = current;
}
// @get symbols into modal form
ui.getSymbols = function () {
setSymbolsControlsDisabled(true);
clear_altered();
common.query("symbols", {
success: function (json) {
const [{data}] = json;
const [rows, , freqParams] = process_symbols_data(data);
common.tables.symbols = new Tabulator("#symbolsTable", {
layout: "fitColumns",
responsiveLayout: "collapse",
responsiveLayoutCollapseStartOpen: false,
selectable: false,
pagination: "local",
paginationSize: 25,
paginationButtonCount: 5,
data: rows,
initialSort: [{column: "group", dir: "asc"}],
columns: [
{
// Toggle to expand collapsed (responsive) columns
formatter: "responsiveCollapse",
width: 36,
minWidth: 36,
responsive: 0,
hozAlign: "center",
resizable: false,
headerSort: false,
},
{
title: "Group",
field: "group",
headerFilter: (cell, onRendered, success) => {
const select = document.createElement("select");
select.className = "form-select";
populateGroupSelect(select, rows);
select.refreshGroups = () => populateGroupSelect(select);
select.addEventListener("change", () => success(select.value));
groupSelectEl = select;
return select;
},
headerFilterFunc: (filterVal, cellVal) => !filterVal || filterVal === cellVal,
minWidth: 110,
width: 205,
responsive: 0,
},
{
title: "Symbol",
field: "symbol",
headerFilter: "input",
minWidth: 270,
responsive: 0,
widthGrow: 2,
},
{
title: "Description",
field: "description",
headerFilter: "input",
minWidth: 105,
responsive: 2,
widthGrow: 4,
},
{
title: "Score",
field: "weight",
formatter: "html",
minWidth: 75,
width: 90,
responsive: 0,
},
{
title: "Frequency, <nobr>hits/s</nobr>",
field: "frequency",
sorter: "number",
formatter: (cell, params) => formatFreq(cell.getValue(), params),
formatterParams: freqParams,
minWidth: 140,
},
{
title: "Stddev, <nobr>hits/s</nobr>",
field: "frequency_stddev",
sorter: "number",
formatter: (cell, params) => formatFreq(cell.getValue(), params),
formatterParams: freqParams,
minWidth: 120,
responsive: 3,
},
{title: "Avg. time, s", field: "time", sorter: "number", minWidth: 110},
],
});
tabUtils.hideFooterOnSinglePage("symbols");
tabUtils.stripTableholderTabindex("symbols");
tabUtils.bindRowClickToggle("symbols");
tabUtils.patchScrollIntoViewOnce();
tabUtils.installScrollPreservation("symbols", {
armTriggers: [document.getElementById("updateSymbols")].filter(Boolean),
});
common.tables.symbols.on("tableBuilt", () => {
if (common.read_only) {
document.querySelectorAll(".mb-disabled").forEach((el) => {
el.disabled = true;
});
}
setSymbolsControlsDisabled(false);
});
common.tables.symbols.on("renderComplete", () => {
setSymbolsControlsDisabled(false);
});
},
error: () => setSymbolsControlsDisabled(false),
server: common.getServer()
});
};
document.getElementById("updateSymbols").addEventListener("click", (e) => {
e.preventDefault();
setSymbolsControlsDisabled(true);
clear_altered();
common.query("symbols", {
success: function (data) {
const [rows, , freqParams] = process_symbols_data(data[0].data);
common.tables.symbols.setData(rows);
if (groupSelectEl) populateGroupSelect(groupSelectEl);
common.tables.symbols.updateColumnDefinition("frequency", {formatterParams: freqParams});
common.tables.symbols.updateColumnDefinition("frequency_stddev", {formatterParams: freqParams});
},
error: () => setSymbolsControlsDisabled(false),
server: common.getServer()
});
});
common.delegate("#symbolsTable", "input", ".scorebar", (event, target) => {
target.classList.remove("scorebar-ham", "scorebar-spam");
if (target.value < 0) {
target.classList.add("scorebar-ham");
} else if (target.value > 0) {
target.classList.add("scorebar-spam");
}
});
common.delegate("#symbolsTable", "change", ".scorebar", (event, target) => {
altered[target.id.substring(5)] = parseFloat(target.value);
document.getElementById("save-alert").classList.remove("d-none");
});
document.querySelectorAll("#save-alert button").forEach((el) => {
el.addEventListener("click", () => saveSymbols(el.dataset.save));
});
return ui;
});