diff --git a/api_FTL.php b/api_FTL.php index bed84b5d..149b61c0 100644 --- a/api_FTL.php +++ b/api_FTL.php @@ -71,7 +71,11 @@ if (isset($_GET['overTimeData10mins'])) if (isset($_GET['topItems']) && $auth) { - if(is_numeric($_GET['topItems'])) + if($_GET['topItems'] === "audit") + { + sendRequestFTL("top-domains for audit"); + } + else if(is_numeric($_GET['topItems'])) { sendRequestFTL("top-domains (".$_GET['topItems'].")"); } @@ -88,7 +92,11 @@ if (isset($_GET['topItems']) && $auth) $top_queries[$tmp[2]] = intval($tmp[1]); } - if(is_numeric($_GET['topItems'])) + if($_GET['topItems'] === "audit") + { + sendRequestFTL("top-ads for audit"); + } + else if(is_numeric($_GET['topItems'])) { sendRequestFTL("top-ads (".$_GET['topItems'].")"); } @@ -102,7 +110,10 @@ if (isset($_GET['topItems']) && $auth) foreach($return as $line) { $tmp = explode(" ",$line); - $top_ads[$tmp[2]] = intval($tmp[1]); + if(count($tmp) === 4) + $top_ads[$tmp[2]." (".$tmp[3].")"] = intval($tmp[1]); + else + $top_ads[$tmp[2]] = intval($tmp[1]); } $result = array('top_queries' => $top_queries, diff --git a/api_db.php b/api_db.php new file mode 100644 index 00000000..876e20e0 --- /dev/null +++ b/api_db.php @@ -0,0 +1,169 @@ +query('SELECT * FROM QUERIES order by TIMESTAMP ASC LIMIT 2'); +// $results = $db->query('SELECT TIMESTAMP,TYPE,DOMAIN,CLIENT,STATUS FROM QUERIES order by TIMESTAMP ASC'); +// echo "N=".$result->numColumns; +// while ($row = $results->fetchArray()) +// var_dump($row); +// } + +if (isset($_GET['getAllQueries']) && $auth) +{ + if($_GET['getAllQueries'] === "empty") + { + $allQueries = array(); + } + else + { + $from = intval($_GET["from"]); + $until = intval($_GET["until"]); + $results = $db->query('SELECT timestamp,type,domain,client,status FROM queries WHERE timestamp >= '.$from.' AND timestamp <= '.$until.' ORDER BY timestamp ASC'); + $allQueries = array(); + while ($row = $results->fetchArray()) + { + $allQueries[] = [$row[0],$row[1] == 1 ? "IPv4" : "IPv6",$row[2],$row[3],$row[4]]; + } + } + $result = array('data' => $allQueries); + $data = array_merge($data, $result); +} + +if (isset($_GET['topClients']) && $auth) +{ + // $from = intval($_GET["from"]); + $limit = ""; + if(isset($_GET["from"]) && isset($_GET["until"])) + { + $limit = "WHERE timestamp >= ".$_GET["from"]." AND timestamp <= ".$_GET["until"]; + } + elseif(isset($_GET["from"]) && !isset($_GET["until"])) + { + $limit = "WHERE timestamp >= ".$_GET["from"]; + } + elseif(!isset($_GET["from"]) && isset($_GET["until"])) + { + $limit = "WHERE timestamp <= ".$_GET["until"]; + } + $results = $db->query('SELECT client,count(client) FROM queries '.$limit.' GROUP by client order by count(client) desc limit 10'); + $clients = array(); + while ($row = $results->fetchArray()) + { + $clients[$row[0]] = intval($row[1]); + // var_dump($row); + } + $result = array('top_sources' => $clients); + $data = array_merge($data, $result); +} + +if (isset($_GET['topDomains']) && $auth) +{ + $limit = ""; + + if(isset($_GET["from"]) && isset($_GET["until"])) + { + $limit = " AND timestamp >= ".$_GET["from"]." AND timestamp <= ".$_GET["until"]; + } + elseif(isset($_GET["from"]) && !isset($_GET["until"])) + { + $limit = " AND timestamp >= ".$_GET["from"]; + } + elseif(!isset($_GET["from"]) && isset($_GET["until"])) + { + $limit = " AND timestamp <= ".$_GET["until"]; + } + $results = $db->query('SELECT domain,count(domain) FROM queries WHERE (STATUS == 2 OR STATUS == 3)'.$limit.' GROUP by domain order by count(domain) desc limit 10'); + $domains = array(); + while ($row = $results->fetchArray()) + { + $domains[$row[0]] = intval($row[1]); + } + $result = array('top_domains' => $domains); + $data = array_merge($data, $result); +} + +if (isset($_GET['topAds']) && $auth) +{ + $limit = ""; + + if(isset($_GET["from"]) && isset($_GET["until"])) + { + $limit = " AND timestamp >= ".$_GET["from"]." AND timestamp <= ".$_GET["until"]; + } + elseif(isset($_GET["from"]) && !isset($_GET["until"])) + { + $limit = " AND timestamp >= ".$_GET["from"]; + } + elseif(!isset($_GET["from"]) && isset($_GET["until"])) + { + $limit = " AND timestamp <= ".$_GET["until"]; + } + $results = $db->query('SELECT domain,count(domain) FROM queries WHERE (STATUS == 1 OR STATUS == 4)'.$limit.' GROUP by domain order by count(domain) desc limit 10'); + $addomains = array(); + while ($row = $results->fetchArray()) + { + $addomains[$row[0]] = intval($row[1]); + } + $result = array('top_ads' => $addomains); + $data = array_merge($data, $result); +} + +if (isset($_GET['getMinTimestamp']) && $auth) +{ + $results = $db->query('SELECT MIN(timestamp) FROM queries'); + $result = array('mintimestamp' => $results->fetchArray()[0]); + $data = array_merge($data, $result); +} + +if (isset($_GET['getMaxTimestamp']) && $auth) +{ + $results = $db->query('SELECT MAX(timestamp) FROM queries'); + $result = array('maxtimestamp' => $results->fetchArray()[0]); + $data = array_merge($data, $result); +} + +if (isset($_GET['getQueriesCount']) && $auth) +{ + $results = $db->query('SELECT COUNT(timestamp) FROM queries'); + $result = array('count' => $results->fetchArray()[0]); + $data = array_merge($data, $result); +} + +if (isset($_GET['getDBfilesize']) && $auth) +{ + $filesize = filesize("/etc/pihole/pihole-FTL.db"); + $result = array('filesize' => $filesize); + $data = array_merge($data, $result); +} + +if(isset($_GET["jsonForceObject"])) +{ + echo json_encode($data, JSON_FORCE_OBJECT); +} +else +{ + echo json_encode($data); +} diff --git a/auditlog.php b/auditlog.php new file mode 100644 index 00000000..96d74577 --- /dev/null +++ b/auditlog.php @@ -0,0 +1,81 @@ + + + + + + +
+
+
+
+

Allowed queries

+
+ +
+
+ + + + + + + + +
DomainHitsActions
+
+
+
+ +
+ +
+ +
+ +
+
+
+

Blocked queries

+
+ +
+
+ + + + + + + + +
DomainHitsActions
+
+
+
+ +
+ +
+ +
+
+

Important: Note that black- and whitelisted domains are not automatically applied on this page to avoid restarting the DNS service too often. Instead, go to Update Lists and run the update, to have the new settings become effective.

+
+ +
+ + + + diff --git a/db_lists.php b/db_lists.php new file mode 100644 index 00000000..86da0c2d --- /dev/null +++ b/db_lists.php @@ -0,0 +1,145 @@ + + + + + + + + +
+
+ +
+ + +
+
+ +
+ + + + +
+ +
+
+
+ +
+
+
+
+

Top Domains

+
+ +
+
+ + + + + + + + +
DomainHitsFrequency
+
+
+ + +
+ +
+ +
+
+
+

Top Blocked Domains

+
+ +
+
+ + + + + + + + +
DomainHitsFrequency
+
+
+ + +
+ +
+ +
+
+
+

Top Clients

+
+ +
+
+ + + + + + + + +
ClientRequestsFrequency
+
+
+ + +
+ +
+ +
+ + + + + + diff --git a/db_queries.php b/db_queries.php new file mode 100644 index 00000000..bca28c4e --- /dev/null +++ b/db_queries.php @@ -0,0 +1,148 @@ + + + + + + + + +
+
+ +
+ + +
+
+ +
+ + + + +
+ +
+
+
+ +
+
+ +
+
+

---

+

Queries Blocked

+
+
+ +
+
+
+ +
+ +
+
+

---

+

Queries Blocked (Wildcards)

+
+
+ +
+
+
+ +
+ +
+
+

---

+

Queries Total

+
+
+ +
+
+
+ +
+ +
+
+

---

+

Queries Blocked

+
+
+ +
+
+
+ +
+ +
+
+
+
+

Recent Queries

+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + +
TimeTypeDomainClientStatusAction
TimeTypeDomainClientStatusAction
+
+
+ +
+ +
+
+ + + + + + + + diff --git a/scripts/pi-hole/js/auditlog.js b/scripts/pi-hole/js/auditlog.js new file mode 100644 index 00000000..3806896f --- /dev/null +++ b/scripts/pi-hole/js/auditlog.js @@ -0,0 +1,113 @@ +/* Pi-hole: A black hole for Internet advertisements +* (c) 2017 Pi-hole, LLC (https://pi-hole.net) +* Network-wide ad blocking via your own hardware. +* +* This file is copyright under the latest version of the EUPL. +* Please see LICENSE file for your rights under this license. */ +// Define global variables +var timeLineChart, queryTypeChart, forwardDestinationChart; + +// Credit: http://stackoverflow.com/questions/1787322/htmlspecialchars-equivalent-in-javascript/4835406#4835406 +function escapeHtml(text) { + var map = { + "&": "&", + "<": "<", + ">": ">", + "\"": """, + "\'": "'" + }; + + return text.replace(/[&<>"']/g, function(m) { return map[m]; }); +} + +function updateTopLists() { + $.getJSON("api.php?topItems=audit", function(data) { + + if("FTLnotrunning" in data) + { + return; + } + + // Clear tables before filling them with data + $("#domain-frequency td").parent().remove(); + $("#ad-frequency td").parent().remove(); + var domaintable = $("#domain-frequency").find("tbody:last"); + var adtable = $("#ad-frequency").find("tbody:last"); + var url, domain, percentage; + for (domain in data.top_queries) { + if ({}.hasOwnProperty.call(data.top_queries,domain)){ + // Sanitize domain + domain = escapeHtml(domain); + url = ""+domain+""; + percentage = data.top_queries[domain] / data.dns_queries_today * 100; + domaintable.append(" " + url + + " " + data.top_queries[domain] + " "); + } + } + + for (domain in data.top_ads) { + if ({}.hasOwnProperty.call(data.top_ads,domain)){ + var input = domain.split(" "); + // Sanitize domain + var printdomain = escapeHtml(input[0]); + if(input.length > 1) + { + url = ""+printdomain+" (wildcard blocked)"; + adtable.append(" " + url + + " " + data.top_ads[domain] + " "); + } + else + { + url = ""+printdomain+""; + adtable.append(" " + url + + " " + data.top_ads[domain] + " "); + } + } + } + + $("#domain-frequency .overlay").hide(); + $("#ad-frequency .overlay").hide(); + // Update top lists data every second + setTimeout(updateTopLists, 1000); + }); +} + + +function add(domain,list) { + var token = $("#token").html(); + $.ajax({ + url: "scripts/pi-hole/php/add.php", + method: "post", + data: {"domain":domain, "list":list, "token":token, "auditlog":1} + }); +} + +$(document).ready(function() { + + // Pull in data via AJAX + updateTopLists(); + + $("#domain-frequency tbody").on( "click", "button", function () { + var url = ($(this).parents("tr"))[0].innerText.split(" ")[0]; + if($(this).context.innerText === "Blacklist") + { + add(url,"black"); + } + else + { + add(url,"audit"); + } + }); + + $("#ad-frequency tbody").on( "click", "button", function () { + var url = ($(this).parents("tr"))[0].innerText.split(" ")[0].split(" ")[0]; + if($(this).context.innerText === "Whitelist") + { + add(url,"white"); + } + else + { + add(url,"audit"); + } + }); +}); diff --git a/scripts/pi-hole/js/db_lists.js b/scripts/pi-hole/js/db_lists.js new file mode 100644 index 00000000..2bf3bad4 --- /dev/null +++ b/scripts/pi-hole/js/db_lists.js @@ -0,0 +1,190 @@ +/* Pi-hole: A black hole for Internet advertisements +* (c) 2017 Pi-hole, LLC (https://pi-hole.net) +* Network-wide ad blocking via your own hardware. +* +* This file is copyright under the latest version of the EUPL. +* Please see LICENSE file for your rights under this license. */ + +/*global + moment +*/ + +var start__ = moment().subtract(6, "days"); +var from = moment(start__).utc().valueOf()/1000; +var end__ = moment(); +var until = moment(end__).utc().valueOf()/1000; + +$(function () { + // Get first time stamp we have valid data for to limit selectable date/time range + $.getJSON("api_db.php?getMinTimestamp", function(data) { + var minDate = parseInt(data.mintimestamp); + if(!isNaN(minDate)) + { + $("#querytime").data("daterangepicker").minDate = moment.unix(minDate); + } + }); + + $("#querytime").daterangepicker( + { + timePicker: true, timePickerIncrement: 15, + locale: { format: "MMMM Do YYYY, h:mm A" }, + ranges: { + "Today": [moment(), moment()], + "Yesterday": [moment().subtract(1, "days"), moment().subtract(1, "days")], + "Last 7 Days": [moment().subtract(6, "days"), moment()], + "Last 30 Days": [moment().subtract(29, "days"), moment()], + "This Month": [moment().startOf("month"), moment().endOf("month")], + "Last Month": [moment().subtract(1, "month").startOf("month"), moment().subtract(1, "month").endOf("month")] + }, + startDate: start__, endDate: end__, + "opens": "center", "showDropdowns": true + }, + function (startt, endt) { + from = moment(startt).utc().valueOf()/1000; + until = moment(endt).utc().valueOf()/1000; + }); +}); + +// Credit: http://stackoverflow.com/questions/1787322/htmlspecialchars-equivalent-in-javascript/4835406#4835406 +function escapeHtml(text) { + var map = { + "&": "&", + "<": "<", + ">": ">", + "\"": """, + "\'": "'" + }; + + return text.replace(/[&<>"']/g, function(m) { return map[m]; }); +} + +function updateTopClientsChart() { + $("#client-frequency .overlay").show(); + $.getJSON("api_db.php?topClients&from="+from+"&until="+until, function(data) { + + // Clear tables before filling them with data + $("#client-frequency td").parent().remove(); + var clienttable = $("#client-frequency").find("tbody:last"); + var client, percentage, clientname, clientip; + var sum = 0; + for (client in data.top_sources) { + if ({}.hasOwnProperty.call(data.top_sources, client)){ + sum += data.top_sources[client]; + } + } + + for (client in data.top_sources) { + + if ({}.hasOwnProperty.call(data.top_sources, client)){ + // Sanitize client + client = escapeHtml(client); + if(escapeHtml(client) !== client) + { + // Make a copy with the escaped index if necessary + data.top_sources[escapeHtml(client)] = data.top_sources[client]; + } + if(client.indexOf("|") > -1) + { + var idx = client.indexOf("|"); + clientname = client.substr(0, idx); + clientip = client.substr(idx+1, client.length-idx); + } + else + { + clientname = client; + clientip = client; + } + + var url = clientname; + percentage = data.top_sources[client] / sum * 100.0; + clienttable.append(" " + url + + " " + data.top_sources[client] + "
"); + } + + } + + $("#client-frequency .overlay").hide(); + }); +} + +function updateTopDomainsChart() { + $("#domain-frequency .overlay").show(); + $.getJSON("api_db.php?topDomains&from="+from+"&until="+until, function(data) { + + // Clear tables before filling them with data + $("#domain-frequency td").parent().remove(); + var domaintable = $("#domain-frequency").find("tbody:last"); + var domain, percentage; + var sum = 0; + for (domain in data.top_domains) { + if ({}.hasOwnProperty.call(data.top_domains, domain)){ + sum += data.top_domains[domain]; + } + } + + for (domain in data.top_domains) { + + if ({}.hasOwnProperty.call(data.top_domains, domain)){ + // Sanitize domain + domain = escapeHtml(domain); + if(escapeHtml(domain) !== domain) + { + // Make a copy with the escaped index if necessary + data.top_domains[escapeHtml(domain)] = data.top_domains[domain]; + } + + percentage = data.top_domains[domain] / sum * 100.0; + domaintable.append(" " + domain + + " " + data.top_domains[domain] + "
"); + } + + } + + $("#domain-frequency .overlay").hide(); + }); +} + +function updateTopAdsChart() { + $("#ad-frequency .overlay").show(); + $.getJSON("api_db.php?topAds&from="+from+"&until="+until, function(data) { + + // Clear tables before filling them with data + $("#ad-frequency td").parent().remove(); + var adtable = $("#ad-frequency").find("tbody:last"); + var ad, percentage; + var sum = 0; + for (ad in data.top_ads) { + if ({}.hasOwnProperty.call(data.top_ads, ad)){ + sum += data.top_ads[ad]; + } + } + + for (ad in data.top_ads) { + + if ({}.hasOwnProperty.call(data.top_ads, ad)){ + // Sanitize ad + ad = escapeHtml(ad); + if(escapeHtml(ad) !== ad) + { + // Make a copy with the escaped index if necessary + data.top_ads[escapeHtml(ad)] = data.top_ads[ad]; + } + + percentage = data.top_ads[ad] / sum * 100.0; + adtable.append(" " + ad + " " + data.top_ads[ad] + "
"); + } + + } + + $("#ad-frequency .overlay").hide(); + }); +} + +// Handle "Go" Button +$("#btnGo").on("click", function() { + updateTopClientsChart(); + updateTopDomainsChart(); + updateTopAdsChart(); +}); diff --git a/scripts/pi-hole/js/db_queries.js b/scripts/pi-hole/js/db_queries.js new file mode 100644 index 00000000..e2d57ab7 --- /dev/null +++ b/scripts/pi-hole/js/db_queries.js @@ -0,0 +1,252 @@ +/* Pi-hole: A black hole for Internet advertisements +* (c) 2017 Pi-hole, LLC (https://pi-hole.net) +* Network-wide ad blocking via your own hardware. +* +* This file is copyright under the latest version of the EUPL. +* Please see LICENSE file for your rights under this license. */ + +/*global + moment +*/ + +var start__ = moment().subtract(6, "days"); +var from = moment(start__).utc().valueOf()/1000; +var end__ = moment(); +var until = moment(end__).utc().valueOf()/1000; + +$(function () { + // Get first time stamp we have valid data for to limit selectable date/time range + $.getJSON("api_db.php?getMinTimestamp", function(data) { + var minDate = parseInt(data.mintimestamp); + if(!isNaN(minDate)) + { + $("#querytime").data("daterangepicker").minDate = moment.unix(minDate); + } + }); + + $("#querytime").daterangepicker( + { + timePicker: true, timePickerIncrement: 15, + locale: { format: "MMMM Do YYYY, h:mm A" }, + ranges: { + "Today": [moment(), moment()], + "Yesterday": [moment().subtract(1, "days"), moment().subtract(1, "days")], + "Last 7 Days": [moment().subtract(6, "days"), moment()], + "Last 30 Days": [moment().subtract(29, "days"), moment()], + "This Month": [moment().startOf("month"), moment().endOf("month")], + "Last Month": [moment().subtract(1, "month").startOf("month"), moment().subtract(1, "month").endOf("month")] + }, + startDate: start__, endDate: end__, + "opens": "center", "showDropdowns": true + }, + function (startt, endt) { + from = moment(startt).utc().valueOf()/1000; + until = moment(endt).utc().valueOf()/1000; + }); +}); + +var tableApi, statistics; + +function escapeRegex(text) { + var map = { + "(": "\\(", + ")": "\\)", + ".": "\\.", + }; + return text.replace(/[().]/g, function(m) { return map[m]; }); +} + +function add(domain,list) { + var token = $("#token").html(); + var alInfo = $("#alInfo"); + var alList = $("#alList"); + var alDomain = $("#alDomain"); + alDomain.html(domain); + var alSuccess = $("#alSuccess"); + var alFailure = $("#alFailure"); + var err = $("#err"); + + if(list === "white") + { + alList.html("Whitelist"); + } + else + { + alList.html("Blacklist"); + } + + alInfo.show(); + alSuccess.hide(); + alFailure.hide(); + $.ajax({ + url: "scripts/pi-hole/php/add.php", + method: "post", + data: {"domain":domain, "list":list, "token":token}, + success: function(response) { + if (response.indexOf("not a valid argument") >= 0 || response.indexOf("is not a valid domain") >= 0) + { + alFailure.show(); + err.html(response); + alFailure.delay(4000).fadeOut(2000, function() { alFailure.hide(); }); + } + else + { + alSuccess.show(); + alSuccess.delay(1000).fadeOut(2000, function() { alSuccess.hide(); }); + } + alInfo.delay(1000).fadeOut(2000, function() { + alInfo.hide(); + alList.html(""); + alDomain.html(""); + }); + }, + error: function(jqXHR, exception) { + alFailure.show(); + err.html(""); + alFailure.delay(1000).fadeOut(2000, function() { + alFailure.hide(); + }); + alInfo.delay(1000).fadeOut(2000, function() { + alInfo.hide(); + alList.html(""); + alDomain.html(""); + }); + } + }); +} +function handleAjaxError( xhr, textStatus, error ) { + if ( textStatus === "timeout" ) + { + alert( "The server took too long to send the data." ); + } + else if(xhr.responseText.indexOf("Connection refused") >= 0) + { + alert( "An error occured while loading the data: Connection refused. Is FTL running?" ); + } + else + { + alert( "An unknown error occured while loading the data.\n"+xhr.responseText ); + } + $("#all-queries_processing").hide(); + tableApi.clear(); + tableApi.draw(); +} + +var reloadCallback = function() +{ + statistics = [0,0,0,0]; + var data = tableApi.rows().data(); + for (var i = 0; i < data.length; i++) { + statistics[0]++; + if(data[i][4] === 1) + { + statistics[2]++; + } + else if(data[i][4] === 3) + { + statistics[1]++; + } + else if(data[i][4] === 4) + { + statistics[3]++; + } + } + $("h3#dns_queries").text(statistics[0]); + $("h3#ads_blocked_exact").text(statistics[2]); + $("h3#ads_wildcard_blocked").text(statistics[3]); + + var percent = 0.0; + if(statistics[2] + statistics[3] > 0) + { + percent = 100.0*(statistics[2] + statistics[3]) / statistics[0]; + } + $("h3#ads_percentage_today").text(parseFloat(percent).toFixed(1)+" %"); +}; + +function refreshTableData() { + var APIstring = "api_db.php?getAllQueries&from="+from+"&until="+until; + statistics = [0,0,0]; + tableApi.ajax.url(APIstring).load(reloadCallback); +} + +$(document).ready(function() { + var status; + + tableApi = $("#all-queries").DataTable( { + "rowCallback": function( row, data, index ){ + if (data[4] === 1) + { + $(row).css("color","red"); + $("td:eq(4)", row).html( "Pi-holed" ); + $("td:eq(5)", row).html( "" ); + // statistics[2]++; + } + else if (data[4] === 2) + { + $(row).css("color","green"); + $("td:eq(4)", row).html( "OK (forwarded)" ); + $("td:eq(5)", row).html( "" ); + } + else if (data[4] === 3) + { + $(row).css("color","green"); + $("td:eq(4)", row).html( "OK (cached)" ); + $("td:eq(5)", row).html( "" ); + // statistics[1]++; + + } + else if (data[4] === 4) + { + $(row).css("color","red"); + $("td:eq(4)", row).html( "Pi-holed (wildcard)" ); + $("td:eq(5)", row).html( "" ); + // statistics[3]++; + } + else + { + $("td:eq(4)", row).html( "Unknown ("+data[4]+")" ); + $("td:eq(5)", row).html( "" ); + } + // statistics[0]++; + }, + dom: "<'row'<'col-sm-12'f>>" + + "<'row'<'col-sm-4'l><'col-sm-8'p>>" + + "<'row'<'col-sm-12'tr>>" + + "<'row'<'col-sm-5'i><'col-sm-7'p>>", + "ajax": {"url": "api_db.php?getAllQueries=empty", "error": handleAjaxError }, + "autoWidth" : false, + "processing": true, + "deferRender": true, + "order" : [[0, "desc"]], + "columns": [ + { "width" : "20%", "render": function (data, type, full, meta) { if(type === "display"){return moment.unix(data).format("Y-MM-DD HH:mm:ss z");}else{return data;} }}, + { "width" : "10%" }, + { "width" : "40%" }, + { "width" : "10%" }, + { "width" : "10%" }, + { "width" : "10%" }, + ], + "lengthMenu": [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]], + "columnDefs": [ { + "targets": -1, + "data": null, + "defaultContent": "" + } ] + }); + $("#all-queries tbody").on( "click", "button", function () { + var data = tableApi.row( $(this).parents("tr") ).data(); + if (data[4] === "1") + { + add(data[2],"white"); + } + else + { + add(data[2],"black"); + } + } ); +} ); + +// Handle "Go" Button +$("#btnGo").on("click", function() { + refreshTableData(); +}); diff --git a/scripts/pi-hole/js/index.js b/scripts/pi-hole/js/index.js index 7689ccbf..4b8acb28 100644 --- a/scripts/pi-hole/js/index.js +++ b/scripts/pi-hole/js/index.js @@ -410,7 +410,7 @@ function updateSummaryData(runOnce) { var todayElement = $("span#" + today); todayElement.text() !== data[today] && todayElement.text() !== data[today] + "%" && - $("h3#" + today).addClass("glow"); + $("span#" + today).addClass("glow"); }); window.setTimeout(function() { @@ -418,7 +418,7 @@ function updateSummaryData(runOnce) { var textData = (idx === 3 && data[header] !== "to") ? data[header] + "%" : data[header]; $("span#" + header).text(textData); }); - $("h3.statistic.glow").removeClass("glow"); + $("span.glow").removeClass("glow"); }, 500); }).done(function() { diff --git a/scripts/pi-hole/js/queries.js b/scripts/pi-hole/js/queries.js index 43952e47..54c3ea71 100644 --- a/scripts/pi-hole/js/queries.js +++ b/scripts/pi-hole/js/queries.js @@ -175,8 +175,8 @@ $(document).ready(function() { "columns": [ { "width" : "20%", "render": function (data, type, full, meta) { if(type === "display"){return moment.unix(data).format("Y-MM-DD HH:mm:ss z");}else{return data;} }}, { "width" : "10%" }, - { "width" : "40%" }, - { "width" : "10%" }, + { "width" : "40%", "render": $.fn.dataTable.render.text() }, + { "width" : "10%", "render": $.fn.dataTable.render.text() }, { "width" : "10%" }, { "width" : "10%" }, ], diff --git a/scripts/pi-hole/php/add.php b/scripts/pi-hole/php/add.php index d4ca64db..a19aae7a 100644 --- a/scripts/pi-hole/php/add.php +++ b/scripts/pi-hole/php/add.php @@ -16,13 +16,33 @@ list_verify($type); switch($type) { case "white": - echo exec("sudo pihole -w -q ${_POST['domain']}"); + if(!isset($_POST["auditlog"])) + echo exec("sudo pihole -w -q ${_POST['domain']}"); + else + { + echo exec("sudo pihole -w -q -n ${_POST['domain']}"); + echo exec("sudo pihole -a audit ${_POST['domain']}"); + } break; case "black": - echo exec("sudo pihole -b -q ${_POST['domain']}"); + if(!isset($_POST["auditlog"])) + echo exec("sudo pihole -b -q ${_POST['domain']}"); + else + { + echo exec("sudo pihole -b -q -n ${_POST['domain']}"); + echo exec("sudo pihole -a audit ${_POST['domain']}"); + } break; case "wild": - echo exec("sudo pihole -wild -q ${_POST['domain']}"); + if(!isset($_POST["auditlog"])) + echo exec("sudo pihole -wild -q ${_POST['domain']}"); + else + { + echo exec("sudo pihole -wild -q -n ${_POST['domain']}"); + echo exec("sudo pihole -a audit ${_POST['domain']}"); + } + case "audit": + echo exec("sudo pihole -a audit ${_POST['domain']}"); break; } diff --git a/scripts/pi-hole/php/func.php b/scripts/pi-hole/php/func.php index 4453d99d..22840bdb 100644 --- a/scripts/pi-hole/php/func.php +++ b/scripts/pi-hole/php/func.php @@ -8,9 +8,9 @@ function is_valid_domain_name($domain_name) { - return (preg_match("/^([a-z\d](-*[a-z\d])*)(\.([a-z\d](-*[a-z\d])*))*$/i", $domain_name) && //valid chars check - preg_match("/^.{1,253}$/", $domain_name) && //overall length check - preg_match("/^[^\.]{1,63}(\.[^\.]{1,63})*$/", $domain_name)); //length of each label + return (preg_match("/^((-|_)*[a-z\d]((-|_)*[a-z\d])*(-|_)*)(\.(-|_)*([a-z\d]((-|_)*[a-z\d])*))*$/i", $domain_name) // Valid chars check + preg_match("/^.{1,253}$/", $domain_name) && // Overall length check + preg_match("/^[^\.]{1,63}(\.[^\.]{1,63})*$/", $domain_name)); // Length of each label } function checkfile($filename) { diff --git a/scripts/pi-hole/php/header.php b/scripts/pi-hole/php/header.php index cb9eecf5..0bef5c26 100644 --- a/scripts/pi-hole/php/header.php +++ b/scripts/pi-hole/php/header.php @@ -184,6 +184,7 @@ + @@ -191,7 +192,7 @@ class="active"> @@ -470,7 +491,7 @@ if($auth) { Enable    -
  • active"> +
  • active"> Tools @@ -490,6 +511,12 @@ if($auth) { Query adlists
  • + + class="active"> + + Audit log + + class="active"> diff --git a/scripts/pi-hole/php/queryads.php b/scripts/pi-hole/php/queryads.php index 457e235c..171e5d52 100644 --- a/scripts/pi-hole/php/queryads.php +++ b/scripts/pi-hole/php/queryads.php @@ -22,9 +22,9 @@ function echoEvent($datatext) { // Credit: http://stackoverflow.com/a/4694816/2087442 function is_valid_domain_name($domain_name) { - return (preg_match("/^([a-z\d](-*[a-z\d])*)(\.([a-z\d](-*[a-z\d])*))*$/i", $domain_name) //valid chars check - && preg_match("/^.{1,253}$/", $domain_name) //overall length check - && preg_match("/^[^\.]{1,63}(\.[^\.]{1,63})*$/", $domain_name) ); //length of each label + return (preg_match("/^((-|_)*[a-z\d]((-|_)*[a-z\d])*(-|_)*)(\.(-|_)*([a-z\d]((-|_)*[a-z\d])*))*$/i", $domain_name) // Valid chars check + && preg_match("/^.{1,253}$/", $domain_name) // Overall length check + && preg_match("/^[^\.]{1,63}(\.[^\.]{1,63})*$/", $domain_name) ); // Length of each label } // Test if domain is set diff --git a/scripts/vendor/daterangepicker.js b/scripts/vendor/daterangepicker.js new file mode 100644 index 00000000..be710052 --- /dev/null +++ b/scripts/vendor/daterangepicker.js @@ -0,0 +1,1542 @@ +/** +* @version: 2.1.19 +* @author: Dan Grossman http://www.dangrossman.info/ +* @copyright: Copyright (c) 2012-2015 Dan Grossman. All rights reserved. +* @license: Licensed under the MIT license. See http://www.opensource.org/licenses/mit-license.php +* @website: https://www.improvely.com/ +*/ + +(function(root, factory) { + + if (typeof define === 'function' && define.amd) { + define(['moment', 'jquery', 'exports'], function(momentjs, $, exports) { + root.daterangepicker = factory(root, exports, momentjs, $); + }); + + } else if (typeof exports !== 'undefined') { + var momentjs = require('moment'); + var jQuery = (typeof window != 'undefined') ? window.jQuery : undefined; //isomorphic issue + if (!jQuery) { + try { + jQuery = require('jquery'); + if (!jQuery.fn) jQuery.fn = {}; //isomorphic issue + } catch (err) { + if (!jQuery) throw new Error('jQuery dependency not found'); + } + } + + factory(root, exports, momentjs, jQuery); + + // Finally, as a browser global. + } else { + root.daterangepicker = factory(root, {}, root.moment || moment, (root.jQuery || root.Zepto || root.ender || root.$)); + } + +}(this || {}, function(root, daterangepicker, moment, $) { // 'this' doesn't exist on a server + + var DateRangePicker = function(element, options, cb) { + + //default settings for options + this.parentEl = 'body'; + this.element = $(element); + this.startDate = moment().startOf('day'); + this.endDate = moment().endOf('day'); + this.minDate = false; + this.maxDate = false; + this.dateLimit = false; + this.autoApply = false; + this.singleDatePicker = false; + this.showDropdowns = false; + this.showWeekNumbers = false; + this.showISOWeekNumbers = false; + this.timePicker = false; + this.timePicker24Hour = false; + this.timePickerIncrement = 1; + this.timePickerSeconds = false; + this.linkedCalendars = true; + this.autoUpdateInput = true; + this.alwaysShowCalendars = false; + this.ranges = {}; + + this.opens = 'right'; + if (this.element.hasClass('pull-right')) + this.opens = 'left'; + + this.drops = 'down'; + if (this.element.hasClass('dropup')) + this.drops = 'up'; + + this.buttonClasses = 'btn btn-sm'; + this.applyClass = 'btn-success'; + this.cancelClass = 'btn-default'; + + this.locale = { + format: 'MM/DD/YYYY', + separator: ' - ', + applyLabel: 'Apply', + cancelLabel: 'Cancel', + weekLabel: 'W', + customRangeLabel: 'Custom Range', + daysOfWeek: moment.weekdaysMin(), + monthNames: moment.monthsShort(), + firstDay: moment.localeData().firstDayOfWeek() + }; + + this.callback = function() { }; + + //some state information + this.isShowing = false; + this.leftCalendar = {}; + this.rightCalendar = {}; + + //custom options from user + if (typeof options !== 'object' || options === null) + options = {}; + + //allow setting options with data attributes + //data-api options will be overwritten with custom javascript options + options = $.extend(this.element.data(), options); + + //html template for the picker UI + if (typeof options.template !== 'string' && !(options.template instanceof $)) + options.template = ''; + + this.parentEl = (options.parentEl && $(options.parentEl).length) ? $(options.parentEl) : $(this.parentEl); + this.container = $(options.template).appendTo(this.parentEl); + + // + // handle all the possible options overriding defaults + // + + if (typeof options.locale === 'object') { + + if (typeof options.locale.format === 'string') + this.locale.format = options.locale.format; + + if (typeof options.locale.separator === 'string') + this.locale.separator = options.locale.separator; + + if (typeof options.locale.daysOfWeek === 'object') + this.locale.daysOfWeek = options.locale.daysOfWeek.slice(); + + if (typeof options.locale.monthNames === 'object') + this.locale.monthNames = options.locale.monthNames.slice(); + + if (typeof options.locale.firstDay === 'number') + this.locale.firstDay = options.locale.firstDay; + + if (typeof options.locale.applyLabel === 'string') + this.locale.applyLabel = options.locale.applyLabel; + + if (typeof options.locale.cancelLabel === 'string') + this.locale.cancelLabel = options.locale.cancelLabel; + + if (typeof options.locale.weekLabel === 'string') + this.locale.weekLabel = options.locale.weekLabel; + + if (typeof options.locale.customRangeLabel === 'string') + this.locale.customRangeLabel = options.locale.customRangeLabel; + + } + + if (typeof options.startDate === 'string') + this.startDate = moment(options.startDate, this.locale.format); + + if (typeof options.endDate === 'string') + this.endDate = moment(options.endDate, this.locale.format); + + if (typeof options.minDate === 'string') + this.minDate = moment(options.minDate, this.locale.format); + + if (typeof options.maxDate === 'string') + this.maxDate = moment(options.maxDate, this.locale.format); + + if (typeof options.startDate === 'object') + this.startDate = moment(options.startDate); + + if (typeof options.endDate === 'object') + this.endDate = moment(options.endDate); + + if (typeof options.minDate === 'object') + this.minDate = moment(options.minDate); + + if (typeof options.maxDate === 'object') + this.maxDate = moment(options.maxDate); + + // sanity check for bad options + if (this.minDate && this.startDate.isBefore(this.minDate)) + this.startDate = this.minDate.clone(); + + // sanity check for bad options + if (this.maxDate && this.endDate.isAfter(this.maxDate)) + this.endDate = this.maxDate.clone(); + + if (typeof options.applyClass === 'string') + this.applyClass = options.applyClass; + + if (typeof options.cancelClass === 'string') + this.cancelClass = options.cancelClass; + + if (typeof options.dateLimit === 'object') + this.dateLimit = options.dateLimit; + + if (typeof options.opens === 'string') + this.opens = options.opens; + + if (typeof options.drops === 'string') + this.drops = options.drops; + + if (typeof options.showWeekNumbers === 'boolean') + this.showWeekNumbers = options.showWeekNumbers; + + if (typeof options.showISOWeekNumbers === 'boolean') + this.showISOWeekNumbers = options.showISOWeekNumbers; + + if (typeof options.buttonClasses === 'string') + this.buttonClasses = options.buttonClasses; + + if (typeof options.buttonClasses === 'object') + this.buttonClasses = options.buttonClasses.join(' '); + + if (typeof options.showDropdowns === 'boolean') + this.showDropdowns = options.showDropdowns; + + if (typeof options.singleDatePicker === 'boolean') { + this.singleDatePicker = options.singleDatePicker; + if (this.singleDatePicker) + this.endDate = this.startDate.clone(); + } + + if (typeof options.timePicker === 'boolean') + this.timePicker = options.timePicker; + + if (typeof options.timePickerSeconds === 'boolean') + this.timePickerSeconds = options.timePickerSeconds; + + if (typeof options.timePickerIncrement === 'number') + this.timePickerIncrement = options.timePickerIncrement; + + if (typeof options.timePicker24Hour === 'boolean') + this.timePicker24Hour = options.timePicker24Hour; + + if (typeof options.autoApply === 'boolean') + this.autoApply = options.autoApply; + + if (typeof options.autoUpdateInput === 'boolean') + this.autoUpdateInput = options.autoUpdateInput; + + if (typeof options.linkedCalendars === 'boolean') + this.linkedCalendars = options.linkedCalendars; + + if (typeof options.isInvalidDate === 'function') + this.isInvalidDate = options.isInvalidDate; + + if (typeof options.alwaysShowCalendars === 'boolean') + this.alwaysShowCalendars = options.alwaysShowCalendars; + + // update day names order to firstDay + if (this.locale.firstDay != 0) { + var iterator = this.locale.firstDay; + while (iterator > 0) { + this.locale.daysOfWeek.push(this.locale.daysOfWeek.shift()); + iterator--; + } + } + + var start, end, range; + + //if no start/end dates set, check if an input element contains initial values + if (typeof options.startDate === 'undefined' && typeof options.endDate === 'undefined') { + if ($(this.element).is('input[type=text]')) { + var val = $(this.element).val(), + split = val.split(this.locale.separator); + + start = end = null; + + if (split.length == 2) { + start = moment(split[0], this.locale.format); + end = moment(split[1], this.locale.format); + } else if (this.singleDatePicker && val !== "") { + start = moment(val, this.locale.format); + end = moment(val, this.locale.format); + } + if (start !== null && end !== null) { + this.setStartDate(start); + this.setEndDate(end); + } + } + } + + if (typeof options.ranges === 'object') { + for (range in options.ranges) { + + if (typeof options.ranges[range][0] === 'string') + start = moment(options.ranges[range][0], this.locale.format); + else + start = moment(options.ranges[range][0]); + + if (typeof options.ranges[range][1] === 'string') + end = moment(options.ranges[range][1], this.locale.format); + else + end = moment(options.ranges[range][1]); + + // If the start or end date exceed those allowed by the minDate or dateLimit + // options, shorten the range to the allowable period. + if (this.minDate && start.isBefore(this.minDate)) + start = this.minDate.clone(); + + var maxDate = this.maxDate; + if (this.dateLimit && start.clone().add(this.dateLimit).isAfter(maxDate)) + maxDate = start.clone().add(this.dateLimit); + if (maxDate && end.isAfter(maxDate)) + end = maxDate.clone(); + + // If the end of the range is before the minimum or the start of the range is + // after the maximum, don't display this range option at all. + if ((this.minDate && end.isBefore(this.minDate)) || (maxDate && start.isAfter(maxDate))) + continue; + + //Support unicode chars in the range names. + var elem = document.createElement('textarea'); + elem.innerHTML = range; + var rangeHtml = elem.value; + + this.ranges[rangeHtml] = [start, end]; + } + + var list = '
      '; + for (range in this.ranges) { + list += '
    • ' + range + '
    • '; + } + list += '
    • ' + this.locale.customRangeLabel + '
    • '; + list += '
    '; + this.container.find('.ranges').prepend(list); + } + + if (typeof cb === 'function') { + this.callback = cb; + } + + if (!this.timePicker) { + this.startDate = this.startDate.startOf('day'); + this.endDate = this.endDate.endOf('day'); + this.container.find('.calendar-time').hide(); + } + + //can't be used together for now + if (this.timePicker && this.autoApply) + this.autoApply = false; + + if (this.autoApply && typeof options.ranges !== 'object') { + this.container.find('.ranges').hide(); + } else if (this.autoApply) { + this.container.find('.applyBtn, .cancelBtn').addClass('hide'); + } + + if (this.singleDatePicker) { + this.container.addClass('single'); + this.container.find('.calendar.left').addClass('single'); + this.container.find('.calendar.left').show(); + this.container.find('.calendar.right').hide(); + this.container.find('.daterangepicker_input input, .daterangepicker_input i').hide(); + if (!this.timePicker) { + this.container.find('.ranges').hide(); + } + } + + if ((typeof options.ranges === 'undefined' && !this.singleDatePicker) || this.alwaysShowCalendars) { + this.container.addClass('show-calendar'); + } + + this.container.addClass('opens' + this.opens); + + //swap the position of the predefined ranges if opens right + if (typeof options.ranges !== 'undefined' && this.opens == 'right') { + var ranges = this.container.find('.ranges'); + var html = ranges.clone(); + ranges.remove(); + this.container.find('.calendar.left').parent().prepend(html); + } + + //apply CSS classes and labels to buttons + this.container.find('.applyBtn, .cancelBtn').addClass(this.buttonClasses); + if (this.applyClass.length) + this.container.find('.applyBtn').addClass(this.applyClass); + if (this.cancelClass.length) + this.container.find('.cancelBtn').addClass(this.cancelClass); + this.container.find('.applyBtn').html(this.locale.applyLabel); + this.container.find('.cancelBtn').html(this.locale.cancelLabel); + + // + // event listeners + // + + this.container.find('.calendar') + .on('click.daterangepicker', '.prev', $.proxy(this.clickPrev, this)) + .on('click.daterangepicker', '.next', $.proxy(this.clickNext, this)) + .on('click.daterangepicker', 'td.available', $.proxy(this.clickDate, this)) + .on('mouseenter.daterangepicker', 'td.available', $.proxy(this.hoverDate, this)) + .on('mouseleave.daterangepicker', 'td.available', $.proxy(this.updateFormInputs, this)) + .on('change.daterangepicker', 'select.yearselect', $.proxy(this.monthOrYearChanged, this)) + .on('change.daterangepicker', 'select.monthselect', $.proxy(this.monthOrYearChanged, this)) + .on('change.daterangepicker', 'select.hourselect,select.minuteselect,select.secondselect,select.ampmselect', $.proxy(this.timeChanged, this)) + .on('click.daterangepicker', '.daterangepicker_input input', $.proxy(this.showCalendars, this)) + //.on('keyup.daterangepicker', '.daterangepicker_input input', $.proxy(this.formInputsChanged, this)) + .on('change.daterangepicker', '.daterangepicker_input input', $.proxy(this.formInputsChanged, this)); + + this.container.find('.ranges') + .on('click.daterangepicker', 'button.applyBtn', $.proxy(this.clickApply, this)) + .on('click.daterangepicker', 'button.cancelBtn', $.proxy(this.clickCancel, this)) + .on('click.daterangepicker', 'li', $.proxy(this.clickRange, this)) + .on('mouseenter.daterangepicker', 'li', $.proxy(this.hoverRange, this)) + .on('mouseleave.daterangepicker', 'li', $.proxy(this.updateFormInputs, this)); + + if (this.element.is('input')) { + this.element.on({ + 'click.daterangepicker': $.proxy(this.show, this), + 'focus.daterangepicker': $.proxy(this.show, this), + 'keyup.daterangepicker': $.proxy(this.elementChanged, this), + 'keydown.daterangepicker': $.proxy(this.keydown, this) + }); + } else { + this.element.on('click.daterangepicker', $.proxy(this.toggle, this)); + } + + // + // if attached to a text input, set the initial value + // + + if (this.element.is('input') && !this.singleDatePicker && this.autoUpdateInput) { + this.element.val(this.startDate.format(this.locale.format) + this.locale.separator + this.endDate.format(this.locale.format)); + this.element.trigger('change'); + } else if (this.element.is('input') && this.autoUpdateInput) { + this.element.val(this.startDate.format(this.locale.format)); + this.element.trigger('change'); + } + + }; + + DateRangePicker.prototype = { + + constructor: DateRangePicker, + + setStartDate: function(startDate) { + if (typeof startDate === 'string') + this.startDate = moment(startDate, this.locale.format); + + if (typeof startDate === 'object') + this.startDate = moment(startDate); + + if (!this.timePicker) + this.startDate = this.startDate.startOf('day'); + + if (this.timePicker && this.timePickerIncrement) + this.startDate.minute(Math.round(this.startDate.minute() / this.timePickerIncrement) * this.timePickerIncrement); + + if (this.minDate && this.startDate.isBefore(this.minDate)) + this.startDate = this.minDate; + + if (this.maxDate && this.startDate.isAfter(this.maxDate)) + this.startDate = this.maxDate; + + if (!this.isShowing) + this.updateElement(); + + this.updateMonthsInView(); + }, + + setEndDate: function(endDate) { + if (typeof endDate === 'string') + this.endDate = moment(endDate, this.locale.format); + + if (typeof endDate === 'object') + this.endDate = moment(endDate); + + if (!this.timePicker) + this.endDate = this.endDate.endOf('day'); + + if (this.timePicker && this.timePickerIncrement) + this.endDate.minute(Math.round(this.endDate.minute() / this.timePickerIncrement) * this.timePickerIncrement); + + if (this.endDate.isBefore(this.startDate)) + this.endDate = this.startDate.clone(); + + if (this.maxDate && this.endDate.isAfter(this.maxDate)) + this.endDate = this.maxDate; + + if (this.dateLimit && this.startDate.clone().add(this.dateLimit).isBefore(this.endDate)) + this.endDate = this.startDate.clone().add(this.dateLimit); + + this.previousRightTime = this.endDate.clone(); + + if (!this.isShowing) + this.updateElement(); + + this.updateMonthsInView(); + }, + + isInvalidDate: function() { + return false; + }, + + updateView: function() { + if (this.timePicker) { + this.renderTimePicker('left'); + this.renderTimePicker('right'); + if (!this.endDate) { + this.container.find('.right .calendar-time select').attr('disabled', 'disabled').addClass('disabled'); + } else { + this.container.find('.right .calendar-time select').removeAttr('disabled').removeClass('disabled'); + } + } + if (this.endDate) { + this.container.find('input[name="daterangepicker_end"]').removeClass('active'); + this.container.find('input[name="daterangepicker_start"]').addClass('active'); + } else { + this.container.find('input[name="daterangepicker_end"]').addClass('active'); + this.container.find('input[name="daterangepicker_start"]').removeClass('active'); + } + this.updateMonthsInView(); + this.updateCalendars(); + this.updateFormInputs(); + }, + + updateMonthsInView: function() { + if (this.endDate) { + + //if both dates are visible already, do nothing + if (!this.singleDatePicker && this.leftCalendar.month && this.rightCalendar.month && + (this.startDate.format('YYYY-MM') == this.leftCalendar.month.format('YYYY-MM') || this.startDate.format('YYYY-MM') == this.rightCalendar.month.format('YYYY-MM')) + && + (this.endDate.format('YYYY-MM') == this.leftCalendar.month.format('YYYY-MM') || this.endDate.format('YYYY-MM') == this.rightCalendar.month.format('YYYY-MM')) + ) { + return; + } + + this.leftCalendar.month = this.startDate.clone().date(2); + if (!this.linkedCalendars && (this.endDate.month() != this.startDate.month() || this.endDate.year() != this.startDate.year())) { + this.rightCalendar.month = this.endDate.clone().date(2); + } else { + this.rightCalendar.month = this.startDate.clone().date(2).add(1, 'month'); + } + + } else { + if (this.leftCalendar.month.format('YYYY-MM') != this.startDate.format('YYYY-MM') && this.rightCalendar.month.format('YYYY-MM') != this.startDate.format('YYYY-MM')) { + this.leftCalendar.month = this.startDate.clone().date(2); + this.rightCalendar.month = this.startDate.clone().date(2).add(1, 'month'); + } + } + }, + + updateCalendars: function() { + + if (this.timePicker) { + var hour, minute, second; + if (this.endDate) { + hour = parseInt(this.container.find('.left .hourselect').val(), 10); + minute = parseInt(this.container.find('.left .minuteselect').val(), 10); + second = this.timePickerSeconds ? parseInt(this.container.find('.left .secondselect').val(), 10) : 0; + if (!this.timePicker24Hour) { + var ampm = this.container.find('.left .ampmselect').val(); + if (ampm === 'PM' && hour < 12) + hour += 12; + if (ampm === 'AM' && hour === 12) + hour = 0; + } + } else { + hour = parseInt(this.container.find('.right .hourselect').val(), 10); + minute = parseInt(this.container.find('.right .minuteselect').val(), 10); + second = this.timePickerSeconds ? parseInt(this.container.find('.right .secondselect').val(), 10) : 0; + if (!this.timePicker24Hour) { + var ampm = this.container.find('.right .ampmselect').val(); + if (ampm === 'PM' && hour < 12) + hour += 12; + if (ampm === 'AM' && hour === 12) + hour = 0; + } + } + this.leftCalendar.month.hour(hour).minute(minute).second(second); + this.rightCalendar.month.hour(hour).minute(minute).second(second); + } + + this.renderCalendar('left'); + this.renderCalendar('right'); + + //highlight any predefined range matching the current start and end dates + this.container.find('.ranges li').removeClass('active'); + if (this.endDate == null) return; + + this.calculateChosenLabel(); + }, + + renderCalendar: function(side) { + + // + // Build the matrix of dates that will populate the calendar + // + + var calendar = side == 'left' ? this.leftCalendar : this.rightCalendar; + var month = calendar.month.month(); + var year = calendar.month.year(); + var hour = calendar.month.hour(); + var minute = calendar.month.minute(); + var second = calendar.month.second(); + var daysInMonth = moment([year, month]).daysInMonth(); + var firstDay = moment([year, month, 1]); + var lastDay = moment([year, month, daysInMonth]); + var lastMonth = moment(firstDay).subtract(1, 'month').month(); + var lastYear = moment(firstDay).subtract(1, 'month').year(); + var daysInLastMonth = moment([lastYear, lastMonth]).daysInMonth(); + var dayOfWeek = firstDay.day(); + + //initialize a 6 rows x 7 columns array for the calendar + var calendar = []; + calendar.firstDay = firstDay; + calendar.lastDay = lastDay; + + for (var i = 0; i < 6; i++) { + calendar[i] = []; + } + + //populate the calendar with date objects + var startDay = daysInLastMonth - dayOfWeek + this.locale.firstDay + 1; + if (startDay > daysInLastMonth) + startDay -= 7; + + if (dayOfWeek == this.locale.firstDay) + startDay = daysInLastMonth - 6; + + var curDate = moment([lastYear, lastMonth, startDay, 12, minute, second]); + + var col, row; + for (var i = 0, col = 0, row = 0; i < 42; i++, col++, curDate = moment(curDate).add(24, 'hour')) { + if (i > 0 && col % 7 === 0) { + col = 0; + row++; + } + calendar[row][col] = curDate.clone().hour(hour).minute(minute).second(second); + curDate.hour(12); + + if (this.minDate && calendar[row][col].format('YYYY-MM-DD') == this.minDate.format('YYYY-MM-DD') && calendar[row][col].isBefore(this.minDate) && side == 'left') { + calendar[row][col] = this.minDate.clone(); + } + + if (this.maxDate && calendar[row][col].format('YYYY-MM-DD') == this.maxDate.format('YYYY-MM-DD') && calendar[row][col].isAfter(this.maxDate) && side == 'right') { + calendar[row][col] = this.maxDate.clone(); + } + + } + + //make the calendar object available to hoverDate/clickDate + if (side == 'left') { + this.leftCalendar.calendar = calendar; + } else { + this.rightCalendar.calendar = calendar; + } + + // + // Display the calendar + // + + var minDate = side == 'left' ? this.minDate : this.startDate; + var maxDate = this.maxDate; + var selected = side == 'left' ? this.startDate : this.endDate; + + var html = ''; + html += ''; + html += ''; + + // add empty cell for week number + if (this.showWeekNumbers || this.showISOWeekNumbers) + html += ''; + + if ((!minDate || minDate.isBefore(calendar.firstDay)) && (!this.linkedCalendars || side == 'left')) { + html += ''; + } else { + html += ''; + } + + var dateHtml = this.locale.monthNames[calendar[1][1].month()] + calendar[1][1].format(" YYYY"); + + if (this.showDropdowns) { + var currentMonth = calendar[1][1].month(); + var currentYear = calendar[1][1].year(); + var maxYear = (maxDate && maxDate.year()) || (currentYear + 5); + var minYear = (minDate && minDate.year()) || (currentYear - 50); + var inMinYear = currentYear == minYear; + var inMaxYear = currentYear == maxYear; + + var monthHtml = '"; + + var yearHtml = ''; + + dateHtml = monthHtml + yearHtml; + } + + html += ''; + if ((!maxDate || maxDate.isAfter(calendar.lastDay)) && (!this.linkedCalendars || side == 'right' || this.singleDatePicker)) { + html += ''; + } else { + html += ''; + } + + html += ''; + html += ''; + + // add week number label + if (this.showWeekNumbers || this.showISOWeekNumbers) + html += ''; + + $.each(this.locale.daysOfWeek, function(index, dayOfWeek) { + html += ''; + }); + + html += ''; + html += ''; + html += ''; + + //adjust maxDate to reflect the dateLimit setting in order to + //grey out end dates beyond the dateLimit + if (this.endDate == null && this.dateLimit) { + var maxLimit = this.startDate.clone().add(this.dateLimit).endOf('day'); + if (!maxDate || maxLimit.isBefore(maxDate)) { + maxDate = maxLimit; + } + } + + for (var row = 0; row < 6; row++) { + html += ''; + + // add week number + if (this.showWeekNumbers) + html += ''; + else if (this.showISOWeekNumbers) + html += ''; + + for (var col = 0; col < 7; col++) { + + var classes = []; + + //highlight today's date + if (calendar[row][col].isSame(new Date(), "day")) + classes.push('today'); + + //highlight weekends + if (calendar[row][col].isoWeekday() > 5) + classes.push('weekend'); + + //grey out the dates in other months displayed at beginning and end of this calendar + if (calendar[row][col].month() != calendar[1][1].month()) + classes.push('off'); + + //don't allow selection of dates before the minimum date + if (this.minDate && calendar[row][col].isBefore(this.minDate, 'day')) + classes.push('off', 'disabled'); + + //don't allow selection of dates after the maximum date + if (maxDate && calendar[row][col].isAfter(maxDate, 'day')) + classes.push('off', 'disabled'); + + //don't allow selection of date if a custom function decides it's invalid + if (this.isInvalidDate(calendar[row][col])) + classes.push('off', 'disabled'); + + //highlight the currently selected start date + if (calendar[row][col].format('YYYY-MM-DD') == this.startDate.format('YYYY-MM-DD')) + classes.push('active', 'start-date'); + + //highlight the currently selected end date + if (this.endDate != null && calendar[row][col].format('YYYY-MM-DD') == this.endDate.format('YYYY-MM-DD')) + classes.push('active', 'end-date'); + + //highlight dates in-between the selected dates + if (this.endDate != null && calendar[row][col] > this.startDate && calendar[row][col] < this.endDate) + classes.push('in-range'); + + var cname = '', disabled = false; + for (var i = 0; i < classes.length; i++) { + cname += classes[i] + ' '; + if (classes[i] == 'disabled') + disabled = true; + } + if (!disabled) + cname += 'available'; + + html += ''; + + } + html += ''; + } + + html += ''; + html += '
    ' + dateHtml + '
    ' + this.locale.weekLabel + '' + dayOfWeek + '
    ' + calendar[row][0].week() + '' + calendar[row][0].isoWeek() + '' + calendar[row][col].date() + '
    '; + + this.container.find('.calendar.' + side + ' .calendar-table').html(html); + + }, + + renderTimePicker: function(side) { + + var html, selected, minDate, maxDate = this.maxDate; + + if (this.dateLimit && (!this.maxDate || this.startDate.clone().add(this.dateLimit).isAfter(this.maxDate))) + maxDate = this.startDate.clone().add(this.dateLimit); + + if (side == 'left') { + selected = this.startDate.clone(); + minDate = this.minDate; + } else if (side == 'right') { + selected = this.endDate ? this.endDate.clone() : this.previousRightTime.clone(); + minDate = this.startDate; + + //Preserve the time already selected + var timeSelector = this.container.find('.calendar.right .calendar-time div'); + if (timeSelector.html() != '') { + + selected.hour(timeSelector.find('.hourselect option:selected').val() || selected.hour()); + selected.minute(timeSelector.find('.minuteselect option:selected').val() || selected.minute()); + selected.second(timeSelector.find('.secondselect option:selected').val() || selected.second()); + + if (!this.timePicker24Hour) { + var ampm = timeSelector.find('.ampmselect option:selected').val(); + if (ampm === 'PM' && selected.hour() < 12) + selected.hour(selected.hour() + 12); + if (ampm === 'AM' && selected.hour() === 12) + selected.hour(0); + } + + if (selected.isBefore(this.startDate)) + selected = this.startDate.clone(); + + if (selected.isAfter(maxDate)) + selected = maxDate.clone(); + + } + } + + // + // hours + // + + html = ' '; + + // + // minutes + // + + html += ': '; + + // + // seconds + // + + if (this.timePickerSeconds) { + html += ': '; + } + + // + // AM/PM + // + + if (!this.timePicker24Hour) { + html += ''; + } + + this.container.find('.calendar.' + side + ' .calendar-time div').html(html); + + }, + + updateFormInputs: function() { + + //ignore mouse movements while an above-calendar text input has focus + if (this.container.find('input[name=daterangepicker_start]').is(":focus") || this.container.find('input[name=daterangepicker_end]').is(":focus")) + return; + + this.container.find('input[name=daterangepicker_start]').val(this.startDate.format(this.locale.format)); + if (this.endDate) + this.container.find('input[name=daterangepicker_end]').val(this.endDate.format(this.locale.format)); + + if (this.singleDatePicker || (this.endDate && (this.startDate.isBefore(this.endDate) || this.startDate.isSame(this.endDate)))) { + this.container.find('button.applyBtn').removeAttr('disabled'); + } else { + this.container.find('button.applyBtn').attr('disabled', 'disabled'); + } + + }, + + move: function() { + var parentOffset = { top: 0, left: 0 }, + containerTop; + var parentRightEdge = $(window).width(); + if (!this.parentEl.is('body')) { + parentOffset = { + top: this.parentEl.offset().top - this.parentEl.scrollTop(), + left: this.parentEl.offset().left - this.parentEl.scrollLeft() + }; + parentRightEdge = this.parentEl[0].clientWidth + this.parentEl.offset().left; + } + + if (this.drops == 'up') + containerTop = this.element.offset().top - this.container.outerHeight() - parentOffset.top; + else + containerTop = this.element.offset().top + this.element.outerHeight() - parentOffset.top; + this.container[this.drops == 'up' ? 'addClass' : 'removeClass']('dropup'); + + if (this.opens == 'left') { + this.container.css({ + top: containerTop, + right: parentRightEdge - this.element.offset().left - this.element.outerWidth(), + left: 'auto' + }); + if (this.container.offset().left < 0) { + this.container.css({ + right: 'auto', + left: 9 + }); + } + } else if (this.opens == 'center') { + this.container.css({ + top: containerTop, + left: this.element.offset().left - parentOffset.left + this.element.outerWidth() / 2 + - this.container.outerWidth() / 2, + right: 'auto' + }); + if (this.container.offset().left < 0) { + this.container.css({ + right: 'auto', + left: 9 + }); + } + } else { + this.container.css({ + top: containerTop, + left: this.element.offset().left - parentOffset.left, + right: 'auto' + }); + if (this.container.offset().left + this.container.outerWidth() > $(window).width()) { + this.container.css({ + left: 'auto', + right: 0 + }); + } + } + }, + + show: function(e) { + if (this.isShowing) return; + + // Create a click proxy that is private to this instance of datepicker, for unbinding + this._outsideClickProxy = $.proxy(function(e) { this.outsideClick(e); }, this); + + // Bind global datepicker mousedown for hiding and + $(document) + .on('mousedown.daterangepicker', this._outsideClickProxy) + // also support mobile devices + .on('touchend.daterangepicker', this._outsideClickProxy) + // also explicitly play nice with Bootstrap dropdowns, which stopPropagation when clicking them + .on('click.daterangepicker', '[data-toggle=dropdown]', this._outsideClickProxy) + // and also close when focus changes to outside the picker (eg. tabbing between controls) + .on('focusin.daterangepicker', this._outsideClickProxy); + + // Reposition the picker if the window is resized while it's open + $(window).on('resize.daterangepicker', $.proxy(function(e) { this.move(e); }, this)); + + this.oldStartDate = this.startDate.clone(); + this.oldEndDate = this.endDate.clone(); + this.previousRightTime = this.endDate.clone(); + + this.updateView(); + this.container.show(); + this.move(); + this.element.trigger('show.daterangepicker', this); + this.isShowing = true; + }, + + hide: function(e) { + if (!this.isShowing) return; + + //incomplete date selection, revert to last values + if (!this.endDate) { + this.startDate = this.oldStartDate.clone(); + this.endDate = this.oldEndDate.clone(); + } + + //if a new date range was selected, invoke the user callback function + if (!this.startDate.isSame(this.oldStartDate) || !this.endDate.isSame(this.oldEndDate)) + this.callback(this.startDate, this.endDate, this.chosenLabel); + + //if picker is attached to a text input, update it + this.updateElement(); + + $(document).off('.daterangepicker'); + $(window).off('.daterangepicker'); + this.container.hide(); + this.element.trigger('hide.daterangepicker', this); + this.isShowing = false; + }, + + toggle: function(e) { + if (this.isShowing) { + this.hide(); + } else { + this.show(); + } + }, + + outsideClick: function(e) { + var target = $(e.target); + // if the page is clicked anywhere except within the daterangerpicker/button + // itself then call this.hide() + if ( + // ie modal dialog fix + e.type == "focusin" || + target.closest(this.element).length || + target.closest(this.container).length || + target.closest('.calendar-table').length + ) return; + this.hide(); + }, + + showCalendars: function() { + this.container.addClass('show-calendar'); + this.move(); + this.element.trigger('showCalendar.daterangepicker', this); + }, + + hideCalendars: function() { + this.container.removeClass('show-calendar'); + this.element.trigger('hideCalendar.daterangepicker', this); + }, + + hoverRange: function(e) { + + //ignore mouse movements while an above-calendar text input has focus + if (this.container.find('input[name=daterangepicker_start]').is(":focus") || this.container.find('input[name=daterangepicker_end]').is(":focus")) + return; + + var label = e.target.innerHTML; + if (label == this.locale.customRangeLabel) { + this.updateView(); + } else { + var dates = this.ranges[label]; + this.container.find('input[name=daterangepicker_start]').val(dates[0].format(this.locale.format)); + this.container.find('input[name=daterangepicker_end]').val(dates[1].format(this.locale.format)); + } + + }, + + clickRange: function(e) { + var label = e.target.innerHTML; + this.chosenLabel = label; + if (label == this.locale.customRangeLabel) { + this.showCalendars(); + } else { + var dates = this.ranges[label]; + this.startDate = dates[0]; + this.endDate = dates[1]; + + if (!this.timePicker) { + this.startDate.startOf('day'); + this.endDate.endOf('day'); + } + + if (!this.alwaysShowCalendars) + this.hideCalendars(); + this.clickApply(); + } + }, + + clickPrev: function(e) { + var cal = $(e.target).parents('.calendar'); + if (cal.hasClass('left')) { + this.leftCalendar.month.subtract(1, 'month'); + if (this.linkedCalendars) + this.rightCalendar.month.subtract(1, 'month'); + } else { + this.rightCalendar.month.subtract(1, 'month'); + } + this.updateCalendars(); + }, + + clickNext: function(e) { + var cal = $(e.target).parents('.calendar'); + if (cal.hasClass('left')) { + this.leftCalendar.month.add(1, 'month'); + } else { + this.rightCalendar.month.add(1, 'month'); + if (this.linkedCalendars) + this.leftCalendar.month.add(1, 'month'); + } + this.updateCalendars(); + }, + + hoverDate: function(e) { + + //ignore mouse movements while an above-calendar text input has focus + if (this.container.find('input[name=daterangepicker_start]').is(":focus") || this.container.find('input[name=daterangepicker_end]').is(":focus")) + return; + + //ignore dates that can't be selected + if (!$(e.target).hasClass('available')) return; + + //have the text inputs above calendars reflect the date being hovered over + var title = $(e.target).attr('data-title'); + var row = title.substr(1, 1); + var col = title.substr(3, 1); + var cal = $(e.target).parents('.calendar'); + var date = cal.hasClass('left') ? this.leftCalendar.calendar[row][col] : this.rightCalendar.calendar[row][col]; + + if (this.endDate) { + this.container.find('input[name=daterangepicker_start]').val(date.format(this.locale.format)); + } else { + this.container.find('input[name=daterangepicker_end]').val(date.format(this.locale.format)); + } + + //highlight the dates between the start date and the date being hovered as a potential end date + var leftCalendar = this.leftCalendar; + var rightCalendar = this.rightCalendar; + var startDate = this.startDate; + if (!this.endDate) { + this.container.find('.calendar td').each(function(index, el) { + + //skip week numbers, only look at dates + if ($(el).hasClass('week')) return; + + var title = $(el).attr('data-title'); + var row = title.substr(1, 1); + var col = title.substr(3, 1); + var cal = $(el).parents('.calendar'); + var dt = cal.hasClass('left') ? leftCalendar.calendar[row][col] : rightCalendar.calendar[row][col]; + + if (dt.isAfter(startDate) && dt.isBefore(date)) { + $(el).addClass('in-range'); + } else { + $(el).removeClass('in-range'); + } + + }); + } + + }, + + clickDate: function(e) { + + if (!$(e.target).hasClass('available')) return; + + var title = $(e.target).attr('data-title'); + var row = title.substr(1, 1); + var col = title.substr(3, 1); + var cal = $(e.target).parents('.calendar'); + var date = cal.hasClass('left') ? this.leftCalendar.calendar[row][col] : this.rightCalendar.calendar[row][col]; + + // + // this function needs to do a few things: + // * alternate between selecting a start and end date for the range, + // * if the time picker is enabled, apply the hour/minute/second from the select boxes to the clicked date + // * if autoapply is enabled, and an end date was chosen, apply the selection + // * if single date picker mode, and time picker isn't enabled, apply the selection immediately + // + + if (this.endDate || date.isBefore(this.startDate, 'day')) { + if (this.timePicker) { + var hour = parseInt(this.container.find('.left .hourselect').val(), 10); + if (!this.timePicker24Hour) { + var ampm = this.container.find('.left .ampmselect').val(); + if (ampm === 'PM' && hour < 12) + hour += 12; + if (ampm === 'AM' && hour === 12) + hour = 0; + } + var minute = parseInt(this.container.find('.left .minuteselect').val(), 10); + var second = this.timePickerSeconds ? parseInt(this.container.find('.left .secondselect').val(), 10) : 0; + date = date.clone().hour(hour).minute(minute).second(second); + } + this.endDate = null; + this.setStartDate(date.clone()); + } else if (!this.endDate && date.isBefore(this.startDate)) { + //special case: clicking the same date for start/end, + //but the time of the end date is before the start date + this.setEndDate(this.startDate.clone()); + } else { + if (this.timePicker) { + var hour = parseInt(this.container.find('.right .hourselect').val(), 10); + if (!this.timePicker24Hour) { + var ampm = this.container.find('.right .ampmselect').val(); + if (ampm === 'PM' && hour < 12) + hour += 12; + if (ampm === 'AM' && hour === 12) + hour = 0; + } + var minute = parseInt(this.container.find('.right .minuteselect').val(), 10); + var second = this.timePickerSeconds ? parseInt(this.container.find('.right .secondselect').val(), 10) : 0; + date = date.clone().hour(hour).minute(minute).second(second); + } + this.setEndDate(date.clone()); + if (this.autoApply) { + this.calculateChosenLabel(); + this.clickApply(); + } + } + + if (this.singleDatePicker) { + this.setEndDate(this.startDate); + if (!this.timePicker) + this.clickApply(); + } + + this.updateView(); + + }, + + calculateChosenLabel: function() { + var customRange = true; + var i = 0; + for (var range in this.ranges) { + if (this.timePicker) { + if (this.startDate.isSame(this.ranges[range][0]) && this.endDate.isSame(this.ranges[range][1])) { + customRange = false; + this.chosenLabel = this.container.find('.ranges li:eq(' + i + ')').addClass('active').html(); + break; + } + } else { + //ignore times when comparing dates if time picker is not enabled + if (this.startDate.format('YYYY-MM-DD') == this.ranges[range][0].format('YYYY-MM-DD') && this.endDate.format('YYYY-MM-DD') == this.ranges[range][1].format('YYYY-MM-DD')) { + customRange = false; + this.chosenLabel = this.container.find('.ranges li:eq(' + i + ')').addClass('active').html(); + break; + } + } + i++; + } + if (customRange) { + this.chosenLabel = this.container.find('.ranges li:last').addClass('active').html(); + this.showCalendars(); + } + }, + + clickApply: function(e) { + this.hide(); + this.element.trigger('apply.daterangepicker', this); + }, + + clickCancel: function(e) { + this.startDate = this.oldStartDate; + this.endDate = this.oldEndDate; + this.hide(); + this.element.trigger('cancel.daterangepicker', this); + }, + + monthOrYearChanged: function(e) { + var isLeft = $(e.target).closest('.calendar').hasClass('left'), + leftOrRight = isLeft ? 'left' : 'right', + cal = this.container.find('.calendar.'+leftOrRight); + + // Month must be Number for new moment versions + var month = parseInt(cal.find('.monthselect').val(), 10); + var year = cal.find('.yearselect').val(); + + if (!isLeft) { + if (year < this.startDate.year() || (year == this.startDate.year() && month < this.startDate.month())) { + month = this.startDate.month(); + year = this.startDate.year(); + } + } + + if (this.minDate) { + if (year < this.minDate.year() || (year == this.minDate.year() && month < this.minDate.month())) { + month = this.minDate.month(); + year = this.minDate.year(); + } + } + + if (this.maxDate) { + if (year > this.maxDate.year() || (year == this.maxDate.year() && month > this.maxDate.month())) { + month = this.maxDate.month(); + year = this.maxDate.year(); + } + } + + if (isLeft) { + this.leftCalendar.month.month(month).year(year); + if (this.linkedCalendars) + this.rightCalendar.month = this.leftCalendar.month.clone().add(1, 'month'); + } else { + this.rightCalendar.month.month(month).year(year); + if (this.linkedCalendars) + this.leftCalendar.month = this.rightCalendar.month.clone().subtract(1, 'month'); + } + this.updateCalendars(); + }, + + timeChanged: function(e) { + + var cal = $(e.target).closest('.calendar'), + isLeft = cal.hasClass('left'); + + var hour = parseInt(cal.find('.hourselect').val(), 10); + var minute = parseInt(cal.find('.minuteselect').val(), 10); + var second = this.timePickerSeconds ? parseInt(cal.find('.secondselect').val(), 10) : 0; + + if (!this.timePicker24Hour) { + var ampm = cal.find('.ampmselect').val(); + if (ampm === 'PM' && hour < 12) + hour += 12; + if (ampm === 'AM' && hour === 12) + hour = 0; + } + + if (isLeft) { + var start = this.startDate.clone(); + start.hour(hour); + start.minute(minute); + start.second(second); + this.setStartDate(start); + if (this.singleDatePicker) { + this.endDate = this.startDate.clone(); + } else if (this.endDate && this.endDate.format('YYYY-MM-DD') == start.format('YYYY-MM-DD') && this.endDate.isBefore(start)) { + this.setEndDate(start.clone()); + } + } else if (this.endDate) { + var end = this.endDate.clone(); + end.hour(hour); + end.minute(minute); + end.second(second); + this.setEndDate(end); + } + + //update the calendars so all clickable dates reflect the new time component + this.updateCalendars(); + + //update the form inputs above the calendars with the new time + this.updateFormInputs(); + + //re-render the time pickers because changing one selection can affect what's enabled in another + this.renderTimePicker('left'); + this.renderTimePicker('right'); + + }, + + formInputsChanged: function(e) { + var isRight = $(e.target).closest('.calendar').hasClass('right'); + var start = moment(this.container.find('input[name="daterangepicker_start"]').val(), this.locale.format); + var end = moment(this.container.find('input[name="daterangepicker_end"]').val(), this.locale.format); + + if (start.isValid() && end.isValid()) { + + if (isRight && end.isBefore(start)) + start = end.clone(); + + this.setStartDate(start); + this.setEndDate(end); + + if (isRight) { + this.container.find('input[name="daterangepicker_start"]').val(this.startDate.format(this.locale.format)); + } else { + this.container.find('input[name="daterangepicker_end"]').val(this.endDate.format(this.locale.format)); + } + + } + + this.updateCalendars(); + if (this.timePicker) { + this.renderTimePicker('left'); + this.renderTimePicker('right'); + } + }, + + elementChanged: function() { + if (!this.element.is('input')) return; + if (!this.element.val().length) return; + if (this.element.val().length < this.locale.format.length) return; + + var dateString = this.element.val().split(this.locale.separator), + start = null, + end = null; + + if (dateString.length === 2) { + start = moment(dateString[0], this.locale.format); + end = moment(dateString[1], this.locale.format); + } + + if (this.singleDatePicker || start === null || end === null) { + start = moment(this.element.val(), this.locale.format); + end = start; + } + + if (!start.isValid() || !end.isValid()) return; + + this.setStartDate(start); + this.setEndDate(end); + this.updateView(); + }, + + keydown: function(e) { + //hide on tab or enter + if ((e.keyCode === 9) || (e.keyCode === 13)) { + this.hide(); + } + }, + + updateElement: function() { + if (this.element.is('input') && !this.singleDatePicker && this.autoUpdateInput) { + this.element.val(this.startDate.format(this.locale.format) + this.locale.separator + this.endDate.format(this.locale.format)); + this.element.trigger('change'); + } else if (this.element.is('input') && this.autoUpdateInput) { + this.element.val(this.startDate.format(this.locale.format)); + this.element.trigger('change'); + } + }, + + remove: function() { + this.container.remove(); + this.element.off('.daterangepicker'); + this.element.removeData(); + } + + }; + + $.fn.daterangepicker = function(options, callback) { + this.each(function() { + var el = $(this); + if (el.data('daterangepicker')) + el.data('daterangepicker').remove(); + el.data('daterangepicker', new DateRangePicker(el, options, callback)); + }); + return this; + }; + + return DateRangePicker; + +})); diff --git a/settings.php b/settings.php index f99cd0bb..ff129f4d 100644 --- a/settings.php +++ b/settings.php @@ -9,6 +9,10 @@ require "scripts/pi-hole/php/savesettings.php"; // Reread ini file as things might have been changed $setupVars = parse_ini_file("/etc/pihole/setupVars.conf"); + +function command_exist($cmd) { + return !empty(shell_exec(sprintf("which %s", escapeshellarg($cmd)))); +} ?>