mirror of
https://github.com/pi-hole/web.git
synced 2026-07-10 00:13:58 +01:00
f1c7122b1f
Signed-off-by: Mordy Ovits <mordyovits@users.noreply.github.com>
480 lines
16 KiB
JavaScript
480 lines
16 KiB
JavaScript
/* 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. */
|
|
|
|
/* global utils:false, applyCheckboxRadioStyle:false, setConfigValues: false, apiFailure: false */
|
|
|
|
"use strict";
|
|
|
|
// Remove an element from an array (inline)
|
|
function removeFromArray(arr, what) {
|
|
let found = arr.indexOf(what);
|
|
|
|
while (found !== -1) {
|
|
arr.splice(found, 1);
|
|
found = arr.indexOf(what);
|
|
}
|
|
}
|
|
|
|
function fillDNSupstreams(value, servers) {
|
|
let disabledStr = "";
|
|
if (value.flags.env_var === true) {
|
|
$("#DNSupstreamsTextfield").prop("disabled", true);
|
|
disabledStr = 'disabled="Disabled"';
|
|
}
|
|
|
|
let i = 0;
|
|
let customServers = value.value.length;
|
|
for (const element of servers) {
|
|
let row = "<tr>";
|
|
// Build checkboxes for IPv4 and IPv6
|
|
const addresses = [element.v4, element.v6];
|
|
// Loop over address types (IPv4, IPv6)
|
|
for (let v = 0; v < 2; v++) {
|
|
const address = addresses[v];
|
|
// Loop over available addresses (up to 2)
|
|
for (let index = 0; index < 2; index++) {
|
|
if (address.length > index) {
|
|
let checkedStr = "";
|
|
if (
|
|
value.value.includes(address[index]) ||
|
|
value.value.includes(address[index] + "#53")
|
|
) {
|
|
checkedStr = "checked";
|
|
customServers--;
|
|
}
|
|
|
|
row += `<td title="${address[index]}">
|
|
<div>
|
|
<input type="checkbox" id="DNSupstreams-${i}" ${disabledStr} ${checkedStr}>
|
|
<label for="DNSupstreams-${i++}"></label>
|
|
</div>
|
|
</td>`;
|
|
} else {
|
|
row += "<td></td>";
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add server name
|
|
row += "<td>" + element.name + "</td>";
|
|
|
|
// Close table row
|
|
row += "</tr>";
|
|
|
|
// Add row to table
|
|
$("#DNSupstreamsTable").append(row);
|
|
}
|
|
|
|
// Add event listener to checkboxes
|
|
$("input[id^='DNSupstreams-']").on("change", () => {
|
|
const upstreams = $("#DNSupstreamsTextfield").val().split("\n");
|
|
let customServers = 0;
|
|
$("#DNSupstreamsTable input").each(function () {
|
|
const title = $(this).closest("td").attr("title");
|
|
if (this.checked && !upstreams.includes(title)) {
|
|
// Add server to array
|
|
upstreams.push(title);
|
|
} else if (!this.checked && upstreams.includes(title)) {
|
|
// Remove server from array
|
|
removeFromArray(upstreams, title);
|
|
}
|
|
|
|
if (upstreams.includes(title)) customServers--;
|
|
});
|
|
// The variable will contain a negative value, we need to add the length to
|
|
// get the correct number of custom servers
|
|
customServers += upstreams.length;
|
|
updateDNSserversTextfield(upstreams, customServers);
|
|
});
|
|
|
|
// Initialize textfield
|
|
updateDNSserversTextfield(value.value, customServers);
|
|
|
|
// Expand the box if there are custom servers
|
|
if (customServers > 0) {
|
|
const customBox = document.getElementById("custom-servers-box");
|
|
utils.toggleBoxCollapse(customBox, true);
|
|
}
|
|
|
|
// Hide the loading animation
|
|
$("#dns-upstreams-overlay").hide();
|
|
|
|
// Apply styling to the new checkboxes
|
|
applyCheckboxRadioStyle();
|
|
}
|
|
|
|
function setInterfaceName(name) {
|
|
// If dns.interface is empty in pihole.toml, we use the first interface
|
|
// (same default value used by FTL)
|
|
if (name === "") {
|
|
$.ajax({
|
|
url: document.body.dataset.apiurl + "/network/gateway",
|
|
async: false,
|
|
})
|
|
.done(data => {
|
|
name = data.gateway[0].interface;
|
|
})
|
|
.fail(data => {
|
|
apiFailure(data);
|
|
name = "not found";
|
|
});
|
|
}
|
|
|
|
$("#interface-name-1").text(name);
|
|
$("#interface-name-2").text(name);
|
|
}
|
|
|
|
// Update the textfield with all (incl. custom) upstream servers
|
|
function updateDNSserversTextfield(upstreams, customServers) {
|
|
$("#DNSupstreamsTextfield").val(upstreams.join("\n"));
|
|
$("#custom-servers-title").text(
|
|
"(" + customServers + " custom server" + (customServers === 1 ? "" : "s") + " enabled)"
|
|
);
|
|
}
|
|
|
|
function getRevServerLines() {
|
|
// Return the lines from the textarea (array of lines)
|
|
return $(".revServers")
|
|
.val()
|
|
.split(/\r?\n/u)
|
|
.filter(line => line.trim() !== "");
|
|
}
|
|
|
|
// Return an array of objects containing the current values from the textarea
|
|
function getRevServerArray() {
|
|
const items = [];
|
|
|
|
const lines = getRevServerLines();
|
|
for (const line of lines) {
|
|
const cols = line.split(",").map(s => s.trim());
|
|
items.push({
|
|
enabled: cols[0] ?? "",
|
|
network: cols[1] ?? "",
|
|
ip: cols[2] ?? "",
|
|
domain: cols[3] ?? "",
|
|
});
|
|
}
|
|
|
|
return items;
|
|
}
|
|
|
|
function createRevServerTable() {
|
|
// Get the data
|
|
const tableRows = getRevServerArray();
|
|
|
|
$("#revServers-table").DataTable({
|
|
data: tableRows,
|
|
autoWidth: false,
|
|
columns: [
|
|
{ data: "enabled", width: "54px", className: "revserver-chkbox text-center" },
|
|
{ data: "network", className: "revserver-network" },
|
|
{ data: "ip", className: "revserver-ip" },
|
|
{ data: "domain", className: "revserver-domain" },
|
|
{ data: null, width: "82px", className: "actions" },
|
|
],
|
|
ordering: false,
|
|
columnDefs: [
|
|
{
|
|
targets: 0,
|
|
// eslint-disable-next-line no-unused-vars
|
|
createdCell(td, cellData, rowData, row, col) {
|
|
$(td).attr("data-initial-value", cellData);
|
|
},
|
|
render(data, type, row, meta) {
|
|
const name = "enabled_" + meta.row;
|
|
const ckbox =
|
|
`<input type="checkbox" name="${name}" id="${name}" class="no-icheck" ` +
|
|
(data === "true" ? "checked " : "") +
|
|
">";
|
|
return ckbox;
|
|
},
|
|
},
|
|
{
|
|
targets: [1, 2, 3],
|
|
// eslint-disable-next-line no-unused-vars
|
|
createdCell(td, cellData, rowData, row, col) {
|
|
$(td).attr("contenteditable", "true").attr("data-initial-value", cellData);
|
|
},
|
|
},
|
|
],
|
|
drawCallback() {
|
|
$(".deleteRevServers").on("click", deleteRecord);
|
|
$("tbody .saveRevServers").on("click", saveRecord);
|
|
$(".cancelRevServers").on("click", restoreRecord);
|
|
},
|
|
rowCallback(row, data, displayNum, displayIndex, dataIndex) {
|
|
$(row).attr("data-index", dataIndex);
|
|
const bt = '<button type="button" class="btn btn-xs"></button>';
|
|
const btDel = $(bt)
|
|
.addClass("btn-danger deleteRevServers")
|
|
.attr("title", "Delete")
|
|
.append('<span class="fa fa-trash"></span>');
|
|
const btSave = $(bt)
|
|
.addClass("btn-success saveRevServers")
|
|
.attr("title", "Confirm changes")
|
|
.append('<span class="fa fa-check"></span>');
|
|
const btCancel = $(bt)
|
|
.addClass("btn-warning cancelRevServers")
|
|
.attr("title", "Undo changes")
|
|
.append('<span class="fa fa-undo"></span>');
|
|
|
|
$("td:eq(4)", row).html(btSave).append(" ", btCancel, " ", btDel);
|
|
},
|
|
dom:
|
|
"<'row'<'col-sm-12 text-right'l>>" +
|
|
"<'row'<'col-sm-12'<'table-responsive'tr>>><'row'<'col-sm-12'i>>",
|
|
lengthMenu: [
|
|
[10, 25, 50, 100, -1],
|
|
[10, 25, 50, 100, "All"],
|
|
],
|
|
language: {
|
|
emptyTable: "No reverse DNS servers defined.",
|
|
},
|
|
stateSave: true,
|
|
stateDuration: 0,
|
|
processing: true,
|
|
stateSaveCallback(settings, data) {
|
|
utils.stateSaveCallback("revServers-records-table", data);
|
|
},
|
|
stateLoadCallback() {
|
|
const data = utils.stateLoadCallback("revServers-records-table");
|
|
// Return if not available
|
|
if (data === null) return null;
|
|
|
|
// Apply loaded state to table
|
|
return data;
|
|
},
|
|
});
|
|
}
|
|
|
|
function addRevServer() {
|
|
const values = [];
|
|
values[0] = $("#enabled-revServers input").prop("checked") ? "true" : "false";
|
|
values[1] = $("#network-revServers").text();
|
|
values[2] = $("#ip-revServers").text();
|
|
values[3] = $("#domain-revServers").text();
|
|
|
|
// Reject empty network range and server IP
|
|
if (values[1] === "" || values[2] === "") {
|
|
// Show error message
|
|
utils.showAlert("error", "fa fa-ban", "Network Range and Server IP are required", "");
|
|
return;
|
|
}
|
|
|
|
// Domain is optional: if empty, remove it from the array
|
|
if (values[3] === "") values.pop();
|
|
|
|
// Add the new values to the textarea
|
|
$(".revServers").val($(".revServers").val() + "\n" + values.join(","));
|
|
|
|
// Clear the table footer fields
|
|
$("#revServers-table tfoot [contenteditable]").text("");
|
|
$("#revServers-table tfoot input[type=checkbox]").prop("checked", false);
|
|
|
|
// Save changes with message
|
|
saveRevServerData("Added values: " + values.join(", "));
|
|
}
|
|
|
|
// Button to add a new reverse server
|
|
$("#btnAddRevServers").on("click", addRevServer);
|
|
|
|
function saveRecord() {
|
|
// Find the row and its index number
|
|
const row = $(this).closest("tr");
|
|
const index = row.attr("data-index");
|
|
|
|
// Get the edited values from each field
|
|
const values = [];
|
|
values[0] = $(".revserver-chkbox input", row).prop("checked") ? "true" : "false";
|
|
values[1] = $(".revserver-network", row).text();
|
|
values[2] = $(".revserver-ip", row).text();
|
|
values[3] = $(".revserver-domain", row).text();
|
|
|
|
// Remove "editing" class from the row. Buttons will be shown/hidden via CSS
|
|
row.removeClass("editing");
|
|
|
|
// Reject empty network range and server IP
|
|
if (values[1] === "" || values[2] === "") {
|
|
// Show error message
|
|
utils.showAlert("error", "fa fa-ban", "Network Range and Server IP are required", "");
|
|
return;
|
|
}
|
|
|
|
// Domain is optional: if empty, remove it from the array
|
|
if (values[3] === "") values.pop();
|
|
|
|
// Get the values from the textarea
|
|
const lines = getRevServerLines();
|
|
|
|
// Update the edited line on the textarea
|
|
lines[index] = values.join(",");
|
|
$(".revServers").val(lines.join("\n"));
|
|
|
|
// Save changes with message
|
|
saveRevServerData("Updated values: " + values.join(", "));
|
|
}
|
|
|
|
function restoreRecord() {
|
|
// Find the row and its index number
|
|
const row = $(this).closest("tr");
|
|
|
|
// Reset values using "data-initial-value"
|
|
$(".revserver-chkbox input", row).prop(
|
|
"checked",
|
|
$(".revserver-chkbox input", row).attr("data-initial-value")
|
|
);
|
|
$('[contenteditable="true"]', row).text(function () {
|
|
return $(this).attr("data-initial-value");
|
|
});
|
|
|
|
// Show cancellation message
|
|
utils.showAlert("info", "fas fa-undo", "Canceled", "Original values restored");
|
|
|
|
// Make sure all highlighted cells are restored
|
|
// Remove "editing" class from the row. The buttons will be shown/hidden via CSS
|
|
row.find(".table-danger").removeClass("table-danger");
|
|
row.removeClass("editing");
|
|
}
|
|
|
|
function deleteRecord() {
|
|
// Find the row index (this is also the index of the deleted row)
|
|
const index = $(this).closest("tr").attr("data-index");
|
|
|
|
// Get the current lines from the textarea
|
|
const lines = getRevServerLines();
|
|
|
|
// Remove the deleted line and update the textearea
|
|
lines.splice(index, 1);
|
|
$(".revServers").val(lines.join("\n"));
|
|
|
|
// Save changes with message
|
|
saveRevServerData("Line successfully deleted");
|
|
}
|
|
|
|
function processDNSConfig() {
|
|
$.ajax({
|
|
url: document.body.dataset.apiurl + "/config/dns?detailed=true", // We need the detailed output to get the DNS server list
|
|
})
|
|
.done(data => {
|
|
// Initialize the DNS upstreams
|
|
fillDNSupstreams(data.config.dns.upstreams, data.dns_servers);
|
|
setInterfaceName(data.config.dns.interface.value);
|
|
setConfigValues("dns", "dns", data.config.dns);
|
|
})
|
|
.done(() => {
|
|
// This will be executed only after the done block above is executed
|
|
|
|
// If Conditional Forwarding option is set via ENV VAR, the textarea will be disabled and no values can be edited
|
|
if ($(".revServers").prop("disabled")) {
|
|
// In this case, we hide the table and show the textarea
|
|
$("#revServers-table").hide();
|
|
$(".revServers").show().attr("title", "Disabled: Set by environment variable");
|
|
} else {
|
|
// We only populate the table when the textarea is enabled
|
|
createRevServerTable();
|
|
}
|
|
})
|
|
.fail(data => {
|
|
apiFailure(data);
|
|
});
|
|
}
|
|
|
|
$(() => {
|
|
processDNSConfig();
|
|
});
|
|
|
|
// Save the Reverse Servers data via API and recreate the table with updated values
|
|
function saveRevServerData(msg) {
|
|
// Get the data from the textarea
|
|
const data = getRevServerLines();
|
|
|
|
// Call the API to save only the dns.revServers option
|
|
$.ajax({
|
|
url: document.body.dataset.apiurl + "/config",
|
|
method: "PATCH",
|
|
dataType: "json",
|
|
processData: false,
|
|
data: JSON.stringify({ config: { dns: { revServers: data } } }),
|
|
contentType: "application/json; charset=utf-8",
|
|
})
|
|
.done(() => {
|
|
utils.enableAll();
|
|
utils.showAlert(
|
|
"success",
|
|
"fa-solid fa-fw fa-floppy-disk",
|
|
"Conditional Forwarding settings successfully saved",
|
|
msg
|
|
);
|
|
// Show loading overlay (without reloading the page)
|
|
utils.loadingOverlay(false);
|
|
|
|
// Reset the table to show the updated data
|
|
// Remove all rows from the table, then create rows with the updated data and finally redraw the table
|
|
const table = $("#revServers-table").DataTable();
|
|
table.clear().rows.add(getRevServerArray()).draw();
|
|
})
|
|
.fail((data, exception) => {
|
|
utils.enableAll();
|
|
utils.showAlert("error", "", "Error while applying settings", data.responseText);
|
|
console.log(exception); // eslint-disable-line no-console
|
|
apiFailure(data);
|
|
});
|
|
}
|
|
|
|
// Mark the row with "editing" class when an editable cell or checkbox is focused/edited
|
|
// This will use CSS rules to show/hide buttons
|
|
$(document).on("focus input", "#revServers-table [contenteditable]", function () {
|
|
$(this).closest("tr").addClass("editing");
|
|
|
|
// Make sure the placeholder text is shown when a contenteditable cell is empty (or only contains spaces)
|
|
if ($(this).text().trim() === "") {
|
|
$(this).empty();
|
|
}
|
|
});
|
|
$(document).on("change", "#revServers-table .revserver-chkbox input", function () {
|
|
$(this).closest("tr").addClass("editing");
|
|
});
|
|
|
|
// Validate data entered on the table
|
|
// If a cell contains an invalid value, it will be highlighted and the save button will be disabled
|
|
$(document).on("input blur paste", ".revserver-network", function () {
|
|
const val = $(this).text().trim();
|
|
if (val && !(utils.validateIPv4(val) || utils.validateIPv6(val))) {
|
|
$(this).addClass("table-danger");
|
|
$(this).attr("title", "Invalid network range");
|
|
$(this).siblings(".actions").find(".saveRevServers").prop("disabled", true);
|
|
} else {
|
|
$(this).removeClass("table-danger");
|
|
$(this).attr("title", "");
|
|
$(this).siblings(".actions").find(".saveRevServers").prop("disabled", false);
|
|
}
|
|
});
|
|
$(document).on("input blur paste", ".revserver-ip", function () {
|
|
const val = $(this).text().trim();
|
|
if (val && !(utils.validateIPv4WithPort(val) || utils.validateIPv6WithPort(val))) {
|
|
$(this).addClass("table-danger");
|
|
$(this).attr("title", "Invalid server IP");
|
|
$(this).siblings(".actions").find(".saveRevServers").prop("disabled", true);
|
|
} else {
|
|
$(this).removeClass("table-danger");
|
|
$(this).attr("title", "");
|
|
$(this).siblings(".actions").find(".saveRevServers").prop("disabled", false);
|
|
}
|
|
});
|
|
$(document).on("input blur paste", ".revserver-domain", function () {
|
|
const val = $(this).text().trim();
|
|
if (val && !utils.validateHostnameStrict(val)) {
|
|
$(this).addClass("table-danger");
|
|
$(this).attr("title", "Invalid domain");
|
|
$(this).siblings(".actions").find(".saveRevServers").prop("disabled", true);
|
|
} else {
|
|
$(this).removeClass("table-danger");
|
|
$(this).attr("title", "");
|
|
$(this).siblings(".actions").find(".saveRevServers").prop("disabled", false);
|
|
}
|
|
});
|