Merge branch 'development-v6' into new/loading

Signed-off-by: DL6ER <dl6er@dl6er.de>
This commit is contained in:
DL6ER
2023-11-25 08:48:34 +01:00
40 changed files with 2165 additions and 2294 deletions

7
.devcontainer/Dockerfile Normal file
View File

@@ -0,0 +1,7 @@
FROM node:21-alpine3.18
RUN apk add --no-cache \
git \
nano\
openssh
USER node

View File

@@ -0,0 +1,23 @@
{
"name": "Pi-hole web devcontainer",
"dockerFile": "Dockerfile",
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "npm install",
"customizations": {
// Configure properties specific to VS Code.
"vscode": {
"settings": {},
"extensions": [
"eamodio.gitlens",
"EditorConfig.EditorConfig",
"github.vscode-github-actions"
]
}
},
"containerEnv": {
"GIT_EDITOR": "nano"
},
"mounts": [
"type=bind,source=/home/${localEnv:USER}/.ssh,target=/home/node/.ssh,readonly"
]
}

View File

@@ -17,7 +17,7 @@ indent_size = 2
[*.js]
indent_size = 2
[package.json]
[*.json]
indent_size = 2
[.yamllint.conf]

View File

@@ -1,6 +1,9 @@
name: Codespell
on:
push:
branches:
- '**'
pull_request:
types: [opened, synchronize, reopened, ready_for_review]

26
error403.lp Normal file
View File

@@ -0,0 +1,26 @@
<? --[[
* Pi-hole: A black hole for Internet advertisements
* (c) 2023 Pi-hole, LLC (https://pi-hole.net)
* Network-wide ad blocking via your own hardware.
*
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license.
--]]
mg.include('scripts/pi-hole/lua/header.lp','r')
?>
<body class="hold-transition layout-boxed login-page">
<div class="box login-box">
<section style="padding: 15px;">
<h2 class="error-headline text-danger">403</h2>
<div class="error-content">
<h3><i class="fa fa-times-circle text-danger"></i> Oops! Access denied.</h3>
<p>
You don't have permission to access <code><?=mg.request_info.request_uri?></code> on this server.<br>
Did you mean to go to <a href="<?=pihole.webhome()?>">your Pi-hole's dashboard</a> instead?
</p>
</div>
</section>
</div>
</body>
</html>

View File

@@ -41,7 +41,8 @@ mg.include('scripts/pi-hole/lua/header_authenticated.lp','r')
</div>
<div class="row">
<div class="col-md-12">
<p>You can select an existing client or add a custom one by typing into the field above and confirming your entry with <kbd>&#x23CE;</kbd>.</p>
<p>You can select an existing client or add a custom one by typing into the field above and confirming your entry with <kbd>&#x23CE;</kbd>.
Multiple clients can be added by separating each client with a space or comma.</p>
<p>Clients may be described either by their IP addresses (IPv4 and IPv6 are supported),
IP subnets (CIDR notation, like <code>192.168.2.0/24</code>),
their MAC addresses (like <code>12:34:56:78:9A:BC</code>),

View File

@@ -86,7 +86,8 @@ mg.include('scripts/pi-hole/lua/header_authenticated.lp','r')
<div>
<p><strong>Note:</strong><br>
The domain or regex filter will be automatically assigned to the Default Group.<br>
Other groups can optionally be assigned in the list below (using <b>Group assignment</b>).
Other groups can optionally be assigned in the list below (using <strong>Group assignment</strong>).<br>
You can add multiple entries at once by separating them with spaces (e.g. <code>example.com example.org</code>).
</p>
</div>
<div class="btn-toolbar pull-right" role="toolbar" aria-label="Toolbar with buttons">

View File

@@ -30,7 +30,7 @@ mg.include('scripts/pi-hole/lua/header_authenticated.lp','r')
<div class="row">
<div class="form-group col-md-6">
<label for="new_address">Address:</label>
<input id="new_address" type="text" class="form-control" placeholder="URL or space-separated URLs" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
<input id="new_address" type="text" class="form-control" placeholder="URL" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
</div>
<div class="form-group col-md-6">
<label for="new_comment">Comment:</label>
@@ -42,7 +42,7 @@ mg.include('scripts/pi-hole/lua/header_authenticated.lp','r')
<strong>Hints:</strong>
<ol>
<li>Please run <code>pihole -g</code> or update your gravity list <a href="<?=webhome?>gravity">online</a> after modifying your lists.</li>
<li>Multiple lists can be added by separating each <i>unique</i> URL with a space</li>
<li>Multiple lists can be added by separating each <i>unique</i> URL with a space or comma</li>
<li>Click on the icon in the first column to get additional information about your lists. The icons correspond to the health of the list.</li>
</ol>
<div class="btn-toolbar pull-right" role="toolbar" aria-label="Toolbar with buttons">

View File

@@ -30,7 +30,7 @@ mg.include('scripts/pi-hole/lua/header_authenticated.lp','r')
<div class="row">
<div class="form-group col-md-6">
<label for="new_name">Name:</label>
<input id="new_name" type="text" class="form-control" placeholder="Group name or space-separated group names">
<input id="new_name" type="text" class="form-control" placeholder="Group name">
</div>
<div class="form-group col-md-6">
<label for="new_comment">Comment:</label>
@@ -41,7 +41,7 @@ mg.include('scripts/pi-hole/lua/header_authenticated.lp','r')
<div class="box-footer clearfix">
<strong>Hints:</strong>
<ol>
<li>Multiple groups can be added by separating each group name with a space</li>
<li>Multiple groups can be added by separating each group name with a space or comma</li>
<li>Group names can have spaces if entered in quotes. e.g "My New Group"</li>
</ol>
<button type="button" id="btnAdd" class="btn btn-primary pull-right">Add</button>

1991
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -29,7 +29,7 @@
"eslint-plugin-compat": "^4.2.0",
"postcss": "^8.4.31",
"postcss-cli": "^10.1.0",
"prettier": "3.0.3",
"prettier": "^3.1.0",
"xo": "^0.56.0"
},
"browserslist": [

View File

@@ -5,6 +5,8 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
/* global upstreams */
// eslint-disable-next-line no-unused-vars
var THEME_COLORS = [
"#f56954",
@@ -97,7 +99,8 @@ const htmlLegendPlugin = {
window.location.href = "queries.lp?type=" + item.text;
} else if (chart.canvas.id === "forwardDestinationPieChart") {
// Encode the forward destination as it may contain an "#" character
window.location.href = "queries.lp?upstream=" + encodeURIComponent(item.text);
const upstream = encodeURIComponent(upstreams[item.text]);
window.location.href = "queries.lp?upstream=" + upstream;
}
});
}

View File

