-Pi-hole[®](https://pi-hole.net/trademark-rules-and-brand-guidelines/)'s Web interface (based off of [AdminLTE](https://almsaeedstudio.com)) provides a central location to manage your Pi-hole and review the statistics generated by FTLDNS[™](https://pi-hole.net/trademark-rules-and-brand-guidelines/).
+Pi-hole[®](https://pi-hole.net/trademark-rules-and-brand-guidelines/)'s Web interface (based off of [AdminLTE](https://github.com/ColorlibHQ/AdminLTE)) provides a central location to manage your Pi-hole and review the statistics generated by FTLDNS[™](https://pi-hole.net/trademark-rules-and-brand-guidelines/).
- **Easy-to-interpret**: simple graphs and beautiful colors make Pi-hole's stats easy to understand
- **Responsive**: looks great on desktop, tablets, and mobile devices
@@ -13,7 +13,6 @@ Pi-hole[®](https://pi-hole.net/trademark-rules-and-brand-guidelines/)'s Web int
- **Insightful**: use the query log, audit log, or long-term stats to gain insight into your networks activity
---
-
# Installation
diff --git a/api_FTL.php b/api_FTL.php
index 571fe74b..f1a882d6 100644
--- a/api_FTL.php
+++ b/api_FTL.php
@@ -104,7 +104,8 @@ else
foreach($return as $line)
{
$tmp = explode(" ",$line);
- $top_queries[$tmp[2]] = intval($tmp[1]);
+ $domain = utf8_encode($tmp[2]);
+ $top_queries[$domain] = intval($tmp[1]);
}
if($_GET['topItems'] === "audit")
@@ -125,10 +126,11 @@ else
foreach($return as $line)
{
$tmp = explode(" ",$line);
+ $domain = utf8_encode($tmp[2]);
if(count($tmp) > 3)
- $top_ads[$tmp[2]." (".$tmp[3].")"] = intval($tmp[1]);
+ $top_ads[$domain." (".$tmp[3].")"] = intval($tmp[1]);
else
- $top_ads[$tmp[2]] = intval($tmp[1]);
+ $top_ads[$domain] = intval($tmp[1]);
}
$result = array('top_queries' => $top_queries,
@@ -163,10 +165,14 @@ else
foreach($return as $line)
{
$tmp = explode(" ",$line);
+ $clientip = utf8_encode($tmp[2]);
if(count($tmp) > 3 && strlen($tmp[3]) > 0)
- $top_clients[$tmp[3]."|".$tmp[2]] = intval($tmp[1]);
+ {
+ $clientname = utf8_encode($tmp[3]);
+ $top_clients[$clientname."|".$clientip] = intval($tmp[1]);
+ }
else
- $top_clients[$tmp[2]] = intval($tmp[1]);
+ $top_clients[$clientip] = intval($tmp[1]);
}
$result = array('top_sources' => $top_clients);
@@ -195,10 +201,14 @@ else
foreach($return as $line)
{
$tmp = explode(" ",$line);
+ $clientip = utf8_encode($tmp[2]);
if(count($tmp) > 3 && strlen($tmp[3]) > 0)
- $top_clients[$tmp[3]."|".$tmp[2]] = intval($tmp[1]);
+ {
+ $clientname = utf8_encode($tmp[3]);
+ $top_clients[$clientname."|".$clientip] = intval($tmp[1]);
+ }
else
- $top_clients[$tmp[2]] = intval($tmp[1]);
+ $top_clients[$clientip] = intval($tmp[1]);
}
$result = array('top_sources_blocked' => $top_clients);
@@ -220,10 +230,14 @@ else
foreach($return as $line)
{
$tmp = explode(" ",$line);
+ $forwardip = utf8_encode($tmp[2]);
if(count($tmp) > 3 && strlen($tmp[3]) > 0)
- $forward_dest[$tmp[3]."|".$tmp[2]] = floatval($tmp[1]);
+ {
+ $forwardname = utf8_encode($tmp[3]);
+ $forward_dest[$forwardname."|".$forwardip] = floatval($tmp[1]);
+ }
else
- $forward_dest[$tmp[2]] = floatval($tmp[1]);
+ $forward_dest[$forwardip] = floatval($tmp[1]);
}
$result = array('forward_destinations' => $forward_dest);
@@ -238,6 +252,7 @@ else
foreach($return as $ret)
{
$tmp = explode(": ",$ret);
+ // Reply cannot contain non-ASCII characters
$querytypes[$tmp[0]] = floatval($tmp[1]);
}
@@ -253,6 +268,7 @@ else
foreach($return as $ret)
{
$tmp = explode(": ",$ret);
+ // Reply cannot contain non-ASCII characters
$cacheinfo[$tmp[0]] = floatval($tmp[1]);
}
@@ -301,6 +317,10 @@ else
foreach($return as $line)
{
$tmp = explode(" ",$line);
+ // UTF-8 encode domain
+ $tmp[2] = utf8_encode($tmp[2]);
+ // UTF-8 encode client host name
+ $tmp[3] = utf8_encode($tmp[3]);;
array_push($allQueries,$tmp);
}
@@ -311,7 +331,7 @@ else
if(isset($_GET["recentBlocked"]))
{
sendRequestFTL("recentBlocked");
- die(getResponseFTL()[0]);
+ die(utf8_encode(getResponseFTL()[0]));
unset($data);
}
@@ -323,13 +343,15 @@ else
foreach($return as $line)
{
$tmp = explode(" ",$line);
+ $forwardip = utf8_encode($tmp[2]);
if(count($tmp) > 3)
{
- $forward_dest[$tmp[3]."|".$tmp[2]] = floatval($tmp[1]);
+ $forwardname = utf8_encode($tmp[3]);
+ $forward_dest[$forwardname."|".$forwardip] = floatval($tmp[1]);
}
else
{
- $forward_dest[$tmp[2]] = floatval($tmp[1]);
+ $forward_dest[$forwardip] = floatval($tmp[1]);
}
}
@@ -363,8 +385,8 @@ else
{
$tmp = explode(" ", $line);
$client_names[] = array(
- "name" => $tmp[0],
- "ip" => $tmp[1]
+ "name" => utf8_encode($tmp[0]),
+ "ip" => utf8_encode($tmp[1])
);
}
diff --git a/api_db.php b/api_db.php
index 5195114e..99358589 100644
--- a/api_db.php
+++ b/api_db.php
@@ -8,6 +8,7 @@
$api = true;
header('Content-type: application/json');
+require("scripts/pi-hole/php/database.php");
require("scripts/pi-hole/php/password.php");
require("scripts/pi-hole/php/auth.php");
check_cors();
@@ -48,51 +49,11 @@ function resolveHostname($clientip, $printIP)
return $clientname;
}
-// Get posible non-standard location of FTL's database
-$FTLsettings = parse_ini_file("/etc/pihole/pihole-FTL.conf");
-if(isset($FTLsettings["DBFILE"]))
-{
- $DBFILE = $FTLsettings["DBFILE"];
-}
-else
-{
- $DBFILE = "/etc/pihole/pihole-FTL.db";
-}
-
// Needs package php5-sqlite, e.g.
// sudo apt-get install php5-sqlite
-function SQLite3_connect($trytoreconnect)
-{
- global $DBFILE;
- try
- {
- // connect to database
- return new SQLite3($DBFILE, SQLITE3_OPEN_READONLY);
- }
- catch (Exception $exception)
- {
- // sqlite3 throws an exception when it is unable to connect, try to reconnect after 3 seconds
- if($trytoreconnect)
- {
- sleep(3);
- $db = SQLite3_connect(false);
- }
- }
-}
-
-if(strlen($DBFILE) > 0)
-{
- $db = SQLite3_connect(true);
-}
-else
-{
- die("No database available");
-}
-if(!$db)
-{
- die("Error connecting to database");
-}
+$QUERYDB = getQueriesDBFilename();
+$db = SQLite3_connect($QUERYDB);
if(isset($_GET["network"]) && $auth)
{
@@ -100,7 +61,19 @@ if(isset($_GET["network"]) && $auth)
$results = $db->query('SELECT * FROM network');
while($results !== false && $res = $results->fetchArray(SQLITE3_ASSOC))
+ {
+ $id = $res["id"];
+ // Empty array for holding the IP addresses
+ $res["ip"] = array();
+ // Get IP addresses for this device
+ $network_addresses = $db->query("SELECT ip FROM network_addresses WHERE network_id = $id ORDER BY lastSeen DESC");
+ while($network_addresses !== false && $ip = $network_addresses->fetchArray(SQLITE3_ASSOC))
+ array_push($res["ip"],$ip["ip"]);
+ // UTF-8 encode host name and vendor
+ $res["name"] = utf8_encode($res["name"]);
+ $res["macVendor"] = utf8_encode($res["macVendor"]);
array_push($network, $res);
+ }
$data = array_merge($data, array('network' => $network));
}
@@ -164,12 +137,15 @@ if (isset($_GET['getAllQueries']) && $auth)
case 7:
$query_type = "TXT";
break;
+ case 8:
+ $query_type = "NAPTR";
+ break;
default:
$query_type = "UNKN";
break;
}
-
- $allQueries[] = [$row[0], $query_type, $row[2], $c, $row[4]];
+ // array: time type domain client status
+ $allQueries[] = [$row[0], $query_type, utf8_encode($row[2]), utf8_encode($c), $row[4]];
}
}
$result = array('data' => $allQueries);
@@ -202,8 +178,8 @@ if (isset($_GET['topClients']) && $auth)
if(!is_bool($results))
while ($row = $results->fetchArray())
{
-
- $c = resolveHostname($row[0],false);
+ // Try to resolve host name and convert to UTF-8
+ $c = utf8_encode(resolveHostname($row[0],false));
if(array_key_exists($c, $clientnums))
{
@@ -253,8 +229,8 @@ if (isset($_GET['topDomains']) && $auth)
if(!is_bool($results))
while ($row = $results->fetchArray())
{
- // Convert client to lower case
- $c = strtolower($row[0]);
+ // Convert domain to lower case UTF-8
+ $c = utf8_encode(strtolower($row[0]));
if(array_key_exists($c, $domains))
{
// Entry already exists, add to it (might appear multiple times due to mixed capitalization in the database)
@@ -303,7 +279,7 @@ if (isset($_GET['topAds']) && $auth)
if(!is_bool($results))
while ($row = $results->fetchArray())
{
- $addomains[$row[0]] = intval($row[1]);
+ $addomains[utf8_encode($row[0])] = intval($row[1]);
}
$result = array('top_ads' => $addomains);
$data = array_merge($data, $result);
@@ -392,20 +368,28 @@ if (isset($_GET['getGraphData']) && $auth)
// Parse the DB result into graph data, filling in missing interval sections with zero
function parseDBData($results, $interval, $from, $until) {
$data = array();
+ $first_db_timestamp = -1;
if(!is_bool($results)) {
// Read in the data
while($row = $results->fetchArray()) {
// $data[timestamp] = value_in_this_interval
$data[$row[0]] = intval($row[1]);
+ if($first_db_timestamp === -1)
+ $first_db_timestamp = intval($row[0]);
}
+ }
- // Fill the missing intervals with zero
- // Advance in steps of interval
- for($i = $from; $i < $until; $i += $interval) {
- if(!array_key_exists($i, $data))
- $data[$i] = 0;
- }
+ // It is unpredictable what the first timestamp returned by the database
+ // will be. This depends on live data. Hence, we re-align the FROM
+ // timestamp to avoid unaligned holes appearing as additional
+ // (incorrect) data points
+ $aligned_from = $from + (($first_db_timestamp - $from) % $interval);
+
+ // Fill gaps in returned data
+ for($i = $aligned_from; $i < $until; $i += $interval) {
+ if(!array_key_exists($i, $data))
+ $data[$i] = 0;
}
return $data;
diff --git a/auditlog.php b/auditlog.php
index 4f105a70..33ed04ac 100644
--- a/auditlog.php
+++ b/auditlog.php
@@ -68,14 +68,9 @@
-
-
Important: Note that black- and whitelisted domains are not automatically applied on this page to avoid restarting the DNS service too often. Instead, click on this button, to have the new settings become effective:
Status: Current status of the Pi-hole - Active (), Offline (), or Starting ()
+
Status: Current status of the Pi-hole - Active (), Offline (), or Starting ()
Temp: Current CPU temperature
Load: load averages for the last minute, 5 minutes and 15 minutes, respectively. A load average of 1 reflects the full workload of a single processor on the system. We show a red icon if the current load exceeds the number of available processors on this machine (which is )
Memory usage: Shows the percentage of memory actually blocked by applications. We show a red icon if the memory usage exceeds 75%
@@ -70,7 +70,7 @@
White- / Blacklist
Add or remove domains (or subdomains) from the white-/blacklist. If a domain is added to e.g. the whitelist, any possible entry of the same domain will be automatically removed from the blacklist and vice versa.
-
Regex blacklisting is supported (entering ^example will block any domain starting with example, see also our Regex documentation). You can still whitelist specific domains even if they fall under a regex pattern.
+
Regex blacklisting is supported (entering ^example will block any domain starting with example, see also our Regex documentation). You can still whitelist specific domains even if they fall under a regex pattern.
You can white-/blacklist multiple entries at a time if you separate the domains by spaces.
@@ -89,7 +89,7 @@
Tools → Query Lists
- This function is useful to find out what list a domain appears on. Since we don't control what the third-parties put on the blocklists, you may find that a domain you normally visit stops working. If this is the case, you could run this command to scan for strings in the list of blocked domains and it will return the list the domain is found on. This proved useful a while back when the Mahakala list was adding apple.com and microsoft.com to their block list.
+ This function is useful to find out what list a domain appears on. Since we don't control what the third-parties put on the blocklists, you may find that a domain you normally visit stops working. If this is the case, you could run this command to scan for strings in the list of blocked domains and it will return the list the domain is found on. This proved useful a while back when the Mahakala list was adding apple.com and microsoft.com to their block list.
diff --git a/img/favicon.png b/img/favicon.png
deleted file mode 100644
index 35dd8c98..00000000
Binary files a/img/favicon.png and /dev/null differ
diff --git a/img/favicons/android-chrome-192x192.png b/img/favicons/android-chrome-192x192.png
new file mode 100644
index 00000000..74da2e5d
Binary files /dev/null and b/img/favicons/android-chrome-192x192.png differ
diff --git a/img/favicons/android-chrome-512x512.png b/img/favicons/android-chrome-512x512.png
new file mode 100644
index 00000000..6a6281f6
Binary files /dev/null and b/img/favicons/android-chrome-512x512.png differ
diff --git a/img/favicons/apple-touch-icon.png b/img/favicons/apple-touch-icon.png
new file mode 100644
index 00000000..1b4b7215
Binary files /dev/null and b/img/favicons/apple-touch-icon.png differ
diff --git a/img/favicons/favicon-16x16.png b/img/favicons/favicon-16x16.png
new file mode 100644
index 00000000..cd2773bf
Binary files /dev/null and b/img/favicons/favicon-16x16.png differ
diff --git a/img/favicons/favicon-32x32.png b/img/favicons/favicon-32x32.png
new file mode 100644
index 00000000..92c1c601
Binary files /dev/null and b/img/favicons/favicon-32x32.png differ
diff --git a/img/favicons/favicon.ico b/img/favicons/favicon.ico
new file mode 100644
index 00000000..c603e12a
Binary files /dev/null and b/img/favicons/favicon.ico differ
diff --git a/img/favicons/manifest.json b/img/favicons/manifest.json
new file mode 100644
index 00000000..a4c4defe
--- /dev/null
+++ b/img/favicons/manifest.json
@@ -0,0 +1,19 @@
+{
+ "name": "Pi-hole Admin Console",
+ "short_name": "Pi-hole",
+ "icons": [
+ {
+ "src": "android-chrome-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "android-chrome-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ }
+ ],
+ "theme_color": "#367fa9",
+ "background_color": "#367fa9",
+ "display": "standalone"
+}
diff --git a/img/favicons/mstile-150x150.png b/img/favicons/mstile-150x150.png
new file mode 100644
index 00000000..2436d899
Binary files /dev/null and b/img/favicons/mstile-150x150.png differ
diff --git a/img/favicons/safari-pinned-tab.svg b/img/favicons/safari-pinned-tab.svg
new file mode 100644
index 00000000..ecbe5a9f
--- /dev/null
+++ b/img/favicons/safari-pinned-tab.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/img/logo.svg b/img/logo.svg
index eeeec322..3955f31b 100644
--- a/img/logo.svg
+++ b/img/logo.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/index.php b/index.php
index f34a7171..78c1a464 100644
--- a/index.php
+++ b/index.php
@@ -32,7 +32,7 @@
diff --git a/scripts/pi-hole/js/auditlog.js b/scripts/pi-hole/js/auditlog.js
index 95c7fc3d..3850b5de 100644
--- a/scripts/pi-hole/js/auditlog.js
+++ b/scripts/pi-hole/js/auditlog.js
@@ -1,11 +1,12 @@
/* 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. */
+ * (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. */
+
// Define global variables
-var timeLineChart, queryTypeChart, forwardDestinationChart, auditList = [], auditTimeout;
+var auditTimeout = null;
// Credit: http://stackoverflow.com/questions/1787322/htmlspecialchars-equivalent-in-javascript/4835406#4835406
function escapeHtml(text) {
@@ -13,124 +14,145 @@ function escapeHtml(text) {
"&": "&",
"<": "<",
">": ">",
- "\"": """,
- "\'": "'"
+ '"': """,
+ "'": "'"
};
- return text.replace(/[&<>"']/g, function(m) { return map[m]; });
+ return text.replace(/[&<>"']/g, function(m) {
+ return map[m];
+ });
}
function updateTopLists() {
- $.getJSON("api.php?topItems=audit", function(data) {
+ $.getJSON("api.php?topItems=audit", function(data) {
+ if ("FTLnotrunning" in data) {
+ return;
+ }
- if("FTLnotrunning" in data)
- {
- return;
+ // Clear tables before filling them with data
+ $("#domain-frequency td")
+ .parent()
+ .remove();
+ $("#ad-frequency td")
+ .parent()
+ .remove();
+ var domaintable = $("#domain-frequency").find("tbody:last");
+ var adtable = $("#ad-frequency").find("tbody:last");
+ var url, domain;
+ for (domain in data.top_queries) {
+ if (Object.prototype.hasOwnProperty.call(data.top_queries, domain)) {
+ // Sanitize domain
+ domain = escapeHtml(domain);
+ url = '' + domain + "";
+ domaintable.append(
+ "
" +
+ url +
+ "
" +
+ data.top_queries[domain] +
+ '
'
+ );
+ }
+ }
+
+ for (domain in data.top_ads) {
+ if (Object.prototype.hasOwnProperty.call(data.top_ads, domain)) {
+ var input = domain.split(" ");
+ // Sanitize domain
+ var printdomain = escapeHtml(input[0]);
+ if (input.length > 1) {
+ url =
+ '' +
+ printdomain +
+ " (wildcard blocked)";
+ adtable.append(
+ "
'
+ );
+ }
+ }
- listsStillLoading--;
- if(listsStillLoading === 0)
- timeoutWarning.hide();
- });
+ $("#client-frequency .overlay").hide();
+
+ listsStillLoading--;
+ if (listsStillLoading === 0) timeoutWarning.hide();
+ });
}
function updateTopDomainsChart() {
- $("#domain-frequency .overlay").show();
- $.getJSON("api_db.php?topDomains&from="+from+"&until="+until, function(data) {
+ $("#domain-frequency .overlay").show();
+ $.getJSON("api_db.php?topDomains&from=" + from + "&until=" + until, function(data) {
+ // Clear tables before filling them with data
+ $("#domain-frequency td")
+ .parent()
+ .remove();
+ var domaintable = $("#domain-frequency").find("tbody:last");
+ var domain, percentage;
+ var sum = 0;
+ for (domain in data.top_domains) {
+ if (Object.prototype.hasOwnProperty.call(data.top_domains, domain)) {
+ sum += data.top_domains[domain];
+ }
+ }
- // Clear tables before filling them with data
- $("#domain-frequency td").parent().remove();
- var domaintable = $("#domain-frequency").find("tbody:last");
- var domain, percentage;
- var sum = 0;
- for (domain in data.top_domains) {
- if ({}.hasOwnProperty.call(data.top_domains, domain)){
- sum += data.top_domains[domain];
- }
+ for (domain in data.top_domains) {
+ if (Object.prototype.hasOwnProperty.call(data.top_domains, domain)) {
+ // Sanitize domain
+ domain = escapeHtml(domain);
+ if (escapeHtml(domain) !== domain) {
+ // Make a copy with the escaped index if necessary
+ data.top_domains[escapeHtml(domain)] = data.top_domains[domain];
}
- for (domain in data.top_domains) {
+ percentage = (data.top_domains[domain] / sum) * 100.0;
+ domaintable.append(
+ "
" +
+ domain +
+ "
" +
+ data.top_domains[domain] +
+ '
'
+ );
+ }
+ }
- if ({}.hasOwnProperty.call(data.top_domains, domain)){
- // Sanitize domain
- domain = escapeHtml(domain);
- if(escapeHtml(domain) !== domain)
- {
- // Make a copy with the escaped index if necessary
- data.top_domains[escapeHtml(domain)] = data.top_domains[domain];
- }
+ $("#domain-frequency .overlay").hide();
- percentage = data.top_domains[domain] / sum * 100.0;
- domaintable.append("
" + domain +
- "
" + data.top_domains[domain] + "
");
- }
-
- }
-
- $("#domain-frequency .overlay").hide();
-
- listsStillLoading--;
- if(listsStillLoading === 0)
- timeoutWarning.hide();
- });
+ listsStillLoading--;
+ if (listsStillLoading === 0) timeoutWarning.hide();
+ });
}
function updateTopAdsChart() {
- $("#ad-frequency .overlay").show();
- $.getJSON("api_db.php?topAds&from="+from+"&until="+until, function(data) {
+ $("#ad-frequency .overlay").show();
+ $.getJSON("api_db.php?topAds&from=" + from + "&until=" + until, function(data) {
+ // Clear tables before filling them with data
+ $("#ad-frequency td")
+ .parent()
+ .remove();
+ var adtable = $("#ad-frequency").find("tbody:last");
+ var ad, percentage;
+ var sum = 0;
+ for (ad in data.top_ads) {
+ if (Object.prototype.hasOwnProperty.call(data.top_ads, ad)) {
+ sum += data.top_ads[ad];
+ }
+ }
- // Clear tables before filling them with data
- $("#ad-frequency td").parent().remove();
- var adtable = $("#ad-frequency").find("tbody:last");
- var ad, percentage;
- var sum = 0;
- for (ad in data.top_ads) {
- if ({}.hasOwnProperty.call(data.top_ads, ad)){
- sum += data.top_ads[ad];
- }
+ for (ad in data.top_ads) {
+ if (Object.prototype.hasOwnProperty.call(data.top_ads, ad)) {
+ // Sanitize ad
+ ad = escapeHtml(ad);
+ if (escapeHtml(ad) !== ad) {
+ // Make a copy with the escaped index if necessary
+ data.top_ads[escapeHtml(ad)] = data.top_ads[ad];
}
- for (ad in data.top_ads) {
+ percentage = (data.top_ads[ad] / sum) * 100.0;
+ adtable.append(
+ "
" +
+ ad +
+ "
" +
+ data.top_ads[ad] +
+ '
'
+ );
+ }
+ }
- if ({}.hasOwnProperty.call(data.top_ads, ad)){
- // Sanitize ad
- ad = escapeHtml(ad);
- if(escapeHtml(ad) !== ad)
- {
- // Make a copy with the escaped index if necessary
- data.top_ads[escapeHtml(ad)] = data.top_ads[ad];
- }
+ $("#ad-frequency .overlay").hide();
- percentage = data.top_ads[ad] / sum * 100.0;
- adtable.append("
" + ad + "
" + data.top_ads[ad] + "
");
- }
-
- }
-
- $("#ad-frequency .overlay").hide();
-
- listsStillLoading--;
- if(listsStillLoading === 0)
- timeoutWarning.hide();
- });
+ listsStillLoading--;
+ if (listsStillLoading === 0) timeoutWarning.hide();
+ });
}
$("#querytime").on("apply.daterangepicker", function(ev, picker) {
- $(this).val(picker.startDate.format(dateformat) + " to " + picker.endDate.format(dateformat));
- timeoutWarning.show();
- listsStillLoading = 3;
- updateTopClientsChart();
- updateTopDomainsChart();
- updateTopAdsChart();
+ $(this).val(picker.startDate.format(dateformat) + " to " + picker.endDate.format(dateformat));
+ timeoutWarning.show();
+ listsStillLoading = 3;
+ updateTopClientsChart();
+ updateTopDomainsChart();
+ updateTopAdsChart();
});
diff --git a/scripts/pi-hole/js/db_queries.js b/scripts/pi-hole/js/db_queries.js
index c7a746b3..0952674a 100644
--- a/scripts/pi-hole/js/db_queries.js
+++ b/scripts/pi-hole/js/db_queries.js
@@ -1,18 +1,22 @@
/* 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. */
+ * (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
- moment
-*/
+/* global moment:false */
var start__ = moment().subtract(6, "days");
-var from = moment(start__).utc().valueOf()/1000;
+var from =
+ moment(start__)
+ .utc()
+ .valueOf() / 1000;
var end__ = moment();
-var until = moment(end__).utc().valueOf()/1000;
+var until =
+ moment(end__)
+ .utc()
+ .valueOf() / 1000;
var instantquery = false;
var daterange;
@@ -22,343 +26,402 @@ var dateformat = "MMMM Do YYYY, HH:mm";
// Do we want to filter queries?
var GETDict = {};
-location.search.substr(1).split("&").forEach(function(item) {GETDict[item.split("=")[0]] = item.split("=")[1];});
+window.location.search
+ .substr(1)
+ .split("&")
+ .forEach(function(item) {
+ GETDict[item.split("=")[0]] = item.split("=")[1];
+ });
-if("from" in GETDict && "until" in GETDict)
-{
- from = parseInt(GETDict["from"]);
- until = parseInt(GETDict["until"]);
- start__ = moment(1000*from);
- end__ = moment(1000*until);
- instantquery = true;
+if ("from" in GETDict && "until" in GETDict) {
+ from = parseInt(GETDict.from);
+ until = parseInt(GETDict.until);
+ start__ = moment(1000 * from);
+ end__ = moment(1000 * until);
+ instantquery = true;
}
-$(function () {
- daterange = $("#querytime").daterangepicker(
+$(function() {
+ daterange = $("#querytime").daterangepicker(
{
- timePicker: true, timePickerIncrement: 15,
- locale: { format: "MMMM Do YYYY, HH:mm" },
- startDate: start__, endDate: end__,
+ timePicker: true,
+ timePickerIncrement: 15,
+ locale: { format: dateformat },
+ startDate: start__,
+ endDate: end__,
ranges: {
- "Today": [moment().startOf("day"), moment()],
- "Yesterday": [moment().subtract(1, "days").startOf("day"), moment().subtract(1, "days").endOf("day")],
+ Today: [moment().startOf("day"), moment()],
+ Yesterday: [
+ moment()
+ .subtract(1, "days")
+ .startOf("day"),
+ moment()
+ .subtract(1, "days")
+ .endOf("day")
+ ],
"Last 7 Days": [moment().subtract(6, "days"), moment()],
"Last 30 Days": [moment().subtract(29, "days"), moment()],
"This Month": [moment().startOf("month"), moment()],
- "Last Month": [moment().subtract(1, "month").startOf("month"), moment().subtract(1, "month").endOf("month")],
+ "Last Month": [
+ moment()
+ .subtract(1, "month")
+ .startOf("month"),
+ moment()
+ .subtract(1, "month")
+ .endOf("month")
+ ],
"This Year": [moment().startOf("year"), moment()],
"All Time": [moment(0), moment()]
},
- "opens": "center", "showDropdowns": true,
- "autoUpdateInput": false
+ opens: "center",
+ showDropdowns: true,
+ autoUpdateInput: false
},
- function (startt, endt) {
- from = moment(startt).utc().valueOf()/1000;
- until = moment(endt).utc().valueOf()/1000;
- });
+ function(startt, endt) {
+ from =
+ moment(startt)
+ .utc()
+ .valueOf() / 1000;
+ until =
+ moment(endt)
+ .utc()
+ .valueOf() / 1000;
+ }
+ );
});
var tableApi, statistics;
-function escapeRegex(text) {
- var map = {
- "(": "\\(",
- ")": "\\)",
- ".": "\\.",
- };
- return text.replace(/[().]/g, function(m) { return map[m]; });
+function add(domain, list) {
+ var token = $("#token").text();
+ var alInfo = $("#alInfo");
+ var alList = $("#alList");
+ var alDomain = $("#alDomain");
+ alDomain.html(domain);
+ var alSuccess = $("#alSuccess");
+ var alFailure = $("#alFailure");
+ var err = $("#err");
+
+ if (list === "white") {
+ alList.html("Whitelist");
+ } else {
+ alList.html("Blacklist");
+ }
+
+ alInfo.show();
+ alSuccess.hide();
+ alFailure.hide();
+ $.ajax({
+ url: "scripts/pi-hole/php/add.php",
+ method: "post",
+ data: { domain: domain, list: list, token: token },
+ success: function(response) {
+ if (
+ response.indexOf("not a valid argument") >= 0 ||
+ response.indexOf("is not a valid domain") >= 0
+ ) {
+ alFailure.show();
+ err.html(response);
+ alFailure.delay(4000).fadeOut(2000, function() {
+ alFailure.hide();
+ });
+ } else {
+ alSuccess.show();
+ alSuccess.delay(1000).fadeOut(2000, function() {
+ alSuccess.hide();
+ });
+ }
+
+ alInfo.delay(1000).fadeOut(2000, function() {
+ alInfo.hide();
+ alList.html("");
+ alDomain.html("");
+ });
+ },
+ error: function() {
+ alFailure.show();
+ err.html("");
+ alFailure.delay(1000).fadeOut(2000, function() {
+ alFailure.hide();
+ });
+ alInfo.delay(1000).fadeOut(2000, function() {
+ alInfo.hide();
+ alList.html("");
+ alDomain.html("");
+ });
+ }
+ });
}
-function add(domain,list) {
- var token = $("#token").html();
- var alInfo = $("#alInfo");
- var alList = $("#alList");
- var alDomain = $("#alDomain");
- alDomain.html(domain);
- var alSuccess = $("#alSuccess");
- var alFailure = $("#alFailure");
- var err = $("#err");
+function handleAjaxError(xhr, textStatus) {
+ if (textStatus === "timeout") {
+ alert("The server took too long to send the data.");
+ } else if (xhr.responseText.indexOf("Connection refused") >= 0) {
+ alert("An error occurred while loading the data: Connection refused. Is FTL running?");
+ } else {
+ alert("An unknown error occurred while loading the data.\n" + xhr.responseText);
+ }
- if(list === "white")
- {
- alList.html("Whitelist");
- }
- else
- {
- alList.html("Blacklist");
- }
-
- alInfo.show();
- alSuccess.hide();
- alFailure.hide();
- $.ajax({
- url: "scripts/pi-hole/php/add.php",
- method: "post",
- data: {"domain":domain, "list":list, "token":token},
- success: function(response) {
- if (response.indexOf("not a valid argument") >= 0 || response.indexOf("is not a valid domain") >= 0)
- {
- alFailure.show();
- err.html(response);
- alFailure.delay(4000).fadeOut(2000, function() { alFailure.hide(); });
- }
- else
- {
- alSuccess.show();
- alSuccess.delay(1000).fadeOut(2000, function() { alSuccess.hide(); });
- }
- alInfo.delay(1000).fadeOut(2000, function() {
- alInfo.hide();
- alList.html("");
- alDomain.html("");
- });
- },
- error: function(jqXHR, exception) {
- alFailure.show();
- err.html("");
- alFailure.delay(1000).fadeOut(2000, function() {
- alFailure.hide();
- });
- alInfo.delay(1000).fadeOut(2000, function() {
- alInfo.hide();
- alList.html("");
- alDomain.html("");
- });
- }
- });
-}
-function handleAjaxError( xhr, textStatus, error ) {
- if ( textStatus === "timeout" )
- {
- alert( "The server took too long to send the data." );
- }
- else if(xhr.responseText.indexOf("Connection refused") >= 0)
- {
- alert( "An error occurred while loading the data: Connection refused. Is FTL running?" );
- }
- else
- {
- alert( "An unknown error occurred while loading the data.\n"+xhr.responseText );
- }
- $("#all-queries_processing").hide();
- tableApi.clear();
- tableApi.draw();
+ $("#all-queries_processing").hide();
+ tableApi.clear();
+ tableApi.draw();
}
-function getQueryTypes()
-{
- var queryType = [];
- if($("#type_gravity").prop("checked"))
- {
- queryType.push(1);
- }
- if($("#type_forwarded").prop("checked"))
- {
- queryType.push(2);
- }
- if($("#type_cached").prop("checked"))
- {
- queryType.push(3);
- }
- if($("#type_regex").prop("checked"))
- {
- queryType.push(4);
- }
- if($("#type_blacklist").prop("checked"))
- {
- queryType.push(5);
- }
- if($("#type_external").prop("checked"))
- {
- // Multiple IDs correspond to this status
- // We request queries with all of them
- queryType.push([6,7,8]);
- }
- return queryType.join(",");
+function getQueryTypes() {
+ var queryType = [];
+ if ($("#type_gravity").prop("checked")) {
+ queryType.push(1);
+ }
+
+ if ($("#type_forwarded").prop("checked")) {
+ queryType.push(2);
+ }
+
+ if ($("#type_cached").prop("checked")) {
+ queryType.push(3);
+ }
+
+ if ($("#type_regex").prop("checked")) {
+ queryType.push(4);
+ }
+
+ if ($("#type_blacklist").prop("checked")) {
+ queryType.push(5);
+ }
+
+ if ($("#type_external").prop("checked")) {
+ // Multiple IDs correspond to this status
+ // We request queries with all of them
+ queryType.push([6, 7, 8]);
+ }
+
+ if ($("#type_gravity_CNAME").prop("checked")) {
+ queryType.push(9);
+ }
+
+ if ($("#type_regex_CNAME").prop("checked")) {
+ queryType.push(10);
+ }
+
+ if ($("#type_blacklist_CNAME").prop("checked")) {
+ queryType.push(11);
+ }
+
+ return queryType.join(",");
}
-var reloadCallback = function()
-{
- timeoutWarning.hide();
- statistics = [0,0,0,0];
- var data = tableApi.rows().data();
- for (var i = 0; i < data.length; i++) {
- statistics[0]++;
- if(data[i][4] === 1)
- {
- statistics[2]++;
- }
- else if(data[i][4] === 3)
- {
- statistics[1]++;
- }
- else if(data[i][4] === 4)
- {
- statistics[3]++;
- }
+var reloadCallback = function() {
+ timeoutWarning.hide();
+ statistics = [0, 0, 0, 0];
+ var data = tableApi.rows().data();
+ for (var i = 0; i < data.length; i++) {
+ statistics[0]++;
+ if (data[i][4] === 1) {
+ statistics[2]++;
+ } else if (data[i][4] === 3) {
+ statistics[1]++;
+ } else if (data[i][4] === 4) {
+ statistics[3]++;
}
- $("h3#dns_queries").text(statistics[0].toLocaleString());
- $("h3#ads_blocked_exact").text(statistics[2].toLocaleString());
- $("h3#ads_wildcard_blocked").text(statistics[3].toLocaleString());
+ }
- var percent = 0.0;
- if(statistics[2] + statistics[3] > 0)
- {
- percent = 100.0*(statistics[2] + statistics[3]) / statistics[0];
- }
- $("h3#ads_percentage_today").text(parseFloat(percent).toFixed(1).toLocaleString()+" %");
+ $("h3#dns_queries").text(statistics[0].toLocaleString());
+ $("h3#ads_blocked_exact").text(statistics[2].toLocaleString());
+ $("h3#ads_wildcard_blocked").text(statistics[3].toLocaleString());
+
+ var percent = 0.0;
+ if (statistics[2] + statistics[3] > 0) {
+ percent = (100.0 * (statistics[2] + statistics[3])) / statistics[0];
+ }
+
+ $("h3#ads_percentage_today").text(
+ parseFloat(percent)
+ .toFixed(1)
+ .toLocaleString() + " %"
+ );
};
function refreshTableData() {
- timeoutWarning.show();
- var APIstring = "api_db.php?getAllQueries&from="+from+"&until="+until;
- // Check if query type filtering is enabled
- var queryType = getQueryTypes();
- if(queryType !== "1,2,3,4,5,6")
- {
- APIstring += "&types="+queryType;
- }
- statistics = [0,0,0];
- tableApi.ajax.url(APIstring).load(reloadCallback);
+ timeoutWarning.show();
+ var APIstring = "api_db.php?getAllQueries&from=" + from + "&until=" + until;
+ // Check if query type filtering is enabled
+ var queryType = getQueryTypes();
+ if (queryType !== "1,2,3,4,5,6") {
+ APIstring += "&types=" + queryType;
+ }
+
+ statistics = [0, 0, 0];
+ tableApi.ajax.url(APIstring).load(reloadCallback);
}
$(document).ready(function() {
- var status;
+ var APIstring;
- var APIstring;
- if(instantquery)
- {
- APIstring = "api_db.php?getAllQueries&from="+from+"&until="+until;
- }
- else
- {
- APIstring = "api_db.php?getAllQueries=empty";
- }
- // Check if query type filtering is enabled
- var queryType = getQueryTypes();
- if(queryType !== 63) // 63 (0b00111111) = all possible query types are selected
- {
- APIstring += "&types="+queryType;
- }
+ if (instantquery) {
+ APIstring = "api_db.php?getAllQueries&from=" + from + "&until=" + until;
+ } else {
+ APIstring = "api_db.php?getAllQueries=empty";
+ }
- tableApi = $("#all-queries").DataTable( {
- "rowCallback": function( row, data, index ){
- var blocked, fieldtext, buttontext, color;
- switch (data[4])
- {
- case 1:
- blocked = true;
- color = "red";
- fieldtext = "Blocked (gravity)";
- buttontext = "";
- break;
- case 2:
- blocked = false;
- color = "green";
- fieldtext = "OK (forwarded)";
- buttontext = "";
- break;
- case 3:
- blocked = false;
- color = "green";
- fieldtext = "OK (cached)";
- buttontext = "";
- break;
- case 4:
- blocked = true;
- color = "red";
- fieldtext = "Blocked (regex/wildcard)";
- buttontext = "" ;
- break;
- case 5:
- blocked = true;
- color = "red";
- fieldtext = "Blocked (blacklist)";
- buttontext = "" ;
- break;
- case 6:
- blocked = true;
- color = "red";
- fieldtext = "Blocked (external, IP)";
- buttontext = "" ;
- break;
- case 7:
- blocked = true;
- color = "red";
- fieldtext = "Blocked (external, NULL)";
- buttontext = "" ;
- break;
- case 8:
- blocked = true;
- color = "red";
- fieldtext = "Blocked (external, NXRA)";
- buttontext = "" ;
- break;
- default:
- blocked = false;
- color = "black";
- fieldtext = "Unknown";
- buttontext = "";
- }
+ // Check if query type filtering is enabled
+ var queryType = getQueryTypes();
+ if (queryType !== 63) {
+ // 63 (0b00111111) = all possible query types are selected
+ APIstring += "&types=" + queryType;
+ }
- $(row).css("color", color);
- $("td:eq(4)", row).html(fieldtext);
- $("td:eq(5)", row).html(buttontext);
- },
- dom: "<'row'<'col-sm-12'f>>" +
- "<'row'<'col-sm-4'l><'col-sm-8'p>>" +
- "<'row'<'col-sm-12'<'table-responsive'tr>>>" +
- "<'row'<'col-sm-5'i><'col-sm-7'p>>",
- "ajax": {
- "url": APIstring,
- "error": handleAjaxError,
- "dataSrc": function(data){
- var dataIndex = 0;
- return data.data.map(function(x){
- x[0] = x[0] * 1e6 + (dataIndex++);
- return x;
- });
- }
- },
- "autoWidth" : false,
- "processing": true,
- "deferRender": true,
- "order" : [[0, "desc"]],
- "columns": [
- { "width" : "15%", "render": function (data, type, full, meta) { if(type === "display"){return moment.unix(Math.floor(data/1e6)).format("Y-MM-DD [ ]HH:mm:ss z");}else{return data;} }},
- { "width" : "10%" },
- { "width" : "40%" },
- { "width" : "20%" },
- { "width" : "10%" },
- { "width" : "5%" },
- ],
- "lengthMenu": [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]],
- "columnDefs": [ {
- "targets": -1,
- "data": null,
- "defaultContent": ""
- } ],
- "initComplete": reloadCallback
- });
- $("#all-queries tbody").on( "click", "button", function () {
- var data = tableApi.row( $(this).parents("tr") ).data();
- if (data[4] === 1 || data[4] === 4 || data[5] === 5)
- {
- add(data[2],"white");
+ tableApi = $("#all-queries").DataTable({
+ rowCallback: function(row, data) {
+ var fieldtext, buttontext, color;
+ switch (data[4]) {
+ case 1:
+ color = "red";
+ fieldtext = "Blocked (gravity)";
+ buttontext =
+ '';
+ break;
+ case 2:
+ color = "green";
+ fieldtext = "OK (forwarded)";
+ buttontext =
+ '';
+ break;
+ case 3:
+ color = "green";
+ fieldtext = "OK (cached)";
+ buttontext =
+ '';
+ break;
+ case 4:
+ color = "red";
+ fieldtext = "Blocked (regex blacklist)";
+ buttontext =
+ '';
+ break;
+ case 5:
+ color = "red";
+ fieldtext = "Blocked (exact blacklist)";
+ buttontext =
+ '';
+ break;
+ case 6:
+ color = "red";
+ fieldtext = "Blocked (external, IP)";
+ buttontext = "";
+ break;
+ case 7:
+ color = "red";
+ fieldtext = "Blocked (external, NULL)";
+ buttontext = "";
+ break;
+ case 8:
+ color = "red";
+ fieldtext = "Blocked (external, NXRA)";
+ buttontext = "";
+ break;
+ case 9:
+ color = "red";
+ fieldtext = "Blocked (gravity, CNAME)";
+ buttontext =
+ '';
+ break;
+ case 10:
+ color = "red";
+ fieldtext = "Blocked (regex blacklist, CNAME)";
+ buttontext =
+ '';
+ break;
+ case 11:
+ color = "red";
+ fieldtext = "Blocked (exact blacklist, CNAME)";
+ buttontext =
+ '';
+ break;
+ default:
+ color = "black";
+ fieldtext = "Unknown";
+ buttontext = "";
+ }
+
+ $(row).css("color", color);
+ $("td:eq(4)", row).html(fieldtext);
+ $("td:eq(5)", row).html(buttontext);
+
+ // Substitute domain by "." if empty
+ var domain = data[2];
+ if (domain.length === 0) {
+ domain = ".";
+ }
+
+ $("td:eq(2)", row).text(domain);
+ },
+ dom:
+ "<'row'<'col-sm-12'f>>" +
+ "<'row'<'col-sm-4'l><'col-sm-8'p>>" +
+ "<'row'<'col-sm-12'<'table-responsive'tr>>>" +
+ "<'row'<'col-sm-5'i><'col-sm-7'p>>",
+ ajax: {
+ url: APIstring,
+ error: handleAjaxError,
+ dataSrc: function(data) {
+ var dataIndex = 0;
+ return data.data.map(function(x) {
+ x[0] = x[0] * 1e6 + dataIndex++;
+ return x;
+ });
+ }
+ },
+ autoWidth: false,
+ processing: true,
+ deferRender: true,
+ order: [[0, "desc"]],
+ columns: [
+ {
+ width: "15%",
+ render: function(data, type) {
+ if (type === "display") {
+ return moment
+ .unix(Math.floor(data / 1e6))
+ .format("Y-MM-DD [ ]HH:mm:ss z");
+ }
+
+ return data;
}
- else
- {
- add(data[2],"black");
- }
- } );
-
- if(instantquery)
- {
- daterange.val(start__.format("MMMM Do YYYY, HH:mm") + " - " + end__.format("MMMM Do YYYY, HH:mm"));
+ },
+ { width: "10%" },
+ { width: "40%" },
+ { width: "20%" },
+ { width: "10%" },
+ { width: "5%" }
+ ],
+ lengthMenu: [
+ [10, 25, 50, 100, -1],
+ [10, 25, 50, 100, "All"]
+ ],
+ columnDefs: [
+ {
+ targets: -1,
+ data: null,
+ defaultContent: ""
+ }
+ ],
+ initComplete: reloadCallback
+ });
+ $("#all-queries tbody").on("click", "button", function() {
+ var data = tableApi.row($(this).parents("tr")).data();
+ if (data[4] === 1 || data[4] === 4 || data[5] === 5) {
+ add(data[2], "white");
+ } else {
+ add(data[2], "black");
}
-} );
+ });
+
+ if (instantquery) {
+ daterange.val(start__.format(dateformat) + " - " + end__.format(dateformat));
+ }
+});
$("#querytime").on("apply.daterangepicker", function(ev, picker) {
- $(this).val(picker.startDate.format(dateformat) + " to " + picker.endDate.format(dateformat));
- refreshTableData();
+ $(this).val(picker.startDate.format(dateformat) + " to " + picker.endDate.format(dateformat));
+ refreshTableData();
});
diff --git a/scripts/pi-hole/js/debug.js b/scripts/pi-hole/js/debug.js
index 0a6b3ab1..fb0fd35b 100644
--- a/scripts/pi-hole/js/debug.js
+++ b/scripts/pi-hole/js/debug.js
@@ -1,66 +1,77 @@
+/* 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 ActiveXObject: false */
+
// Credit: http://stackoverflow.com/a/10642418/2087442
-function httpGet(ta,theUrl)
-{
- var xmlhttp;
- if (window.XMLHttpRequest)
- {
+function httpGet(ta, theUrl) {
+ var xmlhttp;
+ if (window.XMLHttpRequest) {
// code for IE7+
- xmlhttp = new XMLHttpRequest();
- }
- else
- {
+ xmlhttp = new XMLHttpRequest();
+ } else {
// code for IE6, IE5
- xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
+ xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
+ }
+
+ xmlhttp.onreadystatechange = function() {
+ if (xmlhttp.readyState === 4 && xmlhttp.status === 200) {
+ ta.show();
+ ta.empty();
+ ta.append(xmlhttp.responseText);
}
- xmlhttp.onreadystatechange=function()
- {
- if (xmlhttp.readyState === 4 && xmlhttp.status === 200)
- {
- ta.show();
- ta.empty();
- ta.append(xmlhttp.responseText);
- }
- };
- xmlhttp.open("GET", theUrl, false);
- xmlhttp.send();
+ };
+
+ xmlhttp.open("GET", theUrl, false);
+ xmlhttp.send();
}
function eventsource() {
- var ta = $("#output");
- var upload = $( "#upload" );
- var checked = "";
- var token = encodeURIComponent($("#token").html());
+ var ta = $("#output");
+ var upload = $("#upload");
+ var checked = "";
+ var token = encodeURIComponent($("#token").text());
- if(upload.prop("checked"))
- {
- checked = "upload";
- }
+ if (upload.prop("checked")) {
+ checked = "upload";
+ }
- // IE does not support EventSource - load whole content at once
- if (typeof EventSource !== "function") {
- httpGet(ta,"/admin/scripts/pi-hole/php/debug.php?IE&token="+token+"&"+checked);
- return;
- }
+ // IE does not support EventSource - load whole content at once
+ if (typeof EventSource !== "function") {
+ httpGet(ta, "scripts/pi-hole/php/debug.php?IE&token=" + token + "&" + checked);
+ return;
+ }
- var host = window.location.host;
- var source = new EventSource("/admin/scripts/pi-hole/php/debug.php?&token="+token+"&"+checked);
+ var source = new EventSource("scripts/pi-hole/php/debug.php?&token=" + token + "&" + checked);
- // Reset and show field
- ta.empty();
- ta.show();
+ // Reset and show field
+ ta.empty();
+ ta.show();
- source.addEventListener("message", function(e) {
- ta.append(e.data);
- }, false);
+ source.addEventListener(
+ "message",
+ function(e) {
+ ta.append(e.data);
+ },
+ false
+ );
- // Will be called when script has finished
- source.addEventListener("error", function(e) {
- source.close();
- }, false);
+ // Will be called when script has finished
+ source.addEventListener(
+ "error",
+ function() {
+ source.close();
+ },
+ false
+ );
}
-$("#debugBtn").on("click", function(){
- $("#debugBtn").attr("disabled", true);
- $("#upload").attr("disabled", true);
- eventsource();
+$("#debugBtn").on("click", function() {
+ $("#debugBtn").attr("disabled", true);
+ $("#upload").attr("disabled", true);
+ eventsource();
});
diff --git a/scripts/pi-hole/js/footer.js b/scripts/pi-hole/js/footer.js
index 155efa29..7c709625 100644
--- a/scripts/pi-hole/js/footer.js
+++ b/scripts/pi-hole/js/footer.js
@@ -1,208 +1,198 @@
/* 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. */
+ * (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. */
+
//The following functions allow us to display time until pi-hole is enabled after disabling.
//Works between all pages
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
+ 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(action)
-{
- var status = $("#status");
- var ena = $("#pihole-enable");
- var dis = $("#pihole-disable");
+function piholeChanged(action) {
+ var status = $("#status");
+ var ena = $("#pihole-enable");
+ var dis = $("#pihole-disable");
- switch(action) {
- case "enabled":
- status.html(" Active");
- ena.hide();
- dis.show();
- dis.removeClass("active");
- break;
+ switch (action) {
+ case "enabled":
+ status.html(" Active");
+ ena.hide();
+ dis.show();
+ dis.removeClass("active");
+ break;
- case "disabled":
- status.html(" Offline");
- ena.show();
- dis.hide();
- break;
- }
+ case "disabled":
+ status.html(" Offline");
+ ena.show();
+ dis.hide();
+ break;
+ default:
+ // nothing
+ }
}
-function countDown(){
- var ena = $("#enableLabel");
- var enaT = $("#enableTimer");
- var target = new Date(parseInt(enaT.html()));
- var seconds = Math.round((target.getTime() - new Date().getTime()) / 1000);
+function countDown() {
+ var ena = $("#enableLabel");
+ var enaT = $("#enableTimer");
+ var target = new Date(parseInt(enaT.html()));
+ var seconds = Math.round((target.getTime() - new Date().getTime()) / 1000);
- if(seconds > 0){
- setTimeout(countDown,1000);
- ena.text("Enable (" + secondsTimeSpanToHMS(seconds) + ")");
- }
- else
- {
- ena.text("Enable");
- piholeChanged("enabled");
- localStorage.removeItem("countDownTarget");
- }
+ if (seconds > 0) {
+ setTimeout(countDown, 1000);
+ ena.text("Enable (" + secondsTimeSpanToHMS(seconds) + ")");
+ } else {
+ ena.text("Enable");
+ piholeChanged("enabled");
+ localStorage.removeItem("countDownTarget");
+ }
}
-function piholeChange(action, duration)
-{
- var token = encodeURIComponent($("#token").html());
- var enaT = $("#enableTimer");
- var btnStatus;
+function piholeChange(action, duration) {
+ var token = encodeURIComponent($("#token").text());
+ var enaT = $("#enableTimer");
+ var btnStatus;
- switch(action) {
- case "enable":
- btnStatus = $("#flip-status-enable");
- btnStatus.html("");
- $.getJSON("api.php?enable&token=" + token, function(data) {
- if(data.status === "enabled") {
- btnStatus.html("");
- piholeChanged("enabled");
- }
- });
- break;
+ switch (action) {
+ case "enable":
+ btnStatus = $("#flip-status-enable");
+ btnStatus.html("");
+ $.getJSON("api.php?enable&token=" + token, function(data) {
+ if (data.status === "enabled") {
+ btnStatus.html("");
+ piholeChanged("enabled");
+ }
+ });
+ break;
- case "disable":
- btnStatus = $("#flip-status-disable");
- btnStatus.html("");
- $.getJSON("api.php?disable=" + duration + "&token=" + token, function(data) {
- if(data.status === "disabled") {
- btnStatus.html("");
- piholeChanged("disabled");
- if(duration > 0)
- {
- enaT.html(new Date().getTime() + duration * 1000);
- setTimeout(countDown,100);
- }
- }
- });
- break;
- }
+ case "disable":
+ btnStatus = $("#flip-status-disable");
+ btnStatus.html("");
+ $.getJSON("api.php?disable=" + duration + "&token=" + token, function(data) {
+ if (data.status === "disabled") {
+ btnStatus.html("");
+ piholeChanged("disabled");
+ if (duration > 0) {
+ enaT.html(new Date().getTime() + duration * 1000);
+ setTimeout(countDown, 100);
+ }
+ }
+ });
+ break;
+
+ default:
+ // nothing
+ }
}
-$( document ).ready(function() {
- var enaT = $("#enableTimer");
- var target = new Date(parseInt(enaT.html()));
- var seconds = Math.round((target.getTime() - new Date().getTime()) / 1000);
- if (seconds > 0)
- {
- setTimeout(countDown,100);
- }
+$(document).ready(function() {
+ var enaT = $("#enableTimer");
+ var target = new Date(parseInt(enaT.html()));
+ var seconds = Math.round((target.getTime() - new Date().getTime()) / 1000);
+ if (seconds > 0) {
+ setTimeout(countDown, 100);
+ }
});
// Handle Enable/Disable
-$("#pihole-enable").on("click", function(e){
- e.preventDefault();
- localStorage.removeItem("countDownTarget");
- piholeChange("enable","");
+$("#pihole-enable").on("click", function(e) {
+ e.preventDefault();
+ localStorage.removeItem("countDownTarget");
+ piholeChange("enable", "");
});
-$("#pihole-disable-permanently").on("click", function(e){
- e.preventDefault();
- piholeChange("disable","0");
+$("#pihole-disable-permanently").on("click", function(e) {
+ e.preventDefault();
+ piholeChange("disable", "0");
});
-$("#pihole-disable-10s").on("click", function(e){
- e.preventDefault();
- piholeChange("disable","10");
+$("#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-30s").on("click", function(e) {
+ e.preventDefault();
+ piholeChange("disable", "30");
});
-$("#pihole-disable-5m").on("click", function(e){
- e.preventDefault();
- piholeChange("disable","300");
+$("#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);
+$("#pihole-disable-custom").on("click", function(e) {
+ e.preventDefault();
+ var custVal = $("#customTimeout").val();
+ custVal = $("#btnMins").hasClass("active") ? custVal * 60 : custVal;
+ piholeChange("disable", custVal);
});
// Session timer
-var sessionvalidity = parseInt(document.getElementById("sessiontimercounter").textContent);
-var start = new Date;
+var sessionTimerCounter = document.getElementById("sessiontimercounter");
+var sessionvalidity = parseInt(sessionTimerCounter.textContent);
+var start = new Date();
-function updateSessionTimer()
-{
- start = new Date;
- start.setSeconds(start.getSeconds() + sessionvalidity);
+function updateSessionTimer() {
+ start = new Date();
+ start.setSeconds(start.getSeconds() + sessionvalidity);
}
-if(sessionvalidity > 0)
-{
- // setSeconds will correctly handle wrap-around cases
- updateSessionTimer();
+if (sessionvalidity > 0) {
+ // setSeconds will correctly handle wrap-around cases
+ updateSessionTimer();
- setInterval(function() {
- var current = new Date;
- var totalseconds = (start - current) / 1000;
+ setInterval(function() {
+ var current = new Date();
+ var totalseconds = (start - current) / 1000;
+ var minutes = Math.floor(totalseconds / 60);
+ if (minutes < 10) {
+ minutes = "0" + minutes;
+ }
- // var hours = Math.floor(totalseconds / 3600);
- // totalseconds = totalseconds % 3600;
+ var seconds = Math.floor(totalseconds % 60);
+ if (seconds < 10) {
+ seconds = "0" + seconds;
+ }
- var minutes = Math.floor(totalseconds / 60);
- if(minutes < 10){ minutes = "0" + minutes; }
-
- var seconds = Math.floor(totalseconds % 60);
- if(seconds < 10){ seconds = "0" + seconds; }
-
- if(totalseconds > 0)
- {
- document.getElementById("sessiontimercounter").textContent = minutes + ":" + seconds;
- }
- else
- {
- document.getElementById("sessiontimercounter").textContent = "-- : --";
- }
-
- }, 1000);
-}
-else
-{
- document.getElementById("sessiontimer").style.display = "none";
+ if (totalseconds > 0) {
+ sessionTimerCounter.textContent = minutes + ":" + seconds;
+ } else {
+ sessionTimerCounter.textContent = "-- : --";
+ }
+ }, 1000);
+} else {
+ document.getElementById("sessiontimer").style.display = "none";
}
// Handle Strg + Enter button on Login page
$(document).keypress(function(e) {
- if((e.keyCode === 10 || e.keyCode === 13) && e.ctrlKey && $("#loginpw").is(":focus")) {
- $("#loginform").attr("action", "settings.php");
- $("#loginform").submit();
- }
+ if ((e.keyCode === 10 || e.keyCode === 13) && e.ctrlKey && $("#loginpw").is(":focus")) {
+ $("#loginform").attr("action", "settings.php");
+ $("#loginform").submit();
+ }
});
-function testCookies()
-{
- if (navigator.cookieEnabled)
- {
- return true;
- }
+function testCookies() {
+ if (navigator.cookieEnabled) {
+ return true;
+ }
- // set and read cookie
- document.cookie = "cookietest=1";
- var ret = document.cookie.indexOf("cookietest=") !== -1;
+ // 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";
+ // delete cookie
+ document.cookie = "cookietest=1; expires=Thu, 01-Jan-1970 00:00:01 GMT";
- return ret;
+ return ret;
}
$(function() {
- if(!testCookies() && $("#cookieInfo").length)
- {
- $("#cookieInfo").show();
- }
+ if (!testCookies() && $("#cookieInfo").length) {
+ $("#cookieInfo").show();
+ }
});
diff --git a/scripts/pi-hole/js/gravity.js b/scripts/pi-hole/js/gravity.js
index bf31254b..1512a800 100644
--- a/scripts/pi-hole/js/gravity.js
+++ b/scripts/pi-hole/js/gravity.js
@@ -1,72 +1,80 @@
/* 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. */
+ * (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. */
+
function eventsource() {
- var alInfo = $("#alInfo");
- var alSuccess = $("#alSuccess");
- var ta = $("#output");
+ var alInfo = $("#alInfo");
+ var alSuccess = $("#alSuccess");
+ var ta = $("#output");
- // IE does not support EventSource - exit early
- if (typeof EventSource !== "function") {
- ta.show();
- ta.html("Updating lists of ad-serving domains is not supported with this browser!");
- return;
- }
- var source = new EventSource("scripts/pi-hole/php/gravity.sh.php");
-
- ta.html("");
+ // IE does not support EventSource - exit early
+ if (typeof EventSource !== "function") {
ta.show();
- alInfo.show();
- alSuccess.hide();
+ ta.html("Updating lists of ad-serving domains is not supported with this browser!");
+ return;
+ }
- source.addEventListener("message", function(e) {
- if(e.data.indexOf("Pi-hole blocking is") !== -1)
- {
- alSuccess.show();
- }
+ var source = new EventSource("scripts/pi-hole/php/gravity.sh.php");
- // Detect ${OVER}
- if(e.data.indexOf("<------") !== -1)
- {
- ta.text(ta.text().substring(0, ta.text().lastIndexOf("\n")) + "\n");
- var new_string = e.data.replace("<------", "");
- ta.append(new_string);
- }
- else
- {
- ta.append(e.data);
- }
+ ta.html("");
+ ta.show();
+ alInfo.show();
+ alSuccess.hide();
- }, false);
+ source.addEventListener(
+ "message",
+ function(e) {
+ if (e.data.indexOf("Pi-hole blocking is") !== -1) {
+ alSuccess.show();
+ }
- // Will be called when script has finished
- source.addEventListener("error", function(e) {
- alInfo.delay(1000).fadeOut(2000, function() { alInfo.hide(); });
- source.close();
- $("#gravityBtn").removeAttr("disabled");
- }, false);
+ // Detect ${OVER}
+ if (e.data.indexOf("<------") !== -1) {
+ ta.text(ta.text().substring(0, ta.text().lastIndexOf("\n")) + "\n");
+ var new_string = e.data.replace("<------", "");
+ ta.append(new_string);
+ } else {
+ ta.append(e.data);
+ }
+ },
+ false
+ );
+
+ // Will be called when script has finished
+ source.addEventListener(
+ "error",
+ function() {
+ alInfo.delay(1000).fadeOut(2000, function() {
+ alInfo.hide();
+ });
+ source.close();
+ $("#gravityBtn").removeAttr("disabled");
+ },
+ false
+ );
}
-$("#gravityBtn").on("click", function(){
- $("#gravityBtn").attr("disabled", true);
- eventsource();
+$("#gravityBtn").on("click", function() {
+ $("#gravityBtn").attr("disabled", true);
+ eventsource();
});
// Handle hiding of alerts
-$(function(){
- $("[data-hide]").on("click", function(){
- $(this).closest("." + $(this).attr("data-hide")).hide();
- });
+$(function() {
+ $("[data-hide]").on("click", function() {
+ $(this)
+ .closest("." + $(this).attr("data-hide"))
+ .hide();
+ });
- // Do we want to start updating immediately?
- // gravity.php?go
- var searchString = window.location.search.substring(1);
- if(searchString.indexOf("go") !== -1)
- {
- $("#gravityBtn").attr("disabled", true);
- eventsource();
- }
+ // Do we want to start updating immediately?
+ // gravity.php?go
+ var searchString = window.location.search.substring(1);
+ if (searchString.indexOf("go") !== -1) {
+ $("#gravityBtn").attr("disabled", true);
+ eventsource();
+ }
});
diff --git a/scripts/pi-hole/js/groups-adlists.js b/scripts/pi-hole/js/groups-adlists.js
new file mode 100644
index 00000000..a961db98
--- /dev/null
+++ b/scripts/pi-hole/js/groups-adlists.js
@@ -0,0 +1,382 @@
+/* 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 */
+
+var table;
+var groups = [];
+var token = $("#token").html();
+
+function get_groups() {
+ $.post(
+ "scripts/pi-hole/php/groups.php",
+ { action: "get_groups", token: token },
+ function(data) {
+ groups = data.data;
+ initTable();
+ },
+ "json"
+ );
+}
+
+$(document).ready(function() {
+ $("#btnAdd").on("click", addAdlist);
+
+ utils.bsSelect_defaults();
+ get_groups();
+});
+
+function initTable() {
+ table = $("#adlistsTable").DataTable({
+ ajax: {
+ url: "scripts/pi-hole/php/groups.php",
+ data: { action: "get_adlists", token: token },
+ type: "POST"
+ },
+ order: [[0, "asc"]],
+ columns: [
+ { data: "id", visible: false },
+ { data: "address" },
+ { data: "enabled", searchable: false },
+ { data: "comment" },
+ { data: "groups", searchable: false },
+ { data: null, width: "80px", orderable: false }
+ ],
+ drawCallback: function() {
+ $('button[id^="deleteAdlist_"]').on("click", deleteAdlist);
+ // Remove visible dropdown to prevent orphaning
+ $("body > .bootstrap-select.dropdown").remove();
+ },
+ rowCallback: function(row, data) {
+ $(row).attr("data-id", data.id);
+ var tooltip =
+ "Added: " +
+ utils.datetime(data.date_added) +
+ "\nLast modified: " +
+ utils.datetime(data.date_modified) +
+ "\nDatabase ID: " +
+ data.id;
+ $("td:eq(0)", row).html(
+ '' +
+ data.address +
+ ""
+ );
+
+ var disabled = data.enabled === 0;
+ $("td:eq(1)", row).html(
+ '"
+ );
+ var statusEl = $("#status_" + data.id, row);
+ statusEl.bootstrapToggle({
+ on: "Enabled",
+ off: "Disabled",
+ size: "small",
+ onstyle: "success",
+ width: "80px"
+ });
+ statusEl.on("change", editAdlist);
+
+ $("td:eq(2)", row).html('');
+ var commentEl = $("#comment_" + data.id, row);
+ commentEl.val(data.comment);
+ commentEl.on("change", editAdlist);
+
+ $("td:eq(3)", row).empty();
+ $("td:eq(3)", row).append(
+ ''
+ );
+ var selectEl = $("#multiselect_" + data.id, row);
+ // Add all known groups
+ for (var i = 0; i < groups.length; i++) {
+ var data_sub = "";
+ if (!groups[i].enabled) {
+ data_sub = 'data-subtext="(disabled)"';
+ }
+
+ selectEl.append(
+ $("")
+ .val(groups[i].id)
+ .text(groups[i].name)
+ );
+ }
+
+ // Select assigned groups
+ selectEl.val(data.groups);
+ // Initialize bootstrap-select
+ selectEl
+ // fix dropdown if it would stick out right of the viewport
+ .on("show.bs.select", function() {
+ var winWidth = $(window).width();
+ var dropdownEl = $("body > .bootstrap-select.dropdown");
+ if (dropdownEl.length > 0) {
+ dropdownEl.removeClass("align-right");
+ var width = dropdownEl.width();
+ var left = dropdownEl.offset().left;
+ if (left + width > winWidth) {
+ dropdownEl.addClass("align-right");
+ }
+ }
+ })
+ .on("changed.bs.select", function() {
+ // enable Apply button
+ if ($(ApplyBtn).prop("disabled")) {
+ $(ApplyBtn)
+ .addClass("btn-success")
+ .prop("disabled", false)
+ .on("click", function() {
+ editAdlist.call(selectEl);
+ });
+ }
+ })
+ .on("hide.bs.select", function() {
+ // Restore values if drop-down menu is closed without clicking the Apply button
+ if (!$(ApplyBtn).prop("disabled")) {
+ $(this)
+ .val(data.groups)
+ .selectpicker("refresh");
+ $(ApplyBtn)
+ .removeClass("btn-success")
+ .prop("disabled", true)
+ .off("click");
+ }
+ })
+ .selectpicker()
+ .siblings(".dropdown-menu")
+ .find(".bs-actionsbox")
+ .prepend(
+ ''
+ );
+
+ var ApplyBtn = "#btn_apply_" + data.id;
+
+ var button =
+ '";
+ $("td:eq(4)", row).html(button);
+ },
+ dom:
+ "<'row'<'col-sm-4'l><'col-sm-8'f>>" +
+ "<'row'<'col-sm-12'<'table-responsive'tr>>>" +
+ "<'row'<'col-sm-5'i><'col-sm-7'p>>",
+ lengthMenu: [
+ [10, 25, 50, 100, -1],
+ [10, 25, 50, 100, "All"]
+ ],
+ stateSave: true,
+ stateSaveCallback: function(settings, data) {
+ // Store current state in client's local storage area
+ localStorage.setItem("groups-adlists-table", JSON.stringify(data));
+ },
+ stateLoadCallback: function() {
+ // Receive previous state from client's local storage area
+ var data = localStorage.getItem("groups-adlists-table");
+ // Return if not available
+ if (data === null) {
+ return null;
+ }
+
+ data = JSON.parse(data);
+ // Always start on the first page to show most recent queries
+ data.start = 0;
+ // Always start with empty search field
+ data.search.search = "";
+ // Reset visibility of ID column
+ data.columns[0].visible = false;
+ // Apply loaded state to table
+ return data;
+ }
+ });
+
+ table.on("order.dt", function() {
+ var order = table.order();
+ if (order[0][0] !== 0 || order[0][1] !== "asc") {
+ $("#resetButton").show();
+ } else {
+ $("#resetButton").hide();
+ }
+ });
+ $("#resetButton").on("click", function() {
+ table.order([[0, "asc"]]).draw();
+ $("#resetButton").hide();
+ });
+}
+
+function addAdlist() {
+ var address = $("#new_address").val();
+ var comment = $("#new_comment").val();
+
+ utils.disableAll();
+ utils.showAlert("info", "", "Adding adlist...", address);
+
+ if (address.length === 0) {
+ utils.showAlert("warning", "", "Warning", "Please specify an adlist address");
+ return;
+ }
+
+ $.ajax({
+ url: "scripts/pi-hole/php/groups.php",
+ method: "post",
+ dataType: "json",
+ data: {
+ action: "add_adlist",
+ address: address,
+ comment: comment,
+ token: token
+ },
+ success: function(response) {
+ utils.enableAll();
+ if (response.success) {
+ utils.showAlert(
+ "success",
+ "glyphicon glyphicon-plus",
+ "Successfully added adlist",
+ address
+ );
+ table.ajax.reload(null, false);
+ $("#new_address").val("");
+ $("#new_comment").val("");
+ table.ajax.reload();
+ } else {
+ utils.showAlert("error", "", "Error while adding new adlist: ", response.message);
+ }
+ },
+ error: function(jqXHR, exception) {
+ utils.enableAll();
+ utils.showAlert("error", "", "Error while adding new adlist: ", jqXHR.responseText);
+ console.log(exception);
+ }
+ });
+}
+
+function editAdlist() {
+ var elem = $(this).attr("id");
+ var tr = $(this).closest("tr");
+ var id = tr.attr("data-id");
+ var status = tr.find("#status_" + id).is(":checked") ? 1 : 0;
+ var comment = tr.find("#comment_" + id).val();
+ var groups = tr.find("#multiselect_" + id).val();
+ var address = tr.find("#address_" + id).text();
+
+ var done = "edited";
+ var not_done = "editing";
+ switch (elem) {
+ case "status_" + id:
+ if (status === 0) {
+ done = "disabled";
+ not_done = "disabling";
+ } else if (status === 1) {
+ done = "enabled";
+ not_done = "enabling";
+ }
+
+ break;
+ case "comment_" + id:
+ done = "edited comment of";
+ not_done = "editing comment of";
+ break;
+ case "multiselect_" + id:
+ done = "edited groups of";
+ not_done = "editing groups of";
+ break;
+ default:
+ alert("bad element or invalid data-id!");
+ return;
+ }
+
+ utils.disableAll();
+ utils.showAlert("info", "", "Editing adlist...", address);
+
+ $.ajax({
+ url: "scripts/pi-hole/php/groups.php",
+ method: "post",
+ dataType: "json",
+ data: {
+ action: "edit_adlist",
+ id: id,
+ comment: comment,
+ status: status,
+ groups: groups,
+ token: token
+ },
+ success: function(response) {
+ utils.enableAll();
+ if (response.success) {
+ utils.showAlert(
+ "success",
+ "glyphicon glyphicon-pencil",
+ "Successfully " + done + " adlist ",
+ address
+ );
+ table.ajax.reload(null, false);
+ } else {
+ utils.showAlert(
+ "error",
+ "",
+ "Error while " + not_done + " adlist with ID " + id,
+ Number(response.message)
+ );
+ }
+ },
+ error: function(jqXHR, exception) {
+ utils.enableAll();
+ utils.showAlert(
+ "error",
+ "",
+ "Error while " + not_done + " adlist with ID " + id,
+ jqXHR.responseText
+ );
+ console.log(exception);
+ }
+ });
+}
+
+function deleteAdlist() {
+ var tr = $(this).closest("tr");
+ var id = tr.attr("data-id");
+ var address = tr.find("#address_" + id).text();
+
+ utils.disableAll();
+ utils.showAlert("info", "", "Deleting adlist...", address);
+ $.ajax({
+ url: "scripts/pi-hole/php/groups.php",
+ method: "post",
+ dataType: "json",
+ data: { action: "delete_adlist", id: id, token: token },
+ success: function(response) {
+ utils.enableAll();
+ if (response.success) {
+ utils.showAlert(
+ "success",
+ "glyphicon glyphicon-trash",
+ "Successfully deleted adlist ",
+ address
+ );
+ table
+ .row(tr)
+ .remove()
+ .draw(false)
+ .ajax.reload(null, false);
+ } else {
+ utils.showAlert("error", "", "Error while deleting adlist with ID " + id, response.message);
+ }
+ },
+ error: function(jqXHR, exception) {
+ utils.enableAll();
+ utils.showAlert("error", "", "Error while deleting adlist with ID " + id, jqXHR.responseText);
+ console.log(exception);
+ }
+ });
+}
diff --git a/scripts/pi-hole/js/groups-clients.js b/scripts/pi-hole/js/groups-clients.js
new file mode 100644
index 00000000..803c4c3f
--- /dev/null
+++ b/scripts/pi-hole/js/groups-clients.js
@@ -0,0 +1,426 @@
+/* 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 */
+
+var table;
+var groups = [];
+var token = $("#token").html();
+
+function reload_client_suggestions() {
+ $.post(
+ "scripts/pi-hole/php/groups.php",
+ { action: "get_unconfigured_clients", token: token },
+ function(data) {
+ var sel = $("#select");
+ var customWasSelected = sel.val() === "custom";
+ sel.empty();
+ for (var key in data) {
+ if (!Object.prototype.hasOwnProperty.call(data, key)) {
+ continue;
+ }
+
+ var text = key;
+ if (data[key].length > 0) {
+ text += " (" + data[key] + ")";
+ }
+
+ sel.append(
+ $("")
+ .val(key)
+ .text(text)
+ );
+ }
+
+ sel.append(
+ $("")
+ .val("custom")
+ .text("Custom, specified below...")
+ );
+ if (customWasSelected) {
+ sel.val("custom");
+ }
+ },
+ "json"
+ );
+}
+
+function get_groups() {
+ $.post(
+ "scripts/pi-hole/php/groups.php",
+ { action: "get_groups", token: token },
+ function(data) {
+ groups = data.data;
+ initTable();
+ },
+ "json"
+ );
+}
+
+$(document).ready(function() {
+ $("#btnAdd").on("click", addClient);
+
+ reload_client_suggestions();
+ utils.bsSelect_defaults();
+ get_groups();
+
+ $("#select").on("change", function() {
+ $("#ip-custom").val("");
+ $("#ip-custom").prop("disabled", $("#select option:selected").val() !== "custom");
+ });
+});
+
+function initTable() {
+ table = $("#clientsTable").DataTable({
+ ajax: {
+ url: "scripts/pi-hole/php/groups.php",
+ data: { action: "get_clients", token: token },
+ type: "POST"
+ },
+ order: [[0, "asc"]],
+ columns: [
+ { data: "id", visible: false },
+ { data: "ip" },
+ { data: "comment" },
+ { data: "groups", searchable: false },
+ { data: "name", width: "80px", orderable: false }
+ ],
+ drawCallback: function() {
+ $('button[id^="deleteClient_"]').on("click", deleteClient);
+ // Remove visible dropdown to prevent orphaning
+ $("body > .bootstrap-select.dropdown").remove();
+ },
+ rowCallback: function(row, data) {
+ $(row).attr("data-id", data.id);
+ var tooltip =
+ "Added: " +
+ utils.datetime(data.date_added) +
+ "\nLast modified: " +
+ utils.datetime(data.date_modified) +
+ "\nDatabase ID: " +
+ data.id;
+ var ip_name =
+ '' +
+ data.ip +
+ "";
+ if (data.name !== null && data.name.length > 0)
+ ip_name +=
+ ' ' +
+ data.name +
+ "";
+ $("td:eq(0)", row).html(ip_name);
+
+ $("td:eq(1)", row).html('');
+ var commentEl = $("#comment_" + data.id, row);
+ commentEl.val(data.comment);
+ commentEl.on("change", editClient);
+
+ $("td:eq(2)", row).empty();
+ $("td:eq(2)", row).append(
+ ''
+ );
+ var selectEl = $("#multiselect_" + data.id, row);
+ // Add all known groups
+ for (var i = 0; i < groups.length; i++) {
+ var data_sub = "";
+ if (!groups[i].enabled) {
+ data_sub = 'data-subtext="(disabled)"';
+ }
+
+ selectEl.append(
+ $("")
+ .val(groups[i].id)
+ .text(groups[i].name)
+ );
+ }
+
+ // Select assigned groups
+ selectEl.val(data.groups);
+ // Initialize bootstrap-select
+ selectEl
+ // fix dropdown if it would stick out right of the viewport
+ .on("show.bs.select", function() {
+ var winWidth = $(window).width();
+ var dropdownEl = $("body > .bootstrap-select.dropdown");
+ if (dropdownEl.length > 0) {
+ dropdownEl.removeClass("align-right");
+ var width = dropdownEl.width();
+ var left = dropdownEl.offset().left;
+ if (left + width > winWidth) {
+ dropdownEl.addClass("align-right");
+ }
+ }
+ })
+ .on("changed.bs.select", function() {
+ // enable Apply button
+ if ($(ApplyBtn).prop("disabled")) {
+ $(ApplyBtn)
+ .addClass("btn-success")
+ .prop("disabled", false)
+ .on("click", function() {
+ editClient.call(selectEl);
+ });
+ }
+ })
+ .on("hide.bs.select", function() {
+ // Restore values if drop-down menu is closed without clicking the Apply button
+ if (!$(ApplyBtn).prop("disabled")) {
+ $(this)
+ .val(data.groups)
+ .selectpicker("refresh");
+ $(ApplyBtn)
+ .removeClass("btn-success")
+ .prop("disabled", true)
+ .off("click");
+ }
+ })
+ .selectpicker()
+ .siblings(".dropdown-menu")
+ .find(".bs-actionsbox")
+ .prepend(
+ ''
+ );
+
+ var ApplyBtn = "#btn_apply_" + data.id;
+
+ var button =
+ '";
+ $("td:eq(3)", row).html(button);
+ },
+ dom:
+ "<'row'<'col-sm-4'l><'col-sm-8'f>>" +
+ "<'row'<'col-sm-12'<'table-responsive'tr>>>" +
+ "<'row'<'col-sm-5'i><'col-sm-7'p>>",
+ lengthMenu: [
+ [10, 25, 50, 100, -1],
+ [10, 25, 50, 100, "All"]
+ ],
+ stateSave: true,
+ stateSaveCallback: function(settings, data) {
+ // Store current state in client's local storage area
+ localStorage.setItem("groups-clients-table", JSON.stringify(data));
+ },
+ stateLoadCallback: function() {
+ // Receive previous state from client's local storage area
+ var data = localStorage.getItem("groups-clients-table");
+ // Return if not available
+ if (data === null) {
+ return null;
+ }
+
+ data = JSON.parse(data);
+ // Always start on the first page to show most recent queries
+ data.start = 0;
+ // Always start with empty search field
+ data.search.search = "";
+ // Reset visibility of ID column
+ data.columns[0].visible = false;
+ // Apply loaded state to table
+ return data;
+ }
+ });
+
+ table.on("order.dt", function() {
+ var order = table.order();
+ if (order[0][0] !== 0 || order[0][1] !== "asc") {
+ $("#resetButton").show();
+ } else {
+ $("#resetButton").hide();
+ }
+ });
+ $("#resetButton").on("click", function() {
+ table.order([[0, "asc"]]).draw();
+ $("#resetButton").hide();
+ });
+}
+
+function addClient() {
+ var ip = $("#select").val();
+ var comment = $("#new_comment").val();
+ if (ip === "custom") {
+ ip = $("#ip-custom").val();
+ }
+
+ utils.disableAll();
+ utils.showAlert("info", "", "Adding client...", ip);
+
+ if (ip.length === 0) {
+ utils.enableAll();
+ utils.showAlert("warning", "", "Warning", "Please specify a client IP address");
+ return;
+ }
+
+ // Validate IP address (may contain CIDR details)
+ var ipv6format = ip.includes(":");
+
+ if (!ipv6format && !utils.validateIPv4CIDR(ip)) {
+ utils.enableAll();
+ utils.showAlert("warning", "", "Warning", "Invalid IPv4 address!");
+ return;
+ }
+
+ if (ipv6format && !utils.validateIPv6CIDR(ip)) {
+ utils.enableAll();
+ utils.showAlert("warning", "", "Warning", "Invalid IPv6 address!");
+ return;
+ }
+
+ $.ajax({
+ url: "scripts/pi-hole/php/groups.php",
+ method: "post",
+ dataType: "json",
+ data: { action: "add_client", ip: ip, comment: comment, token: token },
+ success: function(response) {
+ utils.enableAll();
+ if (response.success) {
+ utils.showAlert("success", "glyphicon glyphicon-plus", "Successfully added client", ip);
+ reload_client_suggestions();
+ table.ajax.reload(null, false);
+ } else {
+ utils.showAlert("error", "", "Error while adding new client", response.message);
+ }
+ },
+ error: function(jqXHR, exception) {
+ utils.enableAll();
+ utils.showAlert("error", "", "Error while adding new client", jqXHR.responseText);
+ console.log(exception);
+ }
+ });
+}
+
+function editClient() {
+ var elem = $(this).attr("id");
+ var tr = $(this).closest("tr");
+ var id = tr.attr("data-id");
+ var groups = tr.find("#multiselect_" + id).val();
+ var ip = tr.find("#ip_" + id).text();
+ var name = tr.find("#name_" + id).text();
+ var comment = tr.find("#comment_" + id).val();
+
+ var done = "edited";
+ var not_done = "editing";
+ switch (elem) {
+ case "multiselect_" + id:
+ done = "edited groups of";
+ not_done = "editing groups of";
+ break;
+ case "comment_" + id:
+ done = "edited comment of";
+ not_done = "editing comment of";
+ break;
+ default:
+ alert("bad element or invalid data-id!");
+ return;
+ }
+
+ var ip_name = ip;
+ if (name.length > 0) {
+ ip_name += " (" + name + ")";
+ }
+
+ utils.disableAll();
+ utils.showAlert("info", "", "Editing client...", ip_name);
+ $.ajax({
+ url: "scripts/pi-hole/php/groups.php",
+ method: "post",
+ dataType: "json",
+ data: {
+ action: "edit_client",
+ id: id,
+ groups: groups,
+ token: token,
+ comment: comment
+ },
+ success: function(response) {
+ utils.enableAll();
+ if (response.success) {
+ utils.showAlert(
+ "success",
+ "glyphicon glyphicon-pencil",
+ "Successfully " + done + " client",
+ ip_name
+ );
+ table.ajax.reload(null, false);
+ } else {
+ utils.showAlert(
+ "error",
+ "Error while " + not_done + " client with ID " + id,
+ response.message
+ );
+ }
+ },
+ error: function(jqXHR, exception) {
+ utils.enableAll();
+ utils.showAlert(
+ "error",
+ "",
+ "Error while " + not_done + " client with ID " + id,
+ jqXHR.responseText
+ );
+ console.log(exception);
+ }
+ });
+}
+
+function deleteClient() {
+ var tr = $(this).closest("tr");
+ var id = tr.attr("data-id");
+ var ip = tr.find("#ip_" + id).text();
+ var name = tr.find("#name_" + id).text();
+
+ var ip_name = ip;
+ if (name.length > 0) {
+ ip_name += " (" + name + ")";
+ }
+
+ utils.disableAll();
+ utils.showAlert("info", "", "Deleting client...", ip_name);
+ $.ajax({
+ url: "scripts/pi-hole/php/groups.php",
+ method: "post",
+ dataType: "json",
+ data: { action: "delete_client", id: id, token: token },
+ success: function(response) {
+ utils.enableAll();
+ if (response.success) {
+ utils.showAlert(
+ "success",
+ "glyphicon glyphicon-trash",
+ "Successfully deleted client ",
+ ip_name
+ );
+ table
+ .row(tr)
+ .remove()
+ .draw(false)
+ .ajax.reload(null, false);
+ reload_client_suggestions();
+ } else {
+ utils.showAlert("error", "", "Error while deleting client with ID " + id, response.message);
+ }
+ },
+ error: function(jqXHR, exception) {
+ utils.enableAll();
+ utils.showAlert("error", "", "Error while deleting client with ID " + id, jqXHR.responseText);
+ console.log(exception);
+ }
+ });
+}
diff --git a/scripts/pi-hole/js/groups-common.js b/scripts/pi-hole/js/groups-common.js
new file mode 100644
index 00000000..005ce7d8
--- /dev/null
+++ b/scripts/pi-hole/js/groups-common.js
@@ -0,0 +1,162 @@
+/* Pi-hole: A black hole for Internet advertisements
+ * (c) 2020 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 moment:false */
+
+var info = null;
+function showAlert(type, icon, title, message) {
+ var opts = {};
+ title = " " + title + " ";
+ switch (type) {
+ case "info":
+ opts = {
+ type: "info",
+ icon: "glyphicon glyphicon-time",
+ 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);
+ }
+
+ break;
+ case "warning":
+ opts = {
+ type: "warning",
+ icon: "glyphicon glyphicon-warning-sign",
+ title: title,
+ message: message
+ };
+ if (info) {
+ info.update(opts);
+ } else {
+ $.notify(opts);
+ }
+
+ break;
+ case "error":
+ opts = {
+ type: "danger",
+ icon: "glyphicon glyphicon-remove",
+ title: " Error, something went wrong! ",
+ message: message
+ };
+ if (info) {
+ info.update(opts);
+ } else {
+ $.notify(opts);
+ }
+
+ break;
+ default:
+ }
+}
+
+function datetime(date) {
+ return moment.unix(Math.floor(date)).format("Y-MM-DD HH:mm:ss z");
+}
+
+function disableAll() {
+ $("input").attr("disabled", true);
+ $("select").attr("disabled", true);
+ $("button").attr("disabled", true);
+ $("textarea").attr("disabled", true);
+}
+
+function enableAll() {
+ $("input").attr("disabled", false);
+ $("select").attr("disabled", false);
+ $("button").attr("disabled", false);
+ $("textarea").attr("disabled", false);
+
+ // Enable custom input field only if applicable
+ var ip = $("#select") ? $("#select").val() : null;
+ if (ip !== null && ip !== "custom") {
+ ip = $("#ip-custom").attr("disabled", true);
+ }
+}
+
+// Pi-hole IPv4/CIDR validator by DL6ER, see regexr.com/50csh
+function validateIPv4CIDR(ip) {
+ // One IPv4 element is 8bit: 0 - 256
+ var ipv4elem = "(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)";
+ // CIDR for IPv4 is 1 - 32 bit
+ var v4cidr = "(\\/([1-9]|[1-2][0-9]|3[0-2])){0,1}";
+ var ipv4validator = new RegExp(
+ "^" + ipv4elem + "\\." + ipv4elem + "\\." + ipv4elem + "\\." + ipv4elem + v4cidr + "$"
+ );
+ return ipv4validator.test(ip);
+}
+
+// Pi-hole IPv6/CIDR validator by DL6ER, see regexr.com/50csn
+function validateIPv6CIDR(ip) {
+ // One IPv6 element is 16bit: 0000 - FFFF
+ var ipv6elem = "[0-9A-Fa-f]{1,4}";
+ // CIDR for IPv6 is 1- 128 bit
+ var v6cidr = "(\\/([1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])){0,1}";
+ var ipv6validator = new RegExp(
+ "^(((?:" +
+ ipv6elem +
+ "))((?::" +
+ ipv6elem +
+ "))*::((?:" +
+ ipv6elem +
+ "))*((?::" +
+ ipv6elem +
+ "))*|((?:" +
+ ipv6elem +
+ "))((?::" +
+ ipv6elem +
+ ")){7})" +
+ v6cidr +
+ "$"
+ );
+ return ipv6validator.test(ip);
+}
+
+function bsSelect_defaults() {
+ // set bootstrap-select defaults
+ var pickerDEFAULTS = $.fn.selectpicker.Constructor.DEFAULTS;
+ pickerDEFAULTS.noneSelectedText = "none selected";
+ pickerDEFAULTS.selectedTextFormat = "count > 1";
+ pickerDEFAULTS.actionsBox = true;
+ pickerDEFAULTS.width = "fit";
+ pickerDEFAULTS.container = "body";
+ pickerDEFAULTS.dropdownAlignRight = "auto";
+ pickerDEFAULTS.selectAllText = "All";
+ pickerDEFAULTS.deselectAllText = "None";
+ pickerDEFAULTS.countSelectedText = function(num, total) {
+ if (num === total) {
+ return "All selected (" + num + ")";
+ }
+
+ return num + " selected";
+ };
+}
+
+window.utils = (function() {
+ return {
+ showAlert: showAlert,
+ datetime: datetime,
+ disableAll: disableAll,
+ enableAll: enableAll,
+ validateIPv4CIDR: validateIPv4CIDR,
+ validateIPv6CIDR: validateIPv6CIDR,
+ bsSelect_defaults: bsSelect_defaults
+ };
+})();
diff --git a/scripts/pi-hole/js/groups-domains.js b/scripts/pi-hole/js/groups-domains.js
new file mode 100644
index 00000000..92717262
--- /dev/null
+++ b/scripts/pi-hole/js/groups-domains.js
@@ -0,0 +1,550 @@
+/* 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 */
+
+var table;
+var groups = [];
+var token = $("#token").html();
+var GETDict = {};
+var showtype = "all";
+
+function get_groups() {
+ $.post(
+ "scripts/pi-hole/php/groups.php",
+ { action: "get_groups", token: token },
+ function(data) {
+ groups = data.data;
+ initTable();
+ },
+ "json"
+ );
+}
+
+$(document).ready(function() {
+ window.location.search
+ .substr(1)
+ .split("&")
+ .forEach(function(item) {
+ GETDict[item.split("=")[0]] = item.split("=")[1];
+ });
+
+ if ("type" in GETDict && (GETDict.type === "white" || GETDict.type === "black")) {
+ showtype = GETDict.type;
+ }
+
+ // sync description fields, reset inactive inputs on tab change
+ $('a[data-toggle="tab"]').on("shown.bs.tab", function() {
+ var tabHref = $(this).attr("href");
+ var val;
+ if (tabHref === "#tab_domain") {
+ val = $("#new_regex_comment").val();
+ $("#new_domain_comment").val(val);
+ $("#new_regex").val("");
+ } else if (tabHref === "#tab_regex") {
+ val = $("#new_domain_comment").val();
+ $("#new_regex_comment").val(val);
+ $("#new_domain").val("");
+ $("#wildcard_checkbox").prop("checked", false);
+ }
+ });
+
+ $("#add2black, #add2white").on("click", addDomain);
+
+ utils.bsSelect_defaults();
+ get_groups();
+});
+
+function initTable() {
+ table = $("#domainsTable").DataTable({
+ ajax: {
+ url: "scripts/pi-hole/php/groups.php",
+ data: { action: "get_domains", showtype: showtype, token: token },
+ type: "POST"
+ },
+ order: [[0, "asc"]],
+ columns: [
+ { data: "id", visible: false },
+ { data: "domain" },
+ { data: "type", searchable: false },
+ { data: "enabled", searchable: false },
+ { data: "comment" },
+ { data: "groups", searchable: false },
+ { data: null, width: "80px", orderable: false }
+ ],
+ drawCallback: function() {
+ $('button[id^="deleteDomain_"]').on("click", deleteDomain);
+ // Remove visible dropdown to prevent orphaning
+ $("body > .bootstrap-select.dropdown").remove();
+ },
+ rowCallback: function(row, data) {
+ $(row).attr("data-id", data.id);
+ var tooltip =
+ "Added: " +
+ utils.datetime(data.date_added) +
+ "\nLast modified: " +
+ utils.datetime(data.date_modified) +
+ "\nDatabase ID: " +
+ data.id;
+ $("td:eq(0)", row).html(
+ '' +
+ data.domain +
+ ""
+ );
+
+ var whitelist_options = "";
+ if (showtype === "all" || showtype === "white") {
+ whitelist_options =
+ '" +
+ '";
+ }
+
+ var blacklist_options = "";
+ if (showtype === "all" || showtype === "black") {
+ blacklist_options =
+ '" +
+ '";
+ }
+
+ $("td:eq(1)", row).html(
+ '"
+ );
+ var typeEl = $("#type_" + data.id, row);
+ typeEl.on("change", editDomain);
+
+ var disabled = data.enabled === 0;
+ $("td:eq(2)", row).html(
+ '"
+ );
+ var statusEl = $("#status_" + data.id, row);
+ statusEl.bootstrapToggle({
+ on: "Enabled",
+ off: "Disabled",
+ size: "small",
+ onstyle: "success",
+ width: "80px"
+ });
+ statusEl.on("change", editDomain);
+
+ $("td:eq(3)", row).html('');
+ var commentEl = $("#comment_" + data.id, row);
+ commentEl.val(data.comment);
+ commentEl.on("change", editDomain);
+
+ // Show group assignment field only if in full domain management mode
+ if (table.column(5).visible()) {
+ $("td:eq(4)", row).empty();
+ $("td:eq(4)", row).append(
+ ''
+ );
+ var selectEl = $("#multiselect_" + data.id, row);
+ // Add all known groups
+ for (var i = 0; i < groups.length; i++) {
+ var data_sub = "";
+ if (!groups[i].enabled) {
+ data_sub = 'data-subtext="(disabled)"';
+ }
+
+ selectEl.append(
+ $("")
+ .val(groups[i].id)
+ .text(groups[i].name)
+ );
+ }
+
+ // Select assigned groups
+ selectEl.val(data.groups);
+ // Initialize bootstrap-select
+ selectEl
+ // fix dropdown if it would stick out right of the viewport
+ .on("show.bs.select", function() {
+ var winWidth = $(window).width();
+ var dropdownEl = $("body > .bootstrap-select.dropdown");
+ if (dropdownEl.length > 0) {
+ dropdownEl.removeClass("align-right");
+ var width = dropdownEl.width();
+ var left = dropdownEl.offset().left;
+ if (left + width > winWidth) {
+ dropdownEl.addClass("align-right");
+ }
+ }
+ })
+ .on("changed.bs.select", function() {
+ // enable Apply button
+ if ($(ApplyBtn).prop("disabled")) {
+ $(ApplyBtn)
+ .addClass("btn-success")
+ .prop("disabled", false)
+ .on("click", function() {
+ editDomain.call(selectEl);
+ });
+ }
+ })
+ .on("hide.bs.select", function() {
+ // Restore values if drop-down menu is closed without clicking the Apply button
+ if (!$(ApplyBtn).prop("disabled")) {
+ $(this)
+ .val(data.groups)
+ .selectpicker("refresh");
+ $(ApplyBtn)
+ .removeClass("btn-success")
+ .prop("disabled", true)
+ .off("click");
+ }
+ })
+ .selectpicker()
+ .siblings(".dropdown-menu")
+ .find(".bs-actionsbox")
+ .prepend(
+ ''
+ );
+ }
+
+ var ApplyBtn = "#btn_apply_" + data.id;
+
+ // Highlight row (if url parameter "domainid=" is used)
+ if ("domainid" in GETDict && data.id === parseInt(GETDict.domainid)) {
+ $(row)
+ .find("td")
+ .addClass("highlight");
+ }
+
+ var button =
+ '";
+ if (table.column(5).visible()) {
+ $("td:eq(5)", row).html(button);
+ } else {
+ $("td:eq(4)", row).html(button);
+ }
+ },
+ dom:
+ "<'row'<'col-sm-4'l><'col-sm-8'f>>" +
+ "<'row'<'col-sm-12'<'table-responsive'tr>>>" +
+ "<'row'<'col-sm-5'i><'col-sm-7'p>>",
+ lengthMenu: [
+ [10, 25, 50, 100, -1],
+ [10, 25, 50, 100, "All"]
+ ],
+ stateSave: true,
+ stateSaveCallback: function(settings, data) {
+ // Store current state in client's local storage area
+ localStorage.setItem("groups-domains-table", JSON.stringify(data));
+ },
+ stateLoadCallback: function() {
+ // Receive previous state from client's local storage area
+ var data = localStorage.getItem("groups-domains-table");
+ // Return if not available
+ if (data === null) {
+ return null;
+ }
+
+ data = JSON.parse(data);
+ // Always start on the first page to show most recent queries
+ data.start = 0;
+ // Always start with empty search field
+ data.search.search = "";
+ // Reset visibility of ID column
+ data.columns[0].visible = false;
+ // Show group assignment column only on full page
+ data.columns[5].visible = showtype === "all";
+ // Apply loaded state to table
+ return data;
+ },
+ initComplete: function() {
+ if ("domainid" in GETDict) {
+ var pos = table
+ .column(0, { order: "current" })
+ .data()
+ .indexOf(parseInt(GETDict.domainid));
+ if (pos >= 0) {
+ var page = Math.floor(pos / table.page.info().length);
+ table.page(page).draw(false);
+ }
+ }
+ }
+ });
+
+ table.on("order.dt", function() {
+ var order = table.order();
+ if (order[0][0] !== 0 || order[0][1] !== "asc") {
+ $("#resetButton").show();
+ } else {
+ $("#resetButton").hide();
+ }
+ });
+ $("#resetButton").on("click", function() {
+ table.order([[0, "asc"]]).draw();
+ $("#resetButton").hide();
+ });
+}
+
+function addDomain() {
+ var action = this.id;
+ var tabHref = $('a[data-toggle="tab"][aria-expanded="true"]').attr("href");
+ var wildcardEl = $("#wildcard_checkbox");
+ var wildcard_checked = wildcardEl.prop("checked");
+ var type;
+
+ // current tab's inputs
+ var domain_regex, domainEl, commentEl;
+ if (tabHref === "#tab_domain") {
+ domain_regex = "domain";
+ domainEl = $("#new_domain");
+ commentEl = $("#new_domain_comment");
+ } else if (tabHref === "#tab_regex") {
+ domain_regex = "regex";
+ domainEl = $("#new_regex");
+ commentEl = $("#new_regex_comment");
+ }
+
+ var domain = domainEl.val();
+ var comment = commentEl.val();
+
+ utils.disableAll();
+ utils.showAlert("info", "", "Adding " + domain_regex + "...", domain);
+
+ if (domain.length > 0) {
+ // strip "*." if specified by user in wildcard mode
+ if (domain_regex === "domain" && wildcard_checked && domain.startsWith("*.")) {
+ domain = domain.substr(2);
+ }
+
+ // determine list type
+ if (domain_regex === "domain" && action === "add2black" && wildcard_checked) {
+ type = "3W";
+ } else if (domain_regex === "domain" && action === "add2black" && !wildcard_checked) {
+ type = "1";
+ } else if (domain_regex === "domain" && action === "add2white" && wildcard_checked) {
+ type = "2W";
+ } else if (domain_regex === "domain" && action === "add2white" && !wildcard_checked) {
+ type = "0";
+ } else if (domain_regex === "regex" && action === "add2black") {
+ type = "3";
+ } else if (domain_regex === "regex" && action === "add2white") {
+ type = "2";
+ }
+ } else {
+ utils.enableAll();
+ utils.showAlert("warning", "", "Warning", "Please specify a " + domain_regex);
+ return;
+ }
+
+ $.ajax({
+ url: "scripts/pi-hole/php/groups.php",
+ method: "post",
+ dataType: "json",
+ data: {
+ action: "add_domain",
+ domain: domain,
+ type: type,
+ comment: comment,
+ token: token
+ },
+ success: function(response) {
+ utils.enableAll();
+ if (response.success) {
+ utils.showAlert(
+ "success",
+ "glyphicon glyphicon-plus",
+ "Successfully added " + domain_regex,
+ domain
+ );
+ domainEl.val("");
+ commentEl.val("");
+ wildcardEl.prop("checked", false);
+ table.ajax.reload(null, false);
+ } else {
+ utils.showAlert("error", "", "Error while adding new " + domain_regex, response.message);
+ }
+ },
+ error: function(jqXHR, exception) {
+ utils.enableAll();
+ utils.showAlert("error", "", "Error while adding new " + domain_regex, jqXHR.responseText);
+ console.log(exception);
+ }
+ });
+}
+
+function editDomain() {
+ var elem = $(this).attr("id");
+ var tr = $(this).closest("tr");
+ var id = tr.attr("data-id");
+ var domain = tr.find("#domain_" + id).text();
+ var type = tr.find("#type_" + id).val();
+ var status = tr.find("#status_" + id).is(":checked") ? 1 : 0;
+ var comment = tr.find("#comment_" + id).val();
+
+ // Show group assignment field only if in full domain management mode
+ // if not included, just use the row data.
+ var rowData = table.row(tr).data();
+ var groups = table.column(5).visible() ? tr.find("#multiselect_" + id).val() : rowData.groups;
+
+ var domain_regex;
+ if (type === "0" || type === "1") {
+ domain_regex = "domain";
+ } else if (type === "2" || type === "3") {
+ domain_regex = "regex";
+ }
+
+ var done = "edited";
+ var not_done = "editing";
+ switch (elem) {
+ case "status_" + id:
+ if (status === 0) {
+ done = "disabled";
+ not_done = "disabling";
+ } else if (status === 1) {
+ done = "enabled";
+ not_done = "enabling";
+ }
+
+ break;
+ case "name_" + id:
+ done = "edited name of";
+ not_done = "editing name of";
+ break;
+ case "comment_" + id:
+ done = "edited comment of";
+ not_done = "editing comment of";
+ break;
+ case "type_" + id:
+ done = "edited type of";
+ not_done = "editing type of";
+ break;
+ case "multiselect_" + id:
+ done = "edited groups of";
+ not_done = "editing groups of";
+ break;
+ default:
+ alert("bad element or invalid data-id!");
+ return;
+ }
+
+ utils.disableAll();
+ utils.showAlert("info", "", "Editing " + domain_regex + "...", name);
+ $.ajax({
+ url: "scripts/pi-hole/php/groups.php",
+ method: "post",
+ dataType: "json",
+ data: {
+ action: "edit_domain",
+ id: id,
+ type: type,
+ comment: comment,
+ status: status,
+ groups: groups,
+ token: token
+ },
+ success: function(response) {
+ utils.enableAll();
+ if (response.success) {
+ utils.showAlert(
+ "success",
+ "glyphicon glyphicon-pencil",
+ "Successfully " + done + " " + domain_regex,
+ domain
+ );
+ table.ajax.reload(null, false);
+ } else
+ utils.showAlert(
+ "error",
+ "",
+ "Error while " + not_done + " " + domain_regex + " with ID " + id,
+ response.message
+ );
+ },
+ error: function(jqXHR, exception) {
+ utils.enableAll();
+ utils.showAlert(
+ "error",
+ "",
+ "Error while " + not_done + " " + domain_regex + " with ID " + id,
+ jqXHR.responseText
+ );
+ console.log(exception);
+ }
+ });
+}
+
+function deleteDomain() {
+ var tr = $(this).closest("tr");
+ var id = tr.attr("data-id");
+ var domain = tr.find("#domain_" + id).text();
+ var type = tr.find("#type_" + id).val();
+
+ var domain_regex;
+ if (type === "0" || type === "1") {
+ domain_regex = "domain";
+ } else if (type === "2" || type === "3") {
+ domain_regex = "regex";
+ }
+
+ utils.disableAll();
+ utils.showAlert("info", "", "Deleting " + domain_regex + "...", domain);
+ $.ajax({
+ url: "scripts/pi-hole/php/groups.php",
+ method: "post",
+ dataType: "json",
+ data: { action: "delete_domain", id: id, token: token },
+ success: function(response) {
+ utils.enableAll();
+ if (response.success) {
+ utils.showAlert(
+ "success",
+ "glyphicon glyphicon-trash",
+ "Successfully deleted " + domain_regex,
+ domain
+ );
+ table
+ .row(tr)
+ .remove()
+ .draw(false)
+ .ajax.reload(null, false);
+ } else {
+ utils.showAlert(
+ "error",
+ "",
+ "Error while deleting " + domain_regex + " with ID " + id,
+ response.message
+ );
+ }
+ },
+ error: function(jqXHR, exception) {
+ utils.enableAll();
+ utils.showAlert(
+ "error",
+ "",
+ "Error while deleting " + domain_regex + " with ID " + id,
+ jqXHR.responseText
+ );
+ console.log(exception);
+ }
+ });
+}
diff --git a/scripts/pi-hole/js/groups.js b/scripts/pi-hole/js/groups.js
new file mode 100644
index 00000000..6210acbd
--- /dev/null
+++ b/scripts/pi-hole/js/groups.js
@@ -0,0 +1,277 @@
+/* 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 */
+
+var table;
+var token = $("#token").html();
+
+$(document).ready(function() {
+ $("#btnAdd").on("click", addGroup);
+
+ table = $("#groupsTable").DataTable({
+ ajax: {
+ url: "scripts/pi-hole/php/groups.php",
+ data: { action: "get_groups", token: token },
+ type: "POST"
+ },
+ order: [[0, "asc"]],
+ columns: [
+ { data: "id", visible: false },
+ { data: "name" },
+ { data: "enabled", searchable: false },
+ { data: "description" },
+ { data: null, width: "60px", orderable: false }
+ ],
+ drawCallback: function() {
+ $('button[id^="deleteGroup_"]').on("click", deleteGroup);
+ },
+ rowCallback: function(row, data) {
+ $(row).attr("data-id", data.id);
+ var tooltip =
+ "Added: " +
+ utils.datetime(data.date_added) +
+ "\nLast modified: " +
+ utils.datetime(data.date_modified) +
+ "\nDatabase ID: " +
+ data.id;
+ $("td:eq(0)", row).html(
+ ''
+ );
+ var nameEl = $("#name_" + data.id, row);
+ nameEl.val(data.name);
+ nameEl.on("change", editGroup);
+
+ var disabled = data.enabled === 0;
+ $("td:eq(1)", row).html(
+ '"
+ );
+ var statusEl = $("#status_" + data.id, row);
+ statusEl.bootstrapToggle({
+ on: "Enabled",
+ off: "Disabled",
+ size: "small",
+ onstyle: "success",
+ width: "80px"
+ });
+ statusEl.on("change", editGroup);
+
+ $("td:eq(2)", row).html('');
+ var desc = data.description !== null ? data.description : "";
+ var descEl = $("#desc_" + data.id, row);
+ descEl.val(desc);
+ descEl.on("change", editGroup);
+
+ $("td:eq(3)", row).empty();
+ if (data.id !== 0) {
+ var button =
+ '";
+ $("td:eq(3)", row).html(button);
+ }
+ },
+ dom:
+ "<'row'<'col-sm-4'l><'col-sm-8'f>>" +
+ "<'row'<'col-sm-12'<'table-responsive'tr>>>" +
+ "<'row'<'col-sm-5'i><'col-sm-7'p>>",
+ lengthMenu: [
+ [10, 25, 50, 100, -1],
+ [10, 25, 50, 100, "All"]
+ ],
+ stateSave: true,
+ stateSaveCallback: function(settings, data) {
+ // Store current state in client's local storage area
+ localStorage.setItem("groups-table", JSON.stringify(data));
+ },
+ stateLoadCallback: function() {
+ // Receive previous state from client's local storage area
+ var data = localStorage.getItem("groups-table");
+ // Return if not available
+ if (data === null) {
+ return null;
+ }
+
+ data = JSON.parse(data);
+ // Always start on the first page to show most recent queries
+ data.start = 0;
+ // Always start with empty search field
+ data.search.search = "";
+ // Reset visibility of ID column
+ data.columns[0].visible = false;
+ // Apply loaded state to table
+ return data;
+ }
+ });
+
+ table.on("order.dt", function() {
+ var order = table.order();
+ if (order[0][0] !== 0 || order[0][1] !== "asc") {
+ $("#resetButton").show();
+ } else {
+ $("#resetButton").hide();
+ }
+ });
+ $("#resetButton").on("click", function() {
+ table.order([[0, "asc"]]).draw();
+ $("#resetButton").hide();
+ });
+});
+
+function addGroup() {
+ var name = $("#new_name").val();
+ var desc = $("#new_desc").val();
+
+ utils.disableAll();
+ utils.showAlert("info", "", "Adding group...", name);
+
+ if (name.length === 0) {
+ utils.showAlert("warning", "", "Warning", "Please specify a group name");
+ return;
+ }
+
+ $.ajax({
+ url: "scripts/pi-hole/php/groups.php",
+ method: "post",
+ dataType: "json",
+ data: { action: "add_group", name: name, desc: desc, token: token },
+ success: function(response) {
+ utils.enableAll();
+ if (response.success) {
+ utils.showAlert("success", "glyphicon glyphicon-plus", "Successfully added group", name);
+ $("#new_name").val("");
+ $("#new_desc").val("");
+ table.ajax.reload();
+ } else {
+ utils.showAlert("error", "", "Error while adding new group", response.message);
+ }
+ },
+ error: function(jqXHR, exception) {
+ utils.enableAll();
+ utils.showAlert("error", "", "Error while adding new group", jqXHR.responseText);
+ console.log(exception);
+ }
+ });
+}
+
+function editGroup() {
+ var elem = $(this).attr("id");
+ var tr = $(this).closest("tr");
+ var id = tr.attr("data-id");
+ var name = tr.find("#name_" + id).val();
+ var status = tr.find("#status_" + id).is(":checked") ? 1 : 0;
+ var desc = tr.find("#desc_" + id).val();
+
+ var done = "edited";
+ var not_done = "editing";
+ switch (elem) {
+ case "status_" + id:
+ if (status === 0) {
+ done = "disabled";
+ not_done = "disabling";
+ } else if (status === 1) {
+ done = "enabled";
+ not_done = "enabling";
+ }
+
+ break;
+ case "name_" + id:
+ done = "edited name of";
+ not_done = "editing name of";
+ break;
+ case "desc_" + id:
+ done = "edited description of";
+ not_done = "editing description of";
+ break;
+ default:
+ alert("bad element or invalid data-id!");
+ return;
+ }
+
+ utils.disableAll();
+ utils.showAlert("info", "", "Editing group...", name);
+ $.ajax({
+ url: "scripts/pi-hole/php/groups.php",
+ method: "post",
+ dataType: "json",
+ data: {
+ action: "edit_group",
+ id: id,
+ name: name,
+ desc: desc,
+ status: status,
+ token: token
+ },
+ success: function(response) {
+ utils.enableAll();
+ if (response.success) {
+ utils.showAlert(
+ "success",
+ "glyphicon glyphicon-pencil",
+ "Successfully " + done + " group",
+ name
+ );
+ } else {
+ utils.showAlert(
+ "error",
+ "",
+ "Error while " + not_done + " group with ID " + id,
+ response.message
+ );
+ }
+ },
+ error: function(jqXHR, exception) {
+ utils.enableAll();
+ utils.showAlert(
+ "error",
+ "",
+ "Error while " + not_done + " group with ID " + id,
+ jqXHR.responseText
+ );
+ console.log(exception);
+ }
+ });
+}
+
+function deleteGroup() {
+ var tr = $(this).closest("tr");
+ var id = tr.attr("data-id");
+ var name = tr.find("#name_" + id).val();
+
+ utils.disableAll();
+ utils.showAlert("info", "", "Deleting group...", name);
+ $.ajax({
+ url: "scripts/pi-hole/php/groups.php",
+ method: "post",
+ dataType: "json",
+ data: { action: "delete_group", id: id, token: token },
+ success: function(response) {
+ utils.enableAll();
+ if (response.success) {
+ utils.showAlert(
+ "success",
+ "glyphicon glyphicon-trash",
+ "Successfully deleted group ",
+ name
+ );
+ table
+ .row(tr)
+ .remove()
+ .draw(false);
+ } else {
+ utils.showAlert("error", "", "Error while deleting group with ID " + id, response.message);
+ }
+ },
+ error: function(jqXHR, exception) {
+ utils.enableAll();
+ utils.showAlert("error", "", "Error while deleting group with ID " + id, jqXHR.responseText);
+ console.log(exception);
+ }
+ });
+}
diff --git a/scripts/pi-hole/js/header.js b/scripts/pi-hole/js/header.js
deleted file mode 100644
index e9f0478a..00000000
--- a/scripts/pi-hole/js/header.js
+++ /dev/null
@@ -1,9 +0,0 @@
-/* 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. */
-// Remove JS warning
-var jswarn = document.getElementById("js-warn-exit");
-jswarn.parentNode.removeChild(jswarn);
diff --git a/scripts/pi-hole/js/index.js b/scripts/pi-hole/js/index.js
index 8058b16e..bb24db6f 100644
--- a/scripts/pi-hole/js/index.js
+++ b/scripts/pi-hole/js/index.js
@@ -1,441 +1,525 @@
/* 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. */
+ * (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. */
+
// Define global variables
-/* global Chart */
-var timeLineChart, queryTypeChart, forwardDestinationChart;
-var queryTypePieChart, forwardDestinationPieChart, clientsChart;
+/* global Chart:false, updateSessionTimer:false */
+var timeLineChart, clientsChart;
+var queryTypePieChart, forwardDestinationPieChart;
function padNumber(num) {
- return ("00" + num).substr(-2,2);
+ return ("00" + num).substr(-2, 2);
}
// Helper function needed for converting the Objects to Arrays
function objectToArray(p) {
- var keys = Object.keys(p);
- keys.sort(function(a, b) {
- return a - b;
- });
+ var keys = Object.keys(p);
+ keys.sort(function(a, b) {
+ return a - b;
+ });
- var arr = [], idx = [];
- for (var i = 0; i < keys.length; i++)
- {
- arr.push(p[keys[i]]);
- idx.push(keys[i]);
- }
- return [idx,arr];
+ var arr = [],
+ idx = [];
+ for (var i = 0; i < keys.length; i++) {
+ arr.push(p[keys[i]]);
+ idx.push(keys[i]);
+ }
+
+ return [idx, arr];
}
-var lastTooltipTime = 0;
-
var customTooltips = function(tooltip) {
- // Tooltip Element
- var tooltipEl = document.getElementById("chartjs-tooltip");
- if (!tooltipEl)
- {
- tooltipEl = document.createElement("div");
- tooltipEl.id = "chartjs-tooltip";
- document.body.appendChild(tooltipEl);
- $(tooltipEl).html("
");
- }
- // Hide if no tooltip
- if (tooltip.opacity === 0)
- {
- tooltipEl.style.opacity = 0;
- return;
- }
+ 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.classList.add("chartjs-tooltip");
+ tooltipEl.innerHTML = "
";
+ // avoid browser's font-zoom since we know that 's
+ // font-size was set to 14px by bootstrap's css
+ var fontZoom = parseFloat($("body").css("font-size")) / 14;
+ // set styles and font
+ tooltipEl.style.padding = tooltip.yPadding + "px " + tooltip.xPadding + "px";
+ tooltipEl.style.borderRadius = tooltip.cornerRadius + "px";
+ tooltipEl.style.fontFamily = tooltip._bodyFontFamily;
+ tooltipEl.style.fontSize = tooltip.bodyFontSize / fontZoom + "px";
+ tooltipEl.style.fontStyle = tooltip._bodyFontStyle;
+ // append Tooltip next to canvas-containing box
+ tooltipEl.ancestor = this._chart.canvas.closest(".box[id]").parentNode;
+ tooltipEl.ancestor.appendChild(tooltipEl);
+ }
- // Limit rendering to once every 50ms. This gives the DOM time to react,
- // and avoids "lag" caused by not giving the DOM time to reapply CSS.
- var now = Date.now();
- if(now - lastTooltipTime < 50)
- {
- return;
- }
- lastTooltipTime = now;
+ // Hide if no tooltip
+ if (tooltip.opacity === 0) {
+ tooltipEl.style.opacity = 0;
+ return;
+ }
- // Set caret Position
- tooltipEl.classList.remove("above", "below", "no-transform");
- if (tooltip.yAlign)
- {
- tooltipEl.classList.add(tooltip.yAlign);
- } else {
- tooltipEl.classList.add("above");
- }
- function getBody(bodyItem) {
- return bodyItem.lines;
- }
- // Set Text
- if (tooltip.body)
- {
- var titleLines = tooltip.title || [];
- var bodyLines = tooltip.body.map(getBody);
- var innerHtml = "