Browse Source

[WebUI] Rework scan results display

tags/2.0
moisseev 4 years ago
parent
commit
def746a5dd
6 changed files with 566 additions and 491 deletions
  1. 1
    1
      .eslintrc.json
  2. 6
    6
      interface/css/rspamd.css
  3. 28
    17
      interface/index.html
  4. 30
    403
      interface/js/app/history.js
  5. 398
    6
      interface/js/app/rspamd.js
  6. 103
    58
      interface/js/app/upload.js

+ 1
- 1
.eslintrc.json View File

@@ -24,7 +24,7 @@
"singleLine": { "afterColon": false }
}],
"max-params": ["warn", 6],
"max-statements": ["warn", 33],
"max-statements": ["warn", 44],
"max-statements-per-line": ["error", { "max": 2 }],
"multiline-comment-style": "off",
"multiline-ternary": ["error", "always-multiline"],

+ 6
- 6
interface/css/rspamd.css View File

@@ -502,17 +502,17 @@ td.maps-cell {
}

/* history table */
#historyTable > tbody > tr > td,
#historyTable > thead > tr > th {
#historyTable_scan > tbody > tr > td,
#historyTable_scan > thead > tr > th,
#historyTable_history > tbody > tr > td,
#historyTable_history > thead > tr > th {
padding: 4px;
}
#historyTable > thead > tr > th {
#historyTable_scan > thead > tr > th,
#historyTable_history > thead > tr > th {
padding-right: 20px;
}

