mirror of
https://github.com/pi-hole/web.git
synced 2025-12-20 10:48:26 +00:00
165 lines
5.2 KiB
JavaScript
165 lines
5.2 KiB
JavaScript
/* Pi-hole: A black hole for Internet advertisements
|
|
* (c) 2017 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 apiFailure:false, utils:false */
|
|
|
|
"use strict";
|
|
|
|
function eventsource() {
|
|
const $alertInfo = $("#alertInfo");
|
|
const $alertSuccess = $("#alertSuccess");
|
|
const outputElement = document.getElementById("output");
|
|
const gravityBtn = document.getElementById("gravityBtn");
|
|
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute("content");
|
|
const url = `${document.body.dataset.apiurl}/action/gravity`;
|
|
|
|
if (outputElement.innerHTML.length > 0) {
|
|
outputElement.innerHTML = "";
|
|
}
|
|
|
|
if (!outputElement.classList.contains("d-none")) {
|
|
outputElement.classList.add("d-none");
|
|
}
|
|
|
|
$alertSuccess.hide();
|
|
$alertInfo.show();
|
|
|
|
fetch(url, {
|
|
method: "POST",
|
|
headers: { "X-CSRF-TOKEN": csrfToken },
|
|
})
|
|
.then(response => (response.ok ? response : apiFailure(response)))
|
|
// Retrieve the response as ReadableStream
|
|
.then(response => {
|
|
return handleResponseStream({
|
|
response,
|
|
outputElement,
|
|
alertInfo: $alertInfo,
|
|
gravityBtn,
|
|
alertSuccess: $alertSuccess,
|
|
});
|
|
})
|
|
.catch(error => console.error(error)); // eslint-disable-line no-console
|
|
}
|
|
|
|
function handleResponseStream({ response, outputElement, alertInfo, gravityBtn, alertSuccess }) {
|
|
outputElement.classList.remove("d-none");
|
|
|
|
const reader = response.body.getReader();
|
|
|
|
function pump(controller) {
|
|
return reader.read().then(({ done, value }) => {
|
|
// When no more data needs to be consumed, close the stream
|
|
if (done) {
|
|
controller.close();
|
|
alertInfo.hide();
|
|
gravityBtn.removeAttribute("disabled");
|
|
return;
|
|
}
|
|
|
|
// Enqueue the next data chunk into our target stream
|
|
controller.enqueue(value);
|
|
const text = new TextDecoder().decode(value);
|
|
parseLines(outputElement, text);
|
|
|
|
if (text.includes("Done.")) {
|
|
alertSuccess.show();
|
|
}
|
|
|
|
return pump(controller);
|
|
});
|
|
}
|
|
|
|
return new ReadableStream({
|
|
start(controller) {
|
|
return pump(controller);
|
|
},
|
|
});
|
|
}
|
|
|
|
function parseLines(outputElement, text) {
|
|
// text can contain multiple lines.
|
|
// 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);
|
|
|
|
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\u001B[K", "\n").replaceAll("\r", "\n");
|
|
|
|
// Last line from the textarea will be overwritten, so we remove it
|
|
const lastLineIndex = outputElement.innerHTML.lastIndexOf("\n");
|
|
outputElement.innerHTML = outputElement.innerHTML.substring(0, lastLineIndex);
|
|
}
|
|
|
|
// Track the number of opening spans
|
|
let spanCount = 0;
|
|
|
|
// Mapping of ANSI escape codes to their corresponding CSS class names.
|
|
const ansiMappings = {
|
|
"\u001B[1m": "text-bold", //COL_BOLD
|
|
"\u001B[90m": "log-gray", //COL_GRAY
|
|
"\u001B[91m": "log-red", //COL_RED
|
|
"\u001B[32m": "log-green", //COL_GREEN
|
|
"\u001B[33m": "log-yellow", //COL_YELLOW
|
|
"\u001B[94m": "log-blue", //COL_BLUE
|
|
"\u001B[95m": "log-purple", //COL_PURPLE
|
|
"\u001B[96m": "log-cyan", //COL_CYAN
|
|
};
|
|
|
|
// 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;
|
|
|
|
// 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
|
|
/* eslint-disable-next-line unicorn/prefer-string-replace-all */
|
|
line = line.replace(ansiRegex, match => {
|
|
if (match === "\u001B[0m") {
|
|
// Reset/close all open spans
|
|
const closingTags = "</span>".repeat(spanCount);
|
|
spanCount = 0;
|
|
return closingTags;
|
|
}
|
|
|
|
if (ansiMappings[match]) {
|
|
// Opening span
|
|
spanCount++;
|
|
return `<span class="${ansiMappings[match]}">`;
|
|
}
|
|
|
|
return match; // Return unchanged if not recognized
|
|
});
|
|
|
|
// Append the new text to the end of the output
|
|
outputElement.innerHTML += line;
|
|
}
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
const gravityBtn = document.getElementById("gravityBtn");
|
|
|
|
gravityBtn.addEventListener("click", () => {
|
|
gravityBtn.disabled = true;
|
|
eventsource();
|
|
});
|
|
|
|
// Handle hiding of alerts
|
|
const dataHideElements = document.querySelectorAll("[data-hide]");
|
|
for (const element of dataHideElements) {
|
|
element.addEventListener("click", () => {
|
|
const hideClass = element.dataset.hide;
|
|
const closestElement = element.closest(`.${hideClass}`);
|
|
if (closestElement) $(closestElement).hide();
|
|
});
|
|
}
|
|
});
|