diff --git a/db_queries.php b/db_queries.php index fd3b0606..e8962604 100644 --- a/db_queries.php +++ b/db_queries.php @@ -123,6 +123,40 @@ + + +
@@ -162,6 +196,7 @@ + 0)
+
diff --git a/scripts/pi-hole/js/db_queries.js b/scripts/pi-hole/js/db_queries.js index 6751b00b..ec38f751 100644 --- a/scripts/pi-hole/js/db_queries.js +++ b/scripts/pi-hole/js/db_queries.js @@ -5,7 +5,7 @@ * This file is copyright under the latest version of the EUPL. * Please see LICENSE file for your rights under this license. */ -/* global moment:false */ +/* global moment:false, utils:false */ var start__ = moment().subtract(6, "days"); var from = moment(start__).utc().valueOf() / 1000; @@ -73,70 +73,6 @@ $(function () { var tableApi, statistics; -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/groups.php", - method: "post", - data: { - domain: domain, - list: list, - token: token, - action: "add_domain", - comment: "Added from Long-Term-Data Query Log" - }, - success: function (response) { - if (!response.success) { - alFailure.show(); - err.html(response.message); - 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 handleAjaxError(xhr, textStatus) { if (textStatus === "timeout") { alert("The server took too long to send the data."); @@ -409,9 +345,9 @@ $(function () { $("#all-queries tbody").on("click", "button", function () { var data = tableApi.row($(this).parents("tr")).data(); if ([1, 4, 5, 9, 10, 11].indexOf(data[4]) !== -1) { - add(data[2], "white"); + utils.addFromQueryLog(data[2], "white"); } else { - add(data[2], "black"); + utils.addFromQueryLog(data[2], "black"); } }); diff --git a/scripts/pi-hole/js/queries.js b/scripts/pi-hole/js/queries.js index 8ff12674..25d70867 100644 --- a/scripts/pi-hole/js/queries.js +++ b/scripts/pi-hole/js/queries.js @@ -25,81 +25,6 @@ var replyTypes = [ ]; var colTypes = ["time", "query type", "domain", "client", "status", "reply type"]; -function add(domain, list) { - var token = $("#token").text(); - var alertModal = $("#alertModal"); - var alProcessing = alertModal.find(".alProcessing"); - var alSuccess = alertModal.find(".alSuccess"); - var alFailure = alertModal.find(".alFailure"); - var alNetworkErr = alertModal.find(".alFailure #alNetErr"); - var alCustomErr = alertModal.find(".alFailure #alCustomErr"); - var alList = "#alList"; - var alDomain = "#alDomain"; - - // Exit the function here if the Modal is already shown (multiple running interlock) - if (alertModal.css("display") !== "none") { - return; - } - - var listtype = list === "white" ? "Whitelist" : "Blacklist"; - - alProcessing.children(alDomain).html(domain); - alProcessing.children(alList).html(listtype); - alertModal.modal("show"); - - // add Domain to List after Modal has faded in - alertModal.one("shown.bs.modal", function () { - $.ajax({ - url: "scripts/pi-hole/php/groups.php", - method: "post", - data: { - domain: domain, - list: list, - token: token, - action: "add_domain", - comment: "Added from Query Log" - }, - success: function (response) { - alProcessing.hide(); - if (!response.success) { - // Failure - alNetworkErr.hide(); - alCustomErr.html(response.message); - alFailure.fadeIn(1000); - setTimeout(function () { - alertModal.modal("hide"); - }, 3000); - } else { - // Success - alSuccess.children(alDomain).html(domain); - alSuccess.children(alList).html(listtype); - alSuccess.fadeIn(1000); - setTimeout(function () { - alertModal.modal("hide"); - }, 2000); - } - }, - error: function () { - // Network Error - alProcessing.hide(); - alNetworkErr.show(); - alFailure.fadeIn(1000); - setTimeout(function () { - alertModal.modal("hide"); - }, 3000); - } - }); - }); - - // Reset Modal after it has faded out - alertModal.one("hidden.bs.modal", function () { - alProcessing.show(); - alSuccess.add(alFailure).hide(); - alProcessing.add(alSuccess).children(alDomain).html("").end().children(alList).html(""); - alCustomErr.html(""); - }); -} - function handleAjaxError(xhr, textStatus) { if (textStatus === "timeout") { alert("The server took too long to send the data."); @@ -493,9 +418,9 @@ $(function () { $("#all-queries tbody").on("click", "button", function () { var data = tableApi.row($(this).parents("tr")).data(); if (data[4] === "2" || data[4] === "3") { - add(data[2], "black"); + utils.addFromQueryLog(data[2], "black"); } else { - add(data[2], "white"); + utils.addFromQueryLog(data[2], "white"); } }); diff --git a/scripts/pi-hole/js/utils.js b/scripts/pi-hole/js/utils.js index c83e7484..cdc4bead 100644 --- a/scripts/pi-hole/js/utils.js +++ b/scripts/pi-hole/js/utils.js @@ -248,6 +248,81 @@ function getGraphType() { return localStorage.getItem("barchart_chkbox") === "false" ? "line" : "bar"; } +function addFromQueryLog(domain, list) { + var token = $("#token").text(); + var alertModal = $("#alertModal"); + var alProcessing = alertModal.find(".alProcessing"); + var alSuccess = alertModal.find(".alSuccess"); + var alFailure = alertModal.find(".alFailure"); + var alNetworkErr = alertModal.find(".alFailure #alNetErr"); + var alCustomErr = alertModal.find(".alFailure #alCustomErr"); + var alList = "#alList"; + var alDomain = "#alDomain"; + + // Exit the function here if the Modal is already shown (multiple running interlock) + if (alertModal.css("display") !== "none") { + return; + } + + var listtype = list === "white" ? "Whitelist" : "Blacklist"; + + alProcessing.children(alDomain).html(domain); + alProcessing.children(alList).html(listtype); + alertModal.modal("show"); + + // add Domain to List after Modal has faded in + alertModal.one("shown.bs.modal", function () { + $.ajax({ + url: "scripts/pi-hole/php/groups.php", + method: "post", + data: { + domain: domain, + list: list, + token: token, + action: "replace_domain", + comment: "Added from Query Log" + }, + success: function (response) { + alProcessing.hide(); + if (!response.success) { + // Failure + alNetworkErr.hide(); + alCustomErr.html(response.message); + alFailure.fadeIn(1000); + setTimeout(function () { + alertModal.modal("hide"); + }, 10000); + } else { + // Success + alSuccess.children(alDomain).html(domain); + alSuccess.children(alList).html(listtype); + alSuccess.fadeIn(1000); + setTimeout(function () { + alertModal.modal("hide"); + }, 2000); + } + }, + error: function () { + // Network Error + alProcessing.hide(); + alNetworkErr.show(); + alFailure.fadeIn(1000); + setTimeout(function () { + alertModal.modal("hide"); + }, 8000); + } + }); + }); + + // Reset Modal after it has faded out + alertModal.one("hidden.bs.modal", function () { + alProcessing.show(); + alSuccess.add(alFailure).hide(); + alProcessing.add(alSuccess).children(alDomain).html("").end().children(alList).html(""); + alCustomErr.html(""); + }); +} + window.utils = (function () { return { escapeHtml: escapeHtml, @@ -265,6 +340,7 @@ window.utils = (function () { stateLoadCallback: stateLoadCallback, getGraphType: getGraphType, validateMAC: validateMAC, - validateHostname: validateHostname + validateHostname: validateHostname, + addFromQueryLog: addFromQueryLog }; })(); diff --git a/scripts/pi-hole/php/groups.php b/scripts/pi-hole/php/groups.php index 7cdabfef..13e19765 100644 --- a/scripts/pi-hole/php/groups.php +++ b/scripts/pi-hole/php/groups.php @@ -507,18 +507,41 @@ if ($_POST['action'] == 'get_groups') { } catch (\Exception $ex) { JSON_error($ex->getMessage()); } -} elseif ($_POST['action'] == 'add_domain') { +} elseif ($_POST['action'] == 'add_domain' || $_POST['action'] == 'replace_domain') { // Add new domain try { $domains = explode(' ', html_entity_decode(trim($_POST['domain']))); $before = intval($db->querySingle("SELECT COUNT(*) FROM domainlist;")); $total = count($domains); $added = 0; - $stmt = $db->prepare('REPLACE INTO domainlist (domain,type,comment) VALUES (:domain,:type,:comment)'); - if (!$stmt) { + + // Prepare INSERT INTO statement + $insert_stmt = $db->prepare('INSERT OR IGNORE INTO domainlist (domain,type) VALUES (:domain,:type)'); + if (!$insert_stmt) { throw new Exception('While preparing statement: ' . $db->lastErrorMsg()); } + // Prepare UPDATE statement + $update_stmt = $db->prepare('UPDATE domainlist SET comment = :comment WHERE domain = :domain AND type = :type'); + if (!$update_stmt) { + throw new Exception('While preparing statement: ' . $db->lastErrorMsg()); + } + + $check_stmt = null; + $delete_stmt = null; + if($_POST['action'] == 'replace_domain') { + // Check statement will reveal any group associations for a given (domain,type) which do NOT belong to the default group + $check_stmt = $db->prepare('SELECT EXISTS(SELECT domain FROM domainlist_by_group dlbg JOIN domainlist dl on dlbg.domainlist_id = dl.id WHERE dl.domain = :domain AND dlbg.group_id != 0)'); + if (!$check_stmt) { + throw new Exception('While preparing check statement: ' . $db->lastErrorMsg()); + } + // Delete statement will remove this domain from any type of list + $delete_stmt = $db->prepare('DELETE FROM domainlist WHERE domain = :domain'); + if (!$delete_stmt) { + throw new Exception('While preparing delete statement: ' . $db->lastErrorMsg()); + } + } + if (isset($_POST['type'])) { $type = intval($_POST['type']); } else if (isset($_POST['list']) && $_POST['list'] === "white") { @@ -527,7 +550,8 @@ if ($_POST['action'] == 'get_groups') { $type = ListType::blacklist; } - if (!$stmt->bindValue(':type', $type, SQLITE3_TEXT)) { + if (!$insert_stmt->bindValue(':type', $type, SQLITE3_TEXT) || + !$update_stmt->bindValue(':type', $type, SQLITE3_TEXT)) { throw new Exception('While binding type: ' . $db->lastErrorMsg()); } @@ -536,7 +560,7 @@ if ($_POST['action'] == 'get_groups') { // Store NULL in database for empty comments $comment = null; } - if (!$stmt->bindValue(':comment', $comment, SQLITE3_TEXT)) { + if (!$update_stmt->bindValue(':comment', $comment, SQLITE3_TEXT)) { throw new Exception('While binding comment: ' . $db->lastErrorMsg()); } @@ -563,7 +587,7 @@ if ($_POST['action'] == 'get_groups') { } } - if(strlen($_POST['type']) === 2 && $_POST['type'][1] === 'W') + if(isset($_POST['type']) && strlen($_POST['type']) === 2 && $_POST['type'][1] === 'W') { // Apply wildcard-style formatting $domain = "(\\.|^)".str_replace(".","\\.",$domain)."$"; @@ -586,13 +610,62 @@ if ($_POST['action'] == 'get_groups') { } } - if (!$stmt->bindValue(':domain', $domain, SQLITE3_TEXT)) { + // First try to delete any occurrences of this domain if we're in + // replace mode. Only do this when the domain to be replaced is in + // the default group! Otherwise, we would shuffle group settings and + // just throw an error at the user to tell them to change this + // domain manually. This ensures user's will really get what they + // want from us. + if($_POST['action'] == 'replace_domain') { + if (!$check_stmt->bindValue(':domain', $domain, SQLITE3_TEXT)) { + throw new Exception('While binding domain to check: ' . $db->lastErrorMsg() . '
'. + 'Added ' . $added . " out of ". $total . " domains"); + } + + $check_result = $check_stmt->execute(); + if (!$check_result) { + throw new Exception('While executing check: ' . $db->lastErrorMsg() . '
'. + 'Added ' . $added . " out of ". $total . " domains"); + } + + // Check return value of CHECK query (0 = only default group, 1 = special group assignments) + $only_default_group = (($check_result->fetchArray(SQLITE3_NUM)[0]) == 0) ? true : false; + if(!$only_default_group) { + throw new Exception('Domain ' . $domain . ' is configured with special group settings.
'. + 'Please modify the domain on the respective group management pages.'); + } + + if (!$delete_stmt->bindValue(':domain', $domain, SQLITE3_TEXT)) { + throw new Exception('While binding domain: ' . $db->lastErrorMsg() . '
'. + 'Added ' . $added . " out of ". $total . " domains"); + } + + if (!$delete_stmt->execute()) { + throw new Exception('While executing: ' . $db->lastErrorMsg() . '
'. + 'Added ' . $added . " out of ". $total . " domains"); + } + } + + + if (!$insert_stmt->bindValue(':domain', $domain, SQLITE3_TEXT) || + !$update_stmt->bindValue(':domain', $domain, SQLITE3_TEXT)) { throw new Exception('While binding domain: ' . $db->lastErrorMsg() . '
'. 'Added ' . $added . " out of ". $total . " domains"); } - if (!$stmt->execute()) { - throw new Exception('While executing: ' . $db->lastErrorMsg() . '
'. + // First execute INSERT OR IGNORE statement to create a record for + // this domain (ignore if already existing) + if (!$insert_stmt->execute()) { + throw new Exception('While executing INSERT OT IGNORE: ' . $db->lastErrorMsg() . '
'. + 'Added ' . $added . " out of ". $total . " domains"); + } + + // Then update the record with a new comment (and modification date + // due to the trigger event) We are not using REPLACE INTO to avoid + // the initial DELETE event (loosing group assignments in case an + // entry did already exist). + if (!$update_stmt->execute()) { + throw new Exception('While executing UPDATE: ' . $db->lastErrorMsg() . '
'. 'Added ' . $added . " out of ". $total . " domains"); } $added++;