/* 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 utils:false, moment:false */ //The following functions allow us to display time until pi-hole is enabled after disabling. //Works between all pages var settingsLevel = 0; function secondsTimeSpanToHMS(s) { var h = Math.floor(s / 3600); //Get whole hours s -= h * 3600; var m = Math.floor(s / 60); //Get remaining minutes s -= m * 60; return h + ":" + (m < 10 ? "0" + m : m) + ":" + (s < 10 ? "0" + s : s); //zero padding on minutes and seconds } function piholeChanged(blocking) { var status = $("#status"); var ena = $("#pihole-enable"); var dis = $("#pihole-disable"); switch (blocking) { case "enabled": { status.html(" Active"); ena.hide(); dis.show(); dis.removeClass("active"); break; } case "disabled": { status.html(" Blocking disabled"); ena.show(); dis.hide(); break; } case "failure": { status.html( " DNS server failure" ); ena.hide(); dis.hide(); break; } default: { status.html(" Status unknown"); ena.hide(); dis.hide(); } } } function countDown() { var ena = $("#enableLabel"); var enaT = $("#enableTimer"); var target = new Date(parseInt(enaT.html(), 10)); var seconds = Math.round((target.getTime() - Date.now()) / 1000); //Stop and remove timer when user enabled early if ($("#pihole-enable").is(":hidden")) { ena.text("Enable Blocking"); return; } if (seconds > 0) { setTimeout(countDown, 1000); ena.text("Enable Blocking (" + secondsTimeSpanToHMS(seconds) + ")"); } else { ena.text("Enable Blocking"); piholeChanged("enabled"); if (localStorage) { localStorage.removeItem("countDownTarget"); } } } function checkBlocking() { $.ajax({ url: "/api/dns/blocking", method: "GET", }) .done(function (data) { piholeChanged(data.blocking); setTimeout(checkBlocking, 10000); }) .fail(function (data) { apiFailure(data); setTimeout(checkBlocking, 30000); }); } function piholeChange(action, duration) { var enaT = $("#enableTimer"); var btnStatus; switch (action) { case "enable": btnStatus = $("#flip-status-enable"); break; case "disable": btnStatus = $("#flip-status-disable"); break; default: // Do nothing break; } btnStatus.html(" "); $.ajax({ url: "/api/dns/blocking", method: "POST", data: JSON.stringify({ blocking: action === "enable", timer: parseInt(duration, 10) > 0 ? parseInt(duration, 10) : null, }), }) .done(function (data) { if (data.blocking === action + "d") { btnStatus.html(""); piholeChanged(data.blocking); if (duration > 0) { enaT.html(Date.now() + duration * 1000); setTimeout(countDown, 100); } } }) .fail(function (data) { apiFailure(data); }); } function testCookies() { if (navigator.cookieEnabled) { return true; } // set and read cookie document.cookie = "cookietest=1"; var ret = document.cookie.indexOf("cookietest=") !== -1; // delete cookie document.cookie = "cookietest=1; expires=Thu, 01-Jan-1970 00:00:01 GMT"; return ret; } var iCheckStyle = "primary"; function applyCheckboxRadioStyle() { // Get all radio/checkboxes for theming, with the exception of the two radio buttons on the custom disable timer, // as well as every element with an id that starts with "status_" var sel = $("input[type='radio'],input[type='checkbox']") .not("#selSec") .not("#selMin") .not("[id^=status_]"); sel.parent().removeClass(); sel.parent().addClass("icheck-" + iCheckStyle); } function initCheckboxRadioStyle() { function getCheckboxURL(style) { var extra = style.startsWith("material-") ? "material" : "bootstrap"; return "/admin/style/vendor/icheck-" + extra + ".min.css"; } // Read from local storage, initialize if needed var chkboxStyle = localStorage ? localStorage.getItem("theme_icheck") : null; if (chkboxStyle === null) { chkboxStyle = "primary"; } var boxsheet = $(''); // Only add the stylesheet if it's not already present if ($("link[href='" + boxsheet.attr("href") + "']").length === 0) boxsheet.appendTo("head"); iCheckStyle = chkboxStyle; applyCheckboxRadioStyle(); // Add handler when on settings page var iCheckStyle = $("#iCheckStyle"); if (iCheckStyle !== null) { iCheckStyle.val(chkboxStyle); iCheckStyle.on("change", function () { var themename = $(this).val(); localStorage.setItem("theme_icheck", themename); applyCheckboxRadioStyle(themename); }); } } var systemTimer, sensorsTimer, versionTimer; function updateInfo() { updateSystemInfo(); updateSensorsInfo(); updateVersionInfo(); updateFtlInfo(); checkBlocking(); } var ftlinfoTimer = null; function updateFtlInfo() { $.ajax({ url: "/api/info/ftl", }) .done(function (data) { var ftl = data.ftl; var database = ftl.database; var intl = new Intl.NumberFormat(); $("#num_groups").text(intl.format(database.groups)); $("#num_clients").text(intl.format(database.clients)); $("#num_lists").text(intl.format(database.lists)); $("#num_gravity").text(intl.format(database.gravity)); $("#num_allowed").text(intl.format(database.domains.allowed)); $("#num_denied").text(intl.format(database.domains.denied)); $("#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); var startdate = moment() .subtract(ftl.uptime, "milliseconds") .format("dddd, MMMM Do YYYY, HH:mm:ss"); $("#sysinfo-uptime-ftl").text(startdate); $("#sysinfo-privacy_level").text(ftl.privacy_level); $("#sysinfo-ftl-overlay").hide(); $(".destructive_action").prop("disabled", !ftl.allow_destructive); $(".destructive_action").prop( "title", ftl.allow_destructive ? "" : "Destructive actions are disabled by a config setting" ); // Update every 120 seconds clearTimeout(ftlinfoTimer); ftlinfoTimer = setTimeout(updateFtlInfo, 120000); }) .fail(function (data) { apiFailure(data); }); } function updateSystemInfo() { $.ajax({ url: "/api/info/system", }) .done(function (data) { var system = data.system; var percentRAM = system.memory.ram["%used"]; var percentSwap = system.memory.swap["%used"]; var totalRAMGB = system.memory.ram.total / 1024 / 1024; var totalSwapGB = system.memory.swap.total / 1024 / 1024; var swap = system.memory.swap.total > 0 ? ((1e2 * system.memory.swap.used) / system.memory.swap.total).toFixed(1) + " %" : "N/A"; var color; color = percentRAM > 75 ? "text-red" : "text-green-light"; $("#memory").html( '  Memory usage: ' + percentRAM.toFixed(1) + " %" ); $("#memory").prop( "title", "Total memory: " + totalRAMGB.toFixed(1) + " GB, Swap usage: " + swap ); $("#sysinfo-memory-ram").text( percentRAM.toFixed(1) + "% of " + totalRAMGB.toFixed(1) + " GB is used" ); if (system.memory.swap.total > 0) { $("#sysinfo-memory-swap").text( percentSwap.toFixed(1) + "% of " + totalSwapGB.toFixed(1) + " GB is used" ); } else { $("#sysinfo-memory-swap").text("No swap space available"); } color = system.cpu.load.percent[0] > 100 ? "text-red" : "text-green-light"; $("#cpu").html( '  CPU: ' + system.cpu.load.percent[0].toFixed(1) + " %" ); $("#cpu").prop( "title", "Load: " + system.cpu.load.raw[0].toFixed(2) + " " + system.cpu.load.raw[1].toFixed(2) + " " + system.cpu.load.raw[2].toFixed(2) + " on " + system.cpu.nprocs + " cores running " + system.procs + " processes" ); $("#sysinfo-cpu").text( system.cpu.load.percent[0].toFixed(1) + "% (load: " + system.cpu.load.raw[0].toFixed(2) + " " + system.cpu.load.raw[1].toFixed(2) + " " + system.cpu.load.raw[2].toFixed(2) + ") on " + system.cpu.nprocs + " cores running " + system.procs + " processes" ); var startdate = moment() .subtract(system.uptime, "seconds") .format("dddd, MMMM Do YYYY, HH:mm:ss"); $("#status").prop( "title", "System uptime: " + moment.duration(1000 * system.uptime).humanize() + " (running since " + startdate + ")" ); $("#sysinfo-uptime").text( moment.duration(1000 * system.uptime).humanize() + " (running since " + startdate + ")" ); $("#sysinfo-system-overlay").hide(); // Update every 20 seconds clearTimeout(systemTimer); systemTimer = setTimeout(updateSystemInfo, 20000); }) .fail(function (data) { apiFailure(data); }); } function updateSensorsInfo() { $.ajax({ url: "/api/info/sensors", }) .done(function (data) { var unit = "°" + data.sensors.unit; if (data.sensors.unit === "°K") { unit = data.sensors.unit; } if (data.sensors.cpu_temp !== null) { var temp = data.sensors.cpu_temp.toFixed(1) + " " + unit; var color = data.sensors.cpu_temp > data.sensors.hot_limit ? "text-red fa-temperature-high" : "text-green-light fa-temperature-low"; $("#temperature").html( ' Temp: ' + temp ); } else $("#temperature").html( ' Temp: N/A' ); // Get a text listing of all sensors let sensorlist = "Available sensors:\n"; $.each(data.sensors.list, function (index, hwmon) { sensorlist += "- " + hwmon.name + " (" + hwmon.source + "):\n"; $.each(hwmon.temps, function (index, temp) { sensorlist += " - " + temp.name + ": " + temp.value.toFixed(1) + unit + " (max: " + (temp.max === null ? "N/A" : temp.max.toFixed(1) + unit) + ", crit: " + (temp.crit === null ? "N/A" : temp.crit.toFixed(1) + unit) + ")\n"; }); }); $("#temperature").prop("title", sensorlist); // Update every 20 seconds clearTimeout(sensorsTimer); sensorsTimer = setTimeout(updateSensorsInfo, 20000); }) .fail(function (data) { apiFailure(data); }); } function apiFailure(data) { if (data.status === 401) { // Unauthorized, reload page window.location.reload(); } } // Method to compare two versions. Returns 1 if v2 is smaller, -1 if v1 is // smaller, 0 if equal // Credits: https://www.geeksforgeeks.org/compare-two-version-numbers/ function versionCompare(v1, v2) { // vnum stores each numeric part of version var vnum1 = 0, vnum2 = 0; // Remove possible leading "v" in v1 and v2 if (v1[0] === "v") { v1 = v1.substring(1); } if (v2[0] === "v") { v2 = v2.substring(1); } // loop until both string are processed for (var i = 0, j = 0; i < v1.length || j < v2.length; ) { // storing numeric part of version 1 in vnum1 while (i < v1.length && v1[i] !== ".") { vnum1 = vnum1 * 10 + (v1[i] - "0"); i++; } // storing numeric part of version 2 in vnum2 while (j < v2.length && v2[j] !== ".") { vnum2 = vnum2 * 10 + (v2[j] - "0"); j++; } if (vnum1 > vnum2) return 1; if (vnum2 > vnum1) return -1; // if equal, reset variables and go for next numeric part vnum1 = 0; vnum2 = 0; i++; j++; } return 0; } function updateVersionInfo() { $.ajax({ url: "/api/info/version", }).done(function (data) { var version = data.version; var updateAvailable = false; var dockerUpdate = false; $("#versions").empty(); var versions = [ { name: "Docker Tag", local: version.docker.local, remote: version.docker.remote, branch: null, hash: null, url: "https://github.com/pi-hole/docker-pi-hole/releases", }, { name: "Core", local: version.core.local.version, remote: version.core.remote.version, branch: version.core.local.branch, hash: version.core.local.hash, hash_remote: version.core.remote.hash, url: "https://github.com/pi-hole/pi-hole/releases", }, { name: "FTL", local: version.ftl.local.version, remote: version.ftl.remote.version, branch: version.ftl.local.branch, hash: version.ftl.local.hash, hash_remote: version.ftl.remote.hash, url: "https://github.com/pi-hole/FTL/releases", }, { name: "Web interface", local: version.web.local.version, remote: version.web.remote.version, branch: version.web.local.branch, hash: version.web.local.hash, hash_remote: version.web.remote.hash, url: "https://github.com/pi-hole/web/releases", }, ]; versions.forEach(function (v) { if (v.local !== null) { var localVersion = v.local; if (v.branch !== null && v.hash !== null) if (v.branch === "master") { localVersion = v.local.split("-")[0]; localVersion = '' + localVersion + ""; if (versionCompare(v.local, v.remote) === -1) { // Update available updateAvailable = true; } else { // non-master branch localVersion = "vDev (" + v.branch + ", " + v.hash + ")"; if (v.hash != v.hash_remote) { // hash differ > Update available updateAvailable = true; } } } if (updateAvailable) { $("#versions").append( "
  • " + v.name + " " + localVersion + '· Update available!
  • ' ); } else { $("#versions").append("
  • " + v.name + " " + localVersion + "
  • "); } } }); if (dockerUpdate) $("update-hint").html( 'To install updates, replace this old container with a fresh upgraded image.' ); else if (updateAvailable) $("update-hint").html( 'To install updates, run pihole -up.' ); // Update every 120 seconds clearTimeout(versionTimer); versionTimer = setTimeout(updateVersionInfo, 120000); }); } $(function () { if (window.location.pathname !== "/admin/login") updateInfo(); var enaT = $("#enableTimer"); var target = new Date(parseInt(enaT.html(), 10)); var seconds = Math.round((target.getTime() - Date.now()) / 1000); if (seconds > 0) { setTimeout(countDown, 100); } if (!testCookies() && $("#cookieInfo").length > 0) { $("#cookieInfo").show(); } // Apply per-browser styling settings initCheckboxRadioStyle(); if (window.location.pathname !== "/admin/login") { // Run check immediately after page loading ... utils.checkMessages(); // ... and once again with five seconds delay setTimeout(utils.checkMessages, 5000); } }); // Handle Enable/Disable $("#pihole-enable").on("click", function (e) { e.preventDefault(); localStorage.removeItem("countDownTarget"); piholeChange("enable", ""); }); $("#pihole-disable-indefinitely").on("click", function (e) { e.preventDefault(); piholeChange("disable", "0"); }); $("#pihole-disable-10s").on("click", function (e) { e.preventDefault(); piholeChange("disable", "10"); }); $("#pihole-disable-30s").on("click", function (e) { e.preventDefault(); piholeChange("disable", "30"); }); $("#pihole-disable-5m").on("click", function (e) { e.preventDefault(); piholeChange("disable", "300"); }); $("#pihole-disable-custom").on("click", function (e) { e.preventDefault(); var custVal = $("#customTimeout").val(); custVal = $("#btnMins").hasClass("active") ? custVal * 60 : custVal; piholeChange("disable", custVal); }); function initSettingsLevel() { // Restore settings level from local storage (if available) or default to 0 settingsLevel = parseInt(localStorage.getItem("settings-level"), 10); if (isNaN(settingsLevel)) { settingsLevel = 0; localStorage.setItem("settings-level", settingsLevel); } // Set the settings level $("#settings-level").append( '" ); $("#settings-level").append( '" ); $("#settings-level").append( '" ); applySettingsLevel(); } function applySettingsLevel() { if (settingsLevel === 2) { $(".settings-level-0").show(); $(".settings-level-1").show(); $(".settings-level-2").show(); } else if (settingsLevel === 1) { $(".settings-level-0").show(); $(".settings-level-1").show(); $(".settings-level-2").hide(); } else { $(".settings-level-0").show(); $(".settings-level-1").hide(); $(".settings-level-2").hide(); } } $("#settings-level").on("change", function () { settingsLevel = parseInt($(this).val(), 10); localStorage.setItem("settings-level", settingsLevel); applySettingsLevel(); addAdvancedInfo(); }); function addAdvancedInfo() { const advancedInfoSource = $("#advanced-info-data"); const advancedInfoTarget = $("#advanced-info"); const isTLS = advancedInfoSource.data("tls"); const clientIP = advancedInfoSource.data("client-ip"); const starttime = parseFloat(advancedInfoSource.data("starttime")); const endtime = parseFloat(advancedInfoSource.data("endtime")); const totaltime = 1e3 * (endtime - starttime); // Hide advanced info if settings level is lower than 2 if (settingsLevel < 2) { advancedInfoTarget.hide(); return; } // Show advanced info advancedInfoTarget.empty(); // Add TLS and client IP info advancedInfoTarget.append( 'Client:  ' + clientIP + "
    " ); // Add render time info advancedInfoTarget.append( "Render time: " + (totaltime > 0.5 ? totaltime.toFixed(1) : totaltime.toFixed(3)) + " ms" ); // Show advanced info advancedInfoTarget.show(); } $(function () { initSettingsLevel(); addAdvancedInfo(); }); // Install custom AJAX error handler for DataTables // if $.fn.dataTable is available if ($.fn.dataTable) { $.fn.dataTable.ext.errMode = function (settings, helpPage, message) { // eslint-disable-next-line no-console console.log("DataTables warning: " + message); }; }