mirror of
https://github.com/pi-hole/web.git
synced 2026-04-17 23:54:10 +01:00
Merge development into new/simple-dhcp-static-leases and address review feedback
Resolve merge conflict in style/pi-hole.css (keep both StaticDHCPTable styles and DNSSEC query log styles). Address outstanding reviewer feedback: - Change save button icon from floppy-disk to checkmark to clarify it confirms the row edit, not a final save - Update hint text to mention "Save & Apply" is still needed - Add hostname validation on the hostname cell (rejects spaces, commas, and other characters invalid in DNS names) Signed-off-by: Dominik <dl6er@dl6er.de>
This commit is contained in:
@@ -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 upstreams:false */
|
||||
/* global upstreamIPs:false */
|
||||
|
||||
"use strict";
|
||||
|
||||
@@ -63,6 +63,23 @@ globalThis.htmlLegendPlugin = {
|
||||
for (const item of items) {
|
||||
const li = document.createElement("li");
|
||||
|
||||
// Select the corresponding "slice" of the chart when the mouse is over a legend item
|
||||
li.addEventListener("mouseover", () => {
|
||||
chart.setActiveElements([
|
||||
{
|
||||
datasetIndex: 0,
|
||||
index: item.index,
|
||||
},
|
||||
]);
|
||||
chart.update();
|
||||
});
|
||||
|
||||
// Deselect all "slices"
|
||||
li.addEventListener("mouseout", () => {
|
||||
chart.setActiveElements([]);
|
||||
chart.update();
|
||||
});
|
||||
|
||||
// Color checkbox (toggle visibility)
|
||||
const boxSpan = document.createElement("span");
|
||||
boxSpan.title = "Toggle visibility";
|
||||
@@ -96,9 +113,19 @@ globalThis.htmlLegendPlugin = {
|
||||
|
||||
if (isQueryTypeChart) {
|
||||
link.href = `queries?type=${item.text}`;
|
||||
} else if (isForwardDestinationChart) {
|
||||
} else {
|
||||
// Encode the forward destination as it may contain an "#" character
|
||||
link.href = `queries?upstream=${encodeURIComponent(upstreams[item.text])}`;
|
||||
link.href = `queries?upstream=${encodeURIComponent(upstreamIPs[item.index])}`;
|
||||
|
||||
// If server name and IP are different:
|
||||
if (item.text !== upstreamIPs[item.index]) {
|
||||
// replace the title tooltip to include the upstream IP to the text ...
|
||||
link.title = `List ${item.text} (${upstreamIPs[item.index]}) queries`;
|
||||
|
||||
// ... and include the server name (without port) to the querystring, to match
|
||||
// the text used on the SELECT element (sent by suggestions API endpoint)
|
||||
link.href += ` (${item.text.split("#")[0]})`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// no clickable links in other charts
|
||||
@@ -233,6 +260,10 @@ function positionTooltip(tooltipEl, tooltip, context) {
|
||||
const arrowMinIndent = 2 * tooltip.options.cornerRadius;
|
||||
const arrowSize = 5;
|
||||
|
||||
// Check if this is a queryOverTimeChart or clientsChart - these should stick to x-axis
|
||||
const canvasId = context.chart.canvas.id;
|
||||
const isTimelineChart = canvasId === "queryOverTimeChart" || canvasId === "clientsChart";
|
||||
|
||||
let tooltipX = offsetX + caretX;
|
||||
let arrowX;
|
||||
|
||||
@@ -289,27 +320,37 @@ function positionTooltip(tooltipEl, tooltip, context) {
|
||||
arrowX = offsetX + caretX - tooltipX;
|
||||
}
|
||||
|
||||
let tooltipY = offsetY + caretY;
|
||||
let tooltipY;
|
||||
|
||||
// Compute Y position
|
||||
switch (tooltip.yAlign) {
|
||||
case "top": {
|
||||
tooltipY += arrowSize + caretPadding;
|
||||
break;
|
||||
}
|
||||
if (isTimelineChart) {
|
||||
// For timeline charts, always position tooltip below the chart with caret pointing to x-axis
|
||||
const chartArea = context.chart.chartArea;
|
||||
const canvasBottom = chartArea.bottom;
|
||||
tooltipY = offsetY + canvasBottom + arrowSize + caretPadding;
|
||||
|
||||
case "center": {
|
||||
tooltipY -= tooltipHeight / 2;
|
||||
if (tooltip.xAlign === "left") tooltipX += arrowSize;
|
||||
if (tooltip.xAlign === "right") tooltipX -= arrowSize;
|
||||
break;
|
||||
}
|
||||
// Ensure the arrow points to the correct X position
|
||||
arrowX = tooltip.caretX - (tooltipX - offsetX);
|
||||
} else {
|
||||
tooltipY = offsetY + caretY;
|
||||
switch (tooltip.yAlign) {
|
||||
case "top": {
|
||||
tooltipY += arrowSize + caretPadding;
|
||||
break;
|
||||
}
|
||||
|
||||
case "bottom": {
|
||||
tooltipY -= tooltipHeight + arrowSize + caretPadding;
|
||||
break;
|
||||
case "center": {
|
||||
tooltipY -= tooltipHeight / 2;
|
||||
if (tooltip.xAlign === "left") tooltipX += arrowSize;
|
||||
if (tooltip.xAlign === "right") tooltipX -= arrowSize;
|
||||
break;
|
||||
}
|
||||
|
||||
case "bottom": {
|
||||
tooltipY -= tooltipHeight + arrowSize + caretPadding;
|
||||
break;
|
||||
}
|
||||
// No default
|
||||
}
|
||||
// No default
|
||||
}
|
||||
|
||||
// Position tooltip and display
|
||||
|
||||
@@ -241,28 +241,26 @@ function updateFtlInfo() {
|
||||
$("#num_lists").text(intl.format(database.lists));
|
||||
$("#num_gravity").text(intl.format(database.gravity));
|
||||
$("#num_allowed")
|
||||
.text(intl.format(database.domains.allowed + database.regex.allowed))
|
||||
.text(intl.format(database.domains.allowed.enabled + database.regex.allowed.enabled))
|
||||
.attr(
|
||||
"title",
|
||||
"Allowed: " +
|
||||
intl.format(database.domains.allowed) +
|
||||
intl.format(database.domains.allowed.enabled) +
|
||||
" exact domains and " +
|
||||
intl.format(database.regex.allowed) +
|
||||
intl.format(database.regex.allowed.enabled) +
|
||||
" regex filters are enabled"
|
||||
);
|
||||
$("#num_denied")
|
||||
.text(intl.format(database.domains.denied + database.regex.denied))
|
||||
.text(intl.format(database.domains.denied.enabled + database.regex.denied.enabled))
|
||||
.attr(
|
||||
"title",
|
||||
"Denied: " +
|
||||
intl.format(database.domains.denied) +
|
||||
intl.format(database.domains.denied.enabled) +
|
||||
" exact domains and " +
|
||||
intl.format(database.regex.denied) +
|
||||
intl.format(database.regex.denied.enabled) +
|
||||
" regex filters are enabled"
|
||||
);
|
||||
updateQueryFrequency(intl, ftl.query_frequency);
|
||||
$("#sysinfo-cpu-ftl").text("(" + ftl["%cpu"].toFixed(1) + "% used by FTL)");
|
||||
$("#sysinfo-ram-ftl").text("(" + ftl["%mem"].toFixed(1) + "% used by FTL)");
|
||||
$("#sysinfo-pid-ftl").text(ftl.pid);
|
||||
const startdate = moment()
|
||||
.subtract(ftl.uptime, "milliseconds")
|
||||
@@ -346,15 +344,17 @@ function updateSystemInfo() {
|
||||
);
|
||||
$("#cpu").prop(
|
||||
"title",
|
||||
"Load averages for the past 1, 5, and 15 minutes\non a system with " +
|
||||
"CPU usage: " +
|
||||
system.cpu["%cpu"].toFixed(1) +
|
||||
"%\nLoad averages for the past 1, 5, and 15 minutes\non a system with " +
|
||||
system.cpu.nprocs +
|
||||
" core" +
|
||||
(system.cpu.nprocs > 1 ? "s" : "") +
|
||||
" running " +
|
||||
system.procs +
|
||||
" processes " +
|
||||
" processes" +
|
||||
(system.cpu.load.raw[0] > system.cpu.nprocs
|
||||
? " (load is higher than the number of cores)"
|
||||
? "\n(load is higher than the number of cores)"
|
||||
: "")
|
||||
);
|
||||
$("#sysinfo-cpu").text(
|
||||
@@ -368,6 +368,9 @@ function updateSystemInfo() {
|
||||
" processes"
|
||||
);
|
||||
|
||||
$("#sysinfo-cpu-ftl").text("(" + system.ftl["%cpu"].toFixed(1) + "% used by FTL)");
|
||||
$("#sysinfo-ram-ftl").text("(" + system.ftl["%mem"].toFixed(1) + "% used by FTL)");
|
||||
|
||||
const startdate = moment()
|
||||
.subtract(system.uptime, "seconds")
|
||||
.format("dddd, MMMM Do YYYY, HH:mm:ss");
|
||||
|
||||
@@ -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 */
|
||||
/* global apiFailure:false, utils:false */
|
||||
|
||||
"use strict";
|
||||
|
||||
@@ -89,9 +89,11 @@ function parseLines(outputElement, text) {
|
||||
const lines = text.split(/(?=\r)/g);
|
||||
|
||||
for (let line of lines) {
|
||||
// Escape HTML to prevent XSS attacks (both in adlist URL and non-domain entries)
|
||||
line = utils.escapeHtml(line);
|
||||
if (line[0] === "\r") {
|
||||
// This line starts with the "OVER" sequence. Replace them with "\n" before print
|
||||
line = line.replaceAll("\r[K", "\n").replaceAll("\r", "\n");
|
||||
line = line.replaceAll("\r\u001B[K", "\n").replaceAll("\r", "\n");
|
||||
|
||||
// Last line from the textarea will be overwritten, so we remove it
|
||||
const lastLineIndex = outputElement.innerHTML.lastIndexOf("\n");
|
||||
@@ -138,7 +140,7 @@ function parseLines(outputElement, text) {
|
||||
});
|
||||
|
||||
// Append the new text to the end of the output
|
||||
outputElement.append(line);
|
||||
outputElement.innerHTML += line;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -234,23 +234,20 @@ function initTable() {
|
||||
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(3)", row).html(
|
||||
'<code id="address_' +
|
||||
dataId +
|
||||
'" class="breakall">' +
|
||||
utils.escapeHtml(data.address) +
|
||||
"</code>"
|
||||
);
|
||||
const codeElem = document.createElement("code");
|
||||
codeElem.id = "address_" + dataId;
|
||||
codeElem.className = "breakall";
|
||||
codeElem.textContent = data.address;
|
||||
$("td:eq(3)", row).empty().append(codeElem);
|
||||
} else {
|
||||
$("td:eq(3)", row).html(
|
||||
'<a id="address_' +
|
||||
dataId +
|
||||
'" class="breakall" href="' +
|
||||
encodeURI(data.address) +
|
||||
'" target="_blank" rel="noopener noreferrer">' +
|
||||
utils.escapeHtml(data.address) +
|
||||
"</a>"
|
||||
);
|
||||
const aElem = document.createElement("a");
|
||||
aElem.id = "address_" + dataId;
|
||||
aElem.className = "breakall";
|
||||
aElem.href = data.address;
|
||||
aElem.target = "_blank";
|
||||
aElem.rel = "noopener noreferrer";
|
||||
aElem.textContent = data.address;
|
||||
$("td:eq(3)", row).empty().append(aElem);
|
||||
}
|
||||
|
||||
$("td:eq(4)", row).html(
|
||||
@@ -518,12 +515,12 @@ function addList(event) {
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: document.body.dataset.apiurl + "/lists",
|
||||
url: document.body.dataset.apiurl + "/lists?type=" + encodeURIComponent(type),
|
||||
method: "post",
|
||||
dataType: "json",
|
||||
processData: false,
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({ address: addresses, comment, type, groups: group }),
|
||||
data: JSON.stringify({ address: addresses, comment, groups: group }),
|
||||
success(data) {
|
||||
utils.enableAll();
|
||||
utils.listsAlert(type + "list", addresses, data);
|
||||
|
||||
@@ -226,7 +226,7 @@ function updateClientsOverTime() {
|
||||
});
|
||||
}
|
||||
|
||||
const upstreams = {};
|
||||
const upstreamIPs = [];
|
||||
function updateForwardDestinationsPie() {
|
||||
$.getJSON(document.body.dataset.apiurl + "/stats/upstreams", data => {
|
||||
const v = [];
|
||||
@@ -248,11 +248,8 @@ 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;
|
||||
}
|
||||
// Store upstreams IPs for generating links to the Query Log
|
||||
upstreamIPs.push(item.port > 0 ? item.ip + "#" + item.port : item.ip);
|
||||
|
||||
const percent = (100 * item.count) / sum;
|
||||
values.push([label, percent, THEME_COLORS[i++ % THEME_COLORS.length]]);
|
||||
@@ -521,8 +518,8 @@ function labelWithPercentage(tooltipLabel, skipZero = false) {
|
||||
// Sum all queries for the current time by iterating over all keys in the
|
||||
// current dataset
|
||||
let sum = 0;
|
||||
for (const value of Object.values(tooltipLabel.parsed._stacks.y)) {
|
||||
if (value === undefined) continue;
|
||||
for (const [key, value] of Object.entries(tooltipLabel.parsed._stacks.y)) {
|
||||
if (key.startsWith("_") || value === undefined) continue;
|
||||
const num = Number.parseInt(value, 10);
|
||||
if (num) sum += num;
|
||||
}
|
||||
@@ -639,9 +636,11 @@ $(() => {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
// Disable the on-canvas tooltip
|
||||
enabled: false,
|
||||
intersect: false,
|
||||
yAlign: "bottom",
|
||||
external: customTooltips,
|
||||
yAlign: "top",
|
||||
itemSort(a, b) {
|
||||
return b.datasetIndex - a.datasetIndex;
|
||||
},
|
||||
@@ -656,7 +655,7 @@ $(() => {
|
||||
return "Queries from " + from + " to " + to;
|
||||
},
|
||||
label(tooltipLabel) {
|
||||
return labelWithPercentage(tooltipLabel);
|
||||
return labelWithPercentage(tooltipLabel, true);
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -893,6 +892,8 @@ $(() => {
|
||||
elements: {
|
||||
arc: {
|
||||
borderColor: $(".box").css("background-color"),
|
||||
hoverBorderColor: $(".box").css("background-color"),
|
||||
hoverOffset: 10,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
@@ -917,6 +918,9 @@ $(() => {
|
||||
animation: {
|
||||
duration: 750,
|
||||
},
|
||||
layout: {
|
||||
padding: 10,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -939,6 +943,8 @@ $(() => {
|
||||
elements: {
|
||||
arc: {
|
||||
borderColor: $(".box").css("background-color"),
|
||||
hoverBorderColor: $(".box").css("background-color"),
|
||||
hoverOffset: 10,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
@@ -963,6 +969,9 @@ $(() => {
|
||||
animation: {
|
||||
duration: 750,
|
||||
},
|
||||
layout: {
|
||||
padding: 10,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -28,6 +28,14 @@ const filters = [
|
||||
"reply",
|
||||
"dnssec",
|
||||
];
|
||||
let doDNSSEC = false;
|
||||
|
||||
// Check if pihole is validiting DNSSEC
|
||||
function getDnssecConfig() {
|
||||
$.getJSON(document.body.dataset.apiurl + "/config/dns/dnssec", data => {
|
||||
doDNSSEC = data.config.dns.dnssec;
|
||||
});
|
||||
}
|
||||
|
||||
function initDateRangePicker() {
|
||||
$("#querytime").daterangepicker(
|
||||
@@ -480,6 +488,9 @@ function liveUpdate() {
|
||||
}
|
||||
|
||||
$(() => {
|
||||
// Do we want to show DNSSEC icons?
|
||||
getDnssecConfig();
|
||||
|
||||
// Do we want to filter queries?
|
||||
const GETDict = utils.parseQueryString();
|
||||
|
||||
@@ -561,11 +572,13 @@ $(() => {
|
||||
utils.stateSaveCallback("query_log_table", data);
|
||||
},
|
||||
stateLoadCallback() {
|
||||
return utils.stateLoadCallback("query_log_table");
|
||||
const state = utils.stateLoadCallback("query_log_table");
|
||||
// Default to 25 entries if "All" was previously selected
|
||||
if (state) state.length = state.length === -1 ? 25 : state.length;
|
||||
return state;
|
||||
},
|
||||
rowCallback(row, data) {
|
||||
const querystatus = parseQueryStatus(data);
|
||||
const dnssec = parseDNSSEC(data);
|
||||
|
||||
if (querystatus.icon !== false) {
|
||||
$("td:eq(1)", row).html(
|
||||
@@ -589,14 +602,17 @@ $(() => {
|
||||
|
||||
// Prefix colored DNSSEC icon to domain text
|
||||
let dnssecIcon = "";
|
||||
dnssecIcon =
|
||||
'<i class="mr-2 fa fa-fw ' +
|
||||
dnssec.icon +
|
||||
" " +
|
||||
dnssec.color +
|
||||
'" title="DNSSEC: ' +
|
||||
dnssec.text +
|
||||
'"></i>';
|
||||
if (doDNSSEC === true) {
|
||||
const dnssec = parseDNSSEC(data);
|
||||
dnssecIcon =
|
||||
'<i class="mr-2 fa fa-fw ' +
|
||||
dnssec.icon +
|
||||
" " +
|
||||
dnssec.color +
|
||||
'" title="DNSSEC: ' +
|
||||
dnssec.text +
|
||||
'"></i>';
|
||||
}
|
||||
|
||||
// Escape HTML in domain
|
||||
domain = dnssecIcon + utils.escapeHtml(domain);
|
||||
|
||||
@@ -361,7 +361,7 @@ $(document).on("focus input", "#StaticDHCPTable td[contenteditable]", function (
|
||||
if (!row.next().hasClass("edit-hint-row")) {
|
||||
row.next(".edit-hint-row").remove(); // Remove any existing hint
|
||||
row.after(
|
||||
'<tr class="edit-hint-row"><td colspan="4" class="text-info" style="font-style:italic;">Please save this line before editing another or leaving the page, otherwise your changes will be lost.</td></tr>'
|
||||
'<tr class="edit-hint-row"><td colspan="4" class="text-info" style="font-style:italic;">Please confirm changes using the green button, then click "Save & Apply" before leaving the page.</td></tr>'
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -398,10 +398,10 @@ function renderStaticDHCPTable() {
|
||||
$("<td></td>")
|
||||
.append(
|
||||
$(
|
||||
'<button type="button" class="btn btn-success btn-xs save-static-row"><i class="fa fa-fw fa-floppy-disk"></i></button>'
|
||||
'<button type="button" class="btn btn-success btn-xs save-static-row"><i class="fa fa-fw fa-check"></i></button>'
|
||||
)
|
||||
.attr("data-row", idx)
|
||||
.attr("title", "Save changes to this line")
|
||||
.attr("title", "Confirm changes to this line")
|
||||
.attr("data-toggle", "tooltip")
|
||||
)
|
||||
.append(" ")
|
||||
@@ -518,3 +518,17 @@ $(document).on("input blur paste", "#StaticDHCPTable td.static-ipaddr", function
|
||||
$(this).attr("title", "");
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on("input blur paste", "#StaticDHCPTable td.static-hostname", function () {
|
||||
const val = $(this).text().trim();
|
||||
// Hostnames must not contain spaces, commas, or characters invalid in DNS names
|
||||
const hostnameValidator = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$/v;
|
||||
if (val && !hostnameValidator.test(val)) {
|
||||
$(this).addClass("table-danger");
|
||||
$(this).removeClass("table-success");
|
||||
$(this).attr("title", "Invalid hostname: only letters, digits, hyphens, and dots allowed");
|
||||
} else {
|
||||
$(this).removeClass("table-danger table-success");
|
||||
$(this).attr("title", "");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -215,7 +215,7 @@ $(() => {
|
||||
|
||||
$("#btnAdd-host").on("click", () => {
|
||||
utils.disableAll();
|
||||
const elem = $("#Hip").val() + " " + $("#Hdomain").val();
|
||||
const elem = $("#Hip").val().trim() + " " + $("#Hdomain").val().trim();
|
||||
const url = document.body.dataset.apiurl + "/config/dns/hosts/" + encodeURIComponent(elem);
|
||||
utils.showAlert("info", "", "Adding DNS record...", elem);
|
||||
$.ajax({
|
||||
@@ -239,7 +239,7 @@ $(() => {
|
||||
|
||||
$("#btnAdd-cname").on("click", () => {
|
||||
utils.disableAll();
|
||||
let elem = $("#Cdomain").val() + "," + $("#Ctarget").val();
|
||||
let elem = $("#Cdomain").val().trim() + "," + $("#Ctarget").val().trim();
|
||||
const ttlVal = Number.parseInt($("#Cttl").val(), 10);
|
||||
// TODO Fix eslint
|
||||
// eslint-disable-next-line unicorn/prefer-number-properties
|
||||
|
||||
@@ -267,7 +267,7 @@ $(".confirm-flusharp").confirm({
|
||||
title: "Confirmation required",
|
||||
confirm() {
|
||||
$.ajax({
|
||||
url: document.body.dataset.apiurl + "/action/flush/arp",
|
||||
url: document.body.dataset.apiurl + "/action/flush/network",
|
||||
type: "POST",
|
||||
}).fail(data => {
|
||||
apiFailure(data);
|
||||
|
||||
@@ -24,7 +24,7 @@ mg.include('header.lp','r')
|
||||
<script src="<?=pihole.fileversion('vendor/waitMe-js/modernized-waitme-min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/logout.js')?>"></script>
|
||||
</head>
|
||||
<body class="<?=theme.name?> hold-transition sidebar-mini <? if pihole.boxedlayout() then ?>layout-boxed<? end ?> logged-in page-<?=pihole.format_path(mg.request_info.request_uri)?>" data-apiurl="<?=pihole.api_url()?>" data-webhome="<?=webhome?>">
|
||||
<body class="<?=theme.name?> hold-transition sidebar-mini <? if pihole.boxedlayout() then ?>layout-boxed<? end ?> logged-in page-<?=pihole.format_path(scriptname)?>" data-apiurl="<?=pihole.api_url()?>" data-webhome="<?=webhome?>">
|
||||
<noscript>
|
||||
<!-- JS Warning -->
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user