diff --git a/scripts/pi-hole/js/db_graph.js b/scripts/pi-hole/js/db_graph.js
index 422179e8..0ce616d6 100644
--- a/scripts/pi-hole/js/db_graph.js
+++ b/scripts/pi-hole/js/db_graph.js
@@ -11,6 +11,7 @@ var start__ = moment().subtract(6, "days");
var from = moment(start__).utc().valueOf()/1000;
var end__ = moment();
var until = moment(end__).utc().valueOf()/1000;
+var interval = 0;
var timeoutWarning = $("#timeoutWarning");
@@ -71,7 +72,35 @@ function compareNumbers(a, b) {
function updateQueriesOverTime() {
$("#queries-over-time .overlay").show();
timeoutWarning.show();
- $.getJSON("api_db.php?getGraphData&from="+from+"&until="+until, function(data) {
+
+ // Compute interval to obtain about 200 values
+ var num = 200;
+ interval = (until-from)/num;
+ // Default displaying axis scaling
+ timeLineChart.options.scales.xAxes[0].time.unit="hour"
+
+ if(num*interval >= 6*29*24*60*60)
+ {
+ // If the requested data is more than 3 months, set ticks interval to quarterly
+ timeLineChart.options.scales.xAxes[0].time.unit="quarter"
+ }
+ else if(num*interval >= 3*29*24*60*60)
+ {
+ // If the requested data is more than 3 months, set ticks interval to months
+ timeLineChart.options.scales.xAxes[0].time.unit="month"
+ }
+ if(num*interval >= 29*24*60*60)
+ {
+ // If the requested data is more than 1 month, set ticks interval to weeks
+ timeLineChart.options.scales.xAxes[0].time.unit="week"
+ }
+ else if(num*interval >= 6*24*60*60)
+ {
+ // If the requested data is more than 1 week, set ticks interval to days
+ timeLineChart.options.scales.xAxes[0].time.unit="day"
+ }
+
+ $.getJSON("api_db.php?getGraphData&from="+from+"&until="+until+"&interval="+interval, function(data) {
// convert received objects to arrays
data.domains_over_time = objectToArray(data.domains_over_time);
@@ -103,8 +132,8 @@ function updateQueriesOverTime() {
// Add data for each hour that is available
for (hour in dates) {
if (Object.prototype.hasOwnProperty.call(dates, hour)) {
- var d, dom = 0, ads = 0;
- d = new Date(1000*dates[hour]);
+ var date, dom = 0, ads = 0;
+ date = new Date(1000*dates[hour]);
var idx = data.domains_over_time[0].indexOf(dates[hour].toString());
if (idx > -1)
@@ -118,8 +147,8 @@ function updateQueriesOverTime() {
ads = data.ads_over_time[1][idx];
}
- timeLineChart.data.labels.push(d);
- timeLineChart.data.datasets[0].data.push(dom);
+ timeLineChart.data.labels.push(date);
+ timeLineChart.data.datasets[0].data.push(dom - ads);
timeLineChart.data.datasets[1].data.push(ads);
}
}
@@ -134,51 +163,66 @@ function updateQueriesOverTime() {
$(document).ready(function() {
var ctx = document.getElementById("queryOverTimeChart").getContext("2d");
timeLineChart = new Chart(ctx, {
- type: "line",
+ type: "bar",
data: {
- labels: [ 0 ],
+ labels: [ ],
datasets: [
{
- label: "Total DNS Queries",
+ label: "Permitted DNS Queries",
fill: true,
- backgroundColor: "rgba(220,220,220,0.5)",
+ backgroundColor: "rgba(0, 166, 90,.8)",
borderColor: "rgba(0, 166, 90,.8)",
pointBorderColor: "rgba(0, 166, 90,.8)",
pointRadius: 1,
pointHoverRadius: 5,
data: [],
- pointHitRadius: 5,
- cubicInterpolationMode: "monotone"
+ pointHitRadius: 5
},
{
label: "Blocked DNS Queries",
fill: true,
- backgroundColor: "rgba(0,192,239,0.5)",
+ backgroundColor: "rgba(0,192,239,1)",
borderColor: "rgba(0,192,239,1)",
pointBorderColor: "rgba(0,192,239,1)",
pointRadius: 1,
pointHoverRadius: 5,
data: [],
- pointHitRadius: 5,
- cubicInterpolationMode: "monotone"
+ pointHitRadius: 5
}
]
},
options: {
tooltips: {
enabled: true,
- responsive: true,
mode: "x-axis",
callbacks: {
title: function(tooltipItem) {
var label = tooltipItem[0].xLabel;
var time = new Date(label);
- var date = time.getFullYear()+"-"+padNumber(time.getMonth()+1)+"-"+padNumber(time.getDate());
- var h = time.getHours();
- var m = time.getMinutes();
- var from = padNumber(h)+":"+padNumber(m)+":00";
- var to = padNumber(h)+":"+padNumber(m+9)+":59";
- return "Queries from "+from+" to "+to+" on "+date;
+ var from_date = time.getFullYear() +
+ "-" +
+ padNumber(time.getMonth()+1) +
+ "-" +
+ padNumber(time.getDate()) +
+ " " +
+ padNumber(time.getHours()) +
+ ":" +
+ padNumber(time.getMinutes()) +
+ ":" +
+ padNumber(time.getSeconds());
+ time = new Date(time.valueOf() + 1000 * interval);
+ var until_date = time.getFullYear() +
+ "-" +
+ padNumber(time.getMonth()+1) +
+ "-" +
+ padNumber(time.getDate()) +
+ " " +
+ padNumber(time.getHours()) +
+ ":" +
+ padNumber(time.getMinutes()) +
+ ":" +
+ padNumber(time.getSeconds());
+ return "Queries from " + from_date + " to " + until_date;
},
label: function(tooltipItems, data) {
if(tooltipItems.datasetIndex === 1)
@@ -203,20 +247,22 @@ $(document).ready(function() {
scales: {
xAxes: [{
type: "time",
- display: false,
+ stacked: true,
time: {
+ unit: "hour",
displayFormats: {
"minute": "HH:mm",
"hour": "HH:mm",
- "day": "HH:mm",
- "week": "MMM DD HH:mm",
- "month": "MMM DD",
- "quarter": "MMM DD",
- "year": "MMM DD"
+ "day": "MMM DD",
+ "week": "MMM DD",
+ "month": "MMM",
+ "quarter": "MMM",
+ "year": "YYYY MMM"
}
}
}],
yAxes: [{
+ stacked: true,
ticks: {
beginAtZero: true
}
diff --git a/scripts/pi-hole/js/groups-adlists.js b/scripts/pi-hole/js/groups-adlists.js
new file mode 100644
index 00000000..80a7f9a2
--- /dev/null
+++ b/scripts/pi-hole/js/groups-adlists.js
@@ -0,0 +1,384 @@
+var table;
+var groups = [];
+const token = $("#token").html();
+var info = null;
+
+function showAlert(type, icon, title, message) {
+ let 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:
+ return;
+ }
+}
+
+function get_groups() {
+ $.post(
+ "scripts/pi-hole/php/groups.php",
+ { action: "get_groups", token: token },
+ function(data) {
+ groups = data.data;
+ initTable();
+ },
+ "json"
+ );
+}
+
+function datetime(date) {
+ return moment.unix(Math.floor(date)).format("Y-MM-DD HH:mm:ss z");
+}
+
+$(document).ready(function() {
+ $("#btnAdd").on("click", addAdlist);
+
+ get_groups();
+
+ $("#select").on("change", function() {
+ $("#ip-custom").val("");
+ $("#ip-custom").prop(
+ "disabled",
+ $("#select option:selected").val() !== "custom"
+ );
+ });
+});
+
+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(settings) {
+ $(".deleteAdlist").on("click", deleteAdlist);
+ },
+ rowCallback: function(row, data) {
+ const tooltip =
+ "Added: " +
+ datetime(data.date_added) +
+ "\nLast modified: " +
+ datetime(data.date_modified) +
+ "\nDatabase ID: " +
+ data.id;
+ $("td:eq(0)", row).html(
+ '' + data.address + ""
+ );
+
+ const disabled = data.enabled === 0;
+ $("td:eq(1)", row).html(
+ '"
+ );
+ var status = $("#status", row);
+ status.bootstrapToggle({
+ on: "Enabled",
+ off: "Disabled",
+ size: "small",
+ onstyle: "success",
+ width: "80px"
+ });
+ status.on("change", editAdlist);
+
+ $("td:eq(2)", row).html(
+ ''
+ );
+ var comment = $("#comment", row);
+ comment.val(data.comment);
+ comment.on("change", editAdlist);
+
+ $("td:eq(3)", row).empty();
+ $("td:eq(3)", row).append(
+ ''
+ );
+ var sel = $("#multiselect", row);
+ // Add all known groups
+ for (var i = 0; i < groups.length; i++) {
+ var extra = "";
+ if (!groups[i].enabled) {
+ extra = " (disabled)";
+ }
+ sel.append(
+ $("")
+ .val(groups[i].id)
+ .text(groups[i].name + extra)
+ );
+ }
+ // Select assigned groups
+ sel.val(data.groups);
+ // Initialize multiselect
+ sel.multiselect({ includeSelectAllOption: true });
+ sel.on("change", editAdlist);
+
+ let button =
+ '";
+ $("td:eq(4)", row).html(button);
+ },
+ 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(settings) {
+ // 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();
+
+ showAlert("info", "", "Adding adlist...", address);
+
+ if (address.length === 0) {
+ 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) {
+ if (response.success) {
+ showAlert(
+ "success",
+ "glyphicon glyphicon-plus",
+ "Successfully added adlist",
+ address
+ );
+ $("#new_address").val("");
+ $("#new_comment").val("");
+ table.ajax.reload();
+ } else {
+ showAlert(
+ "error",
+ "",
+ "Error while adding new adlist: ",
+ response.message
+ );
+ }
+ },
+ error: function(jqXHR, exception) {
+ 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.find("#id").val();
+ var status = tr.find("#status").is(":checked") ? 1 : 0;
+ var comment = tr.find("#comment").val();
+ var groups = tr.find("#multiselect").val();
+ var address = tr.find("#address").text();
+
+ var done = "edited";
+ var not_done = "editing";
+ if (elem === "status" && status === 1) {
+ done = "enabled";
+ not_done = "enabling";
+ } else if (elem === "status" && status === 0) {
+ done = "disabled";
+ not_done = "disabling";
+ } else if (elem === "comment") {
+ done = "edited comment of";
+ not_done = "editing comment of";
+ } else if (elem === "multiselect") {
+ done = "edited groups of";
+ not_done = "editing groups of";
+ }
+
+ 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) {
+ if (response.success) {
+ showAlert(
+ "success",
+ "glyphicon glyphicon-pencil",
+ "Successfully " + done + " adlist ",
+ address
+ );
+ } else {
+ showAlert(
+ "error",
+ "",
+ "Error while " + not_done + " adlist with ID " + id,
+ +response.message
+ );
+ }
+ },
+ error: function(jqXHR, exception) {
+ showAlert(
+ "error",
+ "",
+ "Error while " + not_done + " adlist with ID " + id,
+ jqXHR.responseText
+ );
+ console.log(exception);
+ }
+ });
+}
+
+function deleteAdlist() {
+ var id = $(this).attr("data-id");
+ var tr = $(this).closest("tr");
+ var address = tr.find("#address").text();
+
+ 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) {
+ if (response.success) {
+ showAlert(
+ "success",
+ "glyphicon glyphicon-trash",
+ "Successfully deleted adlist ",
+ address
+ );
+ table
+ .row(tr)
+ .remove()
+ .draw(false);
+ } else
+ showAlert(
+ "error",
+ "",
+ "Error while deleting adlist with ID " + id,
+ response.message
+ );
+ },
+ error: function(jqXHR, exception) {
+ 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..9b6ca920
--- /dev/null
+++ b/scripts/pi-hole/js/groups-clients.js
@@ -0,0 +1,381 @@
+var table;
+var groups = [];
+const token = $("#token").html();
+var info = null;
+
+function showAlert(type, icon, title, message) {
+ let 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:
+ return;
+ }
+}
+
+function reload_client_suggestions() {
+ $.post(
+ "scripts/pi-hole/php/groups.php",
+ { action: "get_unconfigured_clients", token: token },
+ function(data) {
+ var sel = $("#select");
+ sel.empty();
+ for (var key in data) {
+ if (!data.hasOwnProperty(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 on the right")
+ );
+ },
+ "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();
+ 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: "groups", searchable: false },
+ { data: "name", width: "80px", orderable: false }
+ ],
+ drawCallback: function(settings) {
+ $(".deleteClient").on("click", deleteClient);
+ },
+ rowCallback: function(row, data) {
+ const tooltip = "Database 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).empty();
+ $("td:eq(1)", row).append(
+ ''
+ );
+ var sel = $("#multiselect", row);
+ // Add all known groups
+ for (var i = 0; i < groups.length; i++) {
+ var extra = "";
+ if (!groups[i].enabled) {
+ extra = " (disabled)";
+ }
+ sel.append(
+ $("")
+ .val(groups[i].id)
+ .text(groups[i].name + extra)
+ );
+ }
+ // Select assigned groups
+ sel.val(data.groups);
+ // Initialize multiselect
+ sel.multiselect({ includeSelectAllOption: true });
+ sel.on("change", editClient);
+
+ let button =
+ '";
+ $("td:eq(2)", row).html(button);
+ },
+ 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(settings) {
+ // 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();
+ if (ip === "custom") {
+ ip = $("#ip-custom").val();
+ }
+
+ showAlert("info", "", "Adding client...", ip);
+
+ if (ip.length === 0) {
+ showAlert("warning", "", "Warning", "Please specify a client IP address");
+ return;
+ }
+
+ $.ajax({
+ url: "scripts/pi-hole/php/groups.php",
+ method: "post",
+ dataType: "json",
+ data: { action: "add_client", ip: ip, token: token },
+ success: function(response) {
+ if (response.success) {
+ showAlert(
+ "success",
+ "glyphicon glyphicon-plus",
+ "Successfully added client",
+ ip
+ );
+ reload_client_suggestions();
+ table.ajax.reload();
+ } else {
+ showAlert(
+ "error",
+ "",
+ "Error while adding new client",
+ response.message
+ );
+ }
+ },
+ error: function(jqXHR, exception) {
+ 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.find("#id").val();
+ var groups = tr.find("#multiselect").val();
+ var ip = tr.find("#ip").text();
+ var name = tr.find("#name").text();
+
+ var done = "edited";
+ var not_done = "editing";
+ if (elem === "multiselect") {
+ done = "edited groups of";
+ not_done = "editing groups of";
+ }
+
+ var ip_name = ip;
+ if (name.length > 0) {
+ ip_name += " (" + name + ")";
+ }
+
+ 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 },
+ success: function(response) {
+ if (response.success) {
+ showAlert(
+ "success",
+ "glyphicon glyphicon-plus",
+ "Successfully " + done + " client",
+ ip_name
+ );
+ } else {
+ showAlert(
+ "error",
+ "Error while " + not_done + " client with ID " + id,
+ response.message
+ );
+ }
+ },
+ error: function(jqXHR, exception) {
+ showAlert(
+ "error",
+ "",
+ "Error while " + not_done + " client with ID " + id,
+ jqXHR.responseText
+ );
+ console.log(exception);
+ }
+ });
+}
+
+function deleteClient() {
+ var id = $(this).attr("data-id");
+ var tr = $(this).closest("tr");
+ var ip = tr.find("#ip").text();
+ var name = tr.find("#name").text();
+
+ var ip_name = ip;
+ if (name.length > 0) {
+ ip_name += " (" + name + ")";
+ }
+ 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) {
+ if (response.success) {
+ showAlert(
+ "success",
+ "glyphicon glyphicon-trash",
+ "Successfully deleted client ",
+ ip_name
+ );
+ table
+ .row(tr)
+ .remove()
+ .draw(false);
+ reload_client_suggestions();
+ } else {
+ showAlert(
+ "error",
+ "",
+ "Error while deleting client with ID " + id,
+ response.message
+ );
+ }
+ },
+ error: function(jqXHR, exception) {
+ showAlert(
+ "error",
+ "",
+ "Error while deleting client with ID " + id,
+ jqXHR.responseText
+ );
+ console.log(exception);
+ }
+ });
+}
diff --git a/scripts/pi-hole/js/groups-domains.js b/scripts/pi-hole/js/groups-domains.js
new file mode 100644
index 00000000..8637a7d7
--- /dev/null
+++ b/scripts/pi-hole/js/groups-domains.js
@@ -0,0 +1,408 @@
+var table;
+var groups = [];
+const token = $("#token").html();
+var info = null;
+
+function showAlert(type, icon, title, message) {
+ let 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:
+ return;
+ }
+}
+
+function get_groups() {
+ $.post(
+ "scripts/pi-hole/php/groups.php",
+ { action: "get_groups", token: token },
+ function(data) {
+ groups = data.data;
+ initTable();
+ },
+ "json"
+ );
+}
+
+function datetime(date) {
+ return moment.unix(Math.floor(date)).format("Y-MM-DD HH:mm:ss z");
+}
+
+$(document).ready(function() {
+ $("#btnAdd").on("click", addDomain);
+
+ get_groups();
+
+ $("#select").on("change", function() {
+ $("#ip-custom").val("");
+ $("#ip-custom").prop(
+ "disabled",
+ $("#select option:selected").val() !== "custom"
+ );
+ });
+});
+
+function initTable() {
+ table = $("#domainsTable").DataTable({
+ ajax: {
+ url: "scripts/pi-hole/php/groups.php",
+ data: { action: "get_domains", 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(settings) {
+ $(".deleteDomain").on("click", deleteDomain);
+ },
+ rowCallback: function(row, data) {
+ const tooltip =
+ "Added: " +
+ datetime(data.date_added) +
+ "\nLast modified: " +
+ datetime(data.date_modified) +
+ "\nDatabase ID: " +
+ data.id;
+ $("td:eq(0)", row).html(
+ '' + data.domain + ""
+ );
+
+ $("td:eq(1)", row).html(
+ '"
+ );
+ $("#type", row).on("change", editDomain);
+
+ const disabled = data.enabled === 0;
+ $("td:eq(2)", row).html(
+ '"
+ );
+ $("#status", row).bootstrapToggle({
+ on: "Enabled",
+ off: "Disabled",
+ size: "small",
+ onstyle: "success",
+ width: "80px"
+ });
+ $("#status", row).on("change", editDomain);
+
+ $("td:eq(3)", row).html(
+ ''
+ );
+ $("#comment", row).val(data.comment);
+ $("#comment", row).on("change", editDomain);
+
+ $("td:eq(4)", row).empty();
+ $("td:eq(4)", row).append(
+ ''
+ );
+ var sel = $("#multiselect", row);
+ // Add all known groups
+ for (var i = 0; i < groups.length; i++) {
+ var extra = "";
+ if (!groups[i].enabled) {
+ extra = " (disabled)";
+ }
+ sel.append(
+ $("")
+ .val(groups[i].id)
+ .text(groups[i].name + extra)
+ );
+ }
+ // Select assigned groups
+ sel.val(data.groups);
+ // Initialize multiselect
+ sel.multiselect({ includeSelectAllOption: true });
+ sel.on("change", editDomain);
+
+ let button =
+ '";
+ $("td:eq(5)", row).html(button);
+ },
+ 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(settings) {
+ // 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;
+ // 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 addDomain() {
+ var domain = $("#new_domain").val();
+ var type = $("#new_type").val();
+ var comment = $("#new_comment").val();
+
+ showAlert("info", "", "Adding domain...", domain);
+
+ if (domain.length === 0) {
+ showAlert("warning", "", "Warning", "Please specify a domain");
+ 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) {
+ if (response.success) {
+ showAlert(
+ "success",
+ "glyphicon glyphicon-plus",
+ "Successfully added domain",
+ domain
+ );
+ $("#new_domain").val("");
+ $("#new_comment").val("");
+ table.ajax.reload();
+ } else
+ showAlert(
+ "error",
+ "",
+ "Error while adding new domain",
+ response.message
+ );
+ },
+ error: function(jqXHR, exception) {
+ showAlert(
+ "error",
+ "",
+ "Error while adding new domain",
+ jqXHR.responseText
+ );
+ console.log(exception);
+ }
+ });
+}
+
+function editDomain() {
+ var elem = $(this).attr("id");
+ var tr = $(this).closest("tr");
+ var domain = tr.find("#domain").text();
+ var id = tr.find("#id").val();
+ var type = tr.find("#type").val();
+ var status = tr.find("#status").is(":checked") ? 1 : 0;
+ var comment = tr.find("#comment").val();
+ var groups = tr.find("#multiselect").val();
+
+ var done = "edited";
+ var not_done = "editing";
+ if (elem === "status" && status === 1) {
+ done = "enabled";
+ not_done = "enabling";
+ } else if (elem === "status" && status === 0) {
+ done = "disabled";
+ not_done = "disabling";
+ } else if (elem === "name") {
+ done = "edited name of";
+ not_done = "editing name of";
+ } else if (elem === "comment") {
+ done = "edited comment of";
+ not_done = "editing comment of";
+ } else if (elem === "type") {
+ done = "edited type of";
+ not_done = "editing type of";
+ } else if (elem === "multiselect") {
+ done = "edited groups of";
+ not_done = "editing groups of";
+ }
+
+ showAlert("info", "", "Editing domain...", 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) {
+ if (response.success) {
+ showAlert(
+ "success",
+ "glyphicon glyphicon-pencil",
+ "Successfully " + done + " domain",
+ domain
+ );
+ } else
+ showAlert(
+ "error",
+ "",
+ "Error while " + not_done + " domain with ID " + id,
+ response.message
+ );
+ },
+ error: function(jqXHR, exception) {
+ showAlert(
+ "error",
+ "",
+ "Error while " + not_done + " domain with ID " + id,
+ jqXHR.responseText
+ );
+ console.log(exception);
+ }
+ });
+}
+
+function deleteDomain() {
+ var id = $(this).attr("data-id");
+ var tr = $(this).closest("tr");
+ var domain = tr.find("#domain").text();
+
+ showAlert("info", "", "Deleting domain...", domain);
+ $.ajax({
+ url: "scripts/pi-hole/php/groups.php",
+ method: "post",
+ dataType: "json",
+ data: { action: "delete_domain", id: id, token: token },
+ success: function(response) {
+ if (response.success) {
+ showAlert(
+ "success",
+ "glyphicon glyphicon-trash",
+ "Successfully deleted domain",
+ domain
+ );
+ table
+ .row(tr)
+ .remove()
+ .draw(false);
+ } else
+ showAlert(
+ "error",
+ "",
+ "Error while deleting domain with ID " + id,
+ response.message
+ );
+ },
+ error: function(jqXHR, exception) {
+ showAlert(
+ "error",
+ "",
+ "Error while deleting domain 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..a39ef02b
--- /dev/null
+++ b/scripts/pi-hole/js/groups.js
@@ -0,0 +1,336 @@
+var table;
+const token = $("#token").html();
+var info = null;
+
+function showAlert(type, icon, title, message) {
+ let 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:
+ return;
+ }
+}
+
+function datetime(date) {
+ return moment.unix(Math.floor(date)).format("Y-MM-DD HH:mm:ss z");
+}
+
+$(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(settings) {
+ $(".deleteGroup").on("click", deleteGroup);
+ },
+ rowCallback: function(row, data) {
+ const tooltip =
+ "Added: " +
+ datetime(data.date_added) +
+ "\nLast modified: " +
+ datetime(data.date_modified) +
+ "\nDatabase ID: " +
+ data.id;
+ $("td:eq(0)", row).html(
+ ''
+ );
+ var name = $("#name", row);
+ name.val(data.name);
+ name.on("change", editGroup);
+
+ const disabled = data.enabled === 0;
+ $("td:eq(1)", row).html(
+ '"
+ );
+ var status = $("#status", row);
+ status.bootstrapToggle({
+ on: "Enabled",
+ off: "Disabled",
+ size: "small",
+ onstyle: "success",
+ width: "80px"
+ });
+ status.on("change", editGroup);
+
+ $("td:eq(2)", row).html('');
+ const desc = data.description !== null ? data.description : "";
+ $("#desc", row).val(desc);
+ $("#desc", row).on("change", editGroup);
+
+ $("td:eq(3)", row).empty();
+ if (data.id !== 0) {
+ let button =
+ " " +
+ '";
+ $("td:eq(3)", row).html(button);
+ }
+ },
+ 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(settings) {
+ // 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();
+
+ showAlert("info", "", "Adding group...", name);
+
+ if (name.length === 0) {
+ 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) {
+ if (response.success) {
+ showAlert(
+ "success",
+ "glyphicon glyphicon-plus",
+ "Successfully added group",
+ name
+ );
+ $("#new_name").val("");
+ $("#new_desc").val("");
+ table.ajax.reload();
+ } else {
+ showAlert(
+ "error",
+ "",
+ "Error while adding new group",
+ response.message
+ );
+ }
+ },
+ error: function(jqXHR, exception) {
+ 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.find("#id").val();
+ var name = tr.find("#name").val();
+ var status = tr.find("#status").is(":checked") ? 1 : 0;
+ var desc = tr.find("#desc").val();
+
+ var done = "edited";
+ var not_done = "editing";
+ if (elem === "status" && status === 1) {
+ done = "enabled";
+ not_done = "enabling";
+ } else if (elem === "status" && status === 0) {
+ done = "disabled";
+ not_done = "disabling";
+ } else if (elem === "name") {
+ done = "edited name of";
+ not_done = "editing name of";
+ } else if (elem === "desc") {
+ done = "edited description of";
+ not_done = "editing description of";
+ }
+
+ 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) {
+ if (response.success) {
+ showAlert(
+ "success",
+ "glyphicon glyphicon-pencil",
+ "Successfully " + done + " group",
+ name
+ );
+ } else {
+ showAlert(
+ "error",
+ "",
+ "Error while " + not_done + " group with ID " + id,
+ response.message
+ );
+ }
+ },
+ error: function(jqXHR, exception) {
+ showAlert(
+ "error",
+ "",
+ "Error while " + not_done + " group with ID " + id,
+ jqXHR.responseText
+ );
+ console.log(exception);
+ }
+ });
+}
+
+function deleteGroup() {
+ var id = $(this).attr("data-id");
+ var tr = $(this).closest("tr");
+ var name = tr.find("#name").val();
+
+ 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) {
+ if (response.success) {
+ showAlert(
+ "success",
+ "glyphicon glyphicon-trash",
+ "Successfully deleted group ",
+ name
+ );
+ table
+ .row(tr)
+ .remove()
+ .draw(false);
+ } else {
+ showAlert(
+ "error",
+ "",
+ "Error while deleting group with ID " + id,
+ response.message
+ );
+ }
+ },
+ error: function(jqXHR, exception) {
+ showAlert(
+ "error",
+ "",
+ "Error while deleting group with ID " + id,
+ jqXHR.responseText
+ );
+ console.log(exception);
+ }
+ });
+}
diff --git a/scripts/pi-hole/js/index.js b/scripts/pi-hole/js/index.js
index 18b1773b..969e6cdc 100644
--- a/scripts/pi-hole/js/index.js
+++ b/scripts/pi-hole/js/index.js
@@ -7,8 +7,8 @@
// Define global variables
/* global Chart:false, updateSessionTimer:false */
-var timeLineChart, forwardDestinationChart;
-var queryTypePieChart, forwardDestinationPieChart, clientsChart;
+var timeLineChart, clientsChart;
+var queryTypePieChart, forwardDestinationPieChart;
function padNumber(num) {
return ("00" + num).substr(-2,2);
@@ -166,8 +166,10 @@ function updateQueriesOverTime() {
}
timeLineChart.data.labels.push(d);
- timeLineChart.data.datasets[0].data.push(data.domains_over_time[1][hour]);
- timeLineChart.data.datasets[1].data.push(data.ads_over_time[1][hour]);
+ var blocked = data.ads_over_time[1][hour];
+ var permitted = data.domains_over_time[1][hour] - blocked;
+ timeLineChart.data.datasets[0].data.push(permitted);
+ timeLineChart.data.datasets[1].data.push(blocked);
}
}
$("#queries-over-time .overlay").hide();
@@ -718,33 +720,31 @@ $(document).ready(function() {
var ctx = document.getElementById("queryOverTimeChart").getContext("2d");
timeLineChart = new Chart(ctx, {
- type: "line",
+ type: "bar",
data: {
- labels: [],
+ labels: [ ],
datasets: [
{
- label: "Total DNS Queries",
+ label: "Permitted DNS Queries",
fill: true,
- backgroundColor: "rgba(220,220,220,0.5)",
+ backgroundColor: "rgba(0, 166, 90,.8)",
borderColor: "rgba(0, 166, 90,.8)",
pointBorderColor: "rgba(0, 166, 90,.8)",
pointRadius: 1,
pointHoverRadius: 5,
data: [],
- pointHitRadius: 5,
- cubicInterpolationMode: "monotone"
+ pointHitRadius: 5
},
{
label: "Blocked DNS Queries",
fill: true,
- backgroundColor: "rgba(0,192,239,0.5)",
+ backgroundColor: "rgba(0,192,239,1)",
borderColor: "rgba(0,192,239,1)",
pointBorderColor: "rgba(0,192,239,1)",
pointRadius: 1,
pointHoverRadius: 5,
data: [],
- pointHitRadius: 5,
- cubicInterpolationMode: "monotone"
+ pointHitRadius: 5
}
]
},
@@ -785,6 +785,7 @@ $(document).ready(function() {
scales: {
xAxes: [{
type: "time",
+ stacked: true,
time: {
unit: "hour",
displayFormats: {
@@ -794,6 +795,7 @@ $(document).ready(function() {
}
}],
yAxes: [{
+ stacked: true,
ticks: {
beginAtZero: true
}
@@ -807,73 +809,13 @@ $(document).ready(function() {
updateQueriesOverTime();
- // Create / load "Forward Destinations over Time" only if authorized
- if(document.getElementById("forwardDestinationChart"))
- {
- ctx = document.getElementById("forwardDestinationChart").getContext("2d");
- forwardDestinationChart = new Chart(ctx, {
- type: "line",
- data: {
- labels: [],
- datasets: [{ data: [] }]
- },
- options: {
- tooltips: {
- enabled: true,
- mode: "x-axis",
- callbacks: {
- title: function(tooltipItem) {
- var label = tooltipItem[0].xLabel;
- var time = label.match(/(\d?\d):?(\d?\d?)/);
- var h = parseInt(time[1], 10);
- var m = parseInt(time[2], 10) || 0;
- var from = padNumber(h)+":"+padNumber(m-5)+":00";
- var to = padNumber(h)+":"+padNumber(m+4)+":59";
- return "Forward destinations from "+from+" to "+to;
- },
- label: function(tooltipItems, data) {
- return data.datasets[tooltipItems.datasetIndex].label + ": " + (100.0*tooltipItems.yLabel).toFixed(1) + "%";
- }
- }
- },
- legend: {
- display: false
- },
- scales: {
- xAxes: [{
- type: "time",
- time: {
- unit: "hour",
- displayFormats: {
- hour: "HH:mm"
- },
- tooltipFormat: "HH:mm"
- }
- }],
- yAxes: [{
- ticks: {
- mix: 0.0,
- max: 1.0,
- beginAtZero: true,
- callback: function(value) {
- return Math.round(value*100) + " %";
- }
- },
- stacked: true
- }]
- },
- maintainAspectRatio: true
- }
- });
- }
-
// Create / load "Top Clients over Time" only if authorized
var clientsChartEl = document.getElementById("clientsChart");
if(clientsChartEl)
{
ctx = clientsChartEl.getContext("2d");
clientsChart = new Chart(ctx, {
- type: "line",
+ type: "bar",
data: {
labels: [],
datasets: [{ data: [] }]
@@ -907,6 +849,7 @@ $(document).ready(function() {
scales: {
xAxes: [{
type: "time",
+ stacked: true,
time: {
unit: "hour",
displayFormats: {
diff --git a/scripts/pi-hole/php/add.php b/scripts/pi-hole/php/add.php
index 593bdfbf..d6059a51 100644
--- a/scripts/pi-hole/php/add.php
+++ b/scripts/pi-hole/php/add.php
@@ -34,10 +34,12 @@ $db = SQLite3_connect($GRAVITYDB, SQLITE3_OPEN_READWRITE);
switch($list) {
case "white":
+ $domains = array_map('strtolower', $domains);
echo add_to_table($db, "domainlist", $domains, $comment, false, false, ListType::whitelist);
break;
case "black":
+ $domains = array_map('strtolower', $domains);
echo add_to_table($db, "domainlist", $domains, $comment, false, false, ListType::blacklist);
break;
diff --git a/scripts/pi-hole/php/database.php b/scripts/pi-hole/php/database.php
index a51a88fc..2bad73db 100644
--- a/scripts/pi-hole/php/database.php
+++ b/scripts/pi-hole/php/database.php
@@ -20,6 +20,20 @@ function getGravityDBFilename()
}
}
+function getQueriesDBFilename()
+{
+ // Get possible non-standard location of FTL's database
+ $FTLsettings = parse_ini_file("/etc/pihole/pihole-FTL.conf");
+ if(isset($FTLsettings["DBFILE"]))
+ {
+ return $FTLsettings["DBFILE"];
+ }
+ else
+ {
+ return "/etc/pihole/pihole-FTL.db";
+ }
+}
+
function SQLite3_connect_try($filename, $mode, $trytoreconnect)
{
try
diff --git a/scripts/pi-hole/php/groups.php b/scripts/pi-hole/php/groups.php
new file mode 100644
index 00000000..ff6f7aa5
--- /dev/null
+++ b/scripts/pi-hole/php/groups.php
@@ -0,0 +1,676 @@
+ true, 'message' => $message));
+}
+
+function JSON_error($message = null)
+{
+ header('Content-type: application/json');
+ $response = array('success' => false, 'message' => $message);
+ if (isset($_POST['action'])) {
+ array_push($response, array('action' => $_POST['action']));
+ }
+ echo json_encode($response);
+}
+
+if ($_POST['action'] == 'get_groups') {
+ // List all available groups
+ try {
+ $query = $db->query('SELECT * FROM "group";');
+ $data = array();
+ while (($res = $query->fetchArray(SQLITE3_ASSOC)) !== false) {
+ array_push($data, $res);
+ }
+ echo json_encode(array('data' => $data));
+ } catch (\Exception $ex) {
+ return JSON_error($ex->getMessage());
+ }
+} elseif ($_POST['action'] == 'add_group') {
+ // Add new group
+ try {
+ $stmt = $db->prepare('INSERT INTO "group" (name,description) VALUES (:name,:desc)');
+ if (!$stmt) {
+ throw new Exception('While preparing statement: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->bindValue(':name', $_POST['name'], SQLITE3_TEXT)) {
+ throw new Exception('While binding name: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->bindValue(':desc', $_POST['desc'], SQLITE3_TEXT)) {
+ throw new Exception('While binding desc: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->execute()) {
+ throw new Exception('While executing: ' . $db->lastErrorMsg());
+ }
+
+ $reload = true;
+ return JSON_success();
+ } catch (\Exception $ex) {
+ return JSON_error($ex->getMessage());
+ }
+} elseif ($_POST['action'] == 'edit_group') {
+ // Edit group identified by ID
+ try {
+ $stmt = $db->prepare('UPDATE "group" SET enabled=:enabled, name=:name, description=:desc WHERE id = :id');
+ if (!$stmt) {
+ throw new Exception('While preparing statement: ' . $db->lastErrorMsg());
+ }
+
+ $status = ((int) $_POST['status']) !== 0 ? 1 : 0;
+ if (!$stmt->bindValue(':enabled', $status, SQLITE3_INTEGER)) {
+ throw new Exception('While binding enabled: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->bindValue(':name', $_POST['name'], SQLITE3_TEXT)) {
+ throw new Exception('While binding name: ' . $db->lastErrorMsg());
+ }
+
+ $desc = $_POST['desc'];
+ if (strlen($desc) == 0) {
+ // Store NULL in database for empty descriptions
+ $desc = null;
+ }
+ if (!$stmt->bindValue(':desc', $desc, SQLITE3_TEXT)) {
+ throw new Exception('While binding desc: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->bindValue(':id', intval($_POST['id']), SQLITE3_INTEGER)) {
+ throw new Exception('While binding id: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->execute()) {
+ throw new Exception('While executing: ' . $db->lastErrorMsg());
+ }
+
+ $reload = true;
+ return JSON_success();
+ } catch (\Exception $ex) {
+ return JSON_error($ex->getMessage());
+ }
+} elseif ($_POST['action'] == 'delete_group') {
+ // Delete group identified by ID
+ try {
+ $table_name = ['domainlist_by_group', 'client_by_group', 'adlist_by_group', 'group'];
+ $table_keys = ['group_id', 'group_id', 'group_id', 'id'];
+ for ($i = 0; $i < count($table_name); $i++) {
+ $table = $table_name[$i];
+ $key = $table_keys[$i];
+
+ $stmt = $db->prepare("DELETE FROM \"$table\" WHERE $key = :id;");
+ if (!$stmt) {
+ throw new Exception("While preparing DELETE FROM $table statement: " . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->bindValue(':id', intval($_POST['id']), SQLITE3_INTEGER)) {
+ throw new Exception("While binding id to DELETE FROM $table statement: " . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->execute()) {
+ throw new Exception("While executing DELETE FROM $table statement: " . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->reset()) {
+ throw new Exception("While resetting DELETE FROM $table statement: " . $db->lastErrorMsg());
+ }
+ }
+ $reload = true;
+ return JSON_success();
+ } catch (\Exception $ex) {
+ return JSON_error($ex->getMessage());
+ }
+} elseif ($_POST['action'] == 'get_clients') {
+ // List all available groups
+ try {
+ $QUERYDB = getQueriesDBFilename();
+ $FTLdb = SQLite3_connect($QUERYDB);
+
+ $query = $db->query('SELECT * FROM client;');
+ if (!$query) {
+ throw new Exception('Error while querying gravity\'s client table: ' . $db->lastErrorMsg());
+ }
+
+
+ $data = array();
+ while (($res = $query->fetchArray(SQLITE3_ASSOC)) !== false) {
+ $group_query = $db->query('SELECT group_id FROM client_by_group WHERE client_id = ' . $res['id'] . ';');
+ if (!$group_query) {
+ throw new Exception('Error while querying gravity\'s client_by_group table: ' . $db->lastErrorMsg());
+ }
+
+ $stmt = $FTLdb->prepare('SELECT name FROM network WHERE id = (SELECT network_id FROM network_addresses WHERE ip = :ip);');
+ if (!$stmt) {
+ throw new Exception('Error while preparing network table statement: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->bindValue(':ip', $res['ip'], SQLITE3_TEXT)) {
+ throw new Exception('While binding to network table statement: ' . $db->lastErrorMsg());
+ }
+
+ $result = $stmt->execute();
+ if (!$result) {
+ throw new Exception('While executing network table statement: ' . $db->lastErrorMsg());
+ }
+
+ // There will always be a result. Unknown host names are NULL
+ $name_result = $result->fetchArray(SQLITE3_ASSOC);
+ $res['name'] = $name_result['name'];
+
+ $groups = array();
+ while ($gres = $group_query->fetchArray(SQLITE3_ASSOC)) {
+ array_push($groups, $gres['group_id']);
+ }
+ $res['groups'] = $groups;
+ array_push($data, $res);
+ }
+
+ echo json_encode(array('data' => $data));
+ } catch (\Exception $ex) {
+ return JSON_error($ex->getMessage());
+ }
+} elseif ($_POST['action'] == 'get_unconfigured_clients') {
+ // List all available clients WITHOUT already configured clients
+ try {
+ $QUERYDB = getQueriesDBFilename();
+ $FTLdb = SQLite3_connect($QUERYDB);
+
+ $query = $FTLdb->query('SELECT DISTINCT ip,network.name FROM network_addresses AS name LEFT JOIN network ON network.id = network_id ORDER BY ip ASC;');
+ if (!$query) {
+ throw new Exception('Error while querying FTL\'s database: ' . $db->lastErrorMsg());
+ }
+
+ // Loop over results
+ $ips = array();
+ while ($res = $query->fetchArray(SQLITE3_ASSOC)) {
+ $ips[$res['ip']] = $res['name'];
+ }
+ $FTLdb->close();
+
+ $query = $db->query('SELECT ip FROM client;');
+ if (!$query) {
+ throw new Exception('Error while querying gravity\'s database: ' . $db->lastErrorMsg());
+ }
+
+ // Loop over results, remove already configured clients
+ while (($res = $query->fetchArray(SQLITE3_ASSOC)) !== false) {
+ if (isset($ips[$res['ip']])) {
+ unset($ips[$res['ip']]);
+ }
+ }
+
+ echo json_encode($ips);
+ } catch (\Exception $ex) {
+ return JSON_error($ex->getMessage());
+ }
+} elseif ($_POST['action'] == 'add_client') {
+ // Add new client
+ try {
+ $stmt = $db->prepare('INSERT INTO client (ip) VALUES (:ip)');
+ if (!$stmt) {
+ throw new Exception('While preparing statement: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->bindValue(':ip', $_POST['ip'], SQLITE3_TEXT)) {
+ throw new Exception('While binding ip: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->execute()) {
+ throw new Exception('While executing: ' . $db->lastErrorMsg());
+ }
+
+ $reload = true;
+ return JSON_success();
+ } catch (\Exception $ex) {
+ return JSON_error($ex->getMessage());
+ }
+} elseif ($_POST['action'] == 'edit_client') {
+ // Edit client identified by ID
+ try {
+ $stmt = $db->prepare('DELETE FROM client_by_group WHERE client_id = :id');
+ if (!$stmt) {
+ throw new Exception('While preparing DELETE statement: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->bindValue(':id', intval($_POST['id']), SQLITE3_INTEGER)) {
+ throw new Exception('While binding id: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->execute()) {
+ throw new Exception('While executing DELETE statement: ' . $db->lastErrorMsg());
+ }
+
+ $db->query('BEGIN TRANSACTION;');
+ foreach ($_POST['groups'] as $gid) {
+ $stmt = $db->prepare('INSERT INTO client_by_group (client_id,group_id) VALUES(:id,:gid);');
+ if (!$stmt) {
+ throw new Exception('While preparing INSERT INTO statement: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->bindValue(':id', intval($_POST['id']), SQLITE3_INTEGER)) {
+ throw new Exception('While binding id: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->bindValue(':gid', intval($gid), SQLITE3_INTEGER)) {
+ throw new Exception('While binding gid: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->execute()) {
+ throw new Exception('While executing INSERT INTO statement: ' . $db->lastErrorMsg());
+ }
+ }
+ $db->query('COMMIT;');
+
+ $reload = true;
+ return JSON_success();
+ } catch (\Exception $ex) {
+ return JSON_error($ex->getMessage());
+ }
+} elseif ($_POST['action'] == 'delete_client') {
+ // Delete client identified by ID
+ try {
+ $stmt = $db->prepare('DELETE FROM client_by_group WHERE client_id=:id');
+ if (!$stmt) {
+ throw new Exception('While preparing client_by_group statement: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->bindValue(':id', intval($_POST['id']), SQLITE3_INTEGER)) {
+ throw new Exception('While binding id to client_by_group statement: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->execute()) {
+ throw new Exception('While executing client_by_group statement: ' . $db->lastErrorMsg());
+ }
+
+ $stmt = $db->prepare('DELETE FROM client WHERE id=:id');
+ if (!$stmt) {
+ throw new Exception('While preparing client statement: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->bindValue(':id', intval($_POST['id']), SQLITE3_INTEGER)) {
+ throw new Exception('While binding id to client statement: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->execute()) {
+ throw new Exception('While executing client statement: ' . $db->lastErrorMsg());
+ }
+
+ $reload = true;
+ return JSON_success();
+ } catch (\Exception $ex) {
+ return JSON_error($ex->getMessage());
+ }
+} elseif ($_POST['action'] == 'get_domains') {
+ // List all available groups
+ try {
+ $query = $db->query('SELECT * FROM domainlist;');
+ if (!$query) {
+ throw new Exception('Error while querying gravity\'s domainlist table: ' . $db->lastErrorMsg());
+ }
+
+ $data = array();
+ while (($res = $query->fetchArray(SQLITE3_ASSOC)) !== false) {
+ $group_query = $db->query('SELECT group_id FROM domainlist_by_group WHERE domainlist_id = ' . $res['id'] . ';');
+ if (!$group_query) {
+ throw new Exception('Error while querying gravity\'s domainlist_by_group table: ' . $db->lastErrorMsg());
+ }
+
+ $groups = array();
+ while ($gres = $group_query->fetchArray(SQLITE3_ASSOC)) {
+ array_push($groups, $gres['group_id']);
+ }
+ $res['groups'] = $groups;
+ array_push($data, $res);
+ }
+
+
+ echo json_encode(array('data' => $data));
+ } catch (\Exception $ex) {
+ return JSON_error($ex->getMessage());
+ }
+} elseif ($_POST['action'] == 'add_domain') {
+ // Add new domain
+ try {
+ $stmt = $db->prepare('INSERT INTO domainlist (domain,type,comment) VALUES (:domain,:type,:comment)');
+ if (!$stmt) {
+ throw new Exception('While preparing statement: ' . $db->lastErrorMsg());
+ }
+
+ $type = intval($_POST['type']);
+
+ $domain = $_POST['domain'];
+ if($type === ListType::whitelist || $type === ListType::blacklist)
+ {
+ // If adding to the exact lists, we convert the domain lower case and check whether it is valid
+ $domain = strtolower($domain);
+ if(filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) === false)
+ {
+ throw new Exception('Domain ' . htmlentities(utf8_encode($domain)) . 'is not a valid domain.');
+ }
+ }
+
+ if (!$stmt->bindValue(':domain', $domain, SQLITE3_TEXT)) {
+ throw new Exception('While binding domain: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->bindValue(':type', $type, SQLITE3_TEXT)) {
+ throw new Exception('While binding type: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->bindValue(':comment', $_POST['comment'], SQLITE3_TEXT)) {
+ throw new Exception('While binding comment: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->execute()) {
+ throw new Exception('While executing: ' . $db->lastErrorMsg());
+ }
+
+ $reload = true;
+ return JSON_success();
+ } catch (\Exception $ex) {
+ return JSON_error($ex->getMessage());
+ }
+} elseif ($_POST['action'] == 'edit_domain') {
+ // Edit domain identified by ID
+ try {
+ $stmt = $db->prepare('UPDATE domainlist SET enabled=:enabled, comment=:comment, type=:type WHERE id = :id');
+ if (!$stmt) {
+ throw new Exception('While preparing statement: ' . $db->lastErrorMsg());
+ }
+
+ $status = intval($_POST['status']);
+ if ($status !== 0) {
+ $status = 1;
+ }
+
+ if (!$stmt->bindValue(':enabled', $status, SQLITE3_INTEGER)) {
+ throw new Exception('While binding enabled: ' . $db->lastErrorMsg());
+ }
+
+ $comment = $_POST['comment'];
+ if (strlen($comment) == 0) {
+ // Store NULL in database for empty comments
+ $comment = null;
+ }
+ if (!$stmt->bindValue(':comment', $comment, SQLITE3_TEXT)) {
+ throw new Exception('While binding comment: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->bindValue(':type', intval($_POST['type']), SQLITE3_INTEGER)) {
+ throw new Exception('While binding type: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->bindValue(':id', intval($_POST['id']), SQLITE3_INTEGER)) {
+ throw new Exception('While binding id: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->execute()) {
+ throw new Exception('While executing: ' . $db->lastErrorMsg());
+ }
+
+ $stmt = $db->prepare('DELETE FROM domainlist_by_group WHERE domainlist_id = :id');
+ if (!$stmt) {
+ throw new Exception('While preparing DELETE statement: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->bindValue(':id', intval($_POST['id']), SQLITE3_INTEGER)) {
+ throw new Exception('While binding id: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->execute()) {
+ throw new Exception('While executing DELETE statement: ' . $db->lastErrorMsg());
+ }
+
+ $db->query('BEGIN TRANSACTION;');
+ foreach ($_POST['groups'] as $gid) {
+ $stmt = $db->prepare('INSERT INTO domainlist_by_group (domainlist_id,group_id) VALUES(:id,:gid);');
+ if (!$stmt) {
+ throw new Exception('While preparing INSERT INTO statement: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->bindValue(':id', intval($_POST['id']), SQLITE3_INTEGER)) {
+ throw new Exception('While binding id: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->bindValue(':gid', intval($gid), SQLITE3_INTEGER)) {
+ throw new Exception('While binding gid: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->execute()) {
+ throw new Exception('While executing INSERT INTO statement: ' . $db->lastErrorMsg());
+ }
+ }
+ $db->query('COMMIT;');
+
+ $reload = true;
+ return JSON_success();
+ } catch (\Exception $ex) {
+ return JSON_error($ex->getMessage());
+ }
+} elseif ($_POST['action'] == 'delete_domain') {
+ // Delete domain identified by ID
+ try {
+ $stmt = $db->prepare('DELETE FROM domainlist_by_group WHERE domainlist_id=:id');
+ if (!$stmt) {
+ throw new Exception('While preparing domainlist_by_group statement: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->bindValue(':id', intval($_POST['id']), SQLITE3_INTEGER)) {
+ throw new Exception('While binding id to domainlist_by_group statement: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->execute()) {
+ throw new Exception('While executing domainlist_by_group statement: ' . $db->lastErrorMsg());
+ }
+
+ $stmt = $db->prepare('DELETE FROM domainlist WHERE id=:id');
+ if (!$stmt) {
+ throw new Exception('While preparing domainlist statement: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->bindValue(':id', intval($_POST['id']), SQLITE3_INTEGER)) {
+ throw new Exception('While binding id to domainlist statement: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->execute()) {
+ throw new Exception('While executing domainlist statement: ' . $db->lastErrorMsg());
+ }
+
+ $reload = true;
+ return JSON_success();
+ } catch (\Exception $ex) {
+ return JSON_error($ex->getMessage());
+ }
+} elseif ($_POST['action'] == 'get_adlists') {
+ // List all available groups
+ try {
+ $query = $db->query('SELECT * FROM adlist;');
+ if (!$query) {
+ throw new Exception('Error while querying gravity\'s adlist table: ' . $db->lastErrorMsg());
+ }
+
+ $data = array();
+ while (($res = $query->fetchArray(SQLITE3_ASSOC)) !== false) {
+ $group_query = $db->query('SELECT group_id FROM adlist_by_group WHERE adlist_id = ' . $res['id'] . ';');
+ if (!$group_query) {
+ throw new Exception('Error while querying gravity\'s adlist_by_group table: ' . $db->lastErrorMsg());
+ }
+
+ $groups = array();
+ while ($gres = $group_query->fetchArray(SQLITE3_ASSOC)) {
+ array_push($groups, $gres['group_id']);
+ }
+ $res['groups'] = $groups;
+ array_push($data, $res);
+ }
+
+
+ echo json_encode(array('data' => $data));
+ } catch (\Exception $ex) {
+ return JSON_error($ex->getMessage());
+ }
+} elseif ($_POST['action'] == 'add_adlist') {
+ // Add new adlist
+ try {
+ $stmt = $db->prepare('INSERT INTO adlist (address,comment) VALUES (:address,:comment)');
+ if (!$stmt) {
+ throw new Exception('While preparing statement: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->bindValue(':address', $_POST['address'], SQLITE3_TEXT)) {
+ throw new Exception('While binding address: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->bindValue(':comment', $_POST['comment'], SQLITE3_TEXT)) {
+ throw new Exception('While binding comment: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->execute()) {
+ throw new Exception('While executing: ' . $db->lastErrorMsg());
+ }
+
+ $reload = true;
+ return JSON_success();
+ } catch (\Exception $ex) {
+ return JSON_error($ex->getMessage());
+ }
+} elseif ($_POST['action'] == 'edit_adlist') {
+ // Edit adlist identified by ID
+ try {
+ $stmt = $db->prepare('UPDATE adlist SET enabled=:enabled, comment=:comment WHERE id = :id');
+ if (!$stmt) {
+ throw new Exception('While preparing statement: ' . $db->lastErrorMsg());
+ }
+
+ $status = intval($_POST['status']);
+ if ($status !== 0) {
+ $status = 1;
+ }
+
+ if (!$stmt->bindValue(':enabled', $status, SQLITE3_INTEGER)) {
+ throw new Exception('While binding enabled: ' . $db->lastErrorMsg());
+ }
+
+ $comment = $_POST['comment'];
+ if (strlen($comment) == 0) {
+ // Store NULL in database for empty comments
+ $comment = null;
+ }
+ if (!$stmt->bindValue(':comment', $comment, SQLITE3_TEXT)) {
+ throw new Exception('While binding comment: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->bindValue(':id', intval($_POST['id']), SQLITE3_INTEGER)) {
+ throw new Exception('While binding id: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->execute()) {
+ throw new Exception('While executing: ' . $db->lastErrorMsg());
+ }
+
+ $stmt = $db->prepare('DELETE FROM adlist_by_group WHERE adlist_id = :id');
+ if (!$stmt) {
+ throw new Exception('While preparing DELETE statement: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->bindValue(':id', intval($_POST['id']), SQLITE3_INTEGER)) {
+ throw new Exception('While binding id: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->execute()) {
+ throw new Exception('While executing DELETE statement: ' . $db->lastErrorMsg());
+ }
+
+ $db->query('BEGIN TRANSACTION;');
+ foreach ($_POST['groups'] as $gid) {
+ $stmt = $db->prepare('INSERT INTO adlist_by_group (adlist_id,group_id) VALUES(:id,:gid);');
+ if (!$stmt) {
+ throw new Exception('While preparing INSERT INTO statement: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->bindValue(':id', intval($_POST['id']), SQLITE3_INTEGER)) {
+ throw new Exception('While binding id: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->bindValue(':gid', intval($gid), SQLITE3_INTEGER)) {
+ throw new Exception('While binding gid: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->execute()) {
+ throw new Exception('While executing INSERT INTO statement: ' . $db->lastErrorMsg());
+ }
+ }
+ $db->query('COMMIT;');
+
+ $reload = true;
+ return JSON_success();
+ } catch (\Exception $ex) {
+ return JSON_error($ex->getMessage());
+ }
+} elseif ($_POST['action'] == 'delete_adlist') {
+ // Delete adlist identified by ID
+ try {
+ $stmt = $db->prepare('DELETE FROM adlist_by_group WHERE adlist_id=:id');
+ if (!$stmt) {
+ throw new Exception('While preparing adlist_by_group statement: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->bindValue(':id', intval($_POST['id']), SQLITE3_INTEGER)) {
+ throw new Exception('While binding id to adlist_by_group statement: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->execute()) {
+ throw new Exception('While executing adlist_by_group statement: ' . $db->lastErrorMsg());
+ }
+
+ $stmt = $db->prepare('DELETE FROM adlist WHERE id=:id');
+ if (!$stmt) {
+ throw new Exception('While preparing adlist statement: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->bindValue(':id', intval($_POST['id']), SQLITE3_INTEGER)) {
+ throw new Exception('While binding id to adlist statement: ' . $db->lastErrorMsg());
+ }
+
+ if (!$stmt->execute()) {
+ throw new Exception('While executing adlist statement: ' . $db->lastErrorMsg());
+ }
+
+ $reload = true;
+ return JSON_success();
+ } catch (\Exception $ex) {
+ return JSON_error($ex->getMessage());
+ }
+} else {
+ log_and_die('Requested action not supported!');
+}
+// Reload lists in pihole-FTL after having added something
+if ($reload) {
+ echo shell_exec('sudo pihole restartdns reload-lists');
+}
diff --git a/scripts/pi-hole/php/header.php b/scripts/pi-hole/php/header.php
index 3e48914a..6491aa28 100644
--- a/scripts/pi-hole/php/header.php
+++ b/scripts/pi-hole/php/header.php
@@ -8,6 +8,7 @@
require "scripts/pi-hole/php/auth.php";
require "scripts/pi-hole/php/password.php";
+ $scriptname = basename($_SERVER['SCRIPT_FILENAME']);
check_cors();
@@ -182,7 +183,7 @@
-
+
Pi-hole Admin Console
@@ -209,6 +210,7 @@
+
@@ -217,6 +219,15 @@
+
+
+
+
+
+
+
+
+
@@ -416,7 +427,6 @@ if($auth) {
Blacklist
+
+
active">
+
+
+
+
+ Group Management
+
+
+
hidden="true">
diff --git a/scripts/pi-hole/php/teleporter.php b/scripts/pi-hole/php/teleporter.php
index 5d3227e7..7c34c47f 100644
--- a/scripts/pi-hole/php/teleporter.php
+++ b/scripts/pi-hole/php/teleporter.php
@@ -415,6 +415,7 @@ else
archive_add_table("domain_audit.json", "domain_audit");
archive_add_file("/etc/pihole/","setupVars.conf");
archive_add_file("/etc/pihole/","dhcp.leases");
+ archive_add_file("/etc/","hosts","etc/");
archive_add_directory("/etc/dnsmasq.d/","dnsmasq.d/");
$archive->compress(Phar::GZ); // Creates a gziped copy
diff --git a/scripts/vendor/bootstrap-notify.min.js b/scripts/vendor/bootstrap-notify.min.js
new file mode 100644
index 00000000..f5ad385a
--- /dev/null
+++ b/scripts/vendor/bootstrap-notify.min.js
@@ -0,0 +1,2 @@
+/* Project: Bootstrap Growl = v3.1.3 | Description: Turns standard Bootstrap alerts into "Growl-like" notifications. | Author: Mouse0270 aka Robert McIntosh | License: MIT License | Website: https://github.com/mouse0270/bootstrap-growl */
+!function(t){"function"==typeof define&&define.amd?define(["jquery"],t):t("object"==typeof exports?require("jquery"):jQuery)}(function(t){function e(e,i,n){var i={content:{message:"object"==typeof i?i.message:i,title:i.title?i.title:"",icon:i.icon?i.icon:"",url:i.url?i.url:"#",target:i.target?i.target:"-"}};n=t.extend(!0,{},i,n),this.settings=t.extend(!0,{},s,n),this._defaults=s,"-"==this.settings.content.target&&(this.settings.content.target=this.settings.url_target),this.animations={start:"webkitAnimationStart oanimationstart MSAnimationStart animationstart",end:"webkitAnimationEnd oanimationend MSAnimationEnd animationend"},"number"==typeof this.settings.offset&&(this.settings.offset={x:this.settings.offset,y:this.settings.offset}),this.init()}var s={element:"body",position:null,type:"info",allow_dismiss:!0,newest_on_top:!1,showProgressbar:!1,placement:{from:"top",align:"right"},offset:20,spacing:10,z_index:1031,delay:5e3,timer:1e3,url_target:"_blank",mouse_over:null,animate:{enter:"animated fadeInDown",exit:"animated fadeOutUp"},onShow:null,onShown:null,onClose:null,onClosed:null,icon_type:"class",template:''};String.format=function(){for(var t=arguments[0],e=1;e .progress-bar').removeClass("progress-bar-"+t.settings.type),t.settings.type=i[e],this.$ele.addClass("alert-"+i[e]).find('[data-notify="progressbar"] > .progress-bar').addClass("progress-bar-"+i[e]);break;case"icon":var n=this.$ele.find('[data-notify="icon"]');"class"==t.settings.icon_type.toLowerCase()?n.removeClass(t.settings.content.icon).addClass(i[e]):(n.is("img")||n.find("img"),n.attr("src",i[e]));break;case"progress":var a=t.settings.delay-t.settings.delay*(i[e]/100);this.$ele.data("notify-delay",a),this.$ele.find('[data-notify="progressbar"] > div').attr("aria-valuenow",i[e]).css("width",i[e]+"%");break;case"url":this.$ele.find('[data-notify="url"]').attr("href",i[e]);break;case"target":this.$ele.find('[data-notify="url"]').attr("target",i[e]);break;default:this.$ele.find('[data-notify="'+e+'"]').html(i[e])}var o=this.$ele.outerHeight()+parseInt(t.settings.spacing)+parseInt(t.settings.offset.y);t.reposition(o)},close:function(){t.close()}}},buildNotify:function(){var e=this.settings.content;this.$ele=t(String.format(this.settings.template,this.settings.type,e.title,e.message,e.url,e.target)),this.$ele.attr("data-notify-position",this.settings.placement.from+"-"+this.settings.placement.align),this.settings.allow_dismiss||this.$ele.find('[data-notify="dismiss"]').css("display","none"),(this.settings.delay<=0&&!this.settings.showProgressbar||!this.settings.showProgressbar)&&this.$ele.find('[data-notify="progressbar"]').remove()},setIcon:function(){"class"==this.settings.icon_type.toLowerCase()?this.$ele.find('[data-notify="icon"]').addClass(this.settings.content.icon):this.$ele.find('[data-notify="icon"]').is("img")?this.$ele.find('[data-notify="icon"]').attr("src",this.settings.content.icon):this.$ele.find('[data-notify="icon"]').append('
')},styleURL:function(){this.$ele.find('[data-notify="url"]').css({backgroundImage:"url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)",height:"100%",left:"0px",position:"absolute",top:"0px",width:"100%",zIndex:this.settings.z_index+1}),this.$ele.find('[data-notify="dismiss"]').css({position:"absolute",right:"10px",top:"5px",zIndex:this.settings.z_index+2})},placement:function(){var e=this,s=this.settings.offset.y,i={display:"inline-block",margin:"0px auto",position:this.settings.position?this.settings.position:"body"===this.settings.element?"fixed":"absolute",transition:"all .5s ease-in-out",zIndex:this.settings.z_index},n=!1,a=this.settings;switch(t('[data-notify-position="'+this.settings.placement.from+"-"+this.settings.placement.align+'"]:not([data-closing="true"])').each(function(){return s=Math.max(s,parseInt(t(this).css(a.placement.from))+parseInt(t(this).outerHeight())+parseInt(a.spacing))}),1==this.settings.newest_on_top&&(s=this.settings.offset.y),i[this.settings.placement.from]=s+"px",this.settings.placement.align){case"left":case"right":i[this.settings.placement.align]=this.settings.offset.x+"px";break;case"center":i.left=0,i.right=0}this.$ele.css(i).addClass(this.settings.animate.enter),t.each(Array("webkit","moz","o","ms",""),function(t,s){e.$ele[0].style[s+"AnimationIterationCount"]=1}),t(this.settings.element).append(this.$ele),1==this.settings.newest_on_top&&(s=parseInt(s)+parseInt(this.settings.spacing)+this.$ele.outerHeight(),this.reposition(s)),t.isFunction(e.settings.onShow)&&e.settings.onShow.call(this.$ele),this.$ele.one(this.animations.start,function(){n=!0}).one(this.animations.end,function(){t.isFunction(e.settings.onShown)&&e.settings.onShown.call(this)}),setTimeout(function(){n||t.isFunction(e.settings.onShown)&&e.settings.onShown.call(this)},600)},bind:function(){var e=this;if(this.$ele.find('[data-notify="dismiss"]').on("click",function(){e.close()}),this.$ele.mouseover(function(){t(this).data("data-hover","true")}).mouseout(function(){t(this).data("data-hover","false")}),this.$ele.data("data-hover","false"),this.settings.delay>0){e.$ele.data("notify-delay",e.settings.delay);var s=setInterval(function(){var t=parseInt(e.$ele.data("notify-delay"))-e.settings.timer;if("false"===e.$ele.data("data-hover")&&"pause"==e.settings.mouse_over||"pause"!=e.settings.mouse_over){var i=(e.settings.delay-t)/e.settings.delay*100;e.$ele.data("notify-delay",t),e.$ele.find('[data-notify="progressbar"] > div').attr("aria-valuenow",i).css("width",i+"%")}t<=-e.settings.timer&&(clearInterval(s),e.close())},e.settings.timer)}},close:function(){var e=this,s=parseInt(this.$ele.css(this.settings.placement.from)),i=!1;this.$ele.data("closing","true").addClass(this.settings.animate.exit),e.reposition(s),t.isFunction(e.settings.onClose)&&e.settings.onClose.call(this.$ele),this.$ele.one(this.animations.start,function(){i=!0}).one(this.animations.end,function(){t(this).remove(),t.isFunction(e.settings.onClosed)&&e.settings.onClosed.call(this)}),setTimeout(function(){i||(e.$ele.remove(),e.settings.onClosed&&e.settings.onClosed(e.$ele))},600)},reposition:function(e){var s=this,i='[data-notify-position="'+this.settings.placement.from+"-"+this.settings.placement.align+'"]:not([data-closing="true"])',n=this.$ele.nextAll(i);1==this.settings.newest_on_top&&(n=this.$ele.prevAll(i)),n.each(function(){t(this).css(s.settings.placement.from,e),e=parseInt(e)+parseInt(s.settings.spacing)+t(this).outerHeight()})}}),t.notify=function(t,s){var i=new e(this,t,s);return i.notify},t.notifyDefaults=function(e){return s=t.extend(!0,{},s,e)},t.notifyClose=function(e){"undefined"==typeof e||"all"==e?t("[data-notify]").find('[data-notify="dismiss"]').trigger("click"):t('[data-notify-position="'+e+'"]').find('[data-notify="dismiss"]').trigger("click")}});
\ No newline at end of file
diff --git a/scripts/vendor/jquery.dataTables.min.js b/scripts/vendor/jquery.dataTables.min.js
index f1277251..d297f256 100644
--- a/scripts/vendor/jquery.dataTables.min.js
+++ b/scripts/vendor/jquery.dataTables.min.js
@@ -1,166 +1,180 @@
/*!
- DataTables 1.10.12
- ©2008-2015 SpryMedia Ltd - datatables.net/license
+ Copyright 2008-2019 SpryMedia Ltd.
+
+ This source file is free software, available under the following license:
+ MIT license - http://datatables.net/license
+
+ This source file is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details.
+
+ For details please refer to: http://www.datatables.net
+ DataTables 1.10.20
+ ©2008-2019 SpryMedia Ltd - datatables.net/license
*/
-(function(h){"function"===typeof define&&define.amd?define(["jquery"],function(D){return h(D,window,document)}):"object"===typeof exports?module.exports=function(D,I){D||(D=window);I||(I="undefined"!==typeof window?require("jquery"):require("jquery")(D));return h(I,D,D.document)}:h(jQuery,window,document)})(function(h,D,I,k){function X(a){var b,c,d={};h.each(a,function(e){if((b=e.match(/^([^A-Z]+?)([A-Z])/))&&-1!=="a aa ai ao as b fn i m o s ".indexOf(b[1]+" "))c=e.replace(b[0],b[2].toLowerCase()),
-d[c]=e,"o"===b[1]&&X(a[e])});a._hungarianMap=d}function K(a,b,c){a._hungarianMap||X(a);var d;h.each(b,function(e){d=a._hungarianMap[e];if(d!==k&&(c||b[d]===k))"o"===d.charAt(0)?(b[d]||(b[d]={}),h.extend(!0,b[d],b[e]),K(a[d],b[d],c)):b[d]=b[e]})}function Da(a){var b=m.defaults.oLanguage,c=a.sZeroRecords;!a.sEmptyTable&&(c&&"No data available in table"===b.sEmptyTable)&&E(a,a,"sZeroRecords","sEmptyTable");!a.sLoadingRecords&&(c&&"Loading..."===b.sLoadingRecords)&&E(a,a,"sZeroRecords","sLoadingRecords");
-a.sInfoThousands&&(a.sThousands=a.sInfoThousands);(a=a.sDecimal)&&db(a)}function eb(a){A(a,"ordering","bSort");A(a,"orderMulti","bSortMulti");A(a,"orderClasses","bSortClasses");A(a,"orderCellsTop","bSortCellsTop");A(a,"order","aaSorting");A(a,"orderFixed","aaSortingFixed");A(a,"paging","bPaginate");A(a,"pagingType","sPaginationType");A(a,"pageLength","iDisplayLength");A(a,"searching","bFilter");"boolean"===typeof a.sScrollX&&(a.sScrollX=a.sScrollX?"100%":"");"boolean"===typeof a.scrollX&&(a.scrollX=
-a.scrollX?"100%":"");if(a=a.aoSearchCols)for(var b=0,c=a.length;b").css({position:"fixed",top:0,left:0,height:1,width:1,overflow:"hidden"}).append(h("").css({position:"absolute",top:1,left:1,
-width:100,overflow:"scroll"}).append(h("").css({width:"100%",height:10}))).appendTo("body"),d=c.children(),e=d.children();b.barWidth=d[0].offsetWidth-d[0].clientWidth;b.bScrollOversize=100===e[0].offsetWidth&&100!==d[0].clientWidth;b.bScrollbarLeft=1!==Math.round(e.offset().left);b.bBounding=c[0].getBoundingClientRect().width?!0:!1;c.remove()}h.extend(a.oBrowser,m.__browser);a.oScroll.iBarWidth=m.__browser.barWidth}function hb(a,b,c,d,e,f){var g,j=!1;c!==k&&(g=c,j=!0);for(;d!==e;)a.hasOwnProperty(d)&&
-(g=j?b(g,a[d],d,a):a[d],j=!0,d+=f);return g}function Ea(a,b){var c=m.defaults.column,d=a.aoColumns.length,c=h.extend({},m.models.oColumn,c,{nTh:b?b:I.createElement("th"),sTitle:c.sTitle?c.sTitle:b?b.innerHTML:"",aDataSort:c.aDataSort?c.aDataSort:[d],mData:c.mData?c.mData:d,idx:d});a.aoColumns.push(c);c=a.aoPreSearchCols;c[d]=h.extend({},m.models.oSearch,c[d]);ja(a,d,h(b).data())}function ja(a,b,c){var b=a.aoColumns[b],d=a.oClasses,e=h(b.nTh);if(!b.sWidthOrig){b.sWidthOrig=e.attr("width")||null;var f=
-(e.attr("style")||"").match(/width:\s*(\d+[pxem%]+)/);f&&(b.sWidthOrig=f[1])}c!==k&&null!==c&&(fb(c),K(m.defaults.column,c),c.mDataProp!==k&&!c.mData&&(c.mData=c.mDataProp),c.sType&&(b._sManualType=c.sType),c.className&&!c.sClass&&(c.sClass=c.className),h.extend(b,c),E(b,c,"sWidth","sWidthOrig"),c.iDataSort!==k&&(b.aDataSort=[c.iDataSort]),E(b,c,"aDataSort"));var g=b.mData,j=Q(g),i=b.mRender?Q(b.mRender):null,c=function(a){return"string"===typeof a&&-1!==a.indexOf("@")};b._bAttrSrc=h.isPlainObject(g)&&
-(c(g.sort)||c(g.type)||c(g.filter));b._setter=null;b.fnGetData=function(a,b,c){var d=j(a,b,k,c);return i&&b?i(d,b,a,c):d};b.fnSetData=function(a,b,c){return R(g)(a,b,c)};"number"!==typeof g&&(a._rowReadObject=!0);a.oFeatures.bSort||(b.bSortable=!1,e.addClass(d.sSortableNone));a=-1!==h.inArray("asc",b.asSorting);c=-1!==h.inArray("desc",b.asSorting);!b.bSortable||!a&&!c?(b.sSortingClass=d.sSortableNone,b.sSortingClassJUI=""):a&&!c?(b.sSortingClass=d.sSortableAsc,b.sSortingClassJUI=d.sSortJUIAscAllowed):
-!a&&c?(b.sSortingClass=d.sSortableDesc,b.sSortingClassJUI=d.sSortJUIDescAllowed):(b.sSortingClass=d.sSortable,b.sSortingClassJUI=d.sSortJUI)}function Y(a){if(!1!==a.oFeatures.bAutoWidth){var b=a.aoColumns;Fa(a);for(var c=0,d=b.length;cq[f])d(l.length+q[f],n);else if("string"===typeof q[f]){j=0;for(i=l.length;jb&&a[e]--; -1!=d&&c===k&&a.splice(d,1)}function ca(a,b,c,d){var e=a.aoData[b],f,g=function(c,d){for(;c.childNodes.length;)c.removeChild(c.firstChild);
-c.innerHTML=B(a,b,d,"display")};if("dom"===c||(!c||"auto"===c)&&"dom"===e.src)e._aData=Ia(a,e,d,d===k?k:e._aData).data;else{var j=e.anCells;if(j)if(d!==k)g(j[d],d);else{c=0;for(f=j.length;c").appendTo(g));b=0;for(c=l.length;btr").attr("role","row");h(g).find(">tr>th, >tr>td").addClass(n.sHeaderTH);h(j).find(">tr>th, >tr>td").addClass(n.sFooterTH);
-if(null!==j){a=a.aoFooter[0];b=0;for(c=a.length;b=a.fnRecordsDisplay()?0:g,a.iInitDisplayStart=
--1);var g=a._iDisplayStart,n=a.fnDisplayEnd();if(a.bDeferLoading)a.bDeferLoading=!1,a.iDraw++,C(a,!1);else if(j){if(!a.bDestroying&&!lb(a))return}else a.iDraw++;if(0!==i.length){f=j?a.aoData.length:n;for(j=j?0:g;j",{"class":e?d[0]:""}).append(h(" | ",{valign:"top",colSpan:aa(a),"class":a.oClasses.sRowEmpty}).html(c))[0];u(a,"aoHeaderCallback","header",[h(a.nTHead).children("tr")[0],Ka(a),g,n,i]);u(a,"aoFooterCallback","footer",[h(a.nTFoot).children("tr")[0],Ka(a),g,n,i]);d=h(a.nTBody);d.children().detach();d.append(h(b));u(a,"aoDrawCallback","draw",[a]);a.bSorted=!1;a.bFiltered=!1;a.bDrawing=!1}}function T(a,b){var c=a.oFeatures,d=c.bFilter;
-c.bSort&&mb(a);d?fa(a,a.oPreviousSearch):a.aiDisplay=a.aiDisplayMaster.slice();!0!==b&&(a._iDisplayStart=0);a._drawHold=b;O(a);a._drawHold=!1}function nb(a){var b=a.oClasses,c=h(a.nTable),c=h("").insertBefore(c),d=a.oFeatures,e=h("",{id:a.sTableId+"_wrapper","class":b.sWrapper+(a.nTFoot?"":" "+b.sNoFooter)});a.nHolding=c[0];a.nTableWrapper=e[0];a.nTableReinsertBefore=a.nTable.nextSibling;for(var f=a.sDom.split(""),g,j,i,n,l,q,t=0;t")[0];
-n=f[t+1];if("'"==n||'"'==n){l="";for(q=2;f[t+q]!=n;)l+=f[t+q],q++;"H"==l?l=b.sJUIHeader:"F"==l&&(l=b.sJUIFooter);-1!=l.indexOf(".")?(n=l.split("."),i.id=n[0].substr(1,n[0].length-1),i.className=n[1]):"#"==l.charAt(0)?i.id=l.substr(1,l.length-1):i.className=l;t+=q}e.append(i);e=h(i)}else if(">"==j)e=e.parent();else if("l"==j&&d.bPaginate&&d.bLengthChange)g=ob(a);else if("f"==j&&d.bFilter)g=pb(a);else if("r"==j&&d.bProcessing)g=qb(a);else if("t"==j)g=rb(a);else if("i"==j&&d.bInfo)g=sb(a);else if("p"==
-j&&d.bPaginate)g=tb(a);else if(0!==m.ext.feature.length){i=m.ext.feature;q=0;for(n=i.length;q',j=d.sSearch,j=j.match(/_INPUT_/)?j.replace("_INPUT_",g):j+g,b=h("",{id:!f.f?c+"_filter":null,"class":b.sFilter}).append(h("").append(j)),f=function(){var b=!this.value?
-"":this.value;b!=e.sSearch&&(fa(a,{sSearch:b,bRegex:e.bRegex,bSmart:e.bSmart,bCaseInsensitive:e.bCaseInsensitive}),a._iDisplayStart=0,O(a))},g=null!==a.searchDelay?a.searchDelay:"ssp"===y(a)?400:0,i=h("input",b).val(e.sSearch).attr("placeholder",d.sSearchPlaceholder).bind("keyup.DT search.DT input.DT paste.DT cut.DT",g?Oa(f,g):f).bind("keypress.DT",function(a){if(13==a.keyCode)return!1}).attr("aria-controls",c);h(a.nTable).on("search.dt.DT",function(b,c){if(a===c)try{i[0]!==I.activeElement&&i.val(e.sSearch)}catch(d){}});
-return b[0]}function fa(a,b,c){var d=a.oPreviousSearch,e=a.aoPreSearchCols,f=function(a){d.sSearch=a.sSearch;d.bRegex=a.bRegex;d.bSmart=a.bSmart;d.bCaseInsensitive=a.bCaseInsensitive};Ga(a);if("ssp"!=y(a)){wb(a,b.sSearch,c,b.bEscapeRegex!==k?!b.bEscapeRegex:b.bRegex,b.bSmart,b.bCaseInsensitive);f(b);for(b=0;b=b.length)a.aiDisplay=f.slice();
-else{if(g||c||e.length>b.length||0!==b.indexOf(e)||a.bSorted)a.aiDisplay=f.slice();b=a.aiDisplay;for(c=b.length-1;0<=c;c--)d.test(a.aoData[b[c]]._sFilterRow)||b.splice(c,1)}}function Pa(a,b,c,d){a=b?a:Qa(a);c&&(a="^(?=.*?"+h.map(a.match(/"[^"]+"|[^ ]+/g)||[""],function(a){if('"'===a.charAt(0))var b=a.match(/^"(.*)"$/),a=b?b[1]:a;return a.replace('"',"")}).join(")(?=.*?")+").*$");return RegExp(a,d?"i":"")}function zb(a){var b=a.aoColumns,c,d,e,f,g,j,i,h,l=m.ext.type.search;c=!1;d=0;for(f=a.aoData.length;d<
-f;d++)if(h=a.aoData[d],!h._aFilterData){j=[];e=0;for(g=b.length;e",{"class":a.oClasses.sInfo,id:!c?b+"_info":null});c||(a.aoDrawCallback.push({fn:Cb,sName:"information"}),d.attr("role","status").attr("aria-live","polite"),h(a.nTable).attr("aria-describedby",b+"_info"));return d[0]}function Cb(a){var b=a.aanFeatures.i;if(0!==b.length){var c=a.oLanguage,d=a._iDisplayStart+1,e=a.fnDisplayEnd(),f=a.fnRecordsTotal(),
-g=a.fnRecordsDisplay(),j=g?c.sInfo:c.sInfoEmpty;g!==f&&(j+=" "+c.sInfoFiltered);j+=c.sInfoPostFix;j=Db(a,j);c=c.fnInfoCallback;null!==c&&(j=c.call(a.oInstance,a,d,e,f,g,j));h(b).html(j)}}function Db(a,b){var c=a.fnFormatNumber,d=a._iDisplayStart+1,e=a._iDisplayLength,f=a.fnRecordsDisplay(),g=-1===e;return b.replace(/_START_/g,c.call(a,d)).replace(/_END_/g,c.call(a,a.fnDisplayEnd())).replace(/_MAX_/g,c.call(a,a.fnRecordsTotal())).replace(/_TOTAL_/g,c.call(a,f)).replace(/_PAGE_/g,c.call(a,g?1:Math.ceil(d/
-e))).replace(/_PAGES_/g,c.call(a,g?1:Math.ceil(f/e)))}function ga(a){var b,c,d=a.iInitDisplayStart,e=a.aoColumns,f;c=a.oFeatures;var g=a.bDeferLoading;if(a.bInitialised){nb(a);kb(a);ea(a,a.aoHeader);ea(a,a.aoFooter);C(a,!0);c.bAutoWidth&&Fa(a);b=0;for(c=e.length;b",{name:c+"_length","aria-controls":c,"class":b.sLengthSelect}),g=0,j=f.length;g ").addClass(b.sLength);a.aanFeatures.l||(i[0].id=c+"_length");i.children().append(a.oLanguage.sLengthMenu.replace("_MENU_",e[0].outerHTML));h("select",i).val(a._iDisplayLength).bind("change.DT",function(){Ra(a,h(this).val());O(a)});h(a.nTable).bind("length.dt.DT",function(b,c,d){a===c&&h("select",i).val(d)});return i[0]}function tb(a){var b=a.sPaginationType,c=m.ext.pager[b],d="function"===typeof c,e=function(a){O(a)},b=h("
").addClass(a.oClasses.sPaging+b)[0],f=a.aanFeatures;
-d||c.fnInit(a,b,e);f.p||(b.id=a.sTableId+"_paginate",a.aoDrawCallback.push({fn:function(a){if(d){var b=a._iDisplayStart,i=a._iDisplayLength,h=a.fnRecordsDisplay(),l=-1===i,b=l?0:Math.ceil(b/i),i=l?1:Math.ceil(h/i),h=c(b,i),k,l=0;for(k=f.p.length;l