diff --git a/README.md b/README.md
index 7de3b57b..1caf7ef6 100644
--- a/README.md
+++ b/README.md
@@ -45,9 +45,7 @@ Make no mistake: **your support is absolutely vital to help keep us innovating!*
Sending a donation using our links below is **extremely helpful** in offsetting a portion of our monthly expenses:
- Donate via PayPal
-- [Bitcoin](https://commerce.coinbase.com/checkout/fb7facaf-bebd-46be-bb77-b358f4546763): 1GKnevUnVaQM2pQieMyeHkpr8DXfkpfAtL
-- [Bitcoin Cash](https://commerce.coinbase.com/checkout/fb7facaf-bebd-46be-bb77-b358f4546763): qqh25hlmqaj99xraw00e47xmf8sysnyxhyww2d7dnh
-- [Ethereum](https://commerce.coinbase.com/checkout/fb7facaf-bebd-46be-bb77-b358f4546763): 0xF00aF43d2431BAD585056492b310e48eC40D87e8
+- [Bitcoin, Bitcoin Cash, Ethereum, Litecoin](https://commerce.coinbase.com/checkout/dd304d04-f324-4a77-931b-0db61c77a41b)
### Alternative support
If you'd rather not [donate](https://pi-hole.net/donate/) (_which is okay!_), there are other ways you can help support us:
diff --git a/index.php b/index.php
index ab7ca967..f34a7171 100644
--- a/index.php
+++ b/index.php
@@ -162,66 +162,6 @@
-
-
-
×
@@ -66,14 +62,10 @@ function getFullName() {
-
-
Exact blocking
-
+
Exact blocking
-
-
Regex & Wildcard blocking
+
Regex & Wildcard blocking
-
diff --git a/scripts/pi-hole/js/auditlog.js b/scripts/pi-hole/js/auditlog.js
index 95c7fc3d..343950a3 100644
--- a/scripts/pi-hole/js/auditlog.js
+++ b/scripts/pi-hole/js/auditlog.js
@@ -78,7 +78,7 @@ function add(domain,list) {
$.ajax({
url: "scripts/pi-hole/php/add.php",
method: "post",
- data: {"domain":domain, "list":list, "token":token, "auditlog":1}
+ data: {"domain":domain, "list":list, "token":token}
});
}
@@ -91,6 +91,7 @@ $(document).ready(function() {
var url = ($(this).parents("tr"))[0].innerText.split(" ")[0];
if($(this).context.innerText === " Blacklist")
{
+ add(url,"audit");
add(url,"black");
$("#gravityBtn").prop("disabled", false);
}
@@ -104,6 +105,7 @@ $(document).ready(function() {
var url = ($(this).parents("tr"))[0].innerText.split(" ")[0].split(" ")[0];
if($(this).context.innerText === " Whitelist")
{
+ add(url,"audit");
add(url,"white");
$("#gravityBtn").prop("disabled", false);
}
diff --git a/scripts/pi-hole/js/index.js b/scripts/pi-hole/js/index.js
index 21e00bda..8058b16e 100644
--- a/scripts/pi-hole/js/index.js
+++ b/scripts/pi-hole/js/index.js
@@ -637,8 +637,8 @@ function updateSummaryData(runOnce) {
$("#temperature").html("
FTL offline");
// Show spinner
$("#queries-over-time .overlay").show();
- $("#forward-destinations .overlay").show();
- $("#query-types .overlay").show();
+ $("#forward-destinations-pie .overlay").show();
+ $("#query-types-pie .overlay").show();
$("#client-frequency .overlay").show();
$("#domain-frequency .overlay").show();
$("#ad-frequency .overlay").show();
diff --git a/scripts/pi-hole/js/list.js b/scripts/pi-hole/js/list.js
index 53d6b7e5..f0d8a045 100644
--- a/scripts/pi-hole/js/list.js
+++ b/scripts/pi-hole/js/list.js
@@ -43,18 +43,11 @@ function addListEntry(entry, index, list, button, type)
}
function refresh(fade) {
- var listw;
var list = $("#list");
- if(listType === "black")
- {
- listw = $("#list-regex");
- }
+ var listw = $("#list-regex");
if(fade) {
list.fadeOut(100);
- if(listw)
- {
- listw.fadeOut(100);
- }
+ listw.fadeOut(100);
}
$.ajax({
url: "scripts/pi-hole/php/get.php",
@@ -62,49 +55,51 @@ function refresh(fade) {
data: {"list":listType},
success: function(response) {
list.html("");
- if(listw)
- {
- listw.html("");
- }
+ listw.html("");
if((listType === "black" &&
response.blacklist.length === 0 &&
- response.regex.length === 0) ||
+ response.regex_blacklist.length === 0) ||
(listType === "white" &&
- response.whitelist.length === 0))
+ response.whitelist.length === 0 &&
+ response.regex_whitelist.length === 0))
{
$("h3").hide();
list.html("
Your " + fullName + " is empty!
");
}
else
{
- $("h3").show();
if(listType === "white")
{
data = response.whitelist.sort();
- data2 = []; // No regex data, use empty array
+ data2 = response.regex_whitelist.sort();
}
else if(listType === "black")
{
data = response.blacklist.sort();
- data2 = response.regex.sort();
+ data2 = response.regex_blacklist.sort();
}
+
+ if(data.length > 0)
+ {
+ $("#h3-exact").fadeIn(100);
+ }
+ if(data2.length > 0)
+ {
+ $("#h3-regex").fadeIn(100);
+ }
+
data.forEach(function (entry, index)
{
addListEntry(entry, index, list, "#list", "exact");
});
-
- // Add regex domains if present in returned list data
data2.forEach(function (entry, index)
{
- addListEntry(entry, index, listw, "#list-regex", "regex");
+ addListEntry(entry, index, listw, "#list-regex", listType+"_regex");
});
}
list.fadeIn(100);
- if(listw)
- {
- listw.fadeIn(100);
- }
+ listw.fadeIn(100);
},
error: function(jqXHR, exception) {
$("#alFailure").show();
@@ -115,23 +110,55 @@ function refresh(fade) {
window.onload = refresh(false);
function sub(index, entry, arg) {
- var domain = $("#list #"+index);
+ var list = "#list";
+ var heading = "#h3-exact";
var locallistType = listType;
- if(arg === "regex")
+ if(arg === "black_regex" || arg === "white_regex")
{
- locallistType = "regex";
- domain = $("#list-regex #"+index);
+ list = "#list-regex";
+ heading = "#h3-regex";
+ locallistType = arg;
}
+ var alInfo = $("#alInfo");
+ var alSuccess = $("#alSuccess");
+ var alFailure = $("#alFailure");
+ var alWarning = $("#alWarning");
+ var err = $("#err");
+ var warn = $("#warn");
+ var msg = $("#success-message");
+
+
+ var domain = $(list+" #"+index);
domain.hide("highlight");
$.ajax({
url: "scripts/pi-hole/php/sub.php",
method: "post",
data: {"domain":entry, "list":locallistType, "token":token},
success: function(response) {
- if(response.length !== 0){
- return;
+ if (response.indexOf("Success") === -1) {
+ alFailure.show();
+ err.html(response);
+ alFailure.delay(8000).fadeOut(2000, function() {
+ alFailure.hide();
+ });
+ alInfo.delay(8000).fadeOut(2000, function() {
+ alInfo.hide();
+ });
+ } else {
+ alSuccess.show();
+ msg.html(response);
+ alSuccess.delay(1000).fadeOut(2000, function() {
+ alSuccess.hide();
+ });
+ alInfo.delay(1000).fadeOut(2000, function() {
+ alInfo.hide();
+ });
+ domain.remove();
+ if($(list+" li").length < 1)
+ {
+ $(heading).fadeOut(100);
+ }
}
- domain.remove();
},
error: function(jqXHR, exception) {
alert("Failed to remove the domain!");
@@ -140,18 +167,11 @@ function sub(index, entry, arg) {
});
}
-function add(arg) {
- var locallistType = listType;
+function add(type) {
var domain = $("#domain");
- var wild = false;
if(domain.val().length === 0){
return;
}
- if(arg === "wild" || arg === "regex")
- {
- locallistType = arg;
- wild = true;
- }
var alInfo = $("#alInfo");
var alSuccess = $("#alSuccess");
@@ -159,6 +179,7 @@ function add(arg) {
var alWarning = $("#alWarning");
var err = $("#err");
var warn = $("#warn");
+ var msg = $("#success-message");
alInfo.show();
alSuccess.hide();
alFailure.hide();
@@ -166,37 +187,29 @@ function add(arg) {
$.ajax({
url: "scripts/pi-hole/php/add.php",
method: "post",
- data: {"domain":domain.val().trim(), "list":locallistType, "token":token},
+ data: {"domain":domain.val().trim(), "list":type, "token":token},
success: function(response) {
- if (response.indexOf(" already exists in ") !== -1) {
- alWarning.show();
- warn.html(response);
- alWarning.delay(8000).fadeOut(2000, function() {
- alWarning.hide();
- });
- alInfo.delay(8000).fadeOut(2000, function() {
- alInfo.hide();
- });
- } else if (response.indexOf("DONE") === -1) {
- alFailure.show();
- err.html(response);
- alFailure.delay(8000).fadeOut(2000, function() {
- alFailure.hide();
- });
- alInfo.delay(8000).fadeOut(2000, function() {
- alInfo.hide();
- });
- } else {
- alSuccess.show();
- alSuccess.delay(1000).fadeOut(2000, function() {
- alSuccess.hide();
- });
- alInfo.delay(1000).fadeOut(2000, function() {
- alInfo.hide();
- });
- domain.val("");
- refresh(true);
- }
+ if (response.indexOf("Success") === -1) {
+ alFailure.show();
+ err.html(response);
+ alFailure.delay(8000).fadeOut(2000, function() {
+ alFailure.hide();
+ });
+ alInfo.delay(8000).fadeOut(2000, function() {
+ alInfo.hide();
+ });
+ } else {
+ alSuccess.show();
+ msg.html(response);
+ alSuccess.delay(1000).fadeOut(2000, function() {
+ alSuccess.hide();
+ });
+ alInfo.delay(1000).fadeOut(2000, function() {
+ alInfo.hide();
+ });
+ domain.val("");
+ refresh(true);
+ }
},
error: function(jqXHR, exception) {
alFailure.show();
@@ -217,21 +230,21 @@ function add(arg) {
$(document).keypress(function(e) {
if(e.which === 13 && $("#domain").is(":focus")) {
// Enter was pressed, and the input has focus
- add("exact");
+ add(listType);
}
});
// Handle buttons
$("#btnAdd").on("click", function() {
- add("exact");
+ add(listType);
});
$("#btnAddWildcard").on("click", function() {
- add("wild");
+ add(listType+"_wild");
});
$("#btnAddRegex").on("click", function() {
- add("regex");
+ add(listType+"_regex");
});
$("#btnRefresh").on("click", function() {
diff --git a/scripts/pi-hole/js/network.js b/scripts/pi-hole/js/network.js
index be432c1a..52aa2ec0 100644
--- a/scripts/pi-hole/js/network.js
+++ b/scripts/pi-hole/js/network.js
@@ -142,7 +142,7 @@ $(document).ready(function() {
"processing": true,
"order" : [[5, "desc"]],
"columns": [
- {data: "ip", "width" : "10%", "render": $.fn.dataTable.render.text(), "orderable": false },
+ {data: "ip", "type": "ip-address", "width" : "10%", "render": $.fn.dataTable.render.text() },
{data: "hwaddr", "width" : "10%", "render": $.fn.dataTable.render.text() },
{data: "interface", "width" : "4%", "render": $.fn.dataTable.render.text() },
{data: "name", "width" : "15%", "render": $.fn.dataTable.render.text() },
diff --git a/scripts/pi-hole/js/queries.js b/scripts/pi-hole/js/queries.js
index 95276cad..2b6ca3db 100644
--- a/scripts/pi-hole/js/queries.js
+++ b/scripts/pi-hole/js/queries.js
@@ -183,7 +183,7 @@ $(document).ready(function() {
blocked = true;
color = "red";
fieldtext = "Blocked (gravity)";
- buttontext = "
Whitelist";
+ buttontext = "
Whitelist";
break;
case "2":
blocked = false;
@@ -201,13 +201,13 @@ $(document).ready(function() {
blocked = true;
color = "red";
fieldtext = "Blocked
(regex/wildcard)";
- buttontext = "
Whitelist" ;
+ buttontext = "
Whitelist" ;
break;
case "5":
blocked = true;
color = "red";
fieldtext = "Blocked
(blacklist)";
- buttontext = "
Whitelist" ;
+ buttontext = "
Whitelist" ;
break;
case "6":
blocked = true;
diff --git a/scripts/pi-hole/php/add.php b/scripts/pi-hole/php/add.php
index 68a7496d..895df011 100644
--- a/scripts/pi-hole/php/add.php
+++ b/scripts/pi-hole/php/add.php
@@ -8,51 +8,62 @@
require_once('auth.php');
-$type = $_POST['list'];
+$list = $_POST['list'];
// Perform all of the authentication for list editing
// when NOT invoked and authenticated from API
if (empty($api)) {
- list_verify($type);
+ list_verify($list);
}
-// Don't check if the added item is a valid domain for regex expressions. Regex
-// filters are validated by FTL on import and skipped if invalid
-if($type !== "regex") {
+// Only check domains we add to the exact lists.
+// Regex are validated by FTL during import
+$check_lists = ["white","black","audit"];
+if(in_array($list, $check_lists)) {
check_domain();
}
-// Escape shell metacharacters
-$domains = escapeshellcmd($_POST['domain']);
+// Split individual domains into array
+$domains = preg_split('/\s+/', trim($_POST['domain']));
-switch($type) {
- case "white":
- if(!isset($_POST["auditlog"]))
- echo shell_exec("sudo pihole -w --web ".$domains);
- else
- {
- echo shell_exec("sudo pihole -w --web ".$domains);
- echo shell_exec("sudo pihole -a audit ".$domains);
- }
- break;
- case "black":
- if(!isset($_POST["auditlog"]))
- echo shell_exec("sudo pihole -b --web ".$domains);
- else
- {
- echo shell_exec("sudo pihole -b --web ".$domains);
- echo shell_exec("sudo pihole -a audit ".$domains);
- }
- break;
- case "regex":
- echo shell_exec("sudo pihole --regex --web ".$domains);
- break;
- case "wild":
- echo shell_exec("sudo pihole --wild --web ".$domains);
- break;
- case "audit":
- echo shell_exec("sudo pihole -a audit ".$domains);
- break;
+require_once("func.php");
+require_once("database.php");
+$GRAVITYDB = getGravityDBFilename();
+$db = SQLite3_connect($GRAVITYDB, SQLITE3_OPEN_READWRITE);
+
+switch($list) {
+ case "white":
+ echo add_to_table($db, "whitelist", $domains);
+ break;
+
+ case "black":
+ echo add_to_table($db, "blacklist", $domains);
+ break;
+
+ case "black_regex":
+ echo add_to_table($db, "regex_blacklist", $domains);
+ break;
+
+ case "white_regex":
+ echo add_to_table($db, "regex_whitelist", $domains);
+ break;
+
+ case "black_wild":
+ echo add_to_table($db, "regex_blacklist", $domains, true);
+ break;
+
+ case "white_wild":
+ echo add_to_table($db, "regex_whitelist", $domains, true);
+ break;
+
+ case "audit":
+ echo add_to_table($db, "domain_audit", $domains);
+ break;
+
+ default:
+ die("Invalid list!");
}
+// Reload lists in pihole-FTL after having added something
+echo shell_exec("sudo pihole restartdns reload");
?>
diff --git a/scripts/pi-hole/php/auth.php b/scripts/pi-hole/php/auth.php
index 69fe6b28..1ea6014d 100644
--- a/scripts/pi-hole/php/auth.php
+++ b/scripts/pi-hole/php/auth.php
@@ -11,7 +11,6 @@ $ERRORLOG = getenv('PHP_ERROR_LOG');
if (empty($ERRORLOG)) {
$ERRORLOG = '/var/log/lighttpd/error.log';
}
-$regexfile = "/etc/pihole/regex.list";
function pi_log($message) {
error_log(date('Y-m-d H:i:s') . ': ' . $message . "\n", 3, $GLOBALS['ERRORLOG']);
diff --git a/scripts/pi-hole/php/database.php b/scripts/pi-hole/php/database.php
index 3a453677..f98f2b8a 100644
--- a/scripts/pi-hole/php/database.php
+++ b/scripts/pi-hole/php/database.php
@@ -33,7 +33,13 @@ function SQLite3_connect_try($filename, $mode, $trytoreconnect)
if($trytoreconnect)
{
sleep(3);
- $db = SQLite3_connect_try($filename, $mode, false);
+ return SQLite3_connect_try($filename, $mode, false);
+ }
+ else
+ {
+ // If we should not try again (or are already trying again!), we return the exception string
+ // so the user gets it on the dashboard
+ return $filename.": ".$exception->getMessage();
}
}
}
@@ -48,10 +54,184 @@ function SQLite3_connect($filename, $mode=SQLITE3_OPEN_READONLY)
{
die("No database available");
}
- if(!$db)
+ if(is_string($db))
{
- die("Error connecting to database");
+ die("Error connecting to database\n".$db);
}
+
+ // Add busy timeout so methods don't fail immediately when, e.g., FTL is currently reading from the DB
+ $db->busyTimeout(5000);
+
return $db;
}
+
+
+/**
+ * Add domains to a given table
+ *
+ * @param $db object The SQLite3 database connection object
+ * @param $table string The target table
+ * @param $domains array Array of domains (strings) to be added to the table
+ * @param $wildcardstyle boolean Whether to format the input domains in legacy wildcard notation
+ * @param $returnnum boolean Whether to return an integer or a string
+ * @return string Success/error and number of processed domains
+ */
+function add_to_table($db, $table, $domains, $wildcardstyle=false, $returnnum=false)
+{
+ // Begin transaction
+ if(!$db->exec("BEGIN TRANSACTION;"))
+ {
+ if($returnnum)
+ return 0;
+ else
+ return "Error: Unable to begin transaction for ".$table." table.";
+ }
+ $initialcount = intval($db->querySingle("SELECT COUNT(*) FROM ".$table.";"));
+
+ // Prepare SQLite statememt
+ $stmt = $db->prepare("INSERT OR IGNORE INTO ".$table." (domain) VALUES (:domain);");
+
+ // Return early if we failed to prepare the SQLite statement
+ if(!$stmt)
+ {
+ if($returnnum)
+ return 0;
+ else
+ return "Error: Failed to prepare statement for ".$table." table.";
+ }
+
+ // Loop over domains and inject the lines into the database
+ $num = 0;
+ foreach($domains as $domain)
+ {
+ // Limit max length for a domain entry to 253 chars
+ if(strlen($domain) > 253)
+ continue;
+
+ if($wildcardstyle)
+ $domain = "(\\.|^)".str_replace(".","\\.",$domain)."$";
+
+ $stmt->bindValue(":domain", $domain, SQLITE3_TEXT);
+
+ if($stmt->execute() && $stmt->reset())
+ $num++;
+ else
+ {
+ $stmt->close();
+ if($returnnum)
+ return $num;
+ else
+ {
+ if($num === 1)
+ $plural = "";
+ else
+ $plural = "s";
+ return "Error: ".$db->lastErrorMsg().", added ".$num." domain".$plural;
+ }
+ }
+ }
+
+ // Close prepared statement and return number of processed rows
+ $stmt->close();
+ $db->exec("COMMIT;");
+
+ if($returnnum)
+ return $num;
+ else
+ {
+ $finalcount = intval($db->querySingle("SELECT COUNT(*) FROM ".$table.";"));
+ $modified = $finalcount - $initialcount;
+
+ // If we add less domains than the user specified, then they wanted to add duplicates
+ if($modified !== $num)
+ {
+ $delta = $num - $modified;
+ $extra = " (skipped ".$delta." duplicates)";
+ }
+ else
+ {
+ $extra = "";
+ }
+
+ if($num === 1)
+ $plural = "";
+ else
+ $plural = "s";
+ return "Success, added ".$modified." of ".$num." domain".$plural.$extra;
+ }
+}
+
+/**
+ * Remove domains from a given table
+ *
+ * @param $db object The SQLite3 database connection object
+ * @param $table string The target table
+ * @param $domains array Array of domains (strings) to be removed from the table
+ * @param $returnnum boolean Whether to return an integer or a string
+ * @return string Success/error and number of processed domains
+ */
+function remove_from_table($db, $table, $domains, $returnnum=false)
+{
+ // Begin transaction
+ if(!$db->exec("BEGIN TRANSACTION;"))
+ {
+ if($returnnum)
+ return 0;
+ else
+ return "Error: Unable to begin transaction for ".$table." table.";
+ }
+ $initialcount = intval($db->querySingle("SELECT COUNT(*) FROM ".$table.";"));
+
+ // Prepare SQLite statememt
+ $stmt = $db->prepare("DELETE FROM ".$table." WHERE domain = :domain;");
+
+ // Return early if we failed to prepare the SQLite statement
+ if(!$stmt)
+ {
+ if($returnnum)
+ return 0;
+ else
+ return "Error: Failed to prepare statement for ".$table." table.";
+ }
+
+ // Loop over domains and remove the lines from the database
+ $num = 0;
+ foreach($domains as $domain)
+ {
+ $stmt->bindValue(":domain", $domain, SQLITE3_TEXT);
+
+ if($stmt->execute() && $stmt->reset())
+ $num++;
+ else
+ {
+ $stmt->close();
+ if($returnnum)
+ return $num;
+ else
+ {
+ if($num === 1)
+ $plural = "";
+ else
+ $plural = "s";
+ return "Error: ".$db->lastErrorMsg().", removed ".$num." domain".$plural;
+ }
+ }
+ }
+
+ // Close prepared statement and return number or processed rows
+ $stmt->close();
+ $db->exec("COMMIT;");
+
+ if($returnnum)
+ return $num;
+ else
+ {
+ if($num === 1)
+ $plural = "";
+ else
+ $plural = "s";
+ return "Success, removed ".$num." domain".$plural;
+ }
+}
+
?>
diff --git a/scripts/pi-hole/php/func.php b/scripts/pi-hole/php/func.php
index 7c1d3d65..bc353218 100644
--- a/scripts/pi-hole/php/func.php
+++ b/scripts/pi-hole/php/func.php
@@ -45,21 +45,4 @@ if(!function_exists('hash_equals')) {
}
}
-function add_regex($regex, $mode=FILE_APPEND, $append="\n")
-{
- global $regexfile;
- if(file_put_contents($regexfile, $regex.$append, $mode) === FALSE)
- {
- $err = error_get_last()["message"];
- echo "Unable to add regex \"".htmlspecialchars($regex)."\" to ${regexfile}
Error message: $err";
- }
- else
- {
- // Send SIGHUP to pihole-FTL using a frontend command
- // to force reloading of the regex domains
- // This will also wipe the resolver's cache
- echo exec("sudo pihole restartdns reload");
- }
-}
-
?>
diff --git a/scripts/pi-hole/php/get.php b/scripts/pi-hole/php/get.php
index c84b991a..b38446bc 100644
--- a/scripts/pi-hole/php/get.php
+++ b/scripts/pi-hole/php/get.php
@@ -21,7 +21,7 @@ function getTableContent($listname) {
global $db;
$entries = array();
$querystr = implode(" ",array("SELECT ${listname}.*,\"group\".enabled as group_enabled",
- "FROM $listname",
+ "FROM ${listname}",
"LEFT JOIN ${listname}_by_group ON ${listname}_by_group.${listname}_id = ${listname}.id",
"LEFT JOIN \"group\" ON \"group\".id = ${listname}_by_group.group_id",
"GROUP BY domain;"));
@@ -54,13 +54,15 @@ function filterArray(&$inArray) {
switch ($listtype)
{
case "white":
- $list = getTableContent("whitelist");
+ $exact = getTableContent("whitelist");
+ $regex = getTableContent("regex_whitelist");
+ $list = array_merge($exact, $regex);
break;
case "black":
$exact = getTableContent("blacklist");
- $regex = getTableContent("regex");
- $list = array_merge($exact, $regex);
+ $regex = getTableContent("regex_blacklist");
+ $list = array_merge($exact, $regex);
break;
default:
diff --git a/scripts/pi-hole/php/gravity.php b/scripts/pi-hole/php/gravity.php
index cb3c3404..95922804 100644
--- a/scripts/pi-hole/php/gravity.php
+++ b/scripts/pi-hole/php/gravity.php
@@ -1,51 +1,59 @@
- | April 23rd, 2018:
- Checks when the gravity list was last updated, if it exists at all.
- Returns the info in human-readable format for use on the dashboard,
- or raw for use by the API.
- */
- $gravitylist = "/etc/pihole/gravity.list";
- if (file_exists($gravitylist)){
- $date_file_created_unix = filemtime($gravitylist);
- $date_file_created = date_create("@".$date_file_created_unix);
- $date_now = date_create("now");
- $gravitydiff = date_diff($date_file_created,$date_now);
- if($raw){
- $output = array(
- "file_exists"=> true,
- "absolute" => $date_file_created_unix,
- "relative" => array(
- "days" => $gravitydiff->format("%a"),
- "hours" => $gravitydiff->format("%H"),
- "minutes" => $gravitydiff->format("%I"),
- )
- );
- }else{
- if($gravitydiff->d > 1){
- $output = $gravitydiff->format("Blocking list updated %a days, %H:%I ago");
- }elseif($gravitydiff->d == 1){
- $output = $gravitydiff->format("Blocking list updated one day, %H:%I ago");
- }else{
- $output = $gravitydiff->format("Blocking list updated %H:%I ago");
- }
- }
- }else{
- if($raw){
- $output = array("file_exists"=>false);
- }else{
- $output = "Blocking list not found";
- }
- }
- return $output;
-}
-
-?>
\ No newline at end of file
+querySingle("SELECT value FROM info WHERE property = 'updated';");
+ if($date_file_created_unix === false)
+ {
+ if($raw)
+ {
+ // Array output
+ return array("file_exists" => false);
+ }
+ else
+ {
+ // String output
+ return "Gravity database not available";
+ }
+ }
+ $date_file_created = date_create("@".intval($date_file_created_unix));
+ $date_now = date_create("now");
+ $gravitydiff = date_diff($date_file_created,$date_now);
+ if($raw)
+ {
+ // Array output
+ return array(
+ "file_exists"=> true,
+ "absolute" => $date_file_created_unix,
+ "relative" => array(
+ "days" => $gravitydiff->format("%a"),
+ "hours" => $gravitydiff->format("%H"),
+ "minutes" => $gravitydiff->format("%I"),
+ )
+ );
+ }
+
+ if($gravitydiff->d > 1)
+ {
+ // String output (more than one day ago)
+ return $gravitydiff->format("Blocking list updated %a days, %H:%I (hh:mm) ago");
+ }
+ elseif($gravitydiff->d == 1)
+ {
+ // String output (one day ago)
+ return $gravitydiff->format("Blocking list updated one day, %H:%I (hh:mm) ago");
+ }
+
+ // String output (less than one day ago)
+ return $gravitydiff->format("Blocking list updated %H:%I (hh:mm) ago");
+}
+?>
diff --git a/scripts/pi-hole/php/header.php b/scripts/pi-hole/php/header.php
index e070bc81..9ab0d721 100644
--- a/scripts/pi-hole/php/header.php
+++ b/scripts/pi-hole/php/header.php
@@ -60,6 +60,15 @@
{
$temperatureunit = $_POST["tempunit"];
}
+ // Get user-defined temperature limit if set
+ if(isset($setupVars['TEMPERATURE_LIMIT']))
+ {
+ $temperaturelimit = intval($setupVars['TEMPERATURE_LIMIT']);
+ }
+ else
+ {
+ $temperaturelimit = 60;
+ }
}
else
{
@@ -339,7 +348,7 @@ if($auth) {
{
if ($celsius >= -273.15) {
echo "
60) {
+ if ($celsius > $temperaturelimit) {
echo "#FF0000";
}
else
diff --git a/scripts/pi-hole/php/sub.php b/scripts/pi-hole/php/sub.php
index 97c8f84b..4eb5c3c6 100644
--- a/scripts/pi-hole/php/sub.php
+++ b/scripts/pi-hole/php/sub.php
@@ -16,25 +16,36 @@ if (empty($api)) {
list_verify($type);
}
-// Don't check if the added item is a valid domain for regex expressions.
-// Regex filters are validated by FTL on import and skipped if invalid
-if($type !== "regex") {
- check_domain();
-}
+// Split individual domains into array
+$domains = preg_split('/\s+/', trim($_POST['domain']));
-// Escape shell metacharacters
-$domain = escapeshellcmd($_POST['domain']);
+require_once("func.php");
+
+require("database.php");
+$GRAVITYDB = getGravityDBFilename();
+$db = SQLite3_connect($GRAVITYDB, SQLITE3_OPEN_READWRITE);
switch($type) {
- case "white":
- exec("sudo pihole -w -q -d ".$domain);
- break;
- case "black":
- exec("sudo pihole -b -q -d ".$domain);
- break;
- case "regex":
- exec("sudo pihole --regex -q -d ".$domain);
- break;
+ case "white":
+ echo remove_from_table($db, "whitelist", $domains);
+ break;
+
+ case "black":
+ echo remove_from_table($db, "blacklist", $domains);
+ break;
+
+ case "black_regex":
+ echo remove_from_table($db, "regex_blacklist", $domains);
+ break;
+
+ case "white_regex":
+ echo remove_from_table($db, "regex_whitelist", $domains);
+ break;
+
+ default:
+ die("Invalid list!");
}
+// Reload lists in pihole-FTL after having removed something
+echo shell_exec("sudo pihole restartdns reload");
?>
diff --git a/scripts/pi-hole/php/teleporter.php b/scripts/pi-hole/php/teleporter.php
index f7f075a6..8fb35702 100644
--- a/scripts/pi-hole/php/teleporter.php
+++ b/scripts/pi-hole/php/teleporter.php
@@ -16,7 +16,9 @@ if (php_sapi_name() !== "cli") {
check_csrf(isset($_POST["token"]) ? $_POST["token"] : "");
}
-$db = SQLite3_connect(getGravityDBFilename());
+$db = SQLite3_connect(getGravityDBFilename(), SQLITE3_OPEN_READWRITE);
+
+$flushed_tables = array();
function archive_add_file($path,$name,$subdir="")
{
@@ -30,23 +32,158 @@ function archive_add_file($path,$name,$subdir="")
*
* @param $name string The name of the file in the archive to save the table to
* @param $table string The table to export
- * @param $column string The column on the table to export
*/
-function archive_add_table($name, $table, $column)
+function archive_add_table($name, $table)
{
global $archive, $db;
- $results = $db->query("SELECT $column FROM $table");
- $content = "";
+ $results = $db->query("SELECT * FROM $table");
- while($row = $results->fetchArray()) {
- $content .= $row[0]."\n";
+ // Return early without creating a file if the
+ // requested table cannot be accessed
+ if(is_null($results))
+ return;
+
+ $content = array();
+ while ($row = $results->fetchArray(SQLITE3_ASSOC))
+ {
+ array_push($content, $row);
}
- $archive[$name] = $content;
+ $archive[$name] = json_encode($content);
}
-function archive_add_directory($path, $subdir="")
+
+/**
+ * Restore the contents of a table from an uploaded archive
+ *
+ * @param $file object The file in the archive to restore the table from
+ * @param $table string The table to import
+ * @param $flush boolean Whether to flush the table before importing the archived data
+ * @return integer Number of restored rows
+ */
+function archive_restore_table($file, $table, $flush=false)
+{
+ global $db, $flushed_tables;
+
+ $json_string = file_get_contents($file);
+ // Return early if we cannot extract the JSON string
+ if(is_null($json_string))
+ return 0;
+
+ $contents = json_decode($json_string, true);
+ // Return early if we cannot decode the JSON string
+ if(is_null($contents))
+ return 0;
+
+ // Flush table if requested, only flush each table once
+ if($flush && !in_array($table, $flushed_tables))
+ {
+ $db->exec("DELETE FROM ".$table);
+ array_push($flushed_tables, $table);
+ }
+
+ // Prepare field name for domain/address depending on the table we restore to
+ if($table === "adlist")
+ {
+ $sql = "INSERT OR IGNORE INTO adlist";
+ $sql .= " (id,address,enabled,date_added,comment)";
+ $sql .= " VALUES (:id,:address,:enabled,:date_added,:comment);";
+ $field = "address";
+ }
+ elseif($table === "domain_audit")
+ {
+ $sql = "INSERT OR IGNORE INTO domain_audit";
+ $sql .= " (id,domain,date_added)";
+ $sql .= " VALUES (:id,:domain,:date_added);";
+ $field = "domain";
+ }
+ else
+ {
+ $sql = "INSERT OR IGNORE INTO ".$table;
+ $sql .= " (id,domain,enabled,date_added,comment)";
+ $sql .= " VALUES (:id,:domain,:enabled,:date_added,:comment);";
+ $field = "domain";
+ }
+
+ // Prepare SQLite statememt
+ $stmt = $db->prepare($sql);
+
+ // Return early if we fail to prepare the SQLite statement
+ if(!$stmt)
+ {
+ echo "Failed to prepare statement for ".$table." table.";
+ echo $sql;
+ return 0;
+ }
+
+ // Loop over rows and inject the entries into the database
+ $num = 0;
+ foreach($contents as $row)
+ {
+ // Limit max length for a domain entry to 253 chars
+ if(strlen($row[$field]) > 253)
+ continue;
+
+ $stmt->bindValue(":id", $row["id"], SQLITE3_INTEGER);
+ $stmt->bindValue(":date_added", $row["date_added"], SQLITE3_INTEGER);
+ $stmt->bindValue(":".$field, $row[$field], SQLITE3_TEXT);
+
+ if($table !== "domain_audit")
+ {
+ $stmt->bindValue(":enabled", $row["enabled"], SQLITE3_INTEGER);
+ if(is_null($row["comment"]))
+ $type = SQLITE3_NULL;
+ else
+ $type = SQLITE3_TEXT;
+ $stmt->bindValue(":comment", $row["comment"], $type);
+ }
+
+ if($stmt->execute() && $stmt->reset() && $stmt->clear())
+ $num++;
+ else
+ {
+ $stmt->close();
+ return $num;
+ }
+ }
+
+ // Close database connection and return number or processed rows
+ $stmt->close();
+ return $num;
+}
+
+=======
+/**
+ * Create table rows from an uploaded archive file
+ *
+ * @param $file object The file of the file in the archive to import
+ * @param $table string The target table
+ * @param $flush boolean Whether to flush the table before importing the archived data
+ * @param $wildcardstyle boolean Whether to format the input domains in legacy wildcard notation
+ * @return integer Number of processed rows from the imported file
+ */
+function archive_insert_into_table($file, $table, $flush=false, $wildcardstyle=false)
+{
+ global $db, $flushed_tables;
+
+ $domains = array_filter(explode("\n",file_get_contents($file)));
+ // Return early if we cannot extract the lines in the file
+ if(is_null($domains))
+ return 0;
+
+ // Flush table if requested, only flush each table once
+ if($flush && !in_array($table, $flushed_tables))
+ {
+ $db->exec("DELETE FROM ".$table);
+ array_push($flushed_tables, $table);
+ }
+
+ // Add domains to requested table
+ return add_to_table($db, $table, $domains, $wildcardstyle, true);
+}
+
+function archive_add_directory($path,$subdir="")
{
if($dir = opendir($path))
{
@@ -54,47 +191,13 @@ function archive_add_directory($path, $subdir="")
{
if($entry !== "." && $entry !== "..")
{
- archive_add_file($path, $entry, $subdir);
+ archive_add_file($path,$entry,$subdir);
}
}
closedir($dir);
}
}
-function limit_length(&$item, $key)
-{
- // limit max length for a domain entry to 253 chars
- // return only a part of the string if it is longer
- $item = substr($item, 0, 253);
-}
-
-function process_file($contents, $check=True)
-{
- $domains = array_filter(explode("\n", $contents));
-
- // Walk array and apply a max string length
- // function to every member of the array of domains
- array_walk($domains, "limit_length");
-
- // Check validity of domains (don't do it for regex filters)
- if($check)
- {
- check_domains($domains);
- }
-
- return $domains;
-}
-
-function check_domains($domains)
-{
- foreach($domains as $domain)
- {
- if(!is_valid_domain_name($domain)){
- die(htmlspecialchars($domain).' is not a valid domain');
- }
- }
-}
-
if(isset($_POST["action"]))
{
if($_FILES["zip_file"]["name"] && $_POST["action"] == "in")
@@ -128,74 +231,92 @@ if(isset($_POST["action"]))
$importedsomething = false;
- foreach(new RecursiveIteratorIterator($archive) as $file)
+ $flushtables = isset($_POST["flushtables"]);
+
+ foreach($archive as $file)
{
if(isset($_POST["blacklist"]) && $file->getFilename() === "blacklist.txt")
{
- $blacklist = process_file(file_get_contents($file));
- echo "Processing blacklist.txt (".count($blacklist)." entries) \n";
- exec("sudo pihole -b -nr --nuke");
- exec("sudo pihole -b -q -nr ".implode(" ", $blacklist));
+ $num = archive_insert_into_table($file, "blacklist", $flushtables);
+ echo "Processed blacklist (exact) (".$num." entries) \n";
$importedsomething = true;
}
if(isset($_POST["whitelist"]) && $file->getFilename() === "whitelist.txt")
{
- $whitelist = process_file(file_get_contents($file));
- echo "Processing whitelist.txt (".count($whitelist)." entries) \n";
- exec("sudo pihole -w -nr --nuke");
- exec("sudo pihole -w -q -nr ".implode(" ", $whitelist));
+ $num = archive_insert_into_table($file, "whitelist", $flushtables);
+ echo "Processed whitelist (exact) (".$num." entries) \n";
$importedsomething = true;
}
if(isset($_POST["regexlist"]) && $file->getFilename() === "regex.list")
{
- $regexlist = process_file(file_get_contents($file), false);
- echo "Processing regex.list (".count($regexlist)." entries) \n";
-
- $escapedRegexlist = array_map("escapeshellcmd", $regexlist);
- exec("sudo pihole --regex -nr --nuke");
- exec("sudo pihole --regex -q -nr ".implode(" ", $escapedRegexlist));
+ $num = archive_insert_into_table($file, "regex_blacklist", $flushtables);
+ echo "Processed blacklist (regex) (".$num." entries) \n";
$importedsomething = true;
}
// Also try to import legacy wildcard list if found
if(isset($_POST["regexlist"]) && $file->getFilename() === "wildcardblocking.txt")
{
- $wildlist = process_file(file_get_contents($file));
- echo "Processing wildcardblocking.txt (".count($wildlist)." entries) \n";
- exec("sudo pihole --wild -nr --nuke");
- exec("sudo pihole --wild -q -nr ".implode(" ", $wildlist));
+ $num = archive_insert_into_table($file, "regex_blacklist", $flushtables, true);
+ echo "Processed blacklist (regex, wildcard style) (".$num." entries) \n";
$importedsomething = true;
}
if(isset($_POST["auditlog"]) && $file->getFilename() === "auditlog.list")
{
- $auditlog = process_file(file_get_contents($file));
- echo "Processing auditlog.list (".count($auditlog)." entries) \n";
- exec("sudo pihole -a clearaudit");
- exec("sudo pihole -a audit ".implode(" ",$auditlog));
- }
-
- if(isset($_POST["dhcpleases"]) && $file->getFilename() === "04-pihole-static-dhcp.conf")
- {
- $dhcpleases = processStaticLeasesFile($file->getPathname());
- echo "Processing DHCP leases 04-pihole-static-dhcp.conf (".count($dhcpleases)." entries)";
- $counter_leases_added = 0;
- foreach($dhcpleases as $lease) {
- $result = addDHCPLease($lease["hwaddr"], $lease["IP"], $lease["host"]);
- if($result['value']) {
- $counter_leases_added++;
- }
- }
- echo " - Added ".$counter_leases_added."/".count($dhcpleases)." \n";
+ $num = archive_insert_into_table($file, "domain_audit", $flushtables);
+ echo "Processed blacklist (regex) (".$num." entries) \n";
$importedsomething = true;
}
- if($importedsomething)
+ if(isset($_POST["blacklist"]) && $file->getFilename() === "blacklist.exact.json")
{
- exec("sudo pihole restartdns");
+ $num = archive_restore_table($file, "blacklist", $flushtables);
+ echo "Processed blacklist (exact) (".$num." entries) \n";
+ $importedsomething = true;
}
+
+ if(isset($_POST["regexlist"]) && $file->getFilename() === "blacklist.regex.json")
+ {
+ $num = archive_restore_table($file, "regex_blacklist", $flushtables);
+ echo "Processed blacklist (regex) (".$num." entries) \n";
+ $importedsomething = true;
+ }
+
+ if(isset($_POST["whitelist"]) && $file->getFilename() === "whitelist.exact.json")
+ {
+ $num = archive_restore_table($file, "whitelist", $flushtables);
+ echo "Processed whitelist (exact) (".$num." entries) \n";
+ $importedsomething = true;
+ }
+
+ if(isset($_POST["regex_whitelist"]) && $file->getFilename() === "whitelist.regex.json")
+ {
+ $num = archive_restore_table($file, "regex_whitelist", $flushtables);
+ echo "Processed whitelist (regex) (".$num." entries) \n";
+ $importedsomething = true;
+ }
+
+ if(isset($_POST["adlist"]) && $file->getFilename() === "adlist.json")
+ {
+ $num = archive_restore_table($file, "adlist", $flushtables);
+ echo "Processed adlist (".$num." entries) \n";
+ $importedsomething = true;
+ }
+
+ if(isset($_POST["auditlog"]) && $file->getFilename() === "domain_audit.json")
+ {
+ $num = archive_restore_table($file, "domain_audit", $flushtables);
+ echo "Processed domain_audit (".$num." entries) \n";
+ $importedsomething = true;
+ }
+ }
+
+ if($importedsomething)
+ {
+ exec("sudo pihole restartdns reload");
}
unlink($fullfilename);
@@ -217,12 +338,13 @@ else
exit("cannot open/create ".htmlentities($archive_file_name)." \nPHP user: ".exec('whoami')."\n");
}
- archive_add_table("whitelist.txt", "whitelist", "domain");
- archive_add_table("blacklist.txt", "blacklist", "domain");
- archive_add_table("regex.list", "regex", "domain");
- archive_add_table("adlists.list", "adlist", "address");
+ archive_add_table("whitelist.exact.json", "whitelist");
+ archive_add_table("whitelist.regex.json", "regex_whitelist");
+ archive_add_table("blacklist.exact.json", "blacklist");
+ archive_add_table("blacklist.regex.json", "regex_blacklist");
+ archive_add_table("adlist.json", "adlist");
+ archive_add_table("domain_audit.json", "domain_audit");
archive_add_file("/etc/pihole/","setupVars.conf");
- archive_add_file("/etc/pihole/","auditlog.list");
archive_add_directory("/etc/dnsmasq.d/","dnsmasq.d/");
$archive->compress(Phar::GZ); // Creates a gziped copy
diff --git a/settings.php b/settings.php
index e54e0214..29094927 100644
--- a/settings.php
+++ b/settings.php
@@ -799,7 +799,7 @@ if (isset($_GET['tab']) && in_array($_GET['tab'], array("sysadmin", "blocklists"
checked>
- Listen only on interface
+ Listen only on interface
@@ -1213,19 +1228,19 @@ if (isset($_GET['tab']) && in_array($_GET['tab'], array("sysadmin", "blocklists"