#selSymOrder {
height: auto;
}
.widget-title-form label {
font-size: 12px;
font-weight: normal;

+ 28
- 17
interface/index.html View File

@@ -219,20 +219,6 @@
</div>
</form>
</div>
<div id="scanResult" style="display: none;">
<h4>Scan results:</h4>
<div class="well nomargin nopadding">
<table class="table table-log table-hover" id="scanOutput">
<thead>
<tr>
<th class="col4" title="Action">Action</th>
<th class="col5" title="Score / Req.&nbsp;score">Score / Req.&nbsp;score</th>
<th class="col6" title="Symbols">Symbols</th>
</tr>
</thead>
</table>
</div>
</div>
</div>
</div>
<div class="widget-box learn" style="display: none;">
@@ -274,6 +260,31 @@
</div>
</div>
</div>

<div class="widget-box">
<div class="widget-title">
<div id="scanResult" class="form-inline widget-title-form input-group-sm pull-right buttons">
<label for="selSymOrder_scan">Symbols order:</label>
<select id="selSymOrder_scan" class="form-control">
<option value="magnitude" selected>Score magnitude</option>
<option value="score">Score value</option>
<option value="name">Name</option>
</select>
<label for="scan_page_size">Rows per page:</label>
<input id="scan_page_size" class="form-control" value="25" min="1" type="number">
<button class="btn btn-default btn-sm" id="cleanScanHistory">
<i class="glyphicon glyphicon-trash"></i> Clean history
</button>
</div>
<span class="icon"><i class="glyphicon glyphicon-eye-open"></i></span>
<h5>Scan results history</h5>
</div>
<div class="widget-content nopadding">
<div id="scanLog">
<table class="table" id="historyTable_scan"></table>
</div>
</div>
</div>
</div>

<div class="tab-pane" id="history">
@@ -281,8 +292,8 @@
<div class="widget-box">
<div class="widget-title">
<div class="form-inline widget-title-form input-group-sm pull-right buttons">
<label for="selSymOrder">Symbols order:</label>
<select id="selSymOrder" class="form-control">
<label for="selSymOrder_history">Symbols order:</label>
<select id="selSymOrder_history" class="form-control">
<option value="magnitude" selected>Score magnitude</option>
<option value="score">Score value</option>
<option value="name">Name</option>
@@ -301,7 +312,7 @@
</div>
<div class="widget-content nopadding">
<div id="historyLog">
<table class="table" id="historyTable"></table>
<table class="table" id="historyTable_history"></table>
</div>
</div>
</div>

+ 30
- 403
interface/js/app/history.js View File

@@ -27,245 +27,21 @@
define(["jquery", "footable", "humanize"],
function ($, _, Humanize) {
"use strict";
var page_size = {
errors: 25,
history: 25
};

function set_page_size(n, callback) {
if (n !== page_size.history && n > 0) {
page_size.history = n;
if (callback) {
return callback(n);
}
}
return null;
}

set_page_size($("#history_page_size").val());

var ui = {};
var prevVersion = null;
var htmlEscapes = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
"\"": "&quot;",
"'": "&#39;",
"/": "&#x2F;",
"`": "&#x60;",
"=": "&#x3D;"
};
var htmlEscaper = /[&<>"'/`=]/g;
var symbols = [];
var symbolDescriptions = {};

var escapeHTML = function (string) {
return String(string).replace(htmlEscaper, function (match) {
return htmlEscapes[match];
});
};

var escape_HTML_array = function (arr) {
arr.forEach(function (d, i) { arr[i] = escapeHTML(d); });
};

function unix_time_format(tm) {
var date = new Date(tm ? tm * 1000 : 0);
return date.toLocaleString();
}

function preprocess_item(item) {
for (var prop in item) {
if (!{}.hasOwnProperty.call(item, prop)) continue;
switch (prop) {
case "rcpt_mime":
case "rcpt_smtp":
escape_HTML_array(item[prop]);
break;
case "symbols":
Object.keys(item.symbols).forEach(function (key) {
var sym = item.symbols[key];
if (!sym.name) {
sym.name = key;
}
sym.name = escapeHTML(sym.name);
if (sym.description) {
sym.description = escapeHTML(sym.description);
}

if (sym.options) {
escape_HTML_array(sym.options);
}
});
break;
default:
if (typeof item[prop] === "string") {
item[prop] = escapeHTML(item[prop]);
}
}
}

if (item.action === "clean" || item.action === "no action") {
item.action = "<div style='font-size:11px' class='label label-success'>" + item.action + "</div>";
} else if (item.action === "rewrite subject" || item.action === "add header" || item.action === "probable spam") {
item.action = "<div style='font-size:11px' class='label label-warning'>" + item.action + "</div>";
} else if (item.action === "spam" || item.action === "reject") {
item.action = "<div style='font-size:11px' class='label label-danger'>" + item.action + "</div>";
} else {
item.action = "<div style='font-size:11px' class='label label-info'>" + item.action + "</div>";
}

var score_content = (item.score < item.required_score)
? "<span class='text-success'>" + item.score.toFixed(2) + " / " + item.required_score + "</span>"
: "<span class='text-danger'>" + item.score.toFixed(2) + " / " + item.required_score + "</span>";

item.score = {
options: {
sortValue: item.score
},
value: score_content
};
}

function getSelector(id) {
var e = document.getElementById(id);
return e.options[e.selectedIndex].value;
}

function get_compare_function() {
var 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[getSelector("selSymOrder")];
}

function sort_symbols(o, compare_function) {
return Object.keys(o)
.map(function (key) {
return o[key];
})
.sort(compare_function)
.map(function (e) { return e.str; })
.join("<br>\n");
}

function process_history_v2(data) {
// Display no more than rcpt_lim recipients
var rcpt_lim = 3;
var items = [];
var unsorted_symbols = [];
var compare_function = get_compare_function();

$("#selSymOrder, label[for='selSymOrder']").show();

$.each(data.rows,
function (i, item) {
function more(p) {
var l = item[p].length;
return (l > rcpt_lim) ? " … (" + l + ")" : "";
}
function format_rcpt(smtp, mime) {
var full = "";
var shrt = "";
if (smtp) {
full = "[" + item.rcpt_smtp.join(", ") + "] ";
shrt = "[" + item.rcpt_smtp.slice(0, rcpt_lim).join(",&#8203;") + more("rcpt_smtp") + "]";
if (mime) {
full += " ";
shrt += " ";
}
}
if (mime) {
full += item.rcpt_mime.join(", ");
shrt += item.rcpt_mime.slice(0, rcpt_lim).join(",&#8203;") + more("rcpt_mime");
}
return {full:full, shrt:shrt};
}

function get_symbol_class(name, score) {
if (name.match(/^GREYLIST$/)) {
return "symbol-special";
}

if (score < 0) {
return "symbol-negative";
} else if (score > 0) {
return "symbol-positive";
}
return null;
}

preprocess_item(item);
Object.keys(item.symbols).forEach(function (key) {
var sym = item.symbols[key];
sym.str = '<span class="symbol-default ' + get_symbol_class(sym.name, sym.score) + '"><strong>';

if (sym.description) {
sym.str += '<abbr data-sym-key="' + key + '">' +
sym.name + "</abbr></strong> (" + sym.score + ")</span>";
// Store description for tooltip
symbolDescriptions[key] = sym.description;
} else {
sym.str += sym.name + "</strong> (" + sym.score + ")</span>";
}

if (sym.options) {
sym.str += " [" + sym.options.join(",") + "]";
}
});
unsorted_symbols.push(item.symbols);
item.symbols = sort_symbols(item.symbols, compare_function);
item.time = {
value: unix_time_format(item.unix_time),
options: {
sortValue: item.unix_time
}
};
item.time_real = item.time_real.toFixed(3);
item.id = item["message-id"];

var rcpt = {};
if (!item.rcpt_mime.length) {
rcpt = format_rcpt(true, false);
} else if ($(item.rcpt_mime).not(item.rcpt_smtp).length !== 0 || $(item.rcpt_smtp).not(item.rcpt_mime).length !== 0) {
rcpt = format_rcpt(true, true);
} else {
rcpt = format_rcpt(false, true);
}
item.rcpt_mime_short = rcpt.shrt;
item.rcpt_mime = rcpt.full;

if (item.sender_mime !== item.sender_smtp) {
item.sender_mime = "[" + item.sender_smtp + "] " + item.sender_mime;
}
items.push(item);
});

return {items:items, symbols:unsorted_symbols};
}

function process_history_legacy(data) {
function process_history_legacy(rspamd, data) {
var items = [];

var compare = function (e1, e2) {
return e1.name.localeCompare(e2.name);
};

$("#selSymOrder, label[for='selSymOrder']").hide();
$("#selSymOrder_history, label[for='selSymOrder_history']").hide();

$.each(data, function (i, item) {
item.time = unix_time_format(item.unix_time);
preprocess_item(item);
item.time = rspamd.unix_time_format(item.unix_time);
rspamd.preprocess_item(rspamd, item);
item.symbols = Object.keys(item.symbols)
.map(function (key) {
return item.symbols[key];
@@ -274,7 +50,7 @@ define(["jquery", "footable", "humanize"],
.map(function (e) { return e.name; })
.join(", ");
item.time = {
value: unix_time_format(item.unix_time),
value: rspamd.unix_time_format(item.unix_time),
options: {
sortValue: item.unix_time
}
@@ -362,10 +138,10 @@ define(["jquery", "footable", "humanize"],
name: "symbols",
title: "Symbols<br /><br />" +
'<span style="font-weight:normal;">Sort by:</span><br />' +
'<div class="btn-group btn-group-xs btn-sym-order" data-toggle="buttons">' +
'<button type="button" class="btn btn-default btn-sym-magnitude" value="magnitude">Magnitude</button>' +
'<button type="button" class="btn btn-default btn-sym-score" value="score">Value</button>' +
'<button type="button" class="btn btn-default btn-sym-name" value="name">Name</button>' +
'<div class="btn-group btn-group-xs btn-sym-order-history" data-toggle="buttons">' +
'<button type="button" class="btn btn-default btn-sym-history-magnitude" value="magnitude">Magnitude</button>' +
'<button type="button" class="btn btn-default btn-sym-history-score" value="score">Value</button>' +
'<button type="button" class="btn btn-default btn-sym-history-name" value="name">Name</button>' +
"</div>",
breakpoints: "all",
style: {
@@ -500,17 +276,16 @@ define(["jquery", "footable", "humanize"],
}];
}

var process_functions = {
2: process_history_v2,
legacy: process_history_legacy
};

var columns = {
2: columns_v2,
legacy: columns_legacy
};

function process_history_data(data) {
function process_history_data(rspamd, data) {
var process_functions = {
2: rspamd.process_history_v2,
legacy: process_history_legacy
};
var pf = process_functions.legacy;

if (data.version) {
@@ -520,7 +295,7 @@ define(["jquery", "footable", "humanize"],
}
}

return pf(data);
return pf(rspamd, data, "history");
}

function get_history_columns(data) {
@@ -536,136 +311,7 @@ define(["jquery", "footable", "humanize"],
return func();
}

function drawTooltips() {
// Update symbol description tooltips
$.each(symbolDescriptions, function (key, description) {
$("abbr[data-sym-key=" + key + "]").tooltip({
placement: "bottom",
html: true,
title: description
});
});
}

function initHistoryTable(rspamd, tables, data, items) {
/* 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();
var self = this, $form_grp = $("<div/>", {
class: "form-group"
}).append($("<label/>", {
class: "sr-only",
text: "Action"
})).prependTo(self.$form);

self.$action = $("<select/>", {
class: "form-control"
}).on("change", {
self: self
}, self._onStatusDropdownChanged).append(
$("<option/>", {
text: self.def
})).appendTo($form_grp);

$.each(self.actions, function (i, action) {
self.$action.append($("<option/>").text(action));
});
},
_onStatusDropdownChanged: function (e) {
var self = e.data.self, selected = $(this).val();
if (selected !== self.def) {
if (selected === "reject") {
self.addFilter("action", "reject -soft", ["action"]);
} else {
self.addFilter("action", selected, ["action"]);
}
} else {
self.removeFilter("action");
}
self.filter();
},
draw: function () {
this._super();
var action = this.find("action");
if (action instanceof FooTable.Filter) {
if (action.query.val() === "reject -soft") {
this.$action.val("reject");
} else {
this.$action.val(action.query.val());
}
} else {
this.$action.val(this.def);
}
}
});
/* eslint-enable consistent-this, no-underscore-dangle, one-var-declaration-per-line */

tables.history = FooTable.init("#historyTable", {
columns: get_history_columns(data),
rows: items,
paging: {
enabled: true,
limit: 5,
size: page_size.history
},
filtering: {
enabled: true,
position: "left",
connectors: false
},
sorting: {
enabled: true
},
components: {
filtering: FooTable.actionFilter
},
on: {
"ready.ft.table": drawTooltips,
"after.ft.sorting": drawTooltips,
"after.ft.paging": drawTooltips,
"after.ft.filtering": drawTooltips,
"expand.ft.row": function (e, ft, row) {
setTimeout(function () {
var detail_row = row.$el.next();
var order = getSelector("selSymOrder");
detail_row.find(".btn-sym-" + order)
.addClass("active").siblings().removeClass("active");
}, 5);
}
}
});
}

function destroyTable(tables, table) {
if (tables[table]) {
tables[table].destroy();
delete tables[table];
}
}

ui.getHistory = function (rspamd, tables) {
function waitForRowsDisplayed(rows_total, callback, iteration) {
var i = (typeof iteration === "undefined") ? 10 : iteration;
var num_rows = $("#historyTable > tbody > tr").length;
if (num_rows === page_size.history ||
num_rows === rows_total) {
return callback();
} else if (--i) {
setTimeout(function () {
waitForRowsDisplayed(rows_total, callback, i);
}, 500);
}
return null;
}

rspamd.query("history", {
success: function (req_data) {
function differentVersions(neighbours_data) {
@@ -696,29 +342,29 @@ define(["jquery", "footable", "humanize"],
// Legacy version
data = [].concat.apply([], neighbours_data);
}
var o = process_history_data(data);
var o = process_history_data(rspamd, data);
var items = o.items;
symbols = o.symbols;
rspamd.symbols.history = o.symbols;

if (Object.prototype.hasOwnProperty.call(tables, "history") &&
version === prevVersion) {
tables.history.rows.load(items);
if (version) { // Non-legacy
// Is there a way to get an event when all rows are loaded?
waitForRowsDisplayed(items.length, function () {
drawTooltips();
rspamd.waitForRowsDisplayed("history", items.length, function () {
rspamd.drawTooltips();
});
}
} else {
destroyTable(tables, "history");
rspamd.destroyTable("history");
// Is there a way to get an event when the table is destroyed?
setTimeout(function () {
initHistoryTable(rspamd, tables, data, items);
rspamd.initHistoryTable(rspamd, data, items, "history", get_history_columns(data), false);
}, 200);
}
prevVersion = version;
} else {
destroyTable(tables, "history");
rspamd.destroyTable("history");
}
},
errorMessage: "Cannot receive history",
@@ -726,33 +372,14 @@ define(["jquery", "footable", "humanize"],
};

ui.setup = function (rspamd, tables) {
function change_symbols_order(order) {
$(".btn-sym-" + order).addClass("active").siblings().removeClass("active");
var compare_function = get_compare_function();
$.each(tables.history.rows.all, function (i, row) {
var cell_val = sort_symbols(symbols[i], compare_function);
row.cells[8].val(cell_val, false, true);
});
drawTooltips();
}
rspamd.set_page_size("history", $("#history_page_size").val());
rspamd.bindHistoryTableEventHandlers("history", 8);

$("#updateHistory").off("click");
$("#updateHistory").on("click", function (e) {
e.preventDefault();
ui.getHistory(rspamd, tables);
});
$("#selSymOrder").unbind().change(function () {
var order = this.value;
change_symbols_order(order);
});
$("#history_page_size").change(function () {
set_page_size(this.value, function (n) { tables.history.pageSize(n); });
});
$(document).on("click", ".btn-sym-order button", function () {
var order = this.value;
$("#selSymOrder").val(order);
change_symbols_order(order);
});

// @reset history log
$("#resetHistory").off("click");
@@ -761,8 +388,8 @@ define(["jquery", "footable", "humanize"],
if (!confirm("Are you sure you want to reset history log?")) { // eslint-disable-line no-alert
return;
}
destroyTable(tables, "history");
destroyTable(tables, "errors");
rspamd.destroyTable("history");
rspamd.destroyTable("errors");

rspamd.query("historyreset", {
success: function () {
@@ -774,7 +401,7 @@ define(["jquery", "footable", "humanize"],
});
};

function initErrorsTable(tables, rows) {
function initErrorsTable(rspamd, tables, rows) {
tables.errors = FooTable.init("#errorsLog", {
columns: [
{sorted:true, direction:"DESC", name:"ts", title:"Time", style:{"font-size":"11px", "width":300, "maxWidth":300}},
@@ -788,7 +415,7 @@ define(["jquery", "footable", "humanize"],
paging: {
enabled: true,
limit: 5,
size: page_size.errors
size: rspamd.page_size.errors
},
filtering: {
enabled: true,
@@ -816,7 +443,7 @@ define(["jquery", "footable", "humanize"],
var rows = [].concat.apply([], neighbours_data);
$.each(rows, function (i, item) {
item.ts = {
value: unix_time_format(item.ts),
value: rspamd.unix_time_format(item.ts),
options: {
sortValue: item.ts
}
@@ -825,7 +452,7 @@ define(["jquery", "footable", "humanize"],
if (Object.prototype.hasOwnProperty.call(tables, "errors")) {
tables.errors.rows.load(rows);
} else {
initErrorsTable(tables, rows);
initErrorsTable(rspamd, tables, rows);
}
}
});

+ 398
- 6
interface/js/app/rspamd.js View File

@@ -23,7 +23,7 @@
THE SOFTWARE.
*/

/* global jQuery:false, Visibility:false */
/* global jQuery:false, FooTable:false, Visibility:false */

define(["jquery", "d3pie", "visibility", "nprogress", "stickytabs", "app/stats", "app/graph", "app/config",
"app/symbols", "app/history", "app/upload"],
@@ -31,14 +31,25 @@ define(["jquery", "d3pie", "visibility", "nprogress", "stickytabs", "app/stats",
function ($, D3pie, visibility, NProgress, stickyTabs, tab_stat, tab_graph, tab_config,
tab_symbols, tab_history, tab_upload) {
"use strict";
// begin
var ui = {
page_size: {
scan: 25,
errors: 25,
history: 25
},
symbols: {
scan: [],
history: []
}
};

var graphs = {};
var tables = {};
var neighbours = []; // list of clusters
var checked_server = "All SERVERS";
var ui = {};
var timer_id = [];
var selData = null; // Graph's dataset selector state
var symbolDescriptions = {};

NProgress.configure({
minimum: 0.01,
@@ -124,16 +135,72 @@ function ($, D3pie, visibility, NProgress, stickyTabs, tab_stat, tab_graph, tab_
}, 1000);
}

// @return password
function drawTooltips() {
// Update symbol description tooltips
$.each(symbolDescriptions, function (key, description) {
$("abbr[data-sym-key=" + key + "]").tooltip({
placement: "bottom",
html: true,
title: description
});
});
}

function getPassword() {
return sessionStorage.getItem("Password");
}

// @save credentials
function getSelector(id) {
var e = document.getElementById(id);
return e.options[e.selectedIndex].value;
}

function get_compare_function(table) {
var 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[getSelector("selSymOrder_" + table)];
}

function saveCredentials(password) {
sessionStorage.setItem("Password", password);
}

function set_page_size(table, page_size, callback) {
var n = parseInt(page_size, 10); // HTML Input elements return string representing a number
if (n !== ui.page_size[table] && n > 0) {
ui.page_size[table] = n;
if (callback) {
return callback(n);
}
}
return null;
}

function sort_symbols(o, compare_function) {
return Object.keys(o)
.map(function (key) {
return o[key];
})
.sort(compare_function)
.map(function (e) { return e.str; })
.join("<br>\n");
}

function unix_time_format(tm) {
var date = new Date(tm ? tm * 1000 : 0);
return date.toLocaleString();
}

function displayUI() {
ui.query("auth", {
success: function (neighbours_status) {
@@ -307,7 +374,7 @@ function ($, D3pie, visibility, NProgress, stickyTabs, tab_stat, tab_graph, tab_
tab_config.setup(ui);
tab_history.setup(ui, tables);
tab_symbols.setup(ui, tables);
tab_upload.setup(ui);
tab_upload.setup(ui, tables);
selData = tab_graph.setup();
};

@@ -460,6 +527,7 @@ function ($, D3pie, visibility, NProgress, stickyTabs, tab_stat, tab_graph, tab_
};

ui.getPassword = getPassword;
ui.getSelector = getSelector;

/**
* @param {string} url - A string containing the URL to which the request is sent
@@ -536,5 +604,329 @@ function ($, D3pie, visibility, NProgress, stickyTabs, tab_stat, tab_graph, tab_
}
};

// Scan and History shared functions

ui.drawTooltips = drawTooltips;
ui.unix_time_format = unix_time_format;
ui.set_page_size = set_page_size;

ui.bindHistoryTableEventHandlers = function (table, symbolsCol) {
function change_symbols_order(order) {
$(".btn-sym-" + table + "-" + order).addClass("active").siblings().removeClass("active");
var compare_function = get_compare_function(table);
$.each(tables[table].rows.all, function (i, row) {
var cell_val = sort_symbols(ui.symbols[table][i], compare_function);
row.cells[symbolsCol].val(cell_val, false, true);
});
drawTooltips();
}

$("#selSymOrder_" + table).unbind().change(function () {
var order = this.value;
change_symbols_order(order);
});
$("#" + table + "_page_size").change(function () {
set_page_size(table, this.value, function (n) { tables[table].pageSize(n); });
});
$(document).on("click", ".btn-sym-order-" + table + " button", function () {
var order = this.value;
$("#selSymOrder_" + table).val(order);
change_symbols_order(order);
});
};

ui.destroyTable = function (table) {
if (tables[table]) {
tables[table].destroy();
delete tables[table];
}
};


ui.initHistoryTable = function (rspamd, data, items, table, columns, expandFirst) {
/* 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();
var self = this, $form_grp = $("<div/>", {
class: "form-group"
}).append($("<label/>", {
class: "sr-only",
text: "Action"
})).prependTo(self.$form);

self.$action = $("<select/>", {
class: "form-control"
}).on("change", {
self: self
}, self._onStatusDropdownChanged).append(
$("<option/>", {
text: self.def
})).appendTo($form_grp);

$.each(self.actions, function (i, action) {
self.$action.append($("<option/>").text(action));
});
},
_onStatusDropdownChanged: function (e) {
var self = e.data.self, selected = $(this).val();
if (selected !== self.def) {
if (selected === "reject") {
self.addFilter("action", "reject -soft", ["action"]);
} else {
self.addFilter("action", selected, ["action"]);
}
} else {
self.removeFilter("action");
}
self.filter();
},
draw: function () {
this._super();
var action = this.find("action");
if (action instanceof FooTable.Filter) {
if (action.query.val() === "reject -soft") {
this.$action.val("reject");
} else {
this.$action.val(action.query.val());
}
} else {
this.$action.val(this.def);
}
}
});
/* eslint-enable consistent-this, no-underscore-dangle, one-var-declaration-per-line */

tables[table] = FooTable.init("#historyTable_" + table, {
columns: columns,
rows: items,
expandFirst: expandFirst,
paging: {
enabled: true,
limit: 5,
size: ui.page_size[table]
},
filtering: {
enabled: true,
position: "left",
connectors: false
},
sorting: {
enabled: true
},
components: {
filtering: FooTable.actionFilter
},
on: {
"ready.ft.table": drawTooltips,
"after.ft.sorting": drawTooltips,
"after.ft.paging": drawTooltips,
"after.ft.filtering": drawTooltips,
"expand.ft.row": function (e, ft, row) {
setTimeout(function () {
var detail_row = row.$el.next();
var order = getSelector("selSymOrder_" + table);
detail_row.find(".btn-sym-" + table + "-" + order)
.addClass("active").siblings().removeClass("active");
}, 5);
}
}
});
};

ui.preprocess_item = function (rspamd, item) {
function escapeHTML(string) {
var htmlEscaper = /[&<>"'/`=]/g;
var htmlEscapes = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
"\"": "&quot;",
"'": "&#39;",
"/": "&#x2F;",
"`": "&#x60;",
"=": "&#x3D;"
};
return String(string).replace(htmlEscaper, function (match) {
return htmlEscapes[match];
});
}
function escape_HTML_array(arr) {
arr.forEach(function (d, i) { arr[i] = escapeHTML(d); });
}

for (var prop in item) {
if (!{}.hasOwnProperty.call(item, prop)) continue;
switch (prop) {
case "rcpt_mime":
case "rcpt_smtp":
escape_HTML_array(item[prop]);
break;
case "symbols":
Object.keys(item.symbols).forEach(function (key) {
var sym = item.symbols[key];
if (!sym.name) {
sym.name = key;
}
sym.name = escapeHTML(sym.name);
if (sym.description) {
sym.description = escapeHTML(sym.description);
}

if (sym.options) {
escape_HTML_array(sym.options);
}
});
break;
default:
if (typeof item[prop] === "string") {
item[prop] = escapeHTML(item[prop]);
}
}
}

if (item.action === "clean" || item.action === "no action") {
item.action = "<div style='font-size:11px' class='label label-success'>" + item.action + "</div>";
} else if (item.action === "rewrite subject" || item.action === "add header" || item.action === "probable spam") {
item.action = "<div style='font-size:11px' class='label label-warning'>" + item.action + "</div>";
} else if (item.action === "spam" || item.action === "reject") {
item.action = "<div style='font-size:11px' class='label label-danger'>" + item.action + "</div>";
} else {
item.action = "<div style='font-size:11px' class='label label-info'>" + item.action + "</div>";
}

var score_content = (item.score < item.required_score)
? "<span class='text-success'>" + item.score.toFixed(2) + " / " + item.required_score + "</span>"
: "<span class='text-danger'>" + item.score.toFixed(2) + " / " + item.required_score + "</span>";

item.score = {
options: {
sortValue: item.score
},
value: score_content
};
};

ui.process_history_v2 = function (rspamd, data, table) {
// Display no more than rcpt_lim recipients
var rcpt_lim = 3;
var items = [];
var unsorted_symbols = [];
var compare_function = get_compare_function(table);

$("#selSymOrder_" + table + ", label[for='selSymOrder_" + table + "']").show();

$.each(data.rows,
function (i, item) {
function more(p) {
var l = item[p].length;
return (l > rcpt_lim) ? " … (" + l + ")" : "";
}
function format_rcpt(smtp, mime) {
var full = "";
var shrt = "";
if (smtp) {
full = "[" + item.rcpt_smtp.join(", ") + "] ";
shrt = "[" + item.rcpt_smtp.slice(0, rcpt_lim).join(",&#8203;") + more("rcpt_smtp") + "]";
if (mime) {
full += " ";
shrt += " ";
}
}
if (mime) {
full += item.rcpt_mime.join(", ");
shrt += item.rcpt_mime.slice(0, rcpt_lim).join(",&#8203;") + more("rcpt_mime");
}
return {full:full, shrt:shrt};
}

function get_symbol_class(name, score) {
if (name.match(/^GREYLIST$/)) {
return "symbol-special";
}

if (score < 0) {
return "symbol-negative";
} else if (score > 0) {
return "symbol-positive";
}
return null;
}

rspamd.preprocess_item(rspamd, item);
Object.keys(item.symbols).forEach(function (key) {
var sym = item.symbols[key];
sym.str = '<span class="symbol-default ' + get_symbol_class(sym.name, sym.score) + '"><strong>';

if (sym.description) {
sym.str += '<abbr data-sym-key="' + key + '">' +
sym.name + "</abbr></strong> (" + sym.score + ")</span>";
// Store description for tooltip
symbolDescriptions[key] = sym.description;
} else {
sym.str += sym.name + "</strong> (" + sym.score + ")</span>";
}

if (sym.options) {
sym.str += " [" + sym.options.join(",") + "]";
}
});
unsorted_symbols.push(item.symbols);
item.symbols = sort_symbols(item.symbols, compare_function);
if (table === "scan") {
item.unix_time = (new Date()).getTime() / 1000;
}
item.time = {
value: unix_time_format(item.unix_time),
options: {
sortValue: item.unix_time
}
};
item.time_real = item.time_real.toFixed(3);
item.id = item["message-id"];

if (table === "history") {
var rcpt = {};
if (!item.rcpt_mime.length) {
rcpt = format_rcpt(true, false);
} else if ($(item.rcpt_mime).not(item.rcpt_smtp).length !== 0 || $(item.rcpt_smtp).not(item.rcpt_mime).length !== 0) {
rcpt = format_rcpt(true, true);
} else {
rcpt = format_rcpt(false, true);
}
item.rcpt_mime_short = rcpt.shrt;
item.rcpt_mime = rcpt.full;

if (item.sender_mime !== item.sender_smtp) {
item.sender_mime = "[" + item.sender_smtp + "] " + item.sender_mime;
}
}
items.push(item);
});

return {items:items, symbols:unsorted_symbols};
};

ui.waitForRowsDisplayed = function (table, rows_total, callback, iteration) {
var i = (typeof iteration === "undefined") ? 10 : iteration;
var num_rows = $("#historyTable_" + table + " > tbody > tr:not(.footable-detail-row)").length;
if (num_rows === ui.page_size[table] ||
num_rows === rows_total) {
return callback();
} else if (--i) {
setTimeout(function () {
ui.waitForRowsDisplayed(table, rows_total, callback, i);
}, 500);
}
return null;
};

return ui;
});

+ 103
- 58
interface/js/app/upload.js View File

@@ -59,9 +59,72 @@ define(["jquery"],
}
});
}

function columns_v2() {
return [{
name: "id",
title: "ID",
style: {
"font-size": "11px",
"minWidth": 130,
"overflow": "hidden",
"textOverflow": "ellipsis",
"wordBreak": "break-all",
"whiteSpace": "normal"
}
}, {
name: "action",
title: "Action",
style: {
"font-size": "11px",
"minwidth": 82
}
}, {
name: "score",
title: "Score",
style: {
"font-size": "11px",
"maxWidth": 110
},
sortValue: function (val) { return Number(val.options.sortValue); }
}, {
name: "symbols",
title: "Symbols<br /><br />" +
'<span style="font-weight:normal;">Sort by:</span><br />' +
'<div class="btn-group btn-group-xs btn-sym-order-scan" data-toggle="buttons">' +
'<button type="button" class="btn btn-default btn-sym-scan-magnitude" value="magnitude">Magnitude</button>' +
'<button type="button" class="btn btn-default btn-sym-scan-score" value="score">Value</button>' +
'<button type="button" class="btn btn-default btn-sym-scan-name" value="name">Name</button>' +
"</div>",
breakpoints: "all",
style: {
"font-size": "11px",
"width": 550,
"maxWidth": 550
}
}, {
name: "time_real",
title: "Scan time",
breakpoints: "xs sm md",
style: {
"font-size": "11px",
"maxWidth": 72
},
sortValue: function (val) { return Number(val); }
}, {
sorted: true,
direction: "DESC",
name: "time",
title: "Time",
style: {
"font-size": "11px"
},
sortValue: function (val) { return Number(val.options.sortValue); }
}];
}

// @upload text
function scanText(rspamd, data, server) {
var items = [];
function scanText(rspamd, tables, data, server) {
rspamd.query("checkv2", {
data: data,
params: {
@@ -72,58 +135,33 @@ define(["jquery"],
var json = neighbours_status[0].data;
if (json.action) {
rspamd.alertMessage("alert-success", "Data successfully scanned");
var action = "";

if (json.action === "clean" || json.action === "no action") {
action = "label-success";
} else if (json.action === "rewrite subject" || json.action === "add header" || json.action === "probable spam") {
action = "label-warning";
} else if (json.action === "spam") {
action = "label-danger";
}

var score = "";
if (json.score <= json.required_score) {
score = "label-success";
} else if (json.score >= json.required_score) {
score = "label-danger";
}
$("<tbody id=\"tmpBody\"><tr>" +
"<td><span class=\"label " + action + "\">" + json.action + "</span></td>" +
"<td><span class=\"label " + score + "\">" + json.score.toFixed(2) + "/" + json.required_score.toFixed(2) + "</span></td>" +
"</tr></tbody>")
.insertAfter("#scanOutput thead");
var sym_desc = {};
var nsym = 0;
var rows_total = $("#historyTable_scan > tbody > tr:not(.footable-detail-row)").length + 1;
var o = rspamd.process_history_v2(rspamd, {rows:[json]}, "scan");
var items = o.items;
rspamd.symbols.scan.push(o.symbols[0]);

$.each(json.symbols, function (i, item) {
if (typeof item === "object") {
var sym_id = "sym_" + nsym;
if (item.description) {
sym_desc[sym_id] = item.description;
}
items.push("<div class=\"cell-overflow\" tabindex=\"1\"><abbr id=\"" + sym_id +
"\">" + item.name + "</abbr>: " + item.score.toFixed(2) + "</div>");
nsym++;
}
});
$("<td/>", {
id: "tmpSymbols",
html: items.join("")
}).appendTo("#scanResult");
$("#tmpSymbols").insertAfter("#tmpBody td:last").removeAttr("id");
$("#tmpBody").removeAttr("id");
$("#scanResult").show();
// Show tooltips
$.each(sym_desc, function (k, v) {
$("#" + k).tooltip({
placement: "bottom",
title: v
if (Object.prototype.hasOwnProperty.call(tables, "scan")) {
tables.scan.rows.load(items, true);
// Is there a way to get an event when all rows are loaded?
rspamd.waitForRowsDisplayed("scan", rows_total, function () {
rspamd.drawTooltips();
$("html, body").animate({
scrollTop: $("#scanResult").offset().top
}, 1000);
});
});
$("html, body").animate({
scrollTop: $("#scanResult").offset().top
}, 1000);
} else {
rspamd.destroyTable("scan");
// Is there a way to get an event when the table is destroyed?
setTimeout(function () {
rspamd.initHistoryTable(rspamd, data, items, "scan", columns_v2(), true);
rspamd.waitForRowsDisplayed("scan", rows_total, function () {
$("html, body").animate({
scrollTop: $("#scanResult").offset().top
}, 1000);
});
}, 200);
}
} else {
rspamd.alertMessage("alert-error", "Cannot scan data");
}
@@ -144,12 +182,19 @@ define(["jquery"],
});
}

ui.setup = function (rspamd) {
function getSelector(id) {
var e = document.getElementById(id);
return e.options[e.selectedIndex].value;
}
ui.setup = function (rspamd, tables) {
rspamd.set_page_size("scan", $("#scan_page_size").val());
rspamd.bindHistoryTableEventHandlers("scan", 3);

$("#cleanScanHistory").off("click");
$("#cleanScanHistory").on("click", function (e) {
e.preventDefault();
if (!confirm("Are you sure you want to clean scan history?")) { // eslint-disable-line no-alert
return;
}
rspamd.destroyTable("scan");
rspamd.symbols.scan.length = 0;
});
$("#scan button").attr("disabled", true);
$("textarea").on("input", function () {
var $this = $(this);
@@ -176,9 +221,9 @@ define(["jquery"],
: {};
if ($.trim(data).length > 0) {
if (source === "scan") {
var checked_server = getSelector("selSrv");
var checked_server = rspamd.getSelector("selSrv");
var server = (checked_server === "All SERVERS") ? "local" : checked_server;
scanText(rspamd, data, server);
scanText(rspamd, tables, data, server);
} else {
uploadText(rspamd, data, source, headers);
}

Loading…
Cancel
Save