mirror of
https://github.com/pi-hole/web.git
synced 2026-04-02 08:32:55 +01:00
@@ -5,7 +5,7 @@
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
/* global utils:false, moment:false */
|
||||
/* global utils:false, moment:false, base64ToString:false */
|
||||
|
||||
"use strict";
|
||||
|
||||
@@ -544,7 +544,7 @@ function updateVersionInfo() {
|
||||
// Display update information for the docker tag
|
||||
updateComponentAvailable = true;
|
||||
dockerUpdate = true;
|
||||
} else if (/^\d{4}\.\d{2}\.\d+/.test(v.local)) {
|
||||
} else if (/^\d{4}\.\d{2}\.\d+/v.test(v.local)) {
|
||||
// Display the link if the current tag is a normal date-based tag
|
||||
localVersion =
|
||||
'<a href="' +
|
||||
@@ -714,7 +714,8 @@ function addAdvancedInfo() {
|
||||
const advancedInfoTarget = $("#advanced-info");
|
||||
const isTLS = location.protocol === "https:";
|
||||
const clientIP = advancedInfoSource.data("client-ip");
|
||||
const XForwardedFor = globalThis.atob(advancedInfoSource.data("xff") || "") || null;
|
||||
const xffData = advancedInfoSource.data("xff") || "";
|
||||
const XForwardedFor = xffData ? base64ToString(xffData) : null;
|
||||
const starttime = Number.parseFloat(advancedInfoSource.data("starttime"));
|
||||
const endtime = Number.parseFloat(advancedInfoSource.data("endtime"));
|
||||
const totaltime = 1e3 * (endtime - starttime);
|
||||
|
||||
@@ -86,7 +86,7 @@ function parseLines(outputElement, text) {
|
||||
// We want to split the text before an "OVER" escape sequence to allow overwriting previous line when needed
|
||||
|
||||
// Splitting the text on "\r"
|
||||
const lines = text.split(/(?=\r)/g);
|
||||
const lines = text.split(/(?=\r)/gv);
|
||||
|
||||
for (let line of lines) {
|
||||
// Escape HTML to prevent XSS attacks (both in adlist URL and non-domain entries)
|
||||
@@ -117,7 +117,7 @@ function parseLines(outputElement, text) {
|
||||
|
||||
// Create a regex that matches all ANSI codes (including reset)
|
||||
/* eslint-disable-next-line no-control-regex */
|
||||
const ansiRegex = /(\u001B\[(?:1|90|91|32|33|94|95|96|0)m)/g;
|
||||
const ansiRegex = /(\u001B\[(?:1|90|91|32|33|94|95|96|0)m)/gv;
|
||||
|
||||
// Process the line sequentially, replacing ANSI codes with their corresponding HTML spans
|
||||
// we use a counter to keep track of how many spans are open and close the correct number of spans when we encounter a reset code
|
||||
|
||||
@@ -358,7 +358,7 @@ function addClient() {
|
||||
const ips = $("#select")
|
||||
.val()
|
||||
.trim()
|
||||
.split(/[\s,]+/)
|
||||
.split(/[\s,]+/v)
|
||||
// Remove empty elements
|
||||
.filter(el => el !== "");
|
||||
const ipStr = JSON.stringify(ips);
|
||||
|
||||
@@ -486,7 +486,7 @@ function addDomain() {
|
||||
|
||||
// Check if the user wants to add multiple domains (space or newline separated)
|
||||
// If so, split the input and store it in an array
|
||||
let domains = domainEl.val().split(/\s+/);
|
||||
let domains = domainEl.val().split(/\s+/v);
|
||||
// Remove empty elements
|
||||
domains = domains.filter(el => el !== "");
|
||||
const domainStr = JSON.stringify(domains);
|
||||
|
||||
@@ -499,7 +499,7 @@ function addList(event) {
|
||||
// If so, split the input and store it in an array
|
||||
let addresses = $("#new_address")
|
||||
.val()
|
||||
.split(/[\s,]+/);
|
||||
.split(/[\s,]+/v);
|
||||
// Remove empty elements
|
||||
addresses = addresses.filter(el => el !== "");
|
||||
const addressestr = JSON.stringify(addresses);
|
||||
|
||||
@@ -243,8 +243,8 @@ function addGroup() {
|
||||
let names = utils
|
||||
.escapeHtml($("#new_name"))
|
||||
.val()
|
||||
.match(/(?:[^\s"]+|"[^"]*")+/g)
|
||||
.map(name => name.replaceAll(/(^"|"$)/g, ""));
|
||||
.match(/(?:[^\s"]+|"[^"]*")+/gv)
|
||||
.map(name => name.replaceAll(/(^"|"$)/gv, ""));
|
||||
// Remove empty elements
|
||||
names = names.filter(el => el !== "");
|
||||
const groupStr = JSON.stringify(names);
|
||||
|
||||
@@ -647,7 +647,7 @@ $(() => {
|
||||
callbacks: {
|
||||
title(tooltipTitle) {
|
||||
const label = tooltipTitle[0].label;
|
||||
const time = label.match(/(\d?\d):?(\d?\d?)/);
|
||||
const time = label.match(/(\d?\d):?(\d?\d?)/v);
|
||||
const h = Number.parseInt(time[1], 10);
|
||||
const m = Number.parseInt(time[2], 10) || 0;
|
||||
const from = utils.padNumber(h) + ":" + utils.padNumber(m - 5) + ":00";
|
||||
@@ -751,7 +751,7 @@ $(() => {
|
||||
callbacks: {
|
||||
title(tooltipTitle) {
|
||||
const label = tooltipTitle[0].label;
|
||||
const time = label.match(/(\d?\d):?(\d?\d?)/);
|
||||
const time = label.match(/(\d?\d):?(\d?\d?)/v);
|
||||
const h = Number.parseInt(time[1], 10);
|
||||
const m = Number.parseInt(time[2], 10) || 0;
|
||||
const from = utils.padNumber(h) + ":" + utils.padNumber(m - 5) + ":00";
|
||||
|
||||
@@ -484,7 +484,7 @@ function sortInterfaces(interfaces, masterInterfaces) {
|
||||
}
|
||||
|
||||
// Add interfaces that are not slaves at the top of the list (in reverse order)
|
||||
for (const iface of ifaces.reverse()) {
|
||||
for (const iface of ifaces.toReversed()) {
|
||||
if (!interfaceList.includes(iface)) {
|
||||
interfaceList.unshift(iface);
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ function mixColors(ratio, rgb1, rgb2) {
|
||||
}
|
||||
|
||||
function parseColor(input) {
|
||||
const match = input.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i);
|
||||
const match = input.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/iv);
|
||||
|
||||
if (match) {
|
||||
return [match[1], match[2], match[3]];
|
||||
|
||||
@@ -14,7 +14,7 @@ function hostsDomain(data) {
|
||||
// We split both on spaces and tabs to support both formats
|
||||
// Also, we remove any comments after the name(s)
|
||||
const name = data
|
||||
.split(/[\t ]+/)
|
||||
.split(/[\t ]+/v)
|
||||
.slice(1)
|
||||
.join(" ")
|
||||
.split("#")[0]
|
||||
@@ -25,7 +25,7 @@ function hostsDomain(data) {
|
||||
function hostsIP(data) {
|
||||
// Split record in format IP NAME1 [NAME2 [NAME3 [NAME...]]]
|
||||
// We split both on spaces and tabs to support both formats
|
||||
const ip = data.split(/[\t ]+/)[0].trim();
|
||||
const ip = data.split(/[\t ]+/v)[0].trim();
|
||||
return ip;
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ function updateCachePie(data) {
|
||||
}
|
||||
|
||||
// Sort data by value, put OTHER always as last
|
||||
const sorted = Object.keys(data).sort((a, b) => {
|
||||
const sorted = Object.keys(data).toSorted((a, b) => {
|
||||
if (a === "OTHER") {
|
||||
return 1;
|
||||
}
|
||||
@@ -292,8 +292,7 @@ $("#loggingButton").confirm({
|
||||
"Furthermore, you will be logged out of the web interface.",
|
||||
title: "Confirmation required",
|
||||
confirm() {
|
||||
const data = {};
|
||||
data.config = {};
|
||||
const data = { config: {} };
|
||||
data.config.dns = {};
|
||||
data.config.dns.queryLogging = $("#loggingButton").data("state") !== "enabled";
|
||||
$.ajax({
|
||||
|
||||
@@ -90,7 +90,7 @@ $("#GETTeleporter").on("click", () => {
|
||||
const url = globalThis.URL.createObjectURL(data);
|
||||
|
||||
a.href = url;
|
||||
a.download = xhr.getResponseHeader("Content-Disposition").match(/filename="([^"]*)"/)[1];
|
||||
a.download = xhr.getResponseHeader("Content-Disposition").match(/filename="([^"]*)"/v)[1];
|
||||
document.body.append(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
|
||||
@@ -24,7 +24,7 @@ const markUpdates = true;
|
||||
// Format a line of the dnsmasq log
|
||||
function formatDnsmasq(line) {
|
||||
// Remove dnsmasq + PID
|
||||
let txt = line.replaceAll(/ dnsmasq\[\d*]/g, "");
|
||||
let txt = line.replaceAll(/ dnsmasq\[\d*\]/gv, "");
|
||||
|
||||
if (line.includes("denied") || line.includes("gravity blocked")) {
|
||||
// Red bold text for blocked domains
|
||||
|
||||
@@ -19,6 +19,37 @@ $(() => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Decode a base64 string to UTF-8 text using native browser APIs
|
||||
* This is the replacement for the deprecated atob() function
|
||||
* @param {string} base64 - Base64 encoded string
|
||||
* @returns {string} Decoded UTF-8 string
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars -- Used by other scripts (e.g., footer.js)
|
||||
function base64ToString(base64) {
|
||||
// Remove padding and whitespace
|
||||
const cleanBase64 = base64.replaceAll(/[=\s]/gv, "");
|
||||
const base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
// Decode base64 to bytes
|
||||
const bytes = [];
|
||||
for (let i = 0; i < cleanBase64.length; i += 4) {
|
||||
const encoded1 = base64Chars.indexOf(cleanBase64[i]);
|
||||
const encoded2 = base64Chars.indexOf(cleanBase64[i + 1]);
|
||||
const encoded3 = base64Chars.indexOf(cleanBase64[i + 2]);
|
||||
const encoded4 = base64Chars.indexOf(cleanBase64[i + 3]);
|
||||
|
||||
/* eslint-disable no-bitwise -- Bitwise operations required for base64 decoding */
|
||||
bytes.push((encoded1 << 2) | (encoded2 >> 4));
|
||||
if (encoded3 !== -1) bytes.push(((encoded2 & 15) << 4) | (encoded3 >> 2));
|
||||
if (encoded4 !== -1) bytes.push(((encoded3 & 3) << 6) | encoded4);
|
||||
/* eslint-enable no-bitwise */
|
||||
}
|
||||
|
||||
// Decode bytes as UTF-8
|
||||
return new TextDecoder().decode(new Uint8Array(bytes));
|
||||
}
|
||||
|
||||
// Credit: https://stackoverflow.com/a/4835406
|
||||
function escapeHtml(text) {
|
||||
const map = {
|
||||
@@ -32,7 +63,7 @@ function escapeHtml(text) {
|
||||
// Return early when text is not a string
|
||||
if (typeof text !== "string") return text;
|
||||
|
||||
return text.replaceAll(/[&<>"']/g, m => map[m]);
|
||||
return text.replaceAll(/[&<>"']/gv, m => map[m]);
|
||||
}
|
||||
|
||||
function unescapeHtml(text) {
|
||||
@@ -54,7 +85,7 @@ function unescapeHtml(text) {
|
||||
if (text === null) return null;
|
||||
|
||||
return text.replaceAll(
|
||||
/&(?:amp|lt|gt|quot|#039|Uuml|uuml|Auml|auml|Ouml|ouml|szlig);/g,
|
||||
/&(?:amp|lt|gt|quot|#039|Uuml|uuml|Auml|auml|Ouml|ouml|szlig);/gv,
|
||||
m => map[m]
|
||||
);
|
||||
}
|
||||
@@ -195,7 +226,8 @@ function validateIPv4CIDR(ip) {
|
||||
// Build the complete IPv4/CIDR validator
|
||||
// Format: xxx.xxx.xxx.xxx[/yy] where each xxx is 0-255 and optional yy is 1-32
|
||||
const ipv4validator = new RegExp(
|
||||
`^${ipv4elem}\\.${ipv4elem}\\.${ipv4elem}\\.${ipv4elem}${v4cidr}$`
|
||||
`^${ipv4elem}\\.${ipv4elem}\\.${ipv4elem}\\.${ipv4elem}${v4cidr}$`,
|
||||
"v"
|
||||
);
|
||||
|
||||
return ipv4validator.test(ip);
|
||||
@@ -210,19 +242,20 @@ function validateIPv6CIDR(ip) {
|
||||
const v6cidr = "(\\/([1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])){0,1}";
|
||||
|
||||
const ipv6validator = new RegExp(
|
||||
`^(((?:${ipv6elem}))*((?::${ipv6elem}))*::((?:${ipv6elem}))*((?::${ipv6elem}))*|((?:${ipv6elem}))((?::${ipv6elem})){7})${v6cidr}$`
|
||||
`^(((?:${ipv6elem}))*((?::${ipv6elem}))*::((?:${ipv6elem}))*((?::${ipv6elem}))*|((?:${ipv6elem}))((?::${ipv6elem})){7})${v6cidr}$`,
|
||||
"v"
|
||||
);
|
||||
|
||||
return ipv6validator.test(ip);
|
||||
}
|
||||
|
||||
function validateMAC(mac) {
|
||||
const macvalidator = /^([\da-fA-F]{2}:){5}([\da-fA-F]{2})$/;
|
||||
const macvalidator = /^([\da-fA-F]{2}:){5}([\da-fA-F]{2})$/v;
|
||||
return macvalidator.test(mac);
|
||||
}
|
||||
|
||||
function validateHostname(name) {
|
||||
const namevalidator = /[^<>;"]/;
|
||||
const namevalidator = /[^<>;"]/v;
|
||||
return namevalidator.test(name);
|
||||
}
|
||||
|
||||
@@ -494,7 +527,7 @@ function hexEncode(text) {
|
||||
function hexDecode(text) {
|
||||
if (typeof text !== "string" || text.length === 0) return "";
|
||||
|
||||
const hexes = text.match(/.{1,4}/g);
|
||||
const hexes = text.match(/.{1,4}/gv);
|
||||
if (!hexes || hexes.length === 0) return "";
|
||||
|
||||
return hexes.map(hex => String.fromCodePoint(Number.parseInt(hex, 16))).join("");
|
||||
|
||||
Reference in New Issue
Block a user