@@ -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"], |
@@ -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; |
@@ -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. score">Score / Req. 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> |
@@ -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 = { | |||
"&": "&", | |||
"<": "<", | |||
">": ">", | |||
"\"": """, | |||
"'": "'", | |||
"/": "/", | |||
"`": "`", | |||
"=": "=" | |||
}; | |||
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(",​") + more("rcpt_smtp") + "]"; | |||
if (mime) { | |||
full += " "; | |||
shrt += " "; | |||
} | |||
} | |||
if (mime) { | |||
full += item.rcpt_mime.join(", "); | |||
shrt += item.rcpt_mime.slice(0, rcpt_lim).join(",​") + 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); | |||
} | |||
} | |||
}); |
@@ -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 = { | |||
"&": "&", | |||
"<": "<", | |||
">": ">", | |||
"\"": """, | |||
"'": "'", | |||
"/": "/", | |||
"`": "`", | |||
"=": "=" | |||
}; | |||
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(",​") + more("rcpt_smtp") + "]"; | |||
if (mime) { | |||
full += " "; | |||
shrt += " "; | |||
} | |||
} | |||
if (mime) { | |||
full += item.rcpt_mime.join(", "); | |||
shrt += item.rcpt_mime.slice(0, rcpt_lim).join(",​") + 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; | |||
}); |
@@ -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); | |||
} |