@@ -12,6 +12,24 @@
var settingsLevel = 0;
const REFRESH_INTERVAL = {
logs: 500, // 0.5 sec (logs page)
summary: 1000, // 1 sec (dashboard)
blocking: 10000, // 10 sec (all pages, sidebar)
metrics: 10000, // 10 sec (settings page)
system: 20000, // 20 sec (all pages, sidebar)
sensors: 20000, // 20 sec (all pages, sidebar)
query_types: 60000, // 1 min (dashboard)
upstreams: 60000, // 1 min (dashboard)
top_lists: 60000, // 1 min (dashboard)
messages: 60000, // 1 min (all pages)
version: 120000, // 2 min (all pages, footer)
ftl: 120000, // 2 min (all pages, sidebar)
hosts: 120000, // 2 min (settings page)
history: 600000, // 10 min (dashboard)
clients: 600000, // 10 min (dashboard)
};
function secondsTimeSpanToHMS(s) {
var h = Math.floor(s / 3600); //Get whole hours
s -= h * 3600;
@@ -86,17 +104,23 @@ function countDown() {
}
function checkBlocking() {
// Skip if page is hidden
if (document.hidden) {
utils.setTimer(checkBlocking, REFRESH_INTERVAL.blocking);
return;
}
$.ajax({
url: "/api/dns/blocking",
method: "GET",
})
.done(function (data) {
piholeChanged(data.blocking);
setTimeout(checkBlocking, 10000);
utils.setTimer(checkBlocking, REFRESH_INTERVAL.blocking);
})
.fail(function (data) {
apiFailure(data);
setTimeout(checkBlocking, 30000);
utils.setTimer(checkBlocking, 3 * REFRESH_INTERVAL.blocking);
});
}
@@ -121,6 +145,7 @@ function piholeChange(action, duration) {
method: "POST",
dataType: "json",
processData: false,
contentType: "application/json; charset=utf-8",
data: JSON.stringify({
blocking: action === "enable",
timer: parseInt(duration, 10) > 0 ? parseInt(duration, 10) : null,
@@ -240,9 +265,8 @@ function updateFtlInfo() {
ftl.allow_destructive ? "" : "Destructive actions are disabled by a config setting"
);
// Update every 120 seconds
clearTimeout(ftlinfoTimer);
ftlinfoTimer = setTimeout(updateFtlInfo, 120000);
ftlinfoTimer = utils.setTimer(updateFtlInfo, REFRESH_INTERVAL.ftl);
})
.fail(function (data) {
apiFailure(data);
@@ -339,9 +363,9 @@ function updateSystemInfo() {
moment.duration(1000 * system.uptime).humanize() + " (running since " + startdate + ")"
);
$("#sysinfo-system-overlay").hide();
// Update every 20 seconds
clearTimeout(systemTimer);
systemTimer = setTimeout(updateSystemInfo, 20000);
systemTimer = utils.setTimer(updateSystemInfo, REFRESH_INTERVAL.system);
})
.fail(function (data) {
apiFailure(data);
@@ -394,7 +418,7 @@ function updateSensorsInfo() {
// Update every 20 seconds
clearTimeout(sensorsTimer);
sensorsTimer = setTimeout(updateSensorsInfo, 20000);
sensorsTimer = utils.setTimer(updateSensorsInfo, REFRESH_INTERVAL.sensors);
})
.fail(function (data) {
apiFailure(data);
@@ -563,9 +587,8 @@ function updateVersionInfo() {
'To install updates, run <code><a href="https://docs.pi-hole.net/main/update/" rel="noopener" target="_blank">pihole -up</a></code>.'
);
// Update every 120 seconds
clearTimeout(versionTimer);
versionTimer = setTimeout(updateVersionInfo, 120000);
versionTimer = utils.setTimer(updateVersionInfo, REFRESH_INTERVAL.version);
});
}
@@ -588,8 +611,8 @@ $(function () {
if (window.location.pathname !== "/admin/login") {
// Run check immediately after page loading ...
utils.checkMessages();
// ... and once again with five seconds delay
setTimeout(utils.checkMessages, 5000);
// ... and then periodically
utils.setInter(utils.checkMessages, REFRESH_INTERVAL.messages);
}
});

View File

@@ -6,6 +6,7 @@
* Please see LICENSE file for your rights under this license. */
/* global utils:false, groups:false,, apiFailure:false, updateFtlInfo:false, getGroups:false, processGroupResult:false */
/* exported initTable */
var table;
@@ -82,7 +83,7 @@ $(function () {
reloadClientSuggestions();
utils.setBsSelectDefaults();
initTable();
getGroups();
$("#select").on("change", function () {
$("#ip-custom").val("");
@@ -90,231 +91,228 @@ $(function () {
});
});
// eslint-disable-next-line no-unused-vars
function initTable() {
table = $("#clientsTable")
.on("preXhr.dt", function () {
getGroups();
})
.DataTable({
processing: true,
ajax: {
url: "/api/clients",
dataSrc: "clients",
type: "GET",
table = $("#clientsTable").DataTable({
processing: true,
ajax: {
url: "/api/clients",
dataSrc: "clients",
type: "GET",
},
order: [[0, "asc"]],
columns: [
{ data: "id", visible: false },
{ data: null, visible: true, orderable: false, width: "15px" },
{ data: "client", type: "ip-address" },
{ data: "comment" },
{ data: "groups", searchable: false },
{ data: null, width: "22px", orderable: false },
],
columnDefs: [
{
targets: 1,
className: "select-checkbox",
render: function () {
return "";
},
},
order: [[0, "asc"]],
columns: [
{ data: "id", visible: false },
{ data: null, visible: true, orderable: false, width: "15px" },
{ data: "client", type: "ip-address" },
{ data: "comment" },
{ data: "groups", searchable: false },
{ data: null, width: "22px", orderable: false },
],
columnDefs: [
{
targets: 1,
className: "select-checkbox",
render: function () {
return "";
},
},
{
targets: "_all",
render: $.fn.dataTable.render.text(),
},
],
drawCallback: function () {
// Hide buttons if all clients were deleted
var hasRows = this.api().rows({ filter: "applied" }).data().length > 0;
$(".datatable-bt").css("visibility", hasRows ? "visible" : "hidden");
{
targets: "_all",
render: $.fn.dataTable.render.text(),
},
],
drawCallback: function () {
// Hide buttons if all clients were deleted
var hasRows = this.api().rows({ filter: "applied" }).data().length > 0;
$(".datatable-bt").css("visibility", hasRows ? "visible" : "hidden");
$('button[id^="deleteClient_"]').on("click", deleteClient);
// Remove visible dropdown to prevent orphaning
$("body > .bootstrap-select.dropdown").remove();
},
rowCallback: function (row, data) {
var dataId = utils.hexEncode(data.client);
$(row).attr("data-id", dataId);
var tooltip =
"Added: " +
utils.datetime(data.date_added, false) +
"\nLast modified: " +
utils.datetime(data.date_modified, false) +
"\nDatabase ID: " +
data.id;
var ipName =
'<code id="ip_' +
$('button[id^="deleteClient_"]').on("click", deleteClient);
// Remove visible dropdown to prevent orphaning
$("body > .bootstrap-select.dropdown").remove();
},
rowCallback: function (row, data) {
var dataId = utils.hexEncode(data.client);
$(row).attr("data-id", dataId);
var tooltip =
"Added: " +
utils.datetime(data.date_added, false) +
"\nLast modified: " +
utils.datetime(data.date_modified, false) +
"\nDatabase ID: " +
data.id;
var ipName =
'<code id="ip_' +
dataId +
'" title="' +
tooltip +
'" class="breakall">' +
utils.escapeHtml(data.client) +
"</code>";
if (data.name !== null && data.name.length > 0)
ipName +=
'<br><code id="name_' +
dataId +
'" title="' +
tooltip +
'" class="breakall">' +
data.client +
utils.escapeHtml(data.name) +
"</code>";
if (data.name !== null && data.name.length > 0)
ipName +=
'<br><code id="name_' +
dataId +
'" title="' +
tooltip +
'" class="breakall">' +
data.name +
"</code>";
$("td:eq(1)", row).html(ipName);
$("td:eq(1)", row).html(ipName);
$("td:eq(2)", row).html('<input id="comment_' + dataId + '" class="form-control">');
var commentEl = $("#comment_" + dataId, row);
commentEl.val(utils.unescapeHtml(data.comment));
commentEl.on("change", editClient);
$("td:eq(2)", row).html('<input id="comment_' + dataId + '" class="form-control">');
var commentEl = $("#comment_" + dataId, row);
commentEl.val(data.comment);
commentEl.on("change", editClient);
$("td:eq(3)", row).empty();
$("td:eq(3)", row).append(
'<select class="selectpicker" id="multiselect_' + dataId + '" multiple></select>'
$("td:eq(3)", row).empty();
$("td:eq(3)", row).append(
'<select class="selectpicker" id="multiselect_' + dataId + '" multiple></select>'
);
var selectEl = $("#multiselect_" + dataId, row);
// Add all known groups
for (var i = 0; i < groups.length; i++) {
var dataSub = "";
if (!groups[i].enabled) {
dataSub = 'data-subtext="(disabled)"';
}
selectEl.append(
$("<option " + dataSub + "/>")
.val(groups[i].id)
.text(groups[i].name)
);
var selectEl = $("#multiselect_" + dataId, row);
// Add all known groups
for (var i = 0; i < groups.length; i++) {
var dataSub = "";
if (!groups[i].enabled) {
dataSub = 'data-subtext="(disabled)"';
}
// Select assigned groups
selectEl.val(data.groups);
// Initialize bootstrap-select
selectEl
// fix dropdown if it would stick out right of the viewport
.on("show.bs.select", function () {
var winWidth = $(window).width();
var dropdownEl = $("body > .bootstrap-select.dropdown");
if (dropdownEl.length > 0) {
dropdownEl.removeClass("align-right");
var width = dropdownEl.width();
var left = dropdownEl.offset().left;
if (left + width > winWidth) {
dropdownEl.addClass("align-right");
}
}
})
.on("changed.bs.select", function () {
// enable Apply button
if ($(applyBtn).prop("disabled")) {
$(applyBtn)
.addClass("btn-success")
.prop("disabled", false)
.on("click", function () {
editClient.call(selectEl);
});
}
})
.on("hide.bs.select", function () {
// Restore values if drop-down menu is closed without clicking the Apply button
if (!$(applyBtn).prop("disabled")) {
$(this).val(data.groups).selectpicker("refresh");
$(applyBtn).removeClass("btn-success").prop("disabled", true).off("click");
}
})
.selectpicker()
.siblings(".dropdown-menu")
.find(".bs-actionsbox")
.prepend(
'<button type="button" id=btn_apply_' +
dataId +
' class="btn btn-block btn-sm" disabled>Apply</button>'
);
selectEl.append(
$("<option " + dataSub + "/>")
.val(groups[i].id)
.text(groups[i].name)
);
}
var applyBtn = "#btn_apply_" + dataId;
// Select assigned groups
selectEl.val(data.groups);
// Initialize bootstrap-select
selectEl
// fix dropdown if it would stick out right of the viewport
.on("show.bs.select", function () {
var winWidth = $(window).width();
var dropdownEl = $("body > .bootstrap-select.dropdown");
if (dropdownEl.length > 0) {
dropdownEl.removeClass("align-right");
var width = dropdownEl.width();
var left = dropdownEl.offset().left;
if (left + width > winWidth) {
dropdownEl.addClass("align-right");
}
}
})
.on("changed.bs.select", function () {
// enable Apply button
if ($(applyBtn).prop("disabled")) {
$(applyBtn)
.addClass("btn-success")
.prop("disabled", false)
.on("click", function () {
editClient.call(selectEl);
});
}
})
.on("hide.bs.select", function () {
// Restore values if drop-down menu is closed without clicking the Apply button
if (!$(applyBtn).prop("disabled")) {
$(this).val(data.groups).selectpicker("refresh");
$(applyBtn).removeClass("btn-success").prop("disabled", true).off("click");
}
})
.selectpicker()
.siblings(".dropdown-menu")
.find(".bs-actionsbox")
.prepend(
'<button type="button" id=btn_apply_' +
dataId +
' class="btn btn-block btn-sm" disabled>Apply</button>'
);
var applyBtn = "#btn_apply_" + dataId;
var button =
'<button type="button" class="btn btn-danger btn-xs" id="deleteClient_' +
dataId +
'" data-id="' +
dataId +
'">' +
'<span class="far fa-trash-alt"></span>' +
"</button>";
$("td:eq(4)", row).html(button);
},
select: {
style: "multi",
selector: "td:not(:last-child)",
info: false,
},
buttons: [
{
text: '<span class="far fa-square"></span>',
titleAttr: "Select All",
className: "btn-sm datatable-bt selectAll",
action: function () {
table.rows({ page: "current" }).select();
},
var button =
'<button type="button" class="btn btn-danger btn-xs" id="deleteClient_' +
dataId +
'" data-id="' +
dataId +
'">' +
'<span class="far fa-trash-alt"></span>' +
"</button>";
$("td:eq(4)", row).html(button);
},
select: {
style: "multi",
selector: "td:not(:last-child)",
info: false,
},
buttons: [
{
text: '<span class="far fa-square"></span>',
titleAttr: "Select All",
className: "btn-sm datatable-bt selectAll",
action: function () {
table.rows({ page: "current" }).select();
},
{
text: '<span class="far fa-plus-square"></span>',
titleAttr: "Select All",
className: "btn-sm datatable-bt selectMore",
action: function () {
table.rows({ page: "current" }).select();
},
},
{
extend: "selectNone",
text: '<span class="far fa-check-square"></span>',
titleAttr: "Deselect All",
className: "btn-sm datatable-bt removeAll",
},
{
text: '<span class="far fa-trash-alt"></span>',
titleAttr: "Delete Selected",
className: "btn-sm datatable-bt deleteSelected",
action: function () {
// For each ".selected" row ...
var ids = [];
$("tr.selected").each(function () {
// ... add the row identified by "data-id".
ids.push($(this).attr("data-id"));
});
// Delete all selected rows at once
delItems(ids);
},
},
],
dom:
"<'row'<'col-sm-6'l><'col-sm-6'f>>" +
"<'row'<'col-sm-3'B><'col-sm-9'p>>" +
"<'row'<'col-sm-12'<'table-responsive'tr>>>" +
"<'row'<'col-sm-3'B><'col-sm-9'p>>" +
"<'row'<'col-sm-12'i>>",
lengthMenu: [
[10, 25, 50, 100, -1],
[10, 25, 50, 100, "All"],
],
stateSave: true,
stateDuration: 0,
stateSaveCallback: function (settings, data) {
utils.stateSaveCallback("groups-clients-table", data);
},
stateLoadCallback: function () {
var data = utils.stateLoadCallback("groups-clients-table");
// Return if not available
if (data === null) {
return null;
}
// Reset visibility of ID column
data.columns[0].visible = false;
// Apply loaded state to table
return data;
{
text: '<span class="far fa-plus-square"></span>',
titleAttr: "Select All",
className: "btn-sm datatable-bt selectMore",
action: function () {
table.rows({ page: "current" }).select();
},
},
});
{
extend: "selectNone",
text: '<span class="far fa-check-square"></span>',
titleAttr: "Deselect All",
className: "btn-sm datatable-bt removeAll",
},
{
text: '<span class="far fa-trash-alt"></span>',
titleAttr: "Delete Selected",
className: "btn-sm datatable-bt deleteSelected",
action: function () {
// For each ".selected" row ...
var ids = [];
$("tr.selected").each(function () {
// ... add the row identified by "data-id".
ids.push($(this).attr("data-id"));
});
// Delete all selected rows at once
delItems(ids);
},
},
],
dom:
"<'row'<'col-sm-6'l><'col-sm-6'f>>" +
"<'row'<'col-sm-3'B><'col-sm-9'p>>" +
"<'row'<'col-sm-12'<'table-responsive'tr>>>" +
"<'row'<'col-sm-3'B><'col-sm-9'p>>" +
"<'row'<'col-sm-12'i>>",
lengthMenu: [
[10, 25, 50, 100, -1],
[10, 25, 50, 100, "All"],
],
stateSave: true,
stateDuration: 0,
stateSaveCallback: function (settings, data) {
utils.stateSaveCallback("groups-clients-table", data);
},
stateLoadCallback: function () {
var data = utils.stateLoadCallback("groups-clients-table");
// Return if not available
if (data === null) {
return null;
}
// Reset visibility of ID column
data.columns[0].visible = false;
// Apply loaded state to table
return data;
},
});
// Disable autocorrect in the search box
var input = document.querySelector("input[type=search]");
@@ -405,11 +403,14 @@ function delItems(ids) {
}
function addClient() {
const comment = utils.escapeHtml($("#new_comment").val());
const comment = $("#new_comment").val();
// Check if the user wants to add multiple IPs (space or newline separated)
// If so, split the input and store it in an array
var ips = utils.escapeHtml($("#select").val().trim()).split(/[\s,]+/);
var ips = $("#select")
.val()
.trim()
.split(/[\s,]+/);
// Remove empty elements
ips = ips.filter(function (el) {
return el !== "";
@@ -454,11 +455,13 @@ function addClient() {
method: "post",
dataType: "json",
processData: false,
contentType: "application/json; charset=utf-8",
data: JSON.stringify({ client: ips, comment: comment }),
success: function (data) {
utils.enableAll();
utils.listsAlert("client", ips, data);
reloadClientSuggestions();
$("#new_comment").val("");
table.ajax.reload(null, false);
table.rows().deselect();
@@ -483,7 +486,7 @@ function editClient() {
.find("#multiselect_" + client)
.val()
.map(Number);
const comment = utils.escapeHtml(tr.find("#comment_" + client).val());
const comment = tr.find("#comment_" + client).val();
const enabled = tr.find("#enabled_" + client).is(":checked");
var done = "edited";
@@ -520,6 +523,7 @@ function editClient() {
method: "put",
dataType: "json",
processData: false,
contentType: "application/json; charset=utf-8",
data: JSON.stringify({
client: client,
groups: groups,

View File

@@ -5,7 +5,7 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
/* global apiFailure:false, utils:false */
/* global apiFailure:false, utils:false, initTable:false */
// eslint-disable-next-line no-unused-vars
var groups = [];
@@ -18,6 +18,8 @@ function getGroups() {
dataType: "json",
success: function (data) {
groups = data.groups;
// Actually load table contents
initTable();
},
error: function (data) {
apiFailure(data);
@@ -34,11 +36,6 @@ function processGroupResult(data, type, done, notDone) {
// Loop over errors and display them
data.processed.errors.forEach(function (error) {
console.log(error); // eslint-disable-line no-console
utils.showAlert(
"error",
"",
`Error while ${notDone} ${type} ${utils.escapeHtml(error.item)}`,
error.error
);
utils.showAlert("error", "", `Error while ${notDone} ${type} ${error.item}`, error.error);
});
}

View File

@@ -6,6 +6,7 @@
* Please see LICENSE file for your rights under this license. */
/* global utils:false, groups:false,, getGroups:false, updateFtlInfo:false, apiFailure:false, processGroupResult:false */
/* exported initTable */
var table;
var GETDict = {};
@@ -44,7 +45,7 @@ $(function () {
});
utils.setBsSelectDefaults();
initTable();
getGroups();
});
// Show a list of suggested domains based on the user's input
@@ -91,294 +92,292 @@ function hideSuggestDomains() {
$("#suggest_domains").slideUp("fast");
}
// eslint-disable-next-line no-unused-vars
function initTable() {
table = $("#domainsTable")
.on("preXhr.dt", function () {
getGroups();
})
.DataTable({
processing: true,
ajax: {
url: "/api/domains",
dataSrc: "domains",
type: "GET",
},
order: [[0, "asc"]],
columns: [
{ data: "id", visible: false },
{ data: null, visible: true, orderable: false, width: "15px" },
{ data: "domain" },
{ data: "type", searchable: false },
{ data: "enabled", searchable: false },
{ data: "comment" },
{ data: "groups", searchable: false },
{ data: null, width: "22px", orderable: false },
],
columnDefs: [
{
targets: 1,
className: "select-checkbox",
render: function () {
return "";
},
table = $("#domainsTable").DataTable({
processing: true,
ajax: {
url: "/api/domains",
dataSrc: "domains",
type: "GET",
},
order: [[0, "asc"]],
columns: [
{ data: "id", visible: false },
{ data: null, visible: true, orderable: false, width: "15px" },
{ data: "domain" },
{ data: "type", searchable: false },
{ data: "enabled", searchable: false },
{ data: "comment" },
{ data: "groups", searchable: false },
{ data: null, width: "22px", orderable: false },
],
columnDefs: [
{
targets: 1,
className: "select-checkbox",
render: function () {
return "";
},
{
targets: "_all",
render: $.fn.dataTable.render.text(),
},
],
drawCallback: function () {
// Hide buttons if all domains were deleted
var hasRows = this.api().rows({ filter: "applied" }).data().length > 0;
$(".datatable-bt").css("visibility", hasRows ? "visible" : "hidden");
$('button[id^="deleteDomain_"]').on("click", deleteDomain);
// Remove visible dropdown to prevent orphaning
$("body > .bootstrap-select.dropdown").remove();
},
rowCallback: function (row, data) {
var dataId = utils.hexEncode(data.domain) + "_" + data.type + "_" + data.kind;
$(row).attr("data-id", dataId);
// Tooltip for domain
var tooltip =
"Added: " +
utils.datetime(data.date_added, false) +
"\nLast modified: " +
utils.datetime(data.date_modified, false) +
"\nDatabase ID: " +
data.id;
$("td:eq(1)", row).html(
'<code id="domain_' +
dataId +
'" title="' +
tooltip +
'" class="breakall">' +
data.domain +
"</code>"
);
{
targets: "_all",
render: $.fn.dataTable.render.text(),
},
],
drawCallback: function () {
// Hide buttons if all domains were deleted
var hasRows = this.api().rows({ filter: "applied" }).data().length > 0;
$(".datatable-bt").css("visibility", hasRows ? "visible" : "hidden");
// Drop-down type selector
$("td:eq(2)", row).html(
'<select id="type_' +
dataId +
'" class="form-control">' +
'<option value="allow/exact"' +
(data.type === "allow" && data.kind === "exact" ? " selected" : "") +
">Exact allow</option>" +
'<option value="allow/regex"' +
(data.type === "allow" && data.kind === "regex" ? " selected" : "") +
">Regex allow</option>" +
'<option value="deny/exact"' +
(data.type === "deny" && data.kind === "exact" ? " selected " : "") +
">Exact deny</option>" +
'<option value="deny/regex"' +
(data.type === "deny" && data.kind === "regex" ? " selected" : "") +
">Regex deny</option>" +
"</select>" +
"<input type='hidden' id='old_type_" +
dataId +
"' value='" +
data.type +
"/" +
data.kind +
"'>"
);
var typeEl = $("#type_" + dataId, row);
typeEl.on("change", editDomain);
// Initialize bootstrap-toggle for status field (enabled/disabled)
$("td:eq(3)", row).html(
'<input type="checkbox" id="enabled_' +
dataId +
'"' +
(data.enabled ? " checked" : "") +
">"
);
var statusEl = $("#enabled_" + dataId, row);
statusEl.bootstrapToggle({
on: "Enabled",
off: "Disabled",
size: "small",
onstyle: "success",
width: "80px",
});
statusEl.on("change", editDomain);
// Comment field
$("td:eq(4)", row).html('<input id="comment_' + dataId + '" class="form-control">');
var commentEl = $("#comment_" + dataId, row);
commentEl.val(utils.unescapeHtml(data.comment));
commentEl.on("change", editDomain);
// Group assignment field (multi-select)
$("td:eq(5)", row).empty();
$("td:eq(5)", row).append(
'<select class="selectpicker" id="multiselect_' + dataId + '" multiple></select>'
);
var selectEl = $("#multiselect_" + dataId, row);
// Add all known groups
for (var i = 0; i < groups.length; i++) {
var dataSub = "";
if (!groups[i].enabled) {
dataSub = 'data-subtext="(disabled)"';
}
selectEl.append(
$("<option " + dataSub + "/>")
.val(groups[i].id)
.text(groups[i].name)
);
}
// Select assigned groups
selectEl.val(data.groups);
// Initialize bootstrap-select
const applyBtn = "#btn_apply_" + dataId;
selectEl
// fix dropdown if it would stick out right of the viewport
.on("show.bs.select", function () {
var winWidth = $(window).width();
var dropdownEl = $("body > .bootstrap-select.dropdown");
if (dropdownEl.length > 0) {
dropdownEl.removeClass("align-right");
var width = dropdownEl.width();
var left = dropdownEl.offset().left;
if (left + width > winWidth) {
dropdownEl.addClass("align-right");
}
}
})
.on("changed.bs.select", function () {
// enable Apply button if changes were made to the drop-down menu
// and have it call editDomain() on click
if ($(applyBtn).prop("disabled")) {
$(applyBtn)
.addClass("btn-success")
.prop("disabled", false)
.on("click", function () {
editDomain.call(selectEl);
});
}
})
.on("hide.bs.select", function () {
// Restore values if drop-down menu is closed without clicking the
// Apply button (e.g. by clicking outside) and re-disable the Apply
// button
if (!$(applyBtn).prop("disabled")) {
$(this).val(data.groups).selectpicker("refresh");
$(applyBtn).removeClass("btn-success").prop("disabled", true).off("click");
}
})
.selectpicker()
.siblings(".dropdown-menu")
.find(".bs-actionsbox")
.prepend(
'<button type="button" id=btn_apply_' +
dataId +
' class="btn btn-block btn-sm" disabled>Apply</button>'
);
// Highlight row (if url parameter "domainid=" is used)
if ("domainid" in GETDict && data.id === parseInt(GETDict.domainid, 10)) {
$(row).find("td").addClass("highlight");
}
// Add delete domain button
var button =
'<button type="button" class="btn btn-danger btn-xs" id="deleteDomain_' +
$('button[id^="deleteDomain_"]').on("click", deleteDomain);
// Remove visible dropdown to prevent orphaning
$("body > .bootstrap-select.dropdown").remove();
},
rowCallback: function (row, data) {
var dataId = utils.hexEncode(data.domain) + "_" + data.type + "_" + data.kind;
$(row).attr("data-id", dataId);
// Tooltip for domain
var tooltip =
"Added: " +
utils.datetime(data.date_added, false) +
"\nLast modified: " +
utils.datetime(data.date_modified, false) +
"\nDatabase ID: " +
data.id;
$("td:eq(1)", row).html(
'<code id="domain_' +
dataId +
'" data-id="' +
'" title="' +
tooltip +
'" class="breakall">' +
utils.escapeHtml(data.unicode) +
(data.domain !== data.unicode ? " (" + utils.escapeHtml(data.domain) + ")" : "") +
"</code>"
);
// Drop-down type selector
$("td:eq(2)", row).html(
'<select id="type_' +
dataId +
'">' +
'<span class="far fa-trash-alt"></span>' +
"</button>";
$("td:eq(6)", row).html(button);
},
select: {
style: "multi",
selector: "td:first-child",
info: false,
},
buttons: [
{
text: '<span class="far fa-square"></span>',
titleAttr: "Select All",
className: "btn-sm datatable-bt selectAll",
action: function () {
table.rows({ page: "current" }).select();
},
},
{
text: '<span class="far fa-plus-square"></span>',
titleAttr: "Select All",
className: "btn-sm datatable-bt selectMore",
action: function () {
table.rows({ page: "current" }).select();
},
},
{
extend: "selectNone",
text: '<span class="far fa-check-square"></span>',
titleAttr: "Deselect All",
className: "btn-sm datatable-bt removeAll",
},
{
text: '<span class="far fa-trash-alt"></span>',
titleAttr: "Delete Selected",
className: "btn-sm datatable-bt deleteSelected",
action: function () {
// For each ".selected" row ...
var ids = [];
$("tr.selected").each(function () {
// ... add the row identified by "data-id".
ids.push($(this).attr("data-id"));
});
// Delete all selected rows at once
delItems(ids);
},
},
],
dom:
"<'row'<'col-sm-6'l><'col-sm-6'f>>" +
"<'row'<'col-sm-3'B><'col-sm-9'p>>" +
"<'row'<'col-sm-12'<'table-responsive'tr>>>" +
"<'row'<'col-sm-3'B><'col-sm-9'p>>" +
"<'row'<'col-sm-12'i>>",
lengthMenu: [
[10, 25, 50, 100, -1],
[10, 25, 50, 100, "All"],
],
stateSave: true,
stateDuration: 0,
stateSaveCallback: function (settings, data) {
utils.stateSaveCallback("groups-domains-table", data);
},
stateLoadCallback: function () {
var data = utils.stateLoadCallback("groups-domains-table");
'" class="form-control">' +
'<option value="allow/exact"' +
(data.type === "allow" && data.kind === "exact" ? " selected" : "") +
">Exact allow</option>" +
'<option value="allow/regex"' +
(data.type === "allow" && data.kind === "regex" ? " selected" : "") +
">Regex allow</option>" +
'<option value="deny/exact"' +
(data.type === "deny" && data.kind === "exact" ? " selected " : "") +
">Exact deny</option>" +
'<option value="deny/regex"' +
(data.type === "deny" && data.kind === "regex" ? " selected" : "") +
">Regex deny</option>" +
"</select>" +
"<input type='hidden' id='old_type_" +
dataId +
"' value='" +
data.type +
"/" +
data.kind +
"'>"
);
var typeEl = $("#type_" + dataId, row);
typeEl.on("change", editDomain);
// Return if not available
if (data === null) {
return null;
// Initialize bootstrap-toggle for status field (enabled/disabled)
$("td:eq(3)", row).html(
'<input type="checkbox" id="enabled_' +
dataId +
'"' +
(data.enabled ? " checked" : "") +
">"
);
var statusEl = $("#enabled_" + dataId, row);
statusEl.bootstrapToggle({
on: "Enabled",
off: "Disabled",
size: "small",
onstyle: "success",
width: "80px",
});
statusEl.on("change", editDomain);
// Comment field
$("td:eq(4)", row).html('<input id="comment_' + dataId + '" class="form-control">');
var commentEl = $("#comment_" + dataId, row);
commentEl.val(data.comment);
commentEl.on("change", editDomain);
// Group assignment field (multi-select)
$("td:eq(5)", row).empty();
$("td:eq(5)", row).append(
'<select class="selectpicker" id="multiselect_' + dataId + '" multiple></select>'
);
var selectEl = $("#multiselect_" + dataId, row);
// Add all known groups
for (var i = 0; i < groups.length; i++) {
var dataSub = "";
if (!groups[i].enabled) {
dataSub = 'data-subtext="(disabled)"';
}
// Reset visibility of ID column
data.columns[0].visible = false;
// Apply loaded state to table
return data;
},
initComplete: function () {
if ("domainid" in GETDict) {
var pos = table
.column(0, { order: "current" })
.data()
.indexOf(parseInt(GETDict.domainid, 10));
if (pos >= 0) {
var page = Math.floor(pos / table.page.info().length);
table.page(page).draw(false);
selectEl.append(
$("<option " + dataSub + "/>")
.val(groups[i].id)
.text(groups[i].name)
);
}
// Select assigned groups
selectEl.val(data.groups);
// Initialize bootstrap-select
const applyBtn = "#btn_apply_" + dataId;
selectEl
// fix dropdown if it would stick out right of the viewport
.on("show.bs.select", function () {
var winWidth = $(window).width();
var dropdownEl = $("body > .bootstrap-select.dropdown");
if (dropdownEl.length > 0) {
dropdownEl.removeClass("align-right");
var width = dropdownEl.width();
var left = dropdownEl.offset().left;
if (left + width > winWidth) {
dropdownEl.addClass("align-right");
}
}
}
})
.on("changed.bs.select", function () {
// enable Apply button if changes were made to the drop-down menu
// and have it call editDomain() on click
if ($(applyBtn).prop("disabled")) {
$(applyBtn)
.addClass("btn-success")
.prop("disabled", false)
.on("click", function () {
editDomain.call(selectEl);
});
}
})
.on("hide.bs.select", function () {
// Restore values if drop-down menu is closed without clicking the
// Apply button (e.g. by clicking outside) and re-disable the Apply
// button
if (!$(applyBtn).prop("disabled")) {
$(this).val(data.groups).selectpicker("refresh");
$(applyBtn).removeClass("btn-success").prop("disabled", true).off("click");
}
})
.selectpicker()
.siblings(".dropdown-menu")
.find(".bs-actionsbox")
.prepend(
'<button type="button" id=btn_apply_' +
dataId +
' class="btn btn-block btn-sm" disabled>Apply</button>'
);
// Highlight row (if url parameter "domainid=" is used)
if ("domainid" in GETDict && data.id === parseInt(GETDict.domainid, 10)) {
$(row).find("td").addClass("highlight");
}
// Add delete domain button
var button =
'<button type="button" class="btn btn-danger btn-xs" id="deleteDomain_' +
dataId +
'" data-id="' +
dataId +
'">' +
'<span class="far fa-trash-alt"></span>' +
"</button>";
$("td:eq(6)", row).html(button);
},
select: {
style: "multi",
selector: "td:first-child",
info: false,
},
buttons: [
{
text: '<span class="far fa-square"></span>',
titleAttr: "Select All",
className: "btn-sm datatable-bt selectAll",
action: function () {
table.rows({ page: "current" }).select();
},
},
});
{
text: '<span class="far fa-plus-square"></span>',
titleAttr: "Select All",
className: "btn-sm datatable-bt selectMore",
action: function () {
table.rows({ page: "current" }).select();
},
},
{
extend: "selectNone",
text: '<span class="far fa-check-square"></span>',
titleAttr: "Deselect All",
className: "btn-sm datatable-bt removeAll",
},
{
text: '<span class="far fa-trash-alt"></span>',
titleAttr: "Delete Selected",
className: "btn-sm datatable-bt deleteSelected",
action: function () {
// For each ".selected" row ...
var ids = [];
$("tr.selected").each(function () {
// ... add the row identified by "data-id".
ids.push($(this).attr("data-id"));
});
// Delete all selected rows at once
delItems(ids);
},
},
],
dom:
"<'row'<'col-sm-6'l><'col-sm-6'f>>" +
"<'row'<'col-sm-3'B><'col-sm-9'p>>" +
"<'row'<'col-sm-12'<'table-responsive'tr>>>" +
"<'row'<'col-sm-3'B><'col-sm-9'p>>" +
"<'row'<'col-sm-12'i>>",
lengthMenu: [
[10, 25, 50, 100, -1],
[10, 25, 50, 100, "All"],
],
stateSave: true,
stateDuration: 0,
stateSaveCallback: function (settings, data) {
utils.stateSaveCallback("groups-domains-table", data);
},
stateLoadCallback: function () {
var data = utils.stateLoadCallback("groups-domains-table");
// Return if not available
if (data === null) {
return null;
}
// Reset visibility of ID column
data.columns[0].visible = false;
// Apply loaded state to table
return data;
},
initComplete: function () {
if ("domainid" in GETDict) {
var pos = table
.column(0, { order: "current" })
.data()
.indexOf(parseInt(GETDict.domainid, 10));
if (pos >= 0) {
var page = Math.floor(pos / table.page.info().length);
table.page(page).draw(false);
}
}
},
});
// Disable autocorrect in the search box
var input = document.querySelector("input[type=search]");
if (input !== null) {
@@ -449,7 +448,7 @@ function delItems(ids) {
utils.disableAll();
const idstring = ids.join(", ");
utils.showAlert("info", "", "Deleting domain...", "<ul>" + domain + "</ul>");
utils.showAlert("info", "", "Deleting domain...", domain);
$.ajax({
url: "/api/domains/" + typestr + "/" + encodeURIComponent(domain),
@@ -505,11 +504,11 @@ function addDomain() {
commentEl = $("#new_regex_comment");
}
const comment = utils.escapeHtml(commentEl.val());
const comment = commentEl.val();
// Check if the user wants to add multiple domains (space or newline separated)
// If so, split the input and store it in an array
var domains = utils.escapeHtml(domainEl.val()).split(/[\s,]+/);
var domains = domainEl.val().split(/\s+/);
// Remove empty elements
domains = domains.filter(function (el) {
return el !== "";
@@ -544,6 +543,7 @@ function addDomain() {
method: "post",
dataType: "json",
processData: false,
contentType: "application/json; charset=utf-8",
data: JSON.stringify({
domain: domains,
comment: comment,
@@ -553,6 +553,10 @@ function addDomain() {
success: function (data) {
utils.enableAll();
utils.listsAlert("domain", domains, data);
$("#new_domain").val("");
$("#new_domain_comment").val("");
$("#new_regex").val("");
$("#new_regex_comment").val("");
table.ajax.reload(null, false);
table.rows().deselect();
@@ -575,7 +579,7 @@ function editDomain() {
const newTypestr = tr.find("#type_" + domain).val();
const oldTypeStr = tr.find("#old_type_" + domain).val();
const enabled = tr.find("#enabled_" + domain).is(":checked");
const comment = utils.escapeHtml(tr.find("#comment_" + domain).val());
const comment = tr.find("#comment_" + domain).val();
// Convert list of string integers to list of integers using map
const groups = tr
.find("#multiselect_" + domain)
@@ -621,12 +625,13 @@ function editDomain() {
utils.disableAll();
const domainDecoded = utils.hexDecode(domain.split("_")[0]);
utils.showAlert("info", "", "Editing domain...", domain);
utils.showAlert("info", "", "Editing domain...", domainDecoded);
$.ajax({
url: "/api/domains/" + newTypestr + "/" + encodeURIComponent(domainDecoded),
method: "put",
dataType: "json",
processData: false,
contentType: "application/json; charset=utf-8",
data: JSON.stringify({
groups: groups,
comment: comment,

View File

@@ -6,6 +6,7 @@
* Please see LICENSE file for your rights under this license. */
/* global utils:false, groups:false, apiFailure:false, updateFtlInfo:false, getGroups:false, processGroupResult:false */
/* exported initTable */
var table;
var GETDict = {};
@@ -17,7 +18,7 @@ $(function () {
$("#btnAddBlock").on("click", { type: "block" }, addList);
utils.setBsSelectDefaults();
initTable();
getGroups();
});
function format(data) {
@@ -98,305 +99,306 @@ function format(data) {
);
}
// eslint-disable-next-line no-unused-vars
function initTable() {
table = $("#listsTable")
.on("preXhr.dt", function () {
getGroups();
})
.DataTable({
processing: true,
ajax: {
url: "/api/lists",
dataSrc: "lists",
type: "GET",
},
order: [[0, "asc"]],
columns: [
{ data: "id", visible: false },
{ data: null, visible: true, orderable: false, width: "15px" },
{ data: "status", searchable: false, class: "details-control" },
{ data: "address" },
{ data: "enabled", searchable: false },
{ data: "comment" },
{ data: "groups", searchable: false },
{ data: null, width: "22px", orderable: false },
],
columnDefs: [
{
targets: 1,
className: "select-checkbox",
render: function () {
return "";
},
table = $("#listsTable").DataTable({
processing: true,
ajax: {
url: "/api/lists",
dataSrc: "lists",
type: "GET",
},
order: [[0, "asc"]],
columns: [
{ data: "id", visible: false },
{ data: null, visible: true, orderable: false, width: "15px" },
{ data: "status", searchable: false, class: "details-control" },
{ data: "address" },
{ data: "enabled", searchable: false },
{ data: "comment" },
{ data: "groups", searchable: false },
{ data: null, width: "22px", orderable: false },
],
columnDefs: [
{
targets: 1,
className: "select-checkbox",
render: function () {
return "";
},
{
targets: "_all",
render: $.fn.dataTable.render.text(),
},
],
drawCallback: function () {
// Hide buttons if all lists were deleted
var hasRows = this.api().rows({ filter: "applied" }).data().length > 0;
$(".datatable-bt").css("visibility", hasRows ? "visible" : "hidden");
$('button[id^="deleteList_"]').on("click", deleteList);
// Remove visible dropdown to prevent orphaning
$("body > .bootstrap-select.dropdown").remove();
},
rowCallback: function (row, data) {
var dataId = utils.hexEncode(data.address);
$(row).attr("data-id", dataId);
$(row).attr("data-type", data.type);
{
targets: "_all",
render: $.fn.dataTable.render.text(),
},
],
drawCallback: function () {
// Hide buttons if all lists were deleted
var hasRows = this.api().rows({ filter: "applied" }).data().length > 0;
$(".datatable-bt").css("visibility", hasRows ? "visible" : "hidden");
var statusCode = 0,
statusIcon;
// If there is no status or the list is disabled, we keep
// status 0 (== unknown)
if (data.status !== null && data.enabled) {
statusCode = parseInt(data.status, 10);
}
$('button[id^="deleteList_"]').on("click", deleteList);
// Remove visible dropdown to prevent orphaning
$("body > .bootstrap-select.dropdown").remove();
},
rowCallback: function (row, data) {
var dataId = utils.hexEncode(data.address);
$(row).attr("data-id", dataId);
$(row).attr("data-type", data.type);
switch (statusCode) {
case 1:
statusIcon = "fa-check";
break;
case 2:
statusIcon = "fa-history";
break;
case 3:
statusIcon = "fa-exclamation-circle";
break;
case 4:
statusIcon = "fa-times";
break;
default:
statusIcon = "fa-question-circle";
break;
}
var statusCode = 0,
statusIcon;
// If there is no status or the list is disabled, we keep
// status 0 (== unknown)
if (data.status !== null && data.enabled) {
statusCode = parseInt(data.status, 10);
}
// Add red minus sign icon if data["type"] is "block"
// Add green plus sign icon if data["type"] is "allow"
let status =
"<i class='fa fa-fw fa-question-circle text-orange' title='This list is of unknown type'></i>";
if (data.type === "block") {
status = "<i class='fa fa-fw fa-minus text-red' title='This is a blocklist'></i>";
} else if (data.type === "allow") {
status = "<i class='fa fa-fw fa-plus text-green' title='This is an allowlist'></i>";
}
switch (statusCode) {
case 1:
statusIcon = "fa-check";
break;
case 2:
statusIcon = "fa-history";
break;
case 3:
statusIcon = "fa-exclamation-circle";
break;
case 4:
statusIcon = "fa-times";
break;
default:
statusIcon = "fa-question-circle";
break;
}
$("td:eq(1)", row).addClass("list-status-" + statusCode);
$("td:eq(1)", row).html(
"<i class='fa fa-fw " +
statusIcon +
"' title='Click for details about this list'></i>" +
status
);
// Add red minus sign icon if data["type"] is "block"
// Add green plus sign icon if data["type"] is "allow"
let status =
"<i class='fa fa-fw fa-question-circle text-orange' title='This list is of unknown type'></i>";
if (data.type === "block") {
status = "<i class='fa fa-fw fa-minus text-red' title='This is a blocklist'></i>";
} else if (data.type === "allow") {
status = "<i class='fa fa-fw fa-plus text-green' title='This is an allowlist'></i>";
}
if (data.address.startsWith("file://")) {
// Local files cannot be downloaded from a distant client so don't show
// a link to such a list here
$("td:eq(2)", row).html(
'<code id="address_' + dataId + '" class="breakall">' + data.address + "</code>"
);
} else {
$("td:eq(2)", row).html(
'<a id="address_' +
dataId +
'" class="breakall" href="' +
data.address +
'" target="_blank" rel="noopener noreferrer">' +
data.address +
"</a>"
);
}
$("td:eq(1)", row).addClass("list-status-" + statusCode);
$("td:eq(1)", row).html(
"<i class='fa fa-fw " +
statusIcon +
"' title='Click for details about this list'></i>" +
status
);
$("td:eq(3)", row).html(
'<input type="checkbox" id="enabled_' +
if (data.address.startsWith("file://")) {
// Local files cannot be downloaded from a distant client so don't show
// a link to such a list here
$("td:eq(2)", row).html(
'<code id="address_' +
dataId +
'"' +
(data.enabled ? " checked" : "") +
">"
'" class="breakall">' +
utils.escapeHtml(data.address) +
"</code>"
);
var statusEl = $("#enabled_" + dataId, row);
statusEl.bootstrapToggle({
on: "Enabled",
off: "Disabled",
size: "small",
onstyle: "success",
width: "80px",
});
statusEl.on("change", editList);
$("td:eq(4)", row).html('<input id="comment_' + dataId + '" class="form-control">');
var commentEl = $("#comment_" + dataId, row);
commentEl.val(utils.unescapeHtml(data.comment));
commentEl.on("change", editList);
$("td:eq(5)", row).empty();
$("td:eq(5)", row).append(
'<select class="selectpicker" id="multiselect_' + dataId + '" multiple></select>'
} else {
$("td:eq(2)", row).html(
'<a id="address_' +
dataId +
'" class="breakall" href="' +
encodeURI(data.address) +
'" target="_blank" rel="noopener noreferrer">' +
utils.escapeHtml(data.address) +
"</a>"
);
var selectEl = $("#multiselect_" + dataId, row);
// Add all known groups
for (var i = 0; i < groups.length; i++) {
var dataSub = "";
if (!groups[i].enabled) {
dataSub = 'data-subtext="(disabled)"';
}
}
selectEl.append(
$("<option " + dataSub + "/>")
.val(groups[i].id)
.text(groups[i].name)
);
}
// Select assigned groups
selectEl.val(data.groups);
// Initialize bootstrap-select
selectEl
// fix dropdown if it would stick out right of the viewport
.on("show.bs.select", function () {
var winWidth = $(window).width();
var dropdownEl = $("body > .bootstrap-select.dropdown");
if (dropdownEl.length > 0) {
dropdownEl.removeClass("align-right");
var width = dropdownEl.width();
var left = dropdownEl.offset().left;
if (left + width > winWidth) {
dropdownEl.addClass("align-right");
}
}
})
.on("changed.bs.select", function () {
// enable Apply button
if ($(applyBtn).prop("disabled")) {
$(applyBtn)
.addClass("btn-success")
.prop("disabled", false)
.on("click", function () {
editList.call(selectEl);
});
}
})
.on("hide.bs.select", function () {
// Restore values if drop-down menu is closed without clicking the Apply button
if (!$(applyBtn).prop("disabled")) {
$(this).val(data.groups).selectpicker("refresh");
$(applyBtn).removeClass("btn-success").prop("disabled", true).off("click");
}
})
.selectpicker()
.siblings(".dropdown-menu")
.find(".bs-actionsbox")
.prepend(
'<button type="button" id=btn_apply_' +
dataId +
' class="btn btn-block btn-sm" disabled>Apply</button>'
);
var applyBtn = "#btn_apply_" + dataId;
// Highlight row (if url parameter "listid=" is used)
if ("listid" in GETDict && data.id === parseInt(GETDict.listid, 10)) {
$(row).find("td").addClass("highlight");
}
var button =
'<button type="button" class="btn btn-danger btn-xs" id="deleteList_' +
$("td:eq(3)", row).html(
'<input type="checkbox" id="enabled_' +
dataId +
'" data-id="' +
dataId +
'">' +
'<span class="far fa-trash-alt"></span>' +
"</button>";
$("td:eq(6)", row).html(button);
},
dom:
"<'row'<'col-sm-6'l><'col-sm-6'f>>" +
"<'row'<'col-sm-3'B><'col-sm-9'p>>" +
"<'row'<'col-sm-12'<'table-responsive'tr>>>" +
"<'row'<'col-sm-3'B><'col-sm-9'p>>" +
"<'row'<'col-sm-12'i>>",
lengthMenu: [
[10, 25, 50, 100, -1],
[10, 25, 50, 100, "All"],
],
select: {
style: "multi",
selector: "td:not(:last-child)",
info: false,
},
buttons: [
{
text: '<span class="far fa-square"></span>',
titleAttr: "Select All",
className: "btn-sm datatable-bt selectAll",
action: function () {
table.rows({ page: "current" }).select();
},
},
{
text: '<span class="far fa-plus-square"></span>',
titleAttr: "Select All",
className: "btn-sm datatable-bt selectMore",
action: function () {
table.rows({ page: "current" }).select();
},
},
{
extend: "selectNone",
text: '<span class="far fa-check-square"></span>',
titleAttr: "Deselect All",
className: "btn-sm datatable-bt removeAll",
},
{
text: '<span class="far fa-trash-alt"></span>',
titleAttr: "Delete Selected",
className: "btn-sm datatable-bt deleteSelected",
action: function () {
// For each ".selected" row ...
var ids = [];
$("tr.selected").each(function () {
// ... add the row identified by "data-id".
ids.push($(this).attr("data-id"));
});
// Delete all selected rows at once
delItems(ids);
},
},
],
stateSave: true,
stateDuration: 0,
stateSaveCallback: function (settings, data) {
utils.stateSaveCallback("groups-lists-table", data);
},
stateLoadCallback: function () {
var data = utils.stateLoadCallback("groups-lists-table");
'"' +
(data.enabled ? " checked" : "") +
">"
);
var statusEl = $("#enabled_" + dataId, row);
statusEl.bootstrapToggle({
on: "Enabled",
off: "Disabled",
size: "small",
onstyle: "success",
width: "80px",
});
statusEl.on("change", editList);
// Return if not available
if (data === null) {
return null;
$("td:eq(4)", row).html('<input id="comment_' + dataId + '" class="form-control">');
var commentEl = $("#comment_" + dataId, row);
commentEl.val(data.comment);
commentEl.on("change", editList);
$("td:eq(5)", row).empty();
$("td:eq(5)", row).append(
'<select class="selectpicker" id="multiselect_' + dataId + '" multiple></select>'
);
var selectEl = $("#multiselect_" + dataId, row);
// Add all known groups
for (var i = 0; i < groups.length; i++) {
var dataSub = "";
if (!groups[i].enabled) {
dataSub = 'data-subtext="(disabled)"';
}
// Reset visibility of ID column
data.columns[0].visible = false;
// Apply loaded state to table
return data;
},
initComplete: function () {
if ("listid" in GETDict) {
var pos = table
.column(0, { order: "current" })
.data()
.indexOf(parseInt(GETDict.listid, 10));
if (pos >= 0) {
var page = Math.floor(pos / table.page.info().length);
table.page(page).draw(false);
selectEl.append(
$("<option " + dataSub + "/>")
.val(groups[i].id)
.text(groups[i].name)
);
}
// Select assigned groups
selectEl.val(data.groups);
// Initialize bootstrap-select
selectEl
// fix dropdown if it would stick out right of the viewport
.on("show.bs.select", function () {
var winWidth = $(window).width();
var dropdownEl = $("body > .bootstrap-select.dropdown");
if (dropdownEl.length > 0) {
dropdownEl.removeClass("align-right");
var width = dropdownEl.width();
var left = dropdownEl.offset().left;
if (left + width > winWidth) {
dropdownEl.addClass("align-right");
}
}
}
})
.on("changed.bs.select", function () {
// enable Apply button
if ($(applyBtn).prop("disabled")) {
$(applyBtn)
.addClass("btn-success")
.prop("disabled", false)
.on("click", function () {
editList.call(selectEl);
});
}
})
.on("hide.bs.select", function () {
// Restore values if drop-down menu is closed without clicking the Apply button
if (!$(applyBtn).prop("disabled")) {
$(this).val(data.groups).selectpicker("refresh");
$(applyBtn).removeClass("btn-success").prop("disabled", true).off("click");
}
})
.selectpicker()
.siblings(".dropdown-menu")
.find(".bs-actionsbox")
.prepend(
'<button type="button" id=btn_apply_' +
dataId +
' class="btn btn-block btn-sm" disabled>Apply</button>'
);
var applyBtn = "#btn_apply_" + dataId;
// Highlight row (if url parameter "listid=" is used)
if ("listid" in GETDict && data.id === parseInt(GETDict.listid, 10)) {
$(row).find("td").addClass("highlight");
}
var button =
'<button type="button" class="btn btn-danger btn-xs" id="deleteList_' +
dataId +
'" data-id="' +
dataId +
'">' +
'<span class="far fa-trash-alt"></span>' +
"</button>";
$("td:eq(6)", row).html(button);
},
dom:
"<'row'<'col-sm-6'l><'col-sm-6'f>>" +
"<'row'<'col-sm-3'B><'col-sm-9'p>>" +
"<'row'<'col-sm-12'<'table-responsive'tr>>>" +
"<'row'<'col-sm-3'B><'col-sm-9'p>>" +
"<'row'<'col-sm-12'i>>",
lengthMenu: [
[10, 25, 50, 100, -1],
[10, 25, 50, 100, "All"],
],
select: {
style: "multi",
selector: "td:not(:last-child)",
info: false,
},
buttons: [
{
text: '<span class="far fa-square"></span>',
titleAttr: "Select All",
className: "btn-sm datatable-bt selectAll",
action: function () {
table.rows({ page: "current" }).select();
},
},
});
{
text: '<span class="far fa-plus-square"></span>',
titleAttr: "Select All",
className: "btn-sm datatable-bt selectMore",
action: function () {
table.rows({ page: "current" }).select();
},
},
{
extend: "selectNone",
text: '<span class="far fa-check-square"></span>',
titleAttr: "Deselect All",
className: "btn-sm datatable-bt removeAll",
},
{
text: '<span class="far fa-trash-alt"></span>',
titleAttr: "Delete Selected",
className: "btn-sm datatable-bt deleteSelected",
action: function () {
// For each ".selected" row ...
var ids = [];
$("tr.selected").each(function () {
// ... add the row identified by "data-id".
ids.push($(this).attr("data-id"));
});
// Delete all selected rows at once
delItems(ids);
},
},
],
stateSave: true,
stateDuration: 0,
stateSaveCallback: function (settings, data) {
utils.stateSaveCallback("groups-lists-table", data);
},
stateLoadCallback: function () {
var data = utils.stateLoadCallback("groups-lists-table");
// Return if not available
if (data === null) {
return null;
}
// Reset visibility of ID column
data.columns[0].visible = false;
// Apply loaded state to table
return data;
},
initComplete: function () {
if ("listid" in GETDict) {
var pos = table
.column(0, { order: "current" })
.data()
.indexOf(parseInt(GETDict.listid, 10));
if (pos >= 0) {
var page = Math.floor(pos / table.page.info().length);
table.page(page).draw(false);
}
}
},
});
table.on("init select deselect", function () {
utils.changeBulkDeleteStates(table);
@@ -464,7 +466,7 @@ function delItems(ids) {
utils.disableAll();
const idstring = ids.join(", ");
utils.showAlert("info", "", "Deleting list(s) ...", "<ul>" + address + "</ul>");
utils.showAlert("info", "", "Deleting list(s) ...", address);
$.ajax({
url: "/api/lists/" + encodeURIComponent(address),
@@ -499,11 +501,13 @@ function delItems(ids) {
function addList(event) {
const type = event.data.type;
const comment = utils.escapeHtml($("#new_comment").val());
const comment = $("#new_comment").val();
// Check if the user wants to add multiple domains (space or newline separated)
// If so, split the input and store it in an array
var addresses = utils.escapeHtml($("#new_address").val()).split(/[\s,]+/);
var addresses = $("#new_address")
.val()
.split(/[\s,]+/);
// Remove empty elements
addresses = addresses.filter(function (el) {
return el !== "";
@@ -525,10 +529,13 @@ function addList(event) {
method: "post",
dataType: "json",
processData: false,
contentType: "application/json; charset=utf-8",
data: JSON.stringify({ address: addresses, comment: comment, type: type }),
success: function (data) {
utils.enableAll();
utils.listsAlert("list", addresses, data);
$("#new_address").val("");
$("#new_comment").val("");
table.ajax.reload(null, false);
table.rows().deselect();
@@ -591,6 +598,7 @@ function editList() {
method: "put",
dataType: "json",
processData: false,
contentType: "application/json; charset=utf-8",
data: JSON.stringify({
groups: groups,
comment: comment,

View File

@@ -75,7 +75,7 @@ $(function () {
'<input id="name_' + data.id + '" title="' + tooltip + '" class="form-control">'
);
var nameEl = $("#name_" + data.id, row);
nameEl.val(utils.unescapeHtml(data.name));
nameEl.val(data.name);
nameEl.on("change", editGroup);
$("td:eq(2)", row).html(
@@ -98,7 +98,7 @@ $(function () {
$("td:eq(3)", row).html('<input id="comment_' + data.id + '" class="form-control">');
var comment = data.comment !== null ? data.comment : "";
var commentEl = $("#comment_" + data.id, row);
commentEl.val(utils.unescapeHtml(comment));
commentEl.val(comment);
commentEl.on("change", editGroup);
$("td:eq(4)", row).empty();
@@ -277,7 +277,7 @@ function delItems(ids) {
}
function addGroup() {
const comment = utils.escapeHtml($("#new_comment").val());
const comment = $("#new_comment").val();
// Check if the user wants to add multiple groups (space or newline separated)
// If so, split the input and store it in an array
@@ -306,6 +306,7 @@ function addGroup() {
method: "post",
dataType: "json",
processData: false,
contentType: "application/json; charset=utf-8",
data: JSON.stringify({
name: names,
comment: comment,
@@ -336,9 +337,9 @@ function editGroup() {
const tr = $(this).closest("tr");
const id = tr.attr("data-id");
const oldName = idNames[id];
const name = utils.escapeHtml(tr.find("#name_" + id).val());
const name = tr.find("#name_" + id).val();
const enabled = tr.find("#enabled_" + id).is(":checked");
const comment = utils.escapeHtml(tr.find("#comment_" + id).val());
const comment = tr.find("#comment_" + id).val();
var done = "edited";
var notDone = "editing";
@@ -373,6 +374,7 @@ function editGroup() {
method: "put",
dataType: "json",
processData: false,
contentType: "application/json; charset=utf-8",
data: JSON.stringify({
name: name,
comment: comment,

View File

@@ -5,7 +5,7 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
/* global utils:false, Chart:false, apiFailure:false, THEME_COLORS:false, customTooltips:false, htmlLegendPlugin:false,doughnutTooltip:false, ChartDeferred:false */
/* global utils:false, Chart:false, apiFailure:false, THEME_COLORS:false, customTooltips:false, htmlLegendPlugin:false,doughnutTooltip:false, ChartDeferred:false, REFRESH_INTERVAL: false */
// Define global variables
var timeLineChart, clientsChart;
@@ -71,16 +71,14 @@ function updateQueriesOverTime() {
timeLineChart.update();
})
.done(function () {
// Reload graph after 10 minutes
failures = 0;
setTimeout(updateQueriesOverTime, 600000);
utils.setTimer(updateQueriesOverTime, REFRESH_INTERVAL.history);
})
.fail(function () {
failures++;
if (failures < 5) {
// Try again after 1 minute only if this has not failed more
// than five times in a row
setTimeout(updateQueriesOverTime, 60000);
// Try again ´only if this has not failed more than five times in a row
utils.setTimer(updateQueriesOverTime, 0.1 * REFRESH_INTERVAL.history);
}
})
.fail(function (data) {
@@ -123,8 +121,7 @@ function updateQueryTypesPie() {
queryTypePieChart.update("none");
})
.done(function () {
// Reload graph after minute
setTimeout(updateQueryTypesPie, 60000);
utils.setTimer(updateQueryTypesPie, REFRESH_INTERVAL.query_types);
})
.fail(function (data) {
apiFailure(data);
@@ -185,14 +182,13 @@ function updateClientsOverTime() {
.done(function () {
// Reload graph after 10 minutes
failures = 0;
setTimeout(updateClientsOverTime, 600000);
utils.setTimer(updateClientsOverTime, REFRESH_INTERVAL.clients);
})
.fail(function () {
failures++;
if (failures < 5) {
// Try again after 1 minute only if this has not failed more
// than five times in a row
setTimeout(updateClientsOverTime, 60000);
// Try again only if this has not failed more than five times in a row
utils.setTimer(updateClientsOverTime, 0.1 * REFRESH_INTERVAL.clients);
}
})
.fail(function (data) {
@@ -200,6 +196,7 @@ function updateClientsOverTime() {
});
}
var upstreams = {};
function updateForwardDestinationsPie() {
$.getJSON("/api/stats/upstreams", function (data) {
var v = [],
@@ -221,6 +218,12 @@ function updateForwardDestinationsPie() {
label += "#" + item.port;
}
// Store upstreams for generating links to the Query Log
upstreams[label] = item.ip;
if (item.port > 0) {
upstreams[label] += "#" + item.port;
}
var percent = (100 * item.count) / sum;
values.push([label, percent, THEME_COLORS[i++ % THEME_COLORS.length]]);
});
@@ -246,8 +249,7 @@ function updateForwardDestinationsPie() {
forwardDestinationPieChart.update("none");
})
.done(function () {
// Reload graph after one minute
setTimeout(updateForwardDestinationsPie, 60000);
utils.setTimer(updateForwardDestinationsPie, REFRESH_INTERVAL.upstreams);
})
.fail(function (data) {
apiFailure(data);
@@ -255,7 +257,7 @@ function updateForwardDestinationsPie() {
}
function updateTopClientsTable(blocked) {
var api, style, tablecontent, overlay, clienttable;
let api, style, tablecontent, overlay, clienttable;
if (blocked) {
api = "/api/stats/top_clients?blocked=true";
style = "queries-blocked";
@@ -273,9 +275,8 @@ function updateTopClientsTable(blocked) {
$.getJSON(api, function (data) {
// Clear tables before filling them with data
tablecontent.remove();
var url,
percentage,
sum = blocked ? data.blocked_queries : data.total_queries;
let url, percentage;
const sum = blocked ? data.blocked_queries : data.total_queries;
// Add note if there are no results (e.g. privacy mode enabled)
if (jQuery.isEmptyObject(data.clients)) {
@@ -285,10 +286,14 @@ function updateTopClientsTable(blocked) {
// Populate table with content
data.clients.forEach(function (client) {
// Sanitize client
var clientname = utils.escapeHtml(client.name);
var clientip = utils.escapeHtml(client.ip);
if (clientname.length === 0) clientname = clientip;
url = '<a href="queries.lp?client_ip=' + clientip + '">' + clientname + "</a>";
let clientname = client.name;
if (clientname.length === 0) clientname = client.ip;
url =
'<a href="queries.lp?client_ip=' +
encodeURIComponent(client.ip) +
'">' +
utils.escapeHtml(clientname) +
"</a>";
percentage = (client.count / sum) * 100;
// Add row to table
@@ -309,7 +314,7 @@ function updateTopClientsTable(blocked) {
}
function updateTopDomainsTable(blocked) {
var api, style, tablecontent, overlay, domaintable;
let api, style, tablecontent, overlay, domaintable;
if (blocked) {
api = "/api/stats/top_domains?blocked=true";
style = "queries-blocked";
@@ -327,11 +332,8 @@ function updateTopDomainsTable(blocked) {
$.getJSON(api, function (data) {
// Clear tables before filling them with data
tablecontent.remove();
var url,
domain,
percentage,
urlText,
sum = blocked ? data.blocked_queries : data.total_queries;
let url, domain, percentage, urlText;
const sum = blocked ? data.blocked_queries : data.total_queries;
// Add note if there are no results (e.g. privacy mode enabled)
if (jQuery.isEmptyObject(data.domains)) {
@@ -341,7 +343,7 @@ function updateTopDomainsTable(blocked) {
// Populate table with content
data.domains.forEach(function (item) {
// Sanitize domain
domain = utils.escapeHtml(item.domain);
domain = encodeURIComponent(item.domain);
// Substitute "." for empty domain lookups
urlText = domain === "" ? "." : domain;
url = '<a href="queries.lp?domain=' + domain + '">' + urlText + "</a>";
@@ -375,7 +377,7 @@ function updateTopLists() {
updateTopClientsTable(false);
// Update top lists data every 10 seconds
setTimeout(updateTopLists, 10000);
utils.setTimer(updateTopLists, REFRESH_INTERVAL.top_lists);
}
function glowIfChanged(elem, textData) {
@@ -385,13 +387,7 @@ function glowIfChanged(elem, textData) {
}
}
function updateSummaryData(runOnce) {
var setTimer = function (timeInSeconds) {
if (!runOnce) {
setTimeout(updateSummaryData, timeInSeconds * 1000);
}
};
function updateSummaryData(runOnce = false) {
$.getJSON("/api/stats/summary", function (data) {
var intl = new Intl.NumberFormat();
glowIfChanged($("span#dns_queries"), intl.format(parseInt(data.queries.total, 10)));
@@ -415,10 +411,10 @@ function updateSummaryData(runOnce) {
}, 500);
})
.done(function () {
setTimer(1);
if (!runOnce) utils.setTimer(updateSummaryData, REFRESH_INTERVAL.summary);
})
.fail(function (data) {
setTimer(300);
utils.setTimer(updateSummaryData, 3 * REFRESH_INTERVAL.summary);
apiFailure(data);
});
}

View File

@@ -111,6 +111,7 @@ function doLogin(password) {
method: "POST",
dataType: "json",
processData: false,
contentType: "application/json; charset=utf-8",
data: JSON.stringify({ password: password, totp: parseInt($("#totp").val(), 10) }),
})
.done(function (data) {

View File

@@ -203,7 +203,7 @@ $(function () {
order: [[6, "desc"]],
columns: [
{ data: "id", visible: false },
{ data: "ip", type: "ip-address", width: "25%" },
{ data: "ips[].ip", type: "ip-address", width: "25%" },
{ data: "hwaddr", width: "10%" },
{ data: "interface", width: "4%" },
{

View File

@@ -85,6 +85,7 @@ function parseQueryStatus(data) {
buttontext,
icon = null,
colorClass = false,
blocked = false,
isCNAME = false;
switch (data.status) {
case "GRAVITY":
@@ -93,6 +94,7 @@ function parseQueryStatus(data) {
fieldtext = "Blocked (gravity)";
buttontext =
'<button type="button" class="btn btn-default btn-sm text-green btn-whitelist"><i class="fas fa-check"></i> Allow</button>';
blocked = true;
break;
case "FORWARDED":
colorClass = "text-green";
@@ -114,6 +116,7 @@ function parseQueryStatus(data) {
fieldtext = "Blocked (regex)";
buttontext =
'<button type="button" class="btn btn-default btn-sm text-green btn-whitelist"><i class="fas fa-check"></i> Allow</button>';
blocked = true;
break;
case "DENYLIST":
colorClass = "text-red";
@@ -121,24 +124,28 @@ function parseQueryStatus(data) {
fieldtext = "Blocked (exact)";
buttontext =
'<button type="button" class="btn btn-default btn-sm text-green btn-whitelist"><i class="fas fa-check"></i> Allow</button>';
blocked = true;
break;
case "EXTERNAL_BLOCKED_IP":
colorClass = "text-red";
icon = "fa-solid fa-ban";
fieldtext = "Blocked (external, IP)";
buttontext = "";
blocked = true;
break;
case "EXTERNAL_BLOCKED_NULL":
colorClass = "text-red";
icon = "fa-solid fa-ban";
fieldtext = "Blocked (external, NULL)";
buttontext = "";
blocked = true;
break;
case "EXTERNAL_BLOCKED_NXRA":
colorClass = "text-red";
icon = "fa-solid fa-ban";
fieldtext = "Blocked (external, NXRA)";
buttontext = "";
blocked = true;
break;
case "GRAVITY_CNAME":
colorClass = "text-red";
@@ -147,6 +154,7 @@ function parseQueryStatus(data) {
buttontext =
'<button type="button" class="btn btn-default btn-sm text-green btn-whitelist"><i class="fas fa-check"></i> Allow</button>';
isCNAME = true;
blocked = true;
break;
case "REGEX_CNAME":
colorClass = "text-red";
@@ -155,6 +163,7 @@ function parseQueryStatus(data) {
buttontext =
'<button type="button" class="btn btn-default btn-sm text-green btn-whitelist"><i class="fas fa-check"></i> Allow</button>';
isCNAME = true;
blocked = true;
break;
case "DENYLIST_CNAME":
colorClass = "text-red";
@@ -163,6 +172,7 @@ function parseQueryStatus(data) {
buttontext =
'<button type="button" class="btn btn-default btn-sm text-green btn-whitelist"><i class="fas fa-check"></i> Allow</button>';
isCNAME = true;
blocked = true;
break;
case "RETRIED":
colorClass = "text-green";
@@ -207,6 +217,7 @@ function parseQueryStatus(data) {
icon: icon,
isCNAME: isCNAME,
matchText: matchText,
blocked: blocked,
};
}
@@ -219,8 +230,8 @@ function formatReplyTime(replyTime, type) {
return replyTime < 1e-4
? (1e6 * replyTime).toFixed(1) + " µs"
: replyTime < 1
? (1e3 * replyTime).toFixed(1) + " ms"
: replyTime.toFixed(1) + " s";
? (1e3 * replyTime).toFixed(1) + " ms"
: replyTime.toFixed(1) + " s";
}
// else: return the number itself (for sorting and searching)
@@ -562,6 +573,9 @@ $(function () {
$(row).addClass(querystatus.colorClass);
}
// Define row background color
$(row).addClass(querystatus.blocked === true ? "blocked-row" : "allowed-row");
// Substitute domain by "." if empty
var domain = data.domain === 0 ? "." : data.domain;
@@ -603,8 +617,8 @@ $(function () {
event.stopPropagation();
});
// Add event listener for opening and closing details
$("#all-queries tbody").on("click", "tr", function () {
// Add event listener for opening and closing details, except on rows with "details-row" class
$("#all-queries tbody").on("click", "tr:not(.details-row)", function () {
var tr = $(this);
var row = table.row(tr);
@@ -618,8 +632,8 @@ $(function () {
row.child.hide();
tr.removeClass("shown");
} else {
// Open this row
row.child(formatInfo(row.data())).show();
// Open this row. Add a class to the row
row.child(formatInfo(row.data()), "details-row").show();
tr.addClass("shown");
}
});

View File

@@ -59,11 +59,11 @@ function generateRow(topic, key, value) {
"</div>" +
'<div class="box-body">' +
'<div class="form-group">';
var defaultValueHint = "";
let defaultValueHint = "";
if (value.modified) {
defaultValueHint = "";
if (value.default !== null) {
var defVal = utils.escapeHtml(JSON.stringify(value.default));
let defVal = utils.escapeHtml(JSON.stringify(value.default));
switch (defVal) {
case "true": {
defVal = "enabled";

View File

@@ -88,11 +88,8 @@ $(function () {
$("td:eq(9)", row).html(button);
if (data.current_session) {
ownSessionID = data.id;
$("td:eq(7)", row).html(
'<strong title="This is the session you are currently using for the web interface">' +
data.remote_addr +
"</strong>"
);
$(row).addClass("text-bold");
$(row).attr("title", "This is the session you are currently using for the web interface");
}
let icon = "";
@@ -191,7 +188,7 @@ $(function () {
function deleteThisSession() {
// This function is called when a red trash button is clicked
// We get the ID of the current item from the data-del-id attribute
const thisID = parseInt(this.attr("data-del-id"), 10);
const thisID = parseInt($(this).attr("data-del-id"), 10);
deleted = 0;
deleteOneSession(thisID, 1, false);
}
@@ -346,7 +343,7 @@ function setAppPassword() {
dataType: "json",
processData: false,
data: JSON.stringify({ config: { webserver: { api: { app_pwhash: apppwhash } } } }),
contentType: "application/json",
contentType: "application/json; charset=utf-8",
})
.done(function () {
$("#modal-apppw").modal("hide");
@@ -399,7 +396,7 @@ function setTOTPSecret(secret) {
dataType: "json",
processData: false,
data: JSON.stringify({ config: { webserver: { api: { totp_secret: secret } } } }),
contentType: "application/json",
contentType: "application/json; charset=utf-8",
})
.done(function () {
$("#button-enable-totp").addClass("hidden");

View File

@@ -49,7 +49,7 @@ function populateDataTable(endpoint) {
columns = [
{ data: null, render: CNAMEdomain },
{ data: null, render: CNAMEtarget },
{ data: null, render: CNAMEttl },
{ data: null, width: "40px", render: CNAMEttl },
{ data: null, width: "22px", orderable: false },
];
}
@@ -73,6 +73,7 @@ function populateDataTable(endpoint) {
type: "GET",
dataSrc: `config.dns.${endpoint}`,
},
autoWidth: false,
columns: columns,
columnDefs: [
{
@@ -99,8 +100,10 @@ function populateDataTable(endpoint) {
$(`td:eq(${endpoint === "hosts" ? 2 : 3})`, row).html(button);
},
dom:
"<'row'<'col-sm-6'l><'col-sm-6'f>>" +
"<'row'<'col-sm-5'l><'col-sm-7'f>>" +
"<'row'<'col-sm-12'p>>" +
"<'row'<'col-sm-12'<'table-responsive'tr>>>" +
"<'row'<'col-sm-12'p>>" +
"<'row'<'col-sm-12'i>>",
lengthMenu: [
[10, 25, 50, 100, -1],
@@ -173,7 +176,7 @@ function delHosts(elem) {
utils.showAlert(
"error",
"",
"Error while deleting DNS record: <code>" + utils.escapeHtml(elem) + "</code>",
"Error while deleting DNS record: <code>" + elem + "</code>",
data.responseText
);
console.log(exception); // eslint-disable-line no-console
@@ -205,7 +208,7 @@ function delCNAME(elem) {
utils.showAlert(
"error",
"",
"Error while deleting CNAME record: <code>" + utils.escapeHtml(elem) + "</code>",
"Error while deleting CNAME record: <code>" + elem + "</code>",
data.responseText
);
console.log(exception); // eslint-disable-line no-console
@@ -225,6 +228,8 @@ $(document).ready(function () {
.done(function () {
utils.enableAll();
utils.showAlert("success", "fas fa-plus", "Successfully added DNS record", elem);
$("#Hdomain").val("");
$("#Hip").val("");
$("#hosts-Table").DataTable().ajax.reload(null, false);
})
.fail(function (data, exception) {
@@ -249,6 +254,8 @@ $(document).ready(function () {
.done(function () {
utils.enableAll();
utils.showAlert("success", "fas fa-plus", "Successfully added CNAME record", elem);
$("#Cdomain").val("");
$("#Ctarget").val("");
$("#cnameRecords-Table").DataTable().ajax.reload(null, false);
})
.fail(function (data, exception) {
@@ -258,4 +265,7 @@ $(document).ready(function () {
console.log(exception); // eslint-disable-line no-console
});
});
// Add a small legend below the CNAME table
$("#cnameRecords-Table").after("<small>* <b>TTL</b> in seconds <i>(optional)</i></small>");
});

View File

@@ -5,7 +5,7 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
/* global apiFailure:false, Chart:false, THEME_COLORS:false, customTooltips:false, htmlLegendPlugin:false,doughnutTooltip:false, ChartDeferred:false */
/* global apiFailure:false, Chart:false, THEME_COLORS:false, customTooltips:false, htmlLegendPlugin:false,doughnutTooltip:false, ChartDeferred:false, REFRESH_INTERVAL: false, utils: false */
var hostinfoTimer = null;
var cachePieChart = null;
@@ -105,9 +105,8 @@ function updateHostInfo() {
" " +
uname.machine
);
// Update every 120 seconds
clearTimeout(hostinfoTimer);
hostinfoTimer = setTimeout(updateHostInfo, 120000);
hostinfoTimer = utils.setTimer(updateHostInfo, REFRESH_INTERVAL.hosts);
})
.fail(function (data) {
apiFailure(data);
@@ -173,9 +172,8 @@ function updateMetrics() {
);
$("div[id^='sysinfo-metrics-overlay']").hide();
// Update every 10 seconds
clearTimeout(metricsTimer);
metricsTimer = setTimeout(updateMetrics, 10000);
metricsTimer = utils.setTimer(updateMetrics, REFRESH_INTERVAL.metrics);
})
.fail(function (data) {
apiFailure(data);
@@ -298,6 +296,7 @@ $("#loggingButton").confirm({
type: "PATCH",
dataType: "json",
processData: false,
contentType: "application/json; charset=utf-8",
data: JSON.stringify(data),
})
.done(function (data) {

View File

@@ -114,6 +114,7 @@ function setConfigValues(topic, key, value) {
function saveSettings() {
var settings = {};
utils.disableAll();
$("[data-key]").each(function () {
var key = $(this).data("key");
var value = $(this).val();
@@ -168,6 +169,7 @@ function saveSettings() {
contentType: "application/json; charset=utf-8",
})
.done(function () {
utils.enableAll();
// Success
utils.showAlert(
"success",
@@ -178,7 +180,10 @@ function saveSettings() {
// Show loading overlay
utils.loadingOverlay(true);
})
.fail(function (data) {
.fail(function (data, exception) {
utils.enableAll();
utils.showAlert("error", "", "Error while applying settings", data.responseText);
console.log(exception); // eslint-disable-line no-console
apiFailure(data);
});
}

View File

@@ -5,12 +5,10 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
/* global moment: false, apiFailure: false, utils: false */
/* global moment: false, apiFailure: false, utils: false, REFRESH_INTERVAL: false */
var nextID = 0;
// Check every 0.5s for fresh data
const interval = 500;
var lastPID = -1;
// Maximum number of lines to display
const maxlines = 5000;
@@ -25,7 +23,7 @@ const markUpdates = true;
function getData() {
// Only update when spinner is spinning
if (!$("#feed-icon").hasClass("fa-play")) {
window.setTimeout(getData, interval);
utils.setTimer(getData, REFRESH_INTERVAL.logs);
return;
}
@@ -41,12 +39,28 @@ function getData() {
method: "GET",
})
.done(function (data) {
// Check if we have a new PID -> FTL was restarted
if (lastPID !== data.pid) {
if (lastPID !== -1) {
$("#output").append("<i class='text-danger'>*** FTL restarted ***</i><br>");
}
// Remember PID
lastPID = data.pid;
// Reset nextID
nextID = 0;
getData();
return;
}
// Set placeholder text if log file is empty and we have no new lines
if (data.log.length === 0) {
if (nextID === 0) {
$("#output").html("<i>*** Log file is empty ***</i>");
}
window.setTimeout(getData, interval);
utils.setTimer(getData, REFRESH_INTERVAL.logs);
return;
}
@@ -90,11 +104,11 @@ function getData() {
// Set filename
$("#filename").text(data.file);
window.setTimeout(getData, interval);
utils.setTimer(getData, REFRESH_INTERVAL.logs);
})
.fail(function (data) {
apiFailure(data);
window.setTimeout(getData, 5 * interval);
utils.setTimer(getData, 5 * REFRESH_INTERVAL.logs);
});
}

View File

@@ -86,8 +86,8 @@ function padNumber(num) {
var showAlertBox = null;
function showAlert(type, icon, title, message) {
const options = {
title: "&nbsp;<strong>" + title + "</strong><br>",
message: message,
title: "&nbsp;<strong>" + escapeHtml(title) + "</strong><br>",
message: escapeHtml(message),
icon: icon,
},
settings = {
@@ -123,9 +123,9 @@ function showAlert(type, icon, title, message) {
var data = JSON.parse(message);
console.log(data); // eslint-disable-line no-console
if (data.error !== undefined) {
options.title = "&nbsp;<strong>" + data.error.message + "</strong><br>";
options.title = "&nbsp;<strong>" + escapeHtml(data.error.message) + "</strong><br>";
if (data.error.hint !== null) options.message = data.error.hint;
if (data.error.hint !== null) options.message = escapeHtml(data.error.hint);
}
} catch {
// Do nothing
@@ -319,6 +319,7 @@ function addFromQueryLog(domain, list) {
method: "post",
dataType: "json",
processData: false,
contentType: "application/json; charset=utf-8",
data: JSON.stringify({
domain: domain,
comment: "Added from Query Log",
@@ -655,6 +656,42 @@ function loadingOverlay(reloadAfterTimeout = false) {
return true;
}
// Function that calls a function only if the page is currently visible. This is
// useful to prevent unnecessary API calls when the page is not visible (e.g.
// when the user is on another tab).
function callIfVisible(func) {
if (document.hidden) {
// Page is not visible, try again in 1 second
window.setTimeout(callIfVisible, 1000, func);
return;
}
// Page is visible, call function instead
func();
}
// Timer that calls a function after <interval> milliseconds but only if the
// page is currently visible. We cancel possibly running timers for the same
// function before starting a new one to prevent multiple timers running at
// the same time causing unnecessary identical API calls when the page is
// visible again.
function setTimer(func, interval) {
// Cancel possibly running timer
window.clearTimeout(func.timer);
// Start new timer
func.timer = window.setTimeout(callIfVisible, interval, func);
}
// Same as setTimer() but calls the function every <interval> milliseconds
function setInter(func, interval) {
// Cancel possibly running timer
window.clearTimeout(func.timer);
// Start new timer
func.timer = window.setTimeout(callIfVisible, interval, func);
// Restart timer
window.setTimeout(setInter, interval, func, interval);
}
window.utils = (function () {
return {
escapeHtml: escapeHtml,
@@ -689,5 +726,7 @@ window.utils = (function () {
hexDecode: hexDecode,
listsAlert: listAlert,
loadingOverlay: loadingOverlay,
setTimer: setTimer,
setInter: setInter,
};
})();

View File

@@ -100,7 +100,7 @@ if startsWith(scriptname, 'groups') then
<link rel="stylesheet" href="<?=pihole.fileversion('style/vendor/bootstrap-toggle.min.css')?>">
<? end ?>
<? if is_authenticated then ?>
<link rel="stylesheet" href="<?=pihole.fileversion('style/vendor/datatables.min.css')?>">
<link rel="stylesheet" href="<?=pihole.fileversion('style/vendor/datatables-pihole.min.css')?>">
<link rel="stylesheet" href="<?=pihole.fileversion('style/vendor/datatables_extensions.min.css')?>">
<link rel="stylesheet" href="<?=pihole.fileversion('style/vendor/daterangepicker.min.css')?>">
<? end ?>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -32,7 +32,7 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
<div class="col-xs-12 col-sm-6 col-md-12 col-lg-6">
<div class="form-group">
<div class="input-group">
<div class="input-group-addon">From</div>
<div class="input-group-addon">Start</div>
<input type="text" class="form-control DHCPgroup" id="dhcp.start" data-key="dhcp.start"
autocomplete="off" spellcheck="false" autocapitalize="none"
autocorrect="off" value="">
@@ -42,7 +42,7 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
<div class="col-xs-12 col-sm-6 col-md-12 col-lg-6">
<div class="form-group">
<div class="input-group">
<div class="input-group-addon">To</div>
<div class="input-group-addon">End</div>
<input type="text" class="form-control DHCPgroup" id="dhcp.end" data-key="dhcp.end"
autocomplete="off" spellcheck="false" autocapitalize="none"
autocorrect="off" value="">
@@ -60,6 +60,17 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
</div>
</div>
</div>
<div class="col-xs-12 col-sm-6 col-md-12 col-lg-6">
<label>Netmask (<code>0.0.0.0</code> = auto)</label>
<div class="form-group">
<div class="input-group">
<div class="input-group-addon">Netmask</div>
<input type="text" class="form-control DHCPgroup" id="dhcp.netmask" data-key="dhcp.netmask"
autocomplete="off" spellcheck="false" autocapitalize="none"
autocorrect="off" value="">
</div>
</div>
</div>
<div class="col-md-12">
<div><input type="checkbox" id="dhcp.ipv6" data-key="dhcp.ipv6" class="DHCPgroup">&nbsp;<label for="dhcp.ipv6"><strong>Enable additional IPv6 support (SLAAC + RA)</strong></label></div>
<p>Enable this option to enable IPv6 support for the Pi-hole DHCP server. This will allow the Pi-hole to hand out IPv6 addresses to clients and also provide IPv6 router advertisements (RA) to clients. This option is only useful if the Pi-hole is configured with an IPv6 address.</p>
@@ -85,7 +96,7 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
autocorrect="off" id="dhcp.leaseTime" data-key="dhcp.leaseTime" value="">
</div>
</div>
<p>The lease time can be in seconds, minutes (e.g., "45m"), hours (e.g., "1h"), days (like "2d"), or even weeks ("1w"). You may also use "infinite" as string but be aware of the drawbacks: assigned addresses are will only be made available again after the lease time has passed or when leases are manually deleted below.</p>
<p>The lease time can be in seconds, minutes (e.g., "45m"), hours (e.g., "1h"), days (like "2d"), or even weeks ("1w"). If no lease time is specified (empty), <code>dnsmasq</code>'s default lease time is one hour for IPv4 and one day for IPv6. You may also use "infinite" as string but be aware of the drawbacks: assigned addresses are will only be made available again after the lease time has passed or when leases are manually deleted below.</p>
</div>
</div>
<div class="row">

View File

@@ -14,125 +14,96 @@ PageTitle = "Local DNS Settings"
mg.include('scripts/pi-hole/lua/settings_header.lp','r')
?>
<div class="row">
<div class="col-md-6 settings-level-1">
<div class="col-md-12 col-lg-6 settings-level-1">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title" id="title-hosts" data-configkeys="dns.hosts">Local DNS records</h3>
</div>
<div class="box-body">
<div class="row">
<div class="col-md-12">
<div class="box collapsed-box">
<!-- /.box-header -->
<div class="box-header with-border">
<h3 class="box-title">
Add new DNS record
</h3>
<div class="box-tools pull-right">
<button type="button" class="btn btn-box-tool" data-configkeys="hosts" data-widget="collapse"><i class="fa fa-plus"></i></button>
</div>
</div>
<!-- /.box-header -->
<div class="box-body">
<div class="row">
<div class="form-group col-md-6">
<label for="domain">Domain:</label>
<input id="Hdomain" type="url" class="form-control" data-configkeys="hosts" placeholder="Add a domain (example.com or sub.example.com)" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
</div>
<div class="form-group col-md-6">
<label for="target">IP address:</label>
<input id="Hip" type="text" class="form-control" data-configkeys="hosts" placeholder="Associated IP address" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
</div>
</div>
</div>
<div class="box-footer clearfix">
<strong>Note:</strong>
<p>Adding/removing local DNS records will flush the cache but does not require a restart of the DNS server.</p>
<button type="button" id="btnAdd-host" class="btn btn-primary pull-right" data-configkeys="hosts">Add</button>
</div>
</div>
</div>
<div class="col-md-12">
<h3 class="box-title">List of local DNS records</h3>
<!-- /.box-header -->
<table id="hosts-Table" class="table table-striped table-bordered" width="100%">
<thead>
<tr>
<th>Domain</th>
<th>IP</th>
<th>Action</th>
</tr>
<tr>
<th>Domain</th>
<th>IP</th>
<th></th>
</tr>
</thead>
<tfoot class="add-new-item">
<tr>
<th>
<input id="Hdomain" type="url" class="form-control" data-configkeys="hosts" placeholder="Domain" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
</th>
<th>
<input id="Hip" type="text" class="form-control" data-configkeys="hosts" placeholder="Associated IP" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
</th>
<th>
<button type="button" id="btnAdd-host" class="btn btn-primary btn-xs" data-configkeys="hosts"><i class="fa fa-plus"></i></button>
</th>
</tr>
</tfoot>
</table>
<button type="button" id="resetButton" class="btn btn-default btn-sm text-red hidden">Clear Filters</button>
</div>
</div>
</div>
<div class="box-footer">
<strong>Note:</strong>
<p>Adding/removing local DNS records will flush the cache but does not require a restart of the DNS server.</p>
</div>
</div>
</div>
<div class="col-md-6 settings-level-1">
<div class="col-md-12 col-lg-6 settings-level-1">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title" id="title-cnameRecords" data-configkeys="dns.cnameRecords">Local CNAME records records</h3>
</div>
<div class="box-body">
<div class="row">
<div class="col-md-12">
<div class="box collapsed-box">
<!-- /.box-header -->
<div class="box-header with-border">
<h3 class="box-title">
Add new CNAME record
</h3>
<div class="box-tools pull-right">
<button type="button" class="btn btn-box-tool" data-configkeys="cnameRecords" data-widget="collapse"><i class="fa fa-plus"></i></button>
</div>
</div>
<!-- /.box-header -->
<div class="box-body">
<div class="row">
<div class="form-group col-md-5">
<label for="domain">Domain:</label>
<input id="Cdomain" type="url" class="form-control" data-configkeys="cnameRecords" placeholder="Add a domain (example.com or sub.example.com)" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
</div>
<div class="form-group col-md-5">
<label for="target">Target Domain:</label>
<input id="Ctarget" type="url" class="form-control" data-configkeys="cnameRecords" placeholder="Associated Target Domain" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
</div>
<div class="form-group col-md-2">
<label for="target">TTL (optional):</label>
<input id="Cttl" type="numeric" class="form-control" data-configkeys="cnameRecords" placeholder="TTL in seconds" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
</div>
</div>
</div>
<div class="box-footer clearfix">
<strong>Note:</strong>
<p>The target of a <code>CNAME</code> must be a domain that the Pi-hole already has in its cache or is authoritative for. This is a universal limitation of <code>CNAME</code> records.</p>
<p>The reason for this is that Pi-hole will not send additional queries upstream when serving <code>CNAME</code> replies. As consequence, if you set a target that isn't already known, the reply to the client may be incomplete. Pi-hole just returns the information it knows at the time of the query. This results in certain limitations for <code>CNAME</code> targets,
for instance, only <i>active</i> DHCP leases work as targets - mere DHCP <i>leases</i> aren't sufficient as they aren't (yet) valid DNS records.</p>
<p>Additionally, you can't <code>CNAME</code> external domains (<code>bing.com</code> to <code>google.com</code>) successfully as this could result in invalid SSL certificate errors when the target server does not serve content for the requested domain.</p>
<p>Adding/removing local CNAME records will restart the DNS server.</p>
<button type="button" id="btnAdd-cname" class="btn btn-primary pull-right" data-configkeys="cnameRecords">Add</button>
</div>
</div>
</div>
<div class="col-md-12">
<h3 class="box-title">List of local CNAME records</h3>
<!-- /.box-header -->
<table id="cnameRecords-Table" class="table table-striped table-bordered" width="100%">
<thead>
<tr>
<th>Domain</th>
<th>Target</th>
<th>TTL</th>
<th>Action</th>
</tr>
<tr>
<th>Domain</th>
<th>Target</th>
<th>TTL *</th>
<th></th>
</tr>
</thead>
<tfoot class="add-new-item">
<tr>
<th>
<input id="Cdomain" type="url" class="form-control" data-configkeys="cnameRecords" placeholder="Domain" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
</th>
<th>
<input id="Ctarget" type="url" class="form-control" data-configkeys="cnameRecords" placeholder="Target Domain" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
</th>
<th>
<input id="Cttl" type="numeric" class="form-control" data-configkeys="cnameRecords" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
</th>
<th>
<button type="button" id="btnAdd-cname" class="btn btn-primary btn-xs" data-configkeys="cnameRecords"><i class="fa fa-plus"></i></button>
</th>
</tr>
</tfoot>
</table>
<button type="button" id="resetButton" class="btn btn-default btn-sm text-red hidden">Clear Filters</button>
</div>
</div>
</div>
<div class="box-footer">
<strong>Note:</strong>
<p>The target of a <code>CNAME</code> must be a domain that the Pi-hole already has in its cache or is authoritative for. This is a universal limitation of <code>CNAME</code> records.</p>
<p>The reason for this is that Pi-hole will not send additional queries upstream when serving <code>CNAME</code> replies. As consequence, if you set a target that isn't already known, the reply to the client may be incomplete. Pi-hole just returns the information it knows at the time of the query. This results in certain limitations for <code>CNAME</code> targets,
for instance, only <i>active</i> DHCP leases work as targets - mere DHCP <i>leases</i> aren't sufficient as they aren't (yet) valid DNS records.</p>
<p>Additionally, you can't <code>CNAME</code> external domains (<code>bing.com</code> to <code>google.com</code>) successfully as this could result in invalid SSL certificate errors when the target server does not serve content for the requested domain.</p>
<p>Adding/removing local CNAME records will restart the DNS server.</p>
</div>
</div>
</div>
</div>

View File

@@ -51,15 +51,16 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
<input type="checkbox" id="database.DBexport" data-key="database.DBexport" title="log-queries">
<label for="database.DBexport"><strong>Should FTL store queries in the database?</strong></label>
</div>
<div>
<br>
<div class="row-flex">
<input type="number" id="database.maxDBdays" data-key="database.maxDBdays" data-type="integer" value="" min="0" step="10" style="width: 5em;">
<label for="database.maxDBdays"><strong>Maximum number of days to keep queries in the database</strong></label>
</div>
<div>
<div class="row-flex">
<input type="number" id="database.network.expire" data-key="database.network.expire" data-type="integer" value="" min="0" step="10" style="width: 5em;">
<label for="database.network.expire"><strong>How long should IP addresses be kept in the network_addresses table [days]?</strong></label>
<p>IP addresses (and associated host names) older than the specified number of days are removed to avoid dead entries in the network overview table.</p>
</div>
<p>IP addresses (and associated host names) older than the specified number of days are removed to avoid dead entries in the network overview table.</p>
</div>
</div>
</div>

View File

@@ -92,6 +92,11 @@ td.lookatme {
white-space: pre-wrap;
}
/* Client column */
#all-queries td:nth-of-type(5) {
word-break: break-all;
}
/* Allow Info String to wrap (useful while filtering entries on small screen) */
#all-queries_info {
white-space: unset;
@@ -368,12 +373,18 @@ td.lookatme {
font-family: inherit;
}
.form-inline .dataTables .form-control {
.form-inline .dataTable .form-control {
display: inline-block;
width: 100%;
vertical-align: middle;
}
/* Table footer row used to add new items, using inline input fields */
tfoot.add-new-item > tr > th {
font-weight: normal;
vertical-align: inherit;
}
.select2-container--default .select2-results > .select2-results__options {
max-height: 400px;
}
@@ -680,6 +691,11 @@ li:not(.menu-open) .treeview-menu .warning-count {
border-color: #3c8dbc;
}
/* Fix indentation of <p> elements inside icheck elements */
[class*="icheck-"] > label + p {
padding-left: 29px;
}
/* Fix some datatables layout on small screens */
@media screen and (max-width: 660px), screen and (min-width: 767px) and (max-width: 960px) {
#domainsTable_wrapper .table-responsive {
@@ -998,6 +1014,8 @@ body:not(.lcars) .filter_types [class*="icheck-"] > input:first-child:checked +
/* Datatables Select - bgcolor */
:root {
--datatable-bgcolor: #e0e8ee;
--dt-row-selected: var(--datatable-bgcolor);
--dt-row-selected-text: currentColor;
}
div.dt-buttons {
@@ -1404,3 +1422,12 @@ table.dataTable tbody > tr > .selected {
display: none;
margin-top: -1px;
}
/* General layout - Row containing flex elements */
.row-flex {
display: flex;
flex-direction: row;
gap: 0.5em;
margin: 0.5em 0;
align-items: start;
}

View File

@@ -483,6 +483,11 @@ kbd {
color: #808890;
}
/* Used in error403 */
.text-danger {
color: #f54;
}
.warning-count {
color: #000;
background: #f94;

21
style/vendor/datatables-pihole.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,15 +0,0 @@
/*
* This combined file was created by the DataTables downloader builder:
* https://datatables.net/download
*
* To rebuild or modify this file with the latest versions of the included
* software please visit:
* https://datatables.net/download/#bs/dt-1.10.21
*
* Included libraries:
* DataTables 1.10.21
*/
table.dataTable{clear:both;margin-top:6px !important;margin-bottom:6px !important;max-width:none !important;border-collapse:separate !important}table.dataTable td,table.dataTable th{-webkit-box-sizing:content-box;box-sizing:content-box}table.dataTable td.dataTables_empty,table.dataTable th.dataTables_empty{text-align:center}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}div.dataTables_wrapper div.dataTables_length label{font-weight:normal;text-align:left;white-space:nowrap}div.dataTables_wrapper div.dataTables_length select{width:75px;display:inline-block}div.dataTables_wrapper div.dataTables_filter{text-align:right}div.dataTables_wrapper div.dataTables_filter label{font-weight:normal;white-space:nowrap;text-align:left}div.dataTables_wrapper div.dataTables_filter input{margin-left:0.5em;display:inline-block;width:auto}div.dataTables_wrapper div.dataTables_info{padding-top:8px;white-space:nowrap}div.dataTables_wrapper div.dataTables_paginate{margin:0;white-space:nowrap;text-align:right}div.dataTables_wrapper div.dataTables_paginate ul.pagination{margin:2px 0;white-space:nowrap}div.dataTables_wrapper div.dataTables_processing{position:absolute;top:50%;left:50%;width:200px;margin-left:-100px;margin-top:-26px;text-align:center;padding:1em 0}table.dataTable thead>tr>th.sorting_asc,table.dataTable thead>tr>th.sorting_desc,table.dataTable thead>tr>th.sorting,table.dataTable thead>tr>td.sorting_asc,table.dataTable thead>tr>td.sorting_desc,table.dataTable thead>tr>td.sorting{padding-right:30px}table.dataTable thead>tr>th:active,table.dataTable thead>tr>td:active{outline:none}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc,table.dataTable thead .sorting_asc_disabled,table.dataTable thead .sorting_desc_disabled{cursor:pointer;position:relative}table.dataTable thead .sorting:after,table.dataTable thead .sorting_asc:after,table.dataTable thead .sorting_desc:after,table.dataTable thead .sorting_asc_disabled:after,table.dataTable thead .sorting_desc_disabled:after{position:absolute;bottom:8px;right:8px;display:block;font-family:'Glyphicons Halflings';opacity:0.5}table.dataTable thead .sorting:after{opacity:0.2;content:"\e150"}table.dataTable thead .sorting_asc:after{content:"\e155"}table.dataTable thead .sorting_desc:after{content:"\e156"}table.dataTable thead .sorting_asc_disabled:after,table.dataTable thead .sorting_desc_disabled:after{color:#eee}div.dataTables_scrollHead table.dataTable{margin-bottom:0 !important}div.dataTables_scrollBody>table{border-top:none;margin-top:0 !important;margin-bottom:0 !important}div.dataTables_scrollBody>table>thead .sorting:after,div.dataTables_scrollBody>table>thead .sorting_asc:after,div.dataTables_scrollBody>table>thead .sorting_desc:after{display:none}div.dataTables_scrollBody>table>tbody>tr:first-child>th,div.dataTables_scrollBody>table>tbody>tr:first-child>td{border-top:none}div.dataTables_scrollFoot>.dataTables_scrollFootInner{box-sizing:content-box}div.dataTables_scrollFoot>.dataTables_scrollFootInner>table{margin-top:0 !important;border-top:none}@media screen and (max-width: 767px){div.dataTables_wrapper div.dataTables_length,div.dataTables_wrapper div.dataTables_filter,div.dataTables_wrapper div.dataTables_info,div.dataTables_wrapper div.dataTables_paginate{text-align:center}}table.dataTable.table-condensed>thead>tr>th{padding-right:20px}table.dataTable.table-condensed .sorting:after,table.dataTable.table-condensed .sorting_asc:after,table.dataTable.table-condensed .sorting_desc:after{top:6px;right:6px}table.table-bordered.dataTable{border-right-width:0}table.table-bordered.dataTable th,table.table-bordered.dataTable td{border-left-width:0}table.table-bordered.dataTable th:last-child,table.table-bordered.dataTable th:last-child,table.table-bordered.dataTable td:last-child,table.table-bordered.dataTable td:last-child{border-right-width:1px}table.table-bordered.dataTable tbody th,table.table-bordered.dataTable tbody td{border-bottom-width:0}div.dataTables_scrollHead table.table-bordered{border-bottom-width:0}div.table-responsive>div.dataTables_wrapper>div.row{margin:0}div.table-responsive>div.dataTables_wrapper>div.row>div[class^="col-"]:first-child{padding-left:0}div.table-responsive>div.dataTables_wrapper>div.row>div[class^="col-"]:last-child{padding-right:0}

File diff suppressed because one or more lines are too long