Merge branch 'development-v6' into fix/adlist_list

This commit is contained in:
DL6ER
2023-10-28 21:27:39 +02:00
27 changed files with 379 additions and 212 deletions
+1 -1
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
+1 -1
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
+1 -1
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
+1 -1
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:
+2 -2
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
+2 -2
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>
+2 -1
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')?>
+4 -5
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;
+35 -22
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();
+25 -17
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();
+15 -7
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();
+18 -7
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();
+19 -11
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,
+25 -17
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);
}
},
});
+33 -19
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();
+41 -21
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) {
+25
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);
},
});
});
+109 -51
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,
};
})();
+2 -2
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 -->
-13
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -3
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
+1 -1
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">
+1 -1
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>
+3 -3
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;
+3 -3
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);