Merge branch 'development-v6' into fix/adlist_list

This commit is contained in:
DL6ER 2023-10-28 21:27:39 +02:00
commit be21f650f1
No known key found for this signature in database
GPG Key ID: 00135ACBD90B28DD
27 changed files with 379 additions and 212 deletions

View File

@ -21,7 +21,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4.1.0
uses: actions/checkout@v4.1.1
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2

View File

@ -11,7 +11,7 @@ jobs:
steps:
-
name: Checkout repository
uses: actions/checkout@v4.1.0
uses: actions/checkout@v4.1.1
-
name: Spell-Checking
uses: codespell-project/actions-codespell@master

View File

@ -9,6 +9,6 @@ jobs:
name: editorconfig-checker
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.1.0
- uses: actions/checkout@v4.1.1
- uses: editorconfig-checker/action-editorconfig-checker@main
- run: editorconfig-checker

View File

@ -11,7 +11,7 @@ jobs:
name: Syncing branches
steps:
- name: Checkout
uses: actions/checkout@v4.1.0
uses: actions/checkout@v4.1.1
- name: Opening pull request
run: gh pr create -B devel -H master --title 'Sync master back into development' --body 'Created by Github action' --label 'internal'
env:

View File

@ -19,10 +19,10 @@ jobs:
steps:
- name: Clone repository
uses: actions/checkout@v4.1.0
uses: actions/checkout@v4.1.1
- name: Set up Node.js
uses: actions/setup-node@v3.8.1
uses: actions/setup-node@v4.0.0
with:
node-version: "16.x"
cache: npm

View File

@ -22,8 +22,8 @@ mg.include('scripts/pi-hole/lua/header_authenticated.lp','r')
<div class="icon">
<i class="fas fa-globe-americas"></i>
</div>
<a href="network.lp" class="small-box-footer" title="">
<span id="total_clients">-</span> active clients <i class="fa fa-arrow-circle-right"></i>
<a href="network.lp" class="small-box-footer" title="" id="total_clients">
<span id="active_clients">-</span> active clients <i class="fa fa-arrow-circle-right"></i>
</a>
</div>
</div>

View File

@ -165,6 +165,7 @@ mg.include('scripts/pi-hole/lua/header_authenticated.lp','r')
<th>Type</th>
<th>Domain</th>
<th>Client</th>
<th><i class="fas fa-stopwatch" title="Query reply time"></i></th>
<th></th>
</tr>
</thead>
@ -175,6 +176,7 @@ mg.include('scripts/pi-hole/lua/header_authenticated.lp','r')
<th>Type</th>
<th>Domain</th>
<th>Client</th>
<th><i class="fas fa-stopwatch" title="Query reply time"></i></th>
<th></th>
</tr>
</tfoot>
@ -192,7 +194,6 @@ mg.include('scripts/pi-hole/lua/header_authenticated.lp','r')
<script src="<?=pihole.fileversion('scripts/vendor/bootstrap-select.min.js')?>"></script>
<script src="<?=pihole.fileversion('scripts/pi-hole/js/ip-address-sorting.js')?>"></script>
<script src="<?=pihole.fileversion('scripts/vendor/daterangepicker.min.js')?>"></script>
<script src="<?=pihole.fileversion('scripts/pi-hole/js/utils.js')?>"></script>
<script src="<?=pihole.fileversion('scripts/pi-hole/js/queries.js')?>"></script>
<? mg.include('scripts/pi-hole/lua/footer.lp','r')?>

View File

@ -87,7 +87,6 @@ const htmlLegendPlugin = {
});
}
textLink.style.color = item.fontColor;
textLink.style.margin = 0;
textLink.style.padding = 0;
textLink.style.textDecoration = item.hidden ? "line-through" : "";
@ -103,11 +102,11 @@ const htmlLegendPlugin = {
// eslint-disable-next-line no-unused-vars
var customTooltips = function (context) {
var tooltip = context.tooltip;
var tooltipEl = document.getElementById(this._chart.canvas.id + "-customTooltip");
var tooltipEl = document.getElementById(this.chart.canvas.id + "-customTooltip");
if (!tooltipEl) {
// Create Tooltip Element once per chart
tooltipEl = document.createElement("div");
tooltipEl.id = this._chart.canvas.id + "-customTooltip";
tooltipEl.id = this.chart.canvas.id + "-customTooltip";
tooltipEl.classList.add("chartjs-tooltip");
tooltipEl.innerHTML = "<div class='arrow'></div> <table></table>";
// avoid browser's font-zoom since we know that <body>'s
@ -121,7 +120,7 @@ var customTooltips = function (context) {
tooltipEl.style.fontSize = tooltip.options.bodyFont.size / fontZoom + "px";
tooltipEl.style.fontStyle = tooltip.options.bodyFont.style;
// append Tooltip next to canvas-containing box
tooltipEl.ancestor = this._chart.canvas.closest(".box[id]").parentNode;
tooltipEl.ancestor = this.chart.canvas.closest(".box[id]").parentNode;
tooltipEl.ancestor.append(tooltipEl);
}
@ -175,7 +174,7 @@ var customTooltips = function (context) {
tableRoot.innerHTML = innerHtml;
}
var canvasPos = this._chart.canvas.getBoundingClientRect();
var canvasPos = this.chart.canvas.getBoundingClientRect();
var boxPos = tooltipEl.ancestor.getBoundingClientRect();
var offsetX = canvasPos.left - boxPos.left;
var offsetY = canvasPos.top - boxPos.top;

View File

@ -405,34 +405,47 @@ function delItems(ids) {
}
function addClient() {
var ip = utils.escapeHtml($("#select").val().trim());
const comment = utils.escapeHtml($("#new_comment").val());
utils.disableAll();
utils.showAlert("info", "", "Adding client...", ip);
if (ip.length === 0) {
utils.enableAll();
utils.showAlert("warning", "", "Warning", "Please specify a client IP or MAC address");
return;
}
// 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,]+/);
// Remove empty elements
ips = ips.filter(function (el) {
return el !== "";
});
const ipStr = JSON.stringify(ips);
// Validate input, can be:
// - IPv4 address (with and without CIDR)
// - IPv6 address (with and without CIDR)
// - MAC address (in the form AA:BB:CC:DD:EE:FF)
// - host name (arbitrary form, we're only checking against some reserved characters)
if (utils.validateIPv4CIDR(ip) || utils.validateIPv6CIDR(ip) || utils.validateMAC(ip)) {
// Convert input to upper case (important for MAC addresses)
ip = ip.toUpperCase();
} else if (!utils.validateHostname(ip)) {
for (var i = 0; i < ips.length; i++) {
if (
utils.validateIPv4CIDR(ips[i]) ||
utils.validateIPv6CIDR(ips[i]) ||
utils.validateMAC(ips[i])
) {
// Convert input to upper case (important for MAC addresses)
ips[i] = ips[i].toUpperCase();
} else if (!utils.validateHostname(ips[i])) {
utils.showAlert(
"warning",
"",
"Warning",
"Input is neither a valid IP or MAC address nor a valid host name!"
);
return;
}
}
utils.disableAll();
utils.showAlert("info", "", "Adding client(s)...", ipStr);
if (ips.length === 0) {
utils.enableAll();
utils.showAlert(
"warning",
"",
"Warning",
"Input is neither a valid IP or MAC address nor a valid host name!"
);
utils.showAlert("warning", "", "Warning", "Please specify a client IP or MAC address");
return;
}
@ -441,10 +454,10 @@ function addClient() {
method: "post",
dataType: "json",
processData: false,
data: JSON.stringify({ client: ip, comment: comment }),
success: function () {
data: JSON.stringify({ client: ips, comment: comment }),
success: function (data) {
utils.enableAll();
utils.showAlert("success", "fas fa-plus", "Successfully added client", ip);
utils.listsAlert("client", ips, data);
reloadClientSuggestions();
table.ajax.reload(null, false);
table.rows().deselect();

View File

@ -497,46 +497,54 @@ function addDomain() {
commentEl = $("#new_regex_comment");
}
var domain = utils.escapeHtml(domainEl.val());
const comment = utils.escapeHtml(commentEl.val());
utils.disableAll();
utils.showAlert("info", "", "Adding domain...", domain);
// 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,]+/);
// Remove empty elements
domains = domains.filter(function (el) {
return el !== "";
});
const domainStr = JSON.stringify(domains);
if (domain.length < 2) {
utils.disableAll();
utils.showAlert("info", "", "Adding domain(s)...", domainStr);
if (domains.length === 0) {
utils.enableAll();
utils.showAlert("warning", "", "Warning", "Please specify a domain");
utils.showAlert("warning", "", "Warning", "Please specify at least one domain");
return;
}
// strip "*." if specified by user in wildcard mode
if (kind === "exact" && wildcardChecked && domain.startsWith("*.")) {
domain = domain.substr(2);
for (var i = 0; i < domains.length; i++) {
if (kind === "exact" && wildcardChecked) {
// Transform domain to wildcard if specified by user
domains[i] = "(\\.|^)" + domains[i].replaceAll(".", "\\.") + "$";
kind = "regex";
// strip leading "*." if specified by user in wildcard mode
if (domains[i].startsWith("*.")) domains[i] = domains[i].substr(2);
}
}
// determine list type
const type = action === "add_deny" ? "deny" : "allow";
// Transform domain to wildcard if specified by user
if (kind === "exact" && wildcardChecked) {
domain = "(\\.|^)" + domain.replaceAll(".", "\\.") + "$";
kind = "regex";
}
$.ajax({
url: "/api/domains/" + type + "/" + kind,
method: "post",
dataType: "json",
processData: false,
data: JSON.stringify({
domain: domain,
domain: domains,
comment: comment,
type: type,
kind: kind,
}),
success: function () {
success: function (data) {
utils.enableAll();
utils.showAlert("success", "fas fa-plus", "Successfully added domain", domain);
utils.listsAlert("domain", domains, data);
table.ajax.reload(null, false);
table.rows().deselect();

View File

@ -499,13 +499,21 @@ function delItems(ids) {
function addList(event) {
const type = event.data.type;
const address = utils.escapeHtml($("#new_address").val());
const comment = utils.escapeHtml($("#new_comment").val());
utils.disableAll();
utils.showAlert("info", "", "Adding subscribed " + type + "list...", address);
// 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,]+/);
// Remove empty elements
addresses = addresses.filter(function (el) {
return el !== "";
});
const addressestr = JSON.stringify(addresses);
if (address.length === 0) {
utils.disableAll();
utils.showAlert("info", "", "Adding subscribed " + type + "list(s)...", addressestr);
if (addresses.length === 0) {
// enable the ui elements again
utils.enableAll();
utils.showAlert("warning", "", "Warning", "Please specify " + type + "list address");
@ -517,10 +525,10 @@ function addList(event) {
method: "post",
dataType: "json",
processData: false,
data: JSON.stringify({ address: address, comment: comment, type: type }),
success: function () {
data: JSON.stringify({ address: addresses, comment: comment, type: type }),
success: function (data) {
utils.enableAll();
utils.showAlert("success", "fas fa-plus", "Successfully added " + type + "list", address);
utils.listsAlert("list", addresses, data);
table.ajax.reload(null, false);
table.rows().deselect();

View File

@ -277,13 +277,24 @@ function delItems(ids) {
}
function addGroup() {
const name = utils.escapeHtml($("#new_name").val());
const comment = utils.escapeHtml($("#new_comment").val());
utils.disableAll();
utils.showAlert("info", "", "Adding group...", name);
// Check if the user wants to add multiple groups (space or newline separated)
// If so, split the input and store it in an array
var names = utils
.escapeHtml($("#new_name"))
.val()
.split(/[\s,]+/);
// Remove empty elements
names = names.filter(function (el) {
return el !== "";
});
const groupStr = JSON.stringify(names);
if (name.length === 0) {
utils.disableAll();
utils.showAlert("info", "", "Adding group(s)...", groupStr);
if (names.length === 0) {
// enable the ui elements again
utils.enableAll();
utils.showAlert("warning", "", "Warning", "Please specify a group name");
@ -296,13 +307,13 @@ function addGroup() {
dataType: "json",
processData: false,
data: JSON.stringify({
name: name,
name: names,
comment: comment,
enabled: true,
}),
success: function () {
success: function (data) {
utils.enableAll();
utils.showAlert("success", "fas fa-plus", "Successfully added group", name);
utils.listsAlert("group", names, data);
$("#new_name").val("");
$("#new_comment").val("");
table.ajax.reload();

View File

@ -16,6 +16,13 @@ var queryTypePieChart, forwardDestinationPieChart;
var failures = 0;
function updateQueriesOverTime() {
$.getJSON("/api/history", function (data) {
// Remove graph if there are no results (e.g. new
// installation or privacy mode enabled)
if (jQuery.isEmptyObject(data.history)) {
$("#queries-over-time").remove();
return;
}
// Remove possibly already existing data
timeLineChart.data.labels = [];
timeLineChart.data.datasets = [];
@ -123,13 +130,10 @@ function updateClientsOverTime() {
// Remove graph if there are no results (e.g. new
// installation or privacy mode enabled)
if (jQuery.isEmptyObject(data.history)) {
$("#clients-over-time").parent().remove();
$("#clients").remove();
return;
}
// remove last data point for line charts as it is not representative there
if (utils.getGraphType() === "line") data.history.splice(-1, 1);
var i,
labels = [];
data.clients.forEach(function (client) {
@ -384,7 +388,11 @@ function updateSummaryData(runOnce) {
$.getJSON("/api/stats/summary", function (data) {
var intl = new Intl.NumberFormat();
glowIfChanged($("span#dns_queries"), intl.format(parseInt(data.queries.total, 10)));
glowIfChanged($("span#total_clients"), intl.format(parseInt(data.clients.total, 10)));
glowIfChanged($("span#active_clients"), intl.format(parseInt(data.clients.active, 10)));
$("a#total_clients").attr(
"title",
intl.format(parseInt(data.clients.total, 10)) + " total clients"
);
glowIfChanged($("span#blocked_queries"), intl.format(parseFloat(data.queries.blocked)));
glowIfChanged(
$("span#percent_blocked"),
@ -416,7 +424,7 @@ $(function () {
var ticksColor = utils.getCSSval("graphs-ticks", "color");
var ctx = document.getElementById("queryOverTimeChart").getContext("2d");
timeLineChart = new Chart(ctx, {
type: utils.getGraphType(),
type: "bar",
data: {
labels: [],
datasets: [{ data: [], parsing: false }],
@ -471,7 +479,7 @@ $(function () {
},
},
scales: {
xAxes: {
x: {
type: "time",
stacked: true,
offset: false,
@ -491,7 +499,7 @@ $(function () {
color: ticksColor,
},
},
yAxes: {
y: {
stacked: true,
beginAtZero: true,
ticks: {
@ -527,7 +535,7 @@ $(function () {
if (clientsChartEl) {
ctx = clientsChartEl.getContext("2d");
clientsChart = new Chart(ctx, {
type: utils.getGraphType(),
type: "bar",
data: {
labels: [],
datasets: [{ data: [], parsing: false }],
@ -566,7 +574,7 @@ $(function () {
},
},
scales: {
xAxes: {
x: {
type: "time",
stacked: true,
offset: false,
@ -586,7 +594,7 @@ $(function () {
color: ticksColor,
},
},
yAxes: {
y: {
beginAtZero: true,
ticks: {
color: ticksColor,

View File

@ -208,6 +208,23 @@ function parseQueryStatus(data) {
};
}
function formatReplyTime(replyTime, type) {
if (type === "display") {
// Units:
// - seconds if replytime >= 1 second
// - milliseconds if reply time >= 100 µs
// - microseconds otherwise
return replyTime < 1e-4
? (1e6 * replyTime).toFixed(1) + " µs"
: replyTime < 1
? (1e3 * replyTime).toFixed(1) + " ms"
: replyTime.toFixed(1) + " s";
}
// else: return the number itself (for sorting and searching)
return replyTime;
}
function formatInfo(data) {
// DNSSEC status
var dnssecStatus = data.dnssec,
@ -303,20 +320,10 @@ function formatInfo(data) {
// Always show reply info, add reply delay if applicable
var replyInfo = "";
if (data.reply.type !== "UNKNOWN") {
replyInfo = divStart + "Reply:&nbsp&nbsp;" + data.reply.type;
if (data.reply.time >= 0 && data.reply.type !== "UNKNOWN") {
replyInfo +=
" (" +
(data.reply.time < 1
? (1e3 * data.reply.time).toFixed(1) + " ms)"
: data.reply.time.toFixed(1) + " s)");
}
replyInfo += "</div>";
} else {
replyInfo = divStart + "Reply:&nbsp;&nbsp;No reply received</div>";
}
replyInfo =
data.reply.type !== "UNKNOWN"
? divStart + "Reply:&nbsp&nbsp;" + data.reply.type + "</div>"
: divStart + "Reply:&nbsp;&nbsp;No reply received</div>";
// Compile extra info for displaying
return (
@ -515,9 +522,10 @@ $(function () {
},
},
{ data: "status", width: "1%" },
{ data: "type", width: "5%" },
{ data: "type", width: "1%" },
{ data: "domain", width: "45%" },
{ data: "client.ip", width: "29%", type: "ip-address", render: $.fn.dataTable.render.text() },
{ data: "client.ip", width: "29%", type: "ip-address" },
{ data: "reply.time", width: "4%", render: formatReplyTime },
{ data: null, width: "10%", sortable: false, searchable: false },
],
lengthMenu: [
@ -569,7 +577,7 @@ $(function () {
}
if (querystatus.buttontext !== false) {
$("td:eq(5)", row).html(querystatus.buttontext);
$("td:eq(6)", row).html(querystatus.buttontext);
}
},
});

View File

@ -9,6 +9,7 @@
var apiSessionsTable = null;
var ownSessionID = null;
var deleted = 0;
var TOTPdata = null;
function renderBool(data, type) {
@ -60,7 +61,7 @@ $(function () {
},
],
drawCallback: function () {
$('button[id^="deleteSession_"]').on("click", deleteSession);
$('button[id^="deleteSession_"]').on("click", deleteThisSession);
// Hide buttons if all messages were deleted
var hasRows = this.api().rows({ filter: "applied" }).data().length > 0;
@ -70,7 +71,7 @@ $(function () {
$("body > .bootstrap-select.dropdown").remove();
},
rowCallback: function (row, data) {
$(row).attr("data-id", data.ip);
$(row).attr("data-id", data.id);
var button =
'<button type="button" class="btn btn-danger btn-xs" id="deleteSession_' +
data.id +
@ -148,7 +149,7 @@ $(function () {
ids.push(parseInt($(this).attr("data-id"), 10));
});
// Delete all selected rows at once
delSession(ids);
deleteMultipleSessions(ids);
},
},
],
@ -186,13 +187,18 @@ $(function () {
});
});
function deleteSession() {
// Passes the button data-del-id attribute as ID
var ids = [$(this).attr("data-del-id")];
delSessions(ids);
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);
deleted = 0;
deleteOneSession(thisID, 1, false);
}
function delSessions(ids) {
function deleteMultipleSessions(ids) {
// This function is called when multiple sessions are selected and the gray
// trash button is clicked
// Check input validity
if (!Array.isArray(ids)) return;
@ -206,9 +212,12 @@ function delSessions(ids) {
return parseInt(value, 10);
});
// Check if own session is selected
// Check if own session is selected and remove it when deleting multiple
// We need this only when multiple sessions are removed to ensure we do not
// accidentally remove our own session and thus log us out *before* we can
// remove the other sessions
let ownSessionDelete = false;
if (ids.includes(ownSessionID)) {
if (ids.includes(ownSessionID) && ids.length > 1) {
ownSessionDelete = true;
// Strip own session ID from array
ids = ids.filter(function (value) {
@ -217,24 +226,29 @@ function delSessions(ids) {
}
// Loop through IDs and delete them
deleted = 0;
for (const id of ids) {
if (Object.hasOwnProperty.call(ids, id)) {
delSession(ids[id]);
}
}
// Delete own session last (if selected)
if (ownSessionDelete) {
delSession(ownSessionID);
deleteOneSession(id, ids.length, ownSessionDelete);
}
}
function delSession(id) {
function deleteOneSession(id, len, ownSessionDelete) {
// This function is called to delete a single session
// If we are batch deleting, we ensure that we do not delete our own session
// before having successfully deleted all other sessions, the deletion of
// our own session is then triggered by the last successful deletion of
// another session (ownSessionDelete == true, len == global deleted)
$.ajax({
url: "/api/auth/session/" + id,
method: "DELETE",
})
.done(function () {
// Do not reload page when deleting multiple sessions
if (++deleted < len) return;
// All other sessions have been deleted, now delete own session
if (ownSessionDelete) deleteOneSession(ownSessionID, 1, false);
if (id !== ownSessionID) {
// Reload table to remove session
apiSessionsTable.ajax.reload();

View File

@ -10,6 +10,36 @@
var dnsRecordsTable = null;
var customCNAMETable = null;
function hostsDomain(data) {
// Split record in format IP NAME1 [NAME2 [NAME3 [NAME...]]]
const name = data.substring(data.indexOf(" ") + 1);
return name;
}
function hostsIP(data) {
// Split record in format IP NAME1 [NAME2 [NAME3 [NAME...]]]
const ip = data.substring(0, data.indexOf(" "));
return ip;
}
function CNAMEdomain(data) {
// Split record in format <cname>,<target>[,<TTL>]
const CNAMEarr = data.split(",");
return CNAMEarr[0];
}
function CNAMEtarget(data) {
// Split record in format <cname>,<target>[,<TTL>]
const CNAMEarr = data.split(",");
return CNAMEarr[1];
}
function CNAMEttl(data) {
// Split record in format <cname>,<target>[,<TTL>]
const CNAMEarr = data.split(",");
return CNAMEarr.length > 2 ? CNAMEarr[2] : "-";
}
$(function () {
dnsRecordsTable = $("#dnsRecordsTable").DataTable({
ajax: {
@ -19,8 +49,8 @@ $(function () {
},
order: [[0, "asc"]],
columns: [
{ data: null },
{ data: null, type: "ip-address" },
{ data: null, render: hostsDomain },
{ data: null, type: "ip-address", render: hostsIP },
{ data: null, width: "22px", orderable: false },
],
columnDefs: [
@ -36,11 +66,6 @@ $(function () {
$("body > .bootstrap-select.dropdown").remove();
},
rowCallback: function (row, data) {
// Split record in format IP NAME1 [NAME2 [NAME3 [NAME...]]]
var ip = data.substring(0, data.indexOf(" "));
// The name can be multiple domains separated by spaces
var name = data.substring(data.indexOf(" ") + 1);
$(row).attr("data-id", data);
var button =
'<button type="button" class="btn btn-danger btn-xs" id="deleteRecord' +
@ -51,8 +76,6 @@ $(function () {
'<span class="far fa-trash-alt"></span>' +
"</button>";
$("td:eq(2)", row).html(button);
$("td:eq(0)", row).text(name);
$("td:eq(1)", row).text(ip);
},
dom:
"<'row'<'col-sm-6'l><'col-sm-6'f>>" +
@ -91,9 +114,9 @@ $(function () {
},
order: [[0, "asc"]],
columns: [
{ data: null },
{ data: null },
{ data: null },
{ data: null, render: CNAMEdomain },
{ data: null, render: CNAMEtarget },
{ data: null, render: CNAMEttl },
{ data: null, width: "22px", orderable: false },
],
columnDefs: [
@ -109,9 +132,6 @@ $(function () {
$("body > .bootstrap-select.dropdown").remove();
},
rowCallback: function (row, data) {
// Split record in format <cname>,<target>[,<TTL>]
var CNAMEarr = data.split(",");
$(row).attr("data-id", data);
var button =
'<button type="button" class="btn btn-danger btn-xs" id="deleteCNAME' +
@ -122,10 +142,6 @@ $(function () {
'<span class="far fa-trash-alt"></span>' +
"</button>";
$("td:eq(3)", row).html(button);
$("td:eq(0)", row).text(CNAMEarr[0]);
$("td:eq(1)", row).text(CNAMEarr[1]);
if (CNAMEarr.length > 2) $("td:eq(2)", row).text(CNAMEarr[2]);
else $("td:eq(2)", row).text("-");
},
dom:
"<'row'<'col-sm-6'l><'col-sm-6'f>>" +
@ -230,15 +246,17 @@ function delCNAME(elem) {
$(document).ready(function () {
$("#btnAdd-host").on("click", function () {
utils.disableAll();
const elem = $("#Hip").val() + " " + $("#Hdomain").val();
const url = "/api/config/dns/hosts/" + encodeURIComponent(elem);
utils.showAlert("info", "", "Adding DNS record...", elem);
$.ajax({
url: url,
method: "PUT",
})
.done(function () {
utils.enableAll();
utils.showAlert("success", "far fa-plus", "Successfully added DNS record", "");
utils.showAlert("success", "fas fa-plus", "Successfully added DNS record", elem);
dnsRecordsTable.ajax.reload(null, false);
})
.fail(function (data, exception) {
@ -250,17 +268,19 @@ $(document).ready(function () {
});
$("#btnAdd-cname").on("click", function () {
utils.disableAll();
var elem = $("#Cdomain").val() + "," + $("#Ctarget").val();
var ttlVal = parseInt($("#Cttl").val(), 10);
if (isFinite(ttlVal) && ttlVal >= 0) elem += "," + ttlVal;
const url = "/api/config/dns/cnameRecords/" + encodeURIComponent(elem);
utils.showAlert("info", "", "Adding DNS record...", elem);
$.ajax({
url: url,
method: "PUT",
})
.done(function () {
utils.enableAll();
utils.showAlert("success", "far fa-plus", "Successfully added CNAME record", "");
utils.showAlert("success", "fas fa-plus", "Successfully added CNAME record", elem);
dnsRecordsTable.ajax.reload(null, false);
})
.fail(function (data, exception) {

View File

@ -33,6 +33,7 @@ function importZIP() {
fetch("/api/teleporter", {
method: "POST",
body: formData,
headers: { "X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content") },
})
.then(response => response.json())
.then(data => {
@ -70,3 +71,27 @@ function importZIP() {
console.error(error); // eslint-disable-line no-console
});
}
// Inspired by https://stackoverflow.com/a/59576416/2087442
$("#GETTeleporter").on("click", function () {
$.ajax({
url: "/api/teleporter",
headers: { "X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content") },
method: "GET",
xhrFields: {
responseType: "blob",
},
success: function (data, status, xhr) {
var a = document.createElement("a");
// eslint-disable-next-line compat/compat
var url = window.URL.createObjectURL(data);
a.href = url;
a.download = xhr.getResponseHeader("Content-Disposition").match(/filename="([^"]*)"/)[1];
document.body.append(a);
a.click();
a.remove();
// eslint-disable-next-line compat/compat
window.URL.revokeObjectURL(url);
},
});
});

View File

@ -83,63 +83,51 @@ function padNumber(num) {
return ("00" + num).substr(-2, 2);
}
var info = null;
var showAlertBox = null;
function showAlert(type, icon, title, message) {
var opts = {};
title = "&nbsp;<strong>" + title + "</strong><br>";
const options = {
title: "&nbsp;<strong>" + title + "</strong><br>",
message: message,
},
settings = {
type: type,
delay: 5000, // default value
mouse_over: "pause",
};
switch (type) {
case "info":
opts = {
type: "info",
icon: "far fa-clock",
title: title,
message: message,
};
info = $.notify(opts);
break;
case "success":
opts = {
type: "success",
icon: icon,
title: title,
message: message,
};
if (info) {
info.update(opts);
} else {
$.notify(opts);
}
options.icon = icon !== null && icon.len > 0 ? icon : "far fa-clock";
break;
case "success":
break;
case "warning":
opts = {
type: "warning",
icon: "fas fa-exclamation-triangle",
title: title,
message: message,
};
if (info) {
info.update(opts);
} else {
$.notify(opts);
}
options.icon = "fas fa-exclamation-triangle";
settings.delay *= 2;
break;
case "error":
opts = {
type: "danger",
icon: "fas fa-times",
title: "&nbsp;<strong>Error, something went wrong!</strong><br>",
message: message,
};
if (info) {
info.update(opts);
} else {
$.notify(opts);
}
options.icon = "fas fa-times";
options.title = "&nbsp;<strong>Error, something went wrong!</strong><br>";
settings.delay *= 2;
break;
default:
// Case not handled, do nothing
console.log("Unknown alert type: " + type); // eslint-disable-line no-console
return;
}
if (type === "info") {
// Create a new notification for info boxes
showAlertBox = $.notify(options, settings);
} else if (showAlertBox !== null) {
// Update existing notification for other boxes (if available)
showAlertBox.update(options);
showAlertBox.update(settings);
} else {
// Create a new notification for other boxes if no previous info box exists
$.notify(options, settings);
}
}
@ -283,11 +271,6 @@ function stateLoadCallback(itemName) {
return data;
}
function getGraphType() {
// Only return line if `barchart_chkbox` is explicitly set to false. Else return bar
return localStorage && localStorage.getItem("barchart_chkbox") === "false" ? "line" : "bar";
}
function addFromQueryLog(domain, list) {
var alertModal = $("#alertModal");
var alProcessing = alertModal.find(".alProcessing");
@ -537,6 +520,81 @@ function hexDecode(string) {
return back;
}
function listAlert(type, items, data) {
// Show simple success message if there is no "processed" object in "data" or
// if all items were processed successfully
if (data.processed === undefined || data.processed.success.length === items.length) {
showAlert(
"success",
"fas fa-plus",
"Successfully added " + type + (items.length !== 1 ? "s" : ""),
items.join(", ")
);
return;
}
// Show a more detailed message if there is a "processed" object in "data" and
// not all items were processed successfully
let message = "";
// Show a list of successful items if there are any
if (data.processed.success.length > 0) {
message +=
"<strong>Successfully added " +
data.processed.success.length +
" " +
type +
(data.processed.success.length !== 1 ? "s" : "") +
":</strong>";
// Loop over data.processed.success and print "item"
for (const item in data.processed.success) {
if (Object.prototype.hasOwnProperty.call(data.processed.success, item)) {
message += "<br>- <strong>" + data.processed.success[item].item + "</strong>";
}
}
}
// Add a line break if there are both successful and failed items
if (data.processed.success.length > 0 && data.processed.errors.length > 0) {
message += "<br><br>";
}
// Show a list of failed items if there are any
if (data.processed.errors.length > 0) {
message +=
"<strong>Failed to add " +
data.processed.errors.length +
" " +
type +
(data.processed.errors.length !== 1 ? "s" : "") +
":</strong>\n";
// Loop over data.processed.errors and print "item: error"
for (const item in data.processed.errors) {
if (Object.prototype.hasOwnProperty.call(data.processed.errors, item)) {
let error = data.processed.errors[item].error;
// Replace some error messages with a more user-friendly text
if (error.indexOf("UNIQUE constraint failed") > -1) {
error = "Already present";
}
message += "<br>- <strong>" + data.processed.errors[item].item + "</strong>: " + error;
}
}
}
// Show the warning message
const total = data.processed.success.length + data.processed.errors.length;
const processed = "(" + total + " " + type + (total !== 1 ? "s" : "") + " processed)";
showAlert(
"warning",
"fas fa-exclamation-triangle",
"Some " + type + (items.length !== 1 ? "s" : "") + " could not be added " + processed,
message
);
}
window.utils = (function () {
return {
escapeHtml: escapeHtml,
@ -553,7 +611,6 @@ window.utils = (function () {
setBsSelectDefaults: setBsSelectDefaults,
stateSaveCallback: stateSaveCallback,
stateLoadCallback: stateLoadCallback,
getGraphType: getGraphType,
validateMAC: validateMAC,
validateHostname: validateHostname,
addFromQueryLog: addFromQueryLog,
@ -570,5 +627,6 @@ window.utils = (function () {
parseQueryString: parseQueryString,
hexEncode: hexEncode,
hexDecode: hexDecode,
listsAlert: listAlert,
};
})();

View File

@ -13,8 +13,8 @@ mg.include('header.lp','r')
<script src="<?=pihole.fileversion('scripts/vendor/datatables.min.js')?>"></script>
<script src="<?=pihole.fileversion('scripts/vendor/datatables.select.min.js')?>"></script>
<script src="<?=pihole.fileversion('scripts/vendor/datatables.buttons.min.js')?>"></script>
<script src="<?=pihole.fileversion('scripts/vendor/chart.umd.min.js')?>"></script>
<script src="<?=pihole.fileversion('scripts/vendor/moment.min.js')?>"></script>
<script src="<?=pihole.fileversion('scripts/vendor/chart.min.js')?>"></script>
<script src="<?=pihole.fileversion('scripts/vendor/chartjs-adapter-moment.js')?>"></script>
</head>
<body class="<?=theme.name?> hold-transition sidebar-mini <? if pihole.boxedlayout() then ?>layout-boxed<? end ?> logged-in">
@ -34,7 +34,7 @@ mg.include('header.lp','r')
<div class="wrapper">
<header class="main-header">
<!-- Logo -->
<a href="index.lp" class="logo">
<a href="<?=pihole.webhome()?>" class="logo">
<!-- mini logo for sidebar mini 50x50 pixels -->
<span class="logo-mini">P<strong>h</strong></span>
<!-- logo for regular state and mobile devices -->

File diff suppressed because one or more lines are too long

1
scripts/vendor/chart.umd.js.map vendored Normal file

File diff suppressed because one or more lines are too long

7
scripts/vendor/chart.umd.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,8 +1,7 @@
/*!
* chartjs-adapter-moment v1.0.0
* chartjs-adapter-moment v1.0.1
* https://www.chartjs.org
* (c) 2021 chartjs-adapter-moment Contributors
* (c) 2022 chartjs-adapter-moment Contributors
* Released under the MIT license
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(require("moment"),require("chart.js")):"function"==typeof define&&define.amd?define(["moment","chart.js"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).moment,e.Chart)}(this,(function(e,t){"use strict";function n(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var f=n(e);const a={datetime:"MMM D, YYYY, h:mm:ss a",millisecond:"h:mm:ss.SSS a",second:"h:mm:ss a",minute:"h:mm a",hour:"hA",day:"MMM D",week:"ll",month:"MMM YYYY",quarter:"[Q]Q - YYYY",year:"YYYY"};t._adapters._date.override("function"==typeof f.default?{_id:"moment",formats:function(){return a},parse:function(e,t){return"string"==typeof e&&"string"==typeof t?e=f.default(e,t):e instanceof f.default||(e=f.default(e)),e.isValid()?e.valueOf():null},format:function(e,t){return f.default(e).format(t)},add:function(e,t,n){return f.default(e).add(t,n).valueOf()},diff:function(e,t,n){return f.default(e).diff(f.default(t),n)},startOf:function(e,t,n){return e=f.default(e),"isoWeek"===t?(n=Math.trunc(Math.min(Math.max(0,n),6)),e.isoWeekday(n).startOf("day").valueOf()):e.startOf(t).valueOf()},endOf:function(e,t){return f.default(e).endOf(t).valueOf()}}:{})}));
//# sourceMappingURL=chartjs-adapter-moment.min.js.map

View File

@ -123,7 +123,7 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
<label for="webserver.api.temp.limit"><strong>Temperature limit for "hot":</strong></label>
</div>
<div class="col-md-6">
<input type="number" id="webserver.api.temp.limit" data-key="webserver.api.temp.limit">
<input type="number" data-type="integer" id="webserver.api.temp.limit" data-key="webserver.api.temp.limit">
</div>
</div>
<div class="row">

View File

@ -23,7 +23,7 @@ mg.include('scripts/pi-hole/lua/settings_header.lp','r')
<p>Warning: This archive contains sensitive information about your Pi-hole installation, e.g. the API token and the 2FA-TOTP secret (if enabled). Please be careful with this file and do not share it with anyone even if they claim to help you.</p>
<? if not is_secure then ?><p class='text-danger'>Warning: You are currently not using an end-to-end encryption. This means that your API token and 2FA-TOTP secret will be transmitted in plain text. We recommend to use HTTPS when exporting your configuration.</p><? end ?>
<div class="pull-right">
<a class="btn btn-app btn-success" href="/api/teleporter" target="_blank">
<a class="btn btn-app btn-success" id="GETTeleporter" target="_blank">
<i class="fa fa-save"></i><br>Export
</a>
</div>

View File

@ -80,13 +80,13 @@ td.lookatme {
/* Optimize Queries-Table for small screens */
/* Time column */
#all-queries td:nth-of-type(1),
/* Status column */
#all-queries td:nth-of-type(5) {
/* Reply time column */
#all-queries td:nth-of-type(6) {
white-space: nowrap;
}
/* Domain column */
#all-queries td:nth-of-type(3) {
#all-queries td:nth-of-type(4) {
min-width: 200px;
word-break: break-all;
white-space: pre-wrap;

View File

@ -1,6 +1,6 @@
/* Code courtesy of https://blog.jim-nielsen.com/2019/conditional-syntax-highlighting-in-dark-mode-with-css-imports/ */
/* Assume light mode by default */
@import "default-light.css" screen;
/* Supersede dark mode when applicable */
/* Import light mode if color-scheme is different than dark (even not set) */
@import "default-light.css" screen and not (prefers-color-scheme: dark);
/* Import dark mode when applicable */
@import "default-dark.css" screen and (prefers-color-scheme: dark);