diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 6f277b21..1eab58cf 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,33 +1,37 @@ -**In raising this issue, I confirm the following (please check boxes, eg [X] - no spaces) Failure to fill the template will close your issue:** +**In raising this issue, I confirm the following:** `{please fill the checkboxes, e.g: [X]}` -- [] I have read and understood the [contributors guide](https://github.com/pi-hole/pi-hole/blob/master/CONTRIBUTING.md). -- [] The issue I am reporting can be *replicated* -- [] The issue I'm reporting isn't a duplicate (see [FAQs](https://github.com/pi-hole/pi-hole/wiki/FAQs), [closed issues](https://github.com/pi-hole/pi-hole/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), and [open issues](https://github.com/pi-hole/pi-hole/issues)). +- [] I have read and understood the [contributors guide](https://github.com/pi-hole/AdminLTE/blob/master/CONTRIBUTING.md). +- [] The issue I am reporting can be *replicated*. +- [] The issue I am reporting isn't a duplicate (see [FAQs](https://github.com/pi-hole/pi-hole/wiki/FAQs), [closed issues](https://github.com/pi-hole/AdminLTE/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), and [open issues](https://github.com/pi-hole/AdminLTE/issues)). -**How familiar are you with the codebase?:** +**How familiar are you with the the source code relevant to this issue?:** -_{replace this text with a number from 1 to 10, with 1 being not familiar, and 10 being very familiar}_ +`{Replace this with a number from 1 to 10. 1 being not familiar, and 10 being very familiar}` --- -**[FEATURE REQUEST | QUESTION | OTHER]:** +**Expected behaviour:** -Please [submit your feature request here](https://discourse.pi-hole.net/c/feature-requests), so it is votable by the community. It's also easier for us to track. +`{A detailed description of what you expect to see}` -**[BUG | ISSUE] Expected Behaviour:** +**Actual behaviour:** +`{A detailed description and/or screenshots of what you do see}` -**[BUG | ISSUE] Actual Behaviour:** +**Steps to reproduce:** +`{Detailed steps of how we can reproduce this}` -**[BUG | ISSUE] Steps to reproduce:** +**Debug token provided by [uploading `pihole -d` log](https://discourse.pi-hole.net/t/the-pihole-command-with-examples/738#debug):** -- -- -- -- +`{Alphanumeric token}` -**(Optional) Debug token generated by `pihole -d`:** +**Troubleshooting undertaken, and/or other relevant information:** -`` +`{Steps of what you have done to fix this}` -_This template was created based on the work of [`udemy-dl`](https://github.com/nishad/udemy-dl/blob/master/LICENSE)._ +> * `{Please delete this quoted section when opening your issue}` +> * You must follow the template instructions. Failure to do so will result in your issue being closed. +> * Please [submit any feature requests here](https://discourse.pi-hole.net/c/feature-requests), so it is votable and trackable by the community. +> * Please respect that Pi-hole is developed by volunteers, who can only reply in their spare time. +> * Detail helps us understand and resolve an issue quicker, but please ensure it's relevant. +> * _This template was created based on the work of [`udemy-dl`](https://github.com/nishad/udemy-dl/blob/master/LICENSE)._ diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 2c391428..7c288989 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,19 +1,32 @@ -**By submitting this pull request, I confirm the following (please check boxes, eg [X] - no spaces) _Failure to fill the template will close your PR_:** +**By submitting this pull request, I confirm the following:** `{please fill any appropriate checkboxes, e.g: [X]}` -***Please submit all pull requests against the `development` branch. Failure to do so will delay or deny your request*** +`{Please ensure that your pull request is for the 'devel' branch!}` -- [] I have read and understood the [contributors guide](https://github.com/pi-hole/pi-hole/blob/master/CONTRIBUTING.md). -- [] I have checked that [another pull request](https://github.com/pi-hole/pi-hole/pulls) for this purpose does not exist. -- [] I have considered, and confirmed that this submission will be valuable to others. -- [] I accept that this submission may not be used, and the pull request closed at the will of the maintainer. -- [] I give this submission freely, and claim no ownership to its content. - -**How familiar are you with the codebase?:** - -_{replace this text with a number from 1 to 10, with 1 being not familiar, and 10 being very familiar}_ +- [] I have read and understood the [contributors guide](https://github.com/pi-hole/AdminLTE/blob/master/CONTRIBUTING.md), as well as this entire template. +- [] I have made only one major change in my proposed changes. +- [] I have commented my proposed changes within the code. +- [] I have tested my proposed changes. +- [] I am willing to help maintain this change if there are issues with it later. +- [] I give this submission freely and claim no ownership. +- [] It is compatible with the [EUPL 1.2 license](https://opensource.org/licenses/EUPL-1.1) +- [] I have squashed any insignificant commits. ([`git rebase`](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html)) +- [] I have Signed Off all commits. ([`git commit --signoff`](https://git-scm.com/docs/git-commit#git-commit---signoff)) --- -_{replace this line with your pull request content}_ +**What does this PR aim to accomplish?:** -_This template was created based on the work of [`udemy-dl`](https://github.com/nishad/udemy-dl/blob/master/LICENSE)._ +`{A detailed description, screenshots (if necessary), as well as links to any relevant GitHub issues}` + +**How does this PR accomplish the above?:** + +`{A detailed description (such as a changelog) and screenshots (if necessary) of the implemented fix}` + +**What documentation changes (if any) are needed to support this PR?:** + +`{A detailed list of any necessary changes}` + +> * `{Please delete this quoted section when opening your pull request}` +> * You must follow the template instructions. Failure to do so will result in your issue being closed. +> * Please respect that Pi-hole is developed by volunteers, who can only reply in their spare time. +> * Detail helps us understand an issue quicker, but please ensure it's relevant. diff --git a/.pullapprove.yml b/.pullapprove.yml index dfe83654..42d3b2fb 100644 --- a/.pullapprove.yml +++ b/.pullapprove.yml @@ -35,4 +35,4 @@ groups: - master required: 4 teams: - - admin + - approvers diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bfb67b77..2ea84399 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,10 +1,38 @@ -This is a basic checklist for now, We will update it in the future. +_This template was created based on the work of [`udemy-dl`](https://github.com/nishad/udemy-dl/blob/master/LICENSE)._ -* Fork the repo and create your new branch based on the `devel` (development) branch. -* Use 4 spaces instead of tabs -* Commit Unix line endings -* If you want, try to keep to the theme of black holes/gravity. This can add some fun to your submission. -* Submit Pull Requests to the development branch only. -* Before Submitting your Pull Request, merge `devel` with your new branch and fix any conflicts. (Make sure you don't break anything in development!) -* Be patient. We will review all submitted pull requests, but our focus is on stability.. please don't be offended if we reject your PR, or it appears we're doing nothing with it! We'll get around to it.. -* Please use the Pi-hole brand: **Pi-hole** (Take a special look at the capitalized 'P' and a low 'h' with a hyphen) +# Contributors Guide + +Please read and understand the contribution guide before creating an issue or pull request. + +## Etiquette + +- Our goal for Pi-hole is **stability before features**. This means we focus on squashing critical bugs before adding new features. Often, we can do both in tandem, but bugs will take priority over a new feature. +- Pi-hole is open source and [powered by donations](https://pi-hole.net/donate/), and as such, we give our **free time** to build, maintain, and **provide user support** for this project. It would be extremely unfair for us to suffer abuse or anger for our hard work, so please take a moment to consider that. +- Please be considerate towards the developers and other users when raising issues or presenting pull requests. +- Respect our decision(s), and do not be upset or abusive if your submission is not used. + +## Viability + +When requesting or submitting new features, first consider whether it might be useful to others. Open source projects are used by many people, who may have entirely different needs to your own. Think about whether or not your feature is likely to be used by other users of the project. + +## Procedure + +**Before filing an issue:** + +- Attempt to replicate and **document** the problem, to ensure that it wasn't a coincidental incident. +- Check to make sure your feature suggestion isn't already present within the project. +- Check the pull requests tab to ensure that the bug doesn't have a fix in progress. +- Check the pull requests tab to ensure that the feature isn't already in progress. + +**Before submitting a pull request:** + +- Check the codebase to ensure that your feature doesn't already exist. +- Check the pull requests to ensure that another person hasn't already submitted the feature or fix. + +## Technical Requirements + +- Submit Pull Requests to the **devel branch only**. +- Before Submitting your Pull Request, merge `devel` with your new branch and fix any conflicts. (Make sure you don't break anything in development!) +- Commit Unix line endings. +- Please use the Pi-hole brand: **Pi-hole** (Take a special look at the capitalized 'P' and a low 'h' with a hyphen) +- (Optional fun) keep to the theme of Star Trek/black holes/gravity. diff --git a/api.php b/api.php index d6d68c34..4092540d 100644 --- a/api.php +++ b/api.php @@ -13,11 +13,12 @@ require("scripts/pi-hole/php/password.php"); require("scripts/pi-hole/php/auth.php"); check_cors(); +$FTL_IP = "127.0.0.1"; $data = array(); // Common API functions -if (isset($_GET['status']) && $auth) +if (isset($_GET['status'])) { $pistatus = exec('sudo pihole status web'); if ($pistatus == "1") @@ -74,7 +75,7 @@ elseif (isset($_GET['disable']) && $auth) } // Other API functions -if(!testFTL() && !isset($_GET["PHP"])) +if(!testFTL($FTL_IP) && !isset($_GET["PHP"])) { $data = array_merge($data, array("FTLnotrunning" => true)); } diff --git a/api_FTL.php b/api_FTL.php index bed84b5d..d4e1c085 100644 --- a/api_FTL.php +++ b/api_FTL.php @@ -12,7 +12,8 @@ if(!isset($api)) die("Direct call to api_FTL.php is not allowed!"); } -$socket = connectFTL("127.0.0.1"); +// $FTL_IP is defined in api.php +$socket = connectFTL($FTL_IP); if (isset($_GET['type'])) { $data["type"] = "FTL"; @@ -32,6 +33,12 @@ if (isset($_GET['summary']) || isset($_GET['summaryRaw']) || !count($_GET)) { $tmp = explode(" ",$line); + if(($tmp[0] === "domains_being_blocked" && !is_numeric($tmp[1])) || $tmp[0] === "status") + { + $stats[$tmp[0]] = $tmp[1]; + continue; + } + if(isset($_GET['summary'])) { if($tmp[0] !== "ads_percentage_today") @@ -71,7 +78,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 +99,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 +117,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, @@ -153,7 +171,14 @@ if ((isset($_GET['topClients']) || isset($_GET['getQuerySources'])) && $auth) if (isset($_GET['getForwardDestinations']) && $auth) { - sendRequestFTL("forward-dest"); + if($_GET['getForwardDestinations'] === "unsorted") + { + sendRequestFTL("forward-dest unsorted"); + } + else + { + sendRequestFTL("forward-dest"); + } $return = getResponseFTL(); $forward_dest = array(); foreach($return as $line) @@ -161,11 +186,11 @@ if (isset($_GET['getForwardDestinations']) && $auth) $tmp = explode(" ",$line); if(count($tmp) == 4) { - $forward_dest[$tmp[3]."|".$tmp[2]] = intval($tmp[1]); + $forward_dest[$tmp[3]."|".$tmp[2]] = floatval($tmp[1]); } else { - $forward_dest[$tmp[2]] = intval($tmp[1]); + $forward_dest[$tmp[2]] = floatval($tmp[1]); } } @@ -181,7 +206,7 @@ if (isset($_GET['getQueryTypes']) && $auth) foreach($return as $ret) { $tmp = explode(": ",$ret); - $querytypes[$tmp[0]] = intval($tmp[1]); + $querytypes[$tmp[0]] = floatval($tmp[1]); } $result = array('querytypes' => $querytypes); @@ -239,7 +264,7 @@ if (isset($_GET['overTimeDataForwards']) && $auth) { $tmp = explode(" ",$line); for ($i=0; $i < count($tmp)-1; $i++) { - $over_time[intval($tmp[0])][$i] = intval($tmp[$i+1]); + $over_time[intval($tmp[0])][$i] = floatval($tmp[$i+1]); } } $result = array('over_time' => $over_time); @@ -256,11 +281,11 @@ if (isset($_GET['getForwardDestinationNames']) && $auth) $tmp = explode(" ",$line); if(count($tmp) == 4) { - $forward_dest[$tmp[3]."|".$tmp[2]] = intval($tmp[1]); + $forward_dest[$tmp[3]."|".$tmp[2]] = floatval($tmp[1]); } else { - $forward_dest[$tmp[2]] = intval($tmp[1]); + $forward_dest[$tmp[2]] = floatval($tmp[1]); } } @@ -278,7 +303,46 @@ if (isset($_GET['overTimeDataQueryTypes']) && $auth) { $tmp = explode(" ",$line); for ($i=0; $i < count($tmp)-1; $i++) { - $over_time[intval($tmp[0])][$i] = intval($tmp[$i+1]); + $over_time[intval($tmp[0])][$i] = floatval($tmp[$i+1]); + } + } + $result = array('over_time' => $over_time); + $data = array_merge($data, $result); +} + +if (isset($_GET['getClientNames']) && $auth) +{ + sendRequestFTL("client-names"); + $return = getResponseFTL(); + $forward_dest = array(); + foreach($return as $line) + { + $tmp = explode(" ",$line); + if(count($tmp) == 4) + { + $forward_dest[$tmp[3]."|".$tmp[2]] = floatval($tmp[1]); + } + else + { + $forward_dest[$tmp[2]] = floatval($tmp[1]); + } + } + + $result = array('clients' => $forward_dest); + $data = array_merge($data, $result); +} + +if (isset($_GET['overTimeDataClients']) && $auth) +{ + sendRequestFTL("ClientsoverTime"); + $return = getResponseFTL(); + $over_time = array(); + + foreach($return as $line) + { + $tmp = explode(" ",$line); + for ($i=0; $i < count($tmp)-1; $i++) { + $over_time[intval($tmp[0])][$i] = floatval($tmp[$i+1]); } } $result = array('over_time' => $over_time); diff --git a/api_db.php b/api_db.php new file mode 100644 index 00000000..7090ed49 --- /dev/null +++ b/api_db.php @@ -0,0 +1,225 @@ +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['getGraphData']) && $auth) +{ + $limit = ""; + + if(isset($_GET["from"]) && isset($_GET["until"])) + { + $limit = " AND timestamp >= ".intval($_GET["from"])." AND timestamp <= ".intval($_GET["until"]); + } + elseif(isset($_GET["from"]) && !isset($_GET["until"])) + { + $limit = " AND timestamp >= ".intval($_GET["from"]); + } + elseif(!isset($_GET["from"]) && isset($_GET["until"])) + { + $limit = " AND timestamp <= ".intval($_GET["until"]); + } + + $interval = 600; + + if(isset($_GET["interval"])) + { + $q = intval($_GET["interval"]); + if($q > 10) + $interval = $q; + } + + // Count permitted queries in intervals + $results = $db->query('SELECT (timestamp/'.$interval.')*'.$interval.' interval, COUNT(*) FROM queries WHERE (status == 2 OR status == 3)'.$limit.' GROUP by interval ORDER by interval'); + $domains = array(); + while ($row = $results->fetchArray()) + { + $domains[$row[0]] = intval($row[1]); + } + $result = array('domains_over_time' => $domains); + $data = array_merge($data, $result); + + // Count blocked queries in intervals + $results = $db->query('SELECT (timestamp/'.$interval.')*'.$interval.' interval, COUNT(*) FROM queries WHERE (status == 1 OR status == 4 OR status == 5)'.$limit.' GROUP by interval ORDER by interval'); + $addomains = array(); + while ($row = $results->fetchArray()) + { + $addomains[$row[0]] = intval($row[1]); + } + $result = array('ads_over_time' => $addomains); + $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..cb3db9af --- /dev/null +++ b/auditlog.php @@ -0,0 +1,89 @@ + + + + + + +
+
+
+
+

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, click on this button, to have the new settings become effective:

+ +
+ +
+ +
+
+
+ +
+
+
+ + + diff --git a/db_graph.php b/db_graph.php new file mode 100644 index 00000000..9dbdba73 --- /dev/null +++ b/db_graph.php @@ -0,0 +1,67 @@ + + + + + + + + +
+
+ +
+ + +
+
+ +
+ +
+ +
+
+
+
+
+
+
+

Queries over the selected time period

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

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..a2441b58 --- /dev/null +++ b/db_queries.php @@ -0,0 +1,145 @@ + + + + + + + + +
+
+ +
+ + +
+
+ +
+ +
+ +
+
+
+ +
+
+ +
+
+

---

+

Queries Blocked

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

---

+

Queries Blocked (Wildcards)

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

---

+

Queries Total

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

---

+

Queries Blocked

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

Recent Queries

+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + +
TimeTypeDomainClientStatusAction
TimeTypeDomainClientStatusAction
+
+
+ +
+ +
+
+ + + + + + + + diff --git a/gravity.php b/gravity.php index 5e736725..f11e6ab7 100644 --- a/gravity.php +++ b/gravity.php @@ -1,4 +1,4 @@ - diff --git a/help.php b/help.php index 102c6e72..c3a1f875 100644 --- a/help.php +++ b/help.php @@ -70,7 +70,7 @@

White- / Blacklist

Add or remove domains (or subdomains) from the white-/blacklist. If a domain is added to e.g. the whitelist, any possible entry of the same domain will be automatically removed from the blacklist and vice versa.

-

Wildcard blacklisting is supported (entering something.de will block this domain including all subdomains like a.bb.c.999.something.de). Note that wildcard whitelisting is not supported.

+

Wildcard blacklisting is supported (entering something.de will block this domain including all subdomains like a.bb.c.999.something.de). Note that wildcard whitelisting is not supported.

You can white-/blacklist multiple entries at a time if you separate the domains by spaces.

@@ -89,7 +89,7 @@

Tools → Query adlists

- This function is useful to find out what list a domain appears on. Since we don't control what the third-parties put on the block lists, you may find that a domain you normally visit stops working. If this is the case, you could run this command to scan for strings in the list of blocked domains and it will return the list the domain is found on. This proved useful a while back when the Mahakala list was adding apple.com and microsoft.com to their block list.

+ This function is useful to find out what list a domain appears on. Since we don't control what the third-parties put on the block lists, you may find that a domain you normally visit stops working. If this is the case, you could run this command to scan for strings in the list of blocked domains and it will return the list the domain is found on. This proved useful a while back when the Mahakala list was adding apple.com and microsoft.com to their block list.

diff --git a/index.php b/index.php index 6c161299..2ce90bfe 100644 --- a/index.php +++ b/index.php @@ -10,25 +10,12 @@ ?>
-
- -
-
-

---

-

Queries Blocked Last 24 Hours

-
-
- -
-
-
-
-

---

-

Queries Last 24 Hours

+

Total queries (- clients)

+

---

@@ -38,23 +25,53 @@
-
+
-

---

-

Queries Blocked Last 24 Hours

+

Queries Blocked

+

---

- +
-
+
-

---

-

Domains on Blocklists

+

Percent Blocked

+

---

+
+
+ +
+
+
+d > 1) + $gravitydate = $gravitydiff->format("Blocking list updated %a days, %H:%I ago"); + elseif($gravitydiff->d == 1) + $gravitydate = $gravitydiff->format("Blocking list updated one day, %H:%I ago"); + else + $gravitydate = $gravitydiff->format("Blocking list updated %H:%I ago"); +} +else +{ + $gravitydate = "Blocking list not found"; +} +?> + +
+ +
+
+

Domains on Blocklist

+

---

@@ -72,7 +89,7 @@
- +
@@ -88,11 +105,88 @@ // show since the API will respect the privacy of the user if he defines // a password if($auth){ ?> +
+
+
+
+

Clients (over time)

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

Query Types (integrated)

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

Forward Destinations (integrated)

+
+
+
+ +
+
+
+ +
+ +
+
+
+ + +
+
-

Query Types over Time

+

Query Types (over time)

@@ -105,10 +199,11 @@
+
-

Forward Destinations over Time

+

Forward Destinations (over time)

@@ -121,7 +216,9 @@
+
+ 0)
+
diff --git a/scripts/pi-hole/js/auditlog.js b/scripts/pi-hole/js/auditlog.js new file mode 100644 index 00000000..3aa9912c --- /dev/null +++ b/scripts/pi-hole/js/auditlog.js @@ -0,0 +1,120 @@ +/* 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"); + $("#gravityBtn").prop("disabled", false); + } + 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"); + $("#gravityBtn").prop("disabled", false); + } + else + { + add(url,"audit"); + } + }); +}); + + +$("#gravityBtn").on("click", function() { + window.location.replace("gravity.php?go"); +}); diff --git a/scripts/pi-hole/js/db_graph.js b/scripts/pi-hole/js/db_graph.js new file mode 100644 index 00000000..201cad5d --- /dev/null +++ b/scripts/pi-hole/js/db_graph.js @@ -0,0 +1,262 @@ +/* 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 () { + $("#querytime").daterangepicker( + { + timePicker: true, timePickerIncrement: 15, + locale: { format: "MMMM Do YYYY, HH:mm" }, + ranges: { + "Today": [moment().startOf("day"), moment()], + "Yesterday": [moment().subtract(1, "days").startOf("day"), moment().subtract(1, "days").endOf("day")], + "Last 7 Days": [moment().subtract(6, "days"), moment()], + "Last 30 Days": [moment().subtract(29, "days"), moment()], + "This Month": [moment().startOf("month"), moment()], + "Last Month": [moment().subtract(1, "month").startOf("month"), moment().subtract(1, "month").endOf("month")], + "This Year": [moment().startOf("year"), moment()], + "All Time": [moment(0), moment()] + }, + "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 padNumber(num) { + return ("00" + num).substr(-2,2); +} + +// Helper function needed for converting the Objects to Arrays + +function objectToArray(p){ + var keys = Object.keys(p); + keys.sort(function(a, b) { + return a - b; + }); + + var arr = [], idx = []; + for (var i = 0; i < keys.length; i++) { + arr.push(p[keys[i]]); + idx.push(keys[i]); + } + return [idx,arr]; +} + +var timeLineChart; + +function compareNumbers(a, b) { + return a - b; +} + +function updateQueriesOverTime() { + $("#queries-over-time .overlay").show(); + $.getJSON("api_db.php?getGraphData&from="+from+"&until="+until, function(data) { + + // convert received objects to arrays + data.domains_over_time = objectToArray(data.domains_over_time); + data.ads_over_time = objectToArray(data.ads_over_time); + // Remove possibly already existing data + timeLineChart.data.labels = []; + timeLineChart.data.datasets[0].data = []; + timeLineChart.data.datasets[1].data = []; + + var dates = [], hour; + + for (hour in data.domains_over_time[0]) { + if ({}.hasOwnProperty.call(data.domains_over_time[0], hour)) { + dates.push(parseInt(data.domains_over_time[0][hour])); + } + } + + for (hour in data.ads_over_time[0]) { + if ({}.hasOwnProperty.call(data.ads_over_time[0], hour)) { + if(!dates.includes(parseInt(data.ads_over_time[0][hour]))) + { + dates.push(parseInt(data.ads_over_time[0][hour])); + } + } + } + + dates.sort(compareNumbers); + + // Add data for each hour that is available + for (hour in dates) { + if ({}.hasOwnProperty.call(dates, hour)) { + var d, dom = 0, ads = 0; + d = new Date(1000*dates[hour]); + + var idx = data.domains_over_time[0].indexOf(dates[hour].toString()); + if (idx > -1) + { + dom = data.domains_over_time[1][idx]; + } + + idx = data.ads_over_time[0].indexOf(dates[hour].toString()); + if (idx > -1) + { + ads = data.ads_over_time[1][idx]; + } + + timeLineChart.data.labels.push(d); + timeLineChart.data.datasets[0].data.push(dom); + timeLineChart.data.datasets[1].data.push(ads); + } + } + + timeLineChart.options.scales.xAxes[0].display=true; + $("#queries-over-time .overlay").hide(); + timeLineChart.update(); + }); +} + +/* global Chart */ +$(document).ready(function() { + var ctx = document.getElementById("queryOverTimeChart").getContext("2d"); + timeLineChart = new Chart(ctx, { + type: "line", + data: { + labels: [ 0 ], + datasets: [ + { + label: "Total DNS Queries", + fill: true, + backgroundColor: "rgba(220,220,220,0.5)", + borderColor: "rgba(0, 166, 90,.8)", + pointBorderColor: "rgba(0, 166, 90,.8)", + pointRadius: 1, + pointHoverRadius: 5, + data: [], + pointHitRadius: 5, + cubicInterpolationMode: "monotone" + }, + { + label: "Blocked DNS Queries", + fill: true, + backgroundColor: "rgba(0,192,239,0.5)", + borderColor: "rgba(0,192,239,1)", + pointBorderColor: "rgba(0,192,239,1)", + pointRadius: 1, + pointHoverRadius: 5, + data: [], + pointHitRadius: 5, + cubicInterpolationMode: "monotone" + } + ] + }, + options: { + tooltips: { + enabled: true, + responsive: true, + mode: "x-axis", + callbacks: { + title: function(tooltipItem, data) { + var label = tooltipItem[0].xLabel; + var time = new Date(label); + var date = time.getFullYear()+"-"+padNumber(time.getMonth())+"-"+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; + }, + label: function(tooltipItems, data) { + if(tooltipItems.datasetIndex === 1) + { + var percentage = 0.0; + var total = parseInt(data.datasets[0].data[tooltipItems.index]); + var blocked = parseInt(data.datasets[1].data[tooltipItems.index]); + if(total > 0) + { + percentage = 100.0*blocked/total; + } + return data.datasets[tooltipItems.datasetIndex].label + ": " + tooltipItems.yLabel + " (" + percentage.toFixed(1) + "%)"; + } + else + { + return data.datasets[tooltipItems.datasetIndex].label + ": " + tooltipItems.yLabel; + } + } + } + }, + legend: { + display: false + }, + scales: { + xAxes: [{ + type: "time", + display: false, + time: { + displayFormats: { + "minute": "HH:mm", + "hour": "HH:mm", + "day": "HH:mm", + "week": "MMM DD HH:mm", + "month": "MMM DD", + "quarter": "MMM DD", + "year": "MMM DD" + } + } + }], + yAxes: [{ + ticks: { + beginAtZero: true + } + }] + }, + maintainAspectRatio: false + } + }); +}); + +$("#querytime").on("apply.daterangepicker", function(ev, picker) { + $("#queries-over-time").show(); + updateQueriesOverTime(); +}); + +$("#queryOverTimeChart").click(function(evt){ + var activePoints = timeLineChart.getElementAtEvent(evt); + if(activePoints.length > 0) + { + //get the internal index in the chart + var clickedElementindex = activePoints[0]["_index"]; + + //get specific label by index + var label = timeLineChart.data.labels[clickedElementindex]; + + //get value by index + var from = label/1000; + var until = label/1000 + 600; + window.location.href = "db_queries.php?from="+from+"&until="+until; + } + return false; +}); diff --git a/scripts/pi-hole/js/db_lists.js b/scripts/pi-hole/js/db_lists.js new file mode 100644 index 00000000..75a21746 --- /dev/null +++ b/scripts/pi-hole/js/db_lists.js @@ -0,0 +1,181 @@ +/* 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 () { + $("#querytime").daterangepicker( + { + timePicker: true, timePickerIncrement: 15, + locale: { format: "MMMM Do YYYY, HH:mm" }, + ranges: { + "Today": [moment().startOf("day"), moment()], + "Yesterday": [moment().subtract(1, "days").startOf("day"), moment().subtract(1, "days").endOf("day")], + "Last 7 Days": [moment().subtract(6, "days"), moment()], + "Last 30 Days": [moment().subtract(29, "days"), moment()], + "This Month": [moment().startOf("month"), moment()], + "Last Month": [moment().subtract(1, "month").startOf("month"), moment().subtract(1, "month").endOf("month")], + "This Year": [moment().startOf("year"), moment()], + "All Time": [moment(0), moment()] + }, + "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(); + }); +} + +$("#querytime").on("apply.daterangepicker", function(ev, picker) { + 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..8551e9dc --- /dev/null +++ b/scripts/pi-hole/js/db_queries.js @@ -0,0 +1,275 @@ +/* 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; +var instantquery = false; +var daterange; + +// Do we want to filter queries? +var GETDict = {}; +location.search.substr(1).split("&").forEach(function(item) {GETDict[item.split("=")[0]] = item.split("=")[1];}); + +if("from" in GETDict && "until" in GETDict) +{ + from = parseInt(GETDict["from"]); + until = parseInt(GETDict["until"]); + start__ = moment(1000*from); + end__ = moment(1000*until); + instantquery = true; +} + +$(function () { + daterange = $("#querytime").daterangepicker( + { + timePicker: true, timePickerIncrement: 15, + locale: { format: "MMMM Do YYYY, HH:mm" }, + ranges: { + "Today": [moment().startOf("day"), moment()], + "Yesterday": [moment().subtract(1, "days").startOf("day"), moment().subtract(1, "days").endOf("day")], + "Last 7 Days": [moment().subtract(6, "days"), moment()], + "Last 30 Days": [moment().subtract(29, "days"), moment()], + "This Month": [moment().startOf("month"), moment()], + "Last Month": [moment().subtract(1, "month").startOf("month"), moment().subtract(1, "month").endOf("month")], + "This Year": [moment().startOf("year"), moment()], + "All Time": [moment(0), moment()] + }, + "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; + + var APIstring; + if(instantquery) + { + APIstring = "api_db.php?getAllQueries&from="+from+"&until="+until; + } + else + { + APIstring = "api_db.php?getAllQueries=empty"; + } + + tableApi = $("#all-queries").DataTable( { + "rowCallback": function( row, data, index ){ + 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": APIstring, "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": "" + } ], + "initComplete": reloadCallback + }); + $("#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"); + } + } ); + + if(instantquery) + { + daterange.val(start__.format("MMMM Do YYYY, HH:mm") + " - " + end__.format("MMMM Do YYYY, HH:mm")); + } +} ); + +$("#querytime").on("apply.daterangepicker", function(ev, picker) { + refreshTableData(); +}); + diff --git a/scripts/pi-hole/js/gravity.js b/scripts/pi-hole/js/gravity.js index 7ba64775..bf31254b 100644 --- a/scripts/pi-hole/js/gravity.js +++ b/scripts/pi-hole/js/gravity.js @@ -28,7 +28,17 @@ function eventsource() { alSuccess.show(); } - ta.append(e.data); + // Detect ${OVER} + if(e.data.indexOf("<------") !== -1) + { + ta.text(ta.text().substring(0, ta.text().lastIndexOf("\n")) + "\n"); + var new_string = e.data.replace("<------", ""); + ta.append(new_string); + } + else + { + ta.append(e.data); + } }, false); diff --git a/scripts/pi-hole/js/index.js b/scripts/pi-hole/js/index.js index ecb005af..9f8cb8c6 100644 --- a/scripts/pi-hole/js/index.js +++ b/scripts/pi-hole/js/index.js @@ -5,7 +5,9 @@ * This file is copyright under the latest version of the EUPL. * Please see LICENSE file for your rights under this license. */ // Define global variables +/* global Chart */ var timeLineChart, queryTypeChart, forwardDestinationChart; +var queryTypePieChart, forwardDestinationPieChart, clientsChart; function padNumber(num) { return ("00" + num).substr(-2,2); @@ -96,6 +98,8 @@ function updateQueryTypesOverTime() { // convert received objects to arrays data.over_time = objectToArray(data.over_time); + // remove last data point since it not representative + data.over_time[0].splice(-1,1); var timestamps = data.over_time[0]; var plotdata = data.over_time[1]; // Remove possibly already existing data @@ -116,17 +120,9 @@ function updateQueryTypesOverTime() { // New style: Get Unix timestamps d = new Date(1000*h); - var sum = plotdata[j][0] + plotdata[j][1]; - var A = 0, AAAA = 0; - if(sum > 0) - { - A = plotdata[j][0]/sum; - AAAA = plotdata[j][1]/sum; - } - queryTypeChart.data.labels.push(d); - queryTypeChart.data.datasets[0].data.push(A); - queryTypeChart.data.datasets[1].data.push(AAAA); + queryTypeChart.data.datasets[0].data.push(1e-2*plotdata[j][0]); + queryTypeChart.data.datasets[1].data.push(1e-2*plotdata[j][1]); } } $("#query-types .overlay").hide(); @@ -145,6 +141,48 @@ function updateQueryTypesOverTime() { } }); } +function updateQueryTypesPie() { + $.getJSON("api.php?getQueryTypes", function(data) { + + if("FTLnotrunning" in data) + { + return; + } + + var colors = []; + // Get colors from AdminLTE + $.each($.AdminLTE.options.colors, function(key, value) { colors.push(value); }); + var v = [], c = [], k = [], iter; + // Collect values and colors, and labels + if(data.hasOwnProperty("querytypes")) + { + iter = data.querytypes; + } + else + { + iter = data; + } + $.each(iter, function(key , value) { + v.push(value); + c.push(colors.shift()); + k.push(key); + }); + // Build a single dataset with the data to be pushed + var dd = {data: v, backgroundColor: c}; + // and push it at once + queryTypePieChart.data.datasets[0] = dd; + queryTypePieChart.data.labels = k; + $("#query-types-pie .overlay").hide(); + queryTypePieChart.update(); + queryTypePieChart.chart.config.options.cutoutPercentage=50; + queryTypePieChart.update(); + // Don't use rotation animation for further updates + queryTypePieChart.options.animation.duration=0; + }).done(function() { + // Reload graph after minute + setTimeout(updateQueryTypesPie, 60000); + }); +} function updateForwardedOverTime() { $.getJSON("api.php?overTimeDataForwards&getForwardDestinationNames", function(data) { @@ -156,6 +194,8 @@ function updateForwardedOverTime() { // convert received objects to arrays data.over_time = objectToArray(data.over_time); + // remove last data point since it not representative + data.over_time[0].splice(-1,1); var timestamps = data.over_time[0]; var plotdata = data.over_time[1]; var labels = []; @@ -189,6 +229,7 @@ function updateForwardedOverTime() { forwardDestinationChart.data.datasets[0].pointHitRadius = 5; forwardDestinationChart.data.datasets[0].pointHoverRadius = 5; forwardDestinationChart.data.datasets[0].label = labels[0]; + forwardDestinationChart.data.datasets[0].cubicInterpolationMode = "monotone"; for (i = forwardDestinationChart.data.datasets.length; i < plotdata[0].length; i++) { @@ -199,18 +240,10 @@ function updateForwardedOverTime() { for (j in timestamps) { if (!{}.hasOwnProperty.call(timestamps, j)) continue; - var sum = 0.0; for (key in plotdata[j]) { if (!{}.hasOwnProperty.call(plotdata[j], key)) continue; - sum += plotdata[j][key]; - } - var dd = []; - for (key in plotdata[j]) - { - if (!{}.hasOwnProperty.call(plotdata[j], key)) continue; - var singlepoint = plotdata[j][key]; - forwardDestinationChart.data.datasets[key].data.push(singlepoint/sum); + forwardDestinationChart.data.datasets[key].data.push(1e-2*plotdata[j][key]); } var d = new Date(1000*parseInt(timestamps[j])); @@ -233,6 +266,138 @@ function updateForwardedOverTime() { }); } + +function updateClientsOverTime() { + $.getJSON("api.php?overTimeDataClients&getClientNames", function(data) { + + if("FTLnotrunning" in data) + { + return; + } + + // convert received objects to arrays + data.over_time = objectToArray(data.over_time); + // remove last data point since it not representative + data.over_time[0].splice(-1,1); + var timestamps = data.over_time[0]; + var plotdata = data.over_time[1]; + var labels = []; + var key, i, j; + for (key in data.clients) + { + if (!{}.hasOwnProperty.call(data.clients, key)) continue; + if(key.indexOf("|") > -1) + { + var idx = key.indexOf("|"); + key = key.substr(0, idx); + } + labels.push(key); + } + // Get colors from AdminLTE + var colors = []; + $.each($.AdminLTE.options.colors, function(key, value) { colors.push(value); }); + var v = [], c = [], k = []; + + // Remove possibly already existing data + clientsChart.data.labels = []; + clientsChart.data.datasets[0].data = []; + for (i = 1; i < clientsChart.data.datasets.length; i++) + { + clientsChart.data.datasets[i].data = []; + } + + // Collect values and colors, and labels + clientsChart.data.datasets[0].backgroundColor = colors[0]; + clientsChart.data.datasets[0].pointRadius = 0; + clientsChart.data.datasets[0].pointHitRadius = 5; + clientsChart.data.datasets[0].pointHoverRadius = 5; + clientsChart.data.datasets[0].label = labels[0]; + + for (i = clientsChart.data.datasets.length; plotdata.length && i < plotdata[0].length; i++) + { + clientsChart.data.datasets.push({data: [], backgroundColor: colors[i], pointRadius: 0, pointHitRadius: 5, pointHoverRadius: 5, label: labels[i], cubicInterpolationMode: "monotone" }); + } + + // Add data for each dataset that is available + for (j in timestamps) + { + if (!{}.hasOwnProperty.call(timestamps, j)) continue; + for (key in plotdata[j]) + { + if (!{}.hasOwnProperty.call(plotdata[j], key)) continue; + clientsChart.data.datasets[key].data.push(plotdata[j][key]); + } + + var d = new Date(1000*parseInt(timestamps[j])); + clientsChart.data.labels.push(d); + } + $("#clients .overlay").hide(); + clientsChart.update(); + }).done(function() { + // Reload graph after 10 minutes + failures = 0; + setTimeout(updateClientsOverTime, 600000); + }).fail(function() { + failures++; + if(failures < 5) + { + // Try again after 1 minute only if this has not failed more + // than five times in a row + setTimeout(updateClientsOverTime, 60000); + } + }); +} + +function updateForwardDestinationsPie() { + $.getJSON("api.php?getForwardDestinations=unsorted", function(data) { + + if("FTLnotrunning" in data) + { + return; + } + + var colors = []; + // Get colors from AdminLTE + $.each($.AdminLTE.options.colors, function(key, value) { colors.push(value); }); + var v = [], c = [], k = [], values = []; + + // Collect values and colors + $.each(data.forward_destinations, function(key , value) { + if(key.indexOf("|") > -1) + { + key = key.substr(0, key.indexOf("|")); + } + values.push([key, value, colors.shift()]); + }); + + // Sort data ASC accorwing to 2nd column, keep already assigned labels and colors + values = values.sort(function(a,b) { return b[1] - a[1]; }); + + // Split data into individual arrays for the graphs + $.each(values, function(key , value) { + k.push(value[0]); + v.push(value[1]); + c.push(value[2]); + }); + + // Build a single dataset with the data to be pushed + var dd = {data: v, backgroundColor: c}; + // and push it at once + forwardDestinationPieChart.data.labels = k; + forwardDestinationPieChart.data.datasets[0] = dd; + // and push it at once + $("#forward-destinations-pie .overlay").hide(); + forwardDestinationPieChart.update(); + forwardDestinationPieChart.chart.config.options.cutoutPercentage=50; + forwardDestinationPieChart.update(); + // Don't use rotation animation for further updates + forwardDestinationPieChart.options.animation.duration=0; + }).done(function() { + // Reload graph after one minute + setTimeout(updateForwardDestinationsPie, 60000); + }); +} + // Credit: http://stackoverflow.com/questions/1787322/htmlspecialchars-equivalent-in-javascript/4835406#4835406 function escapeHtml(text) { var map = { @@ -375,8 +540,8 @@ function updateSummaryData(runOnce) { if("FTLnotrunning" in data) { - data["ads_blocked_today"] = "Lost"; - data["dns_queries_today"] = "connection"; + data["dns_queries_today"] = "Lost"; + data["ads_blocked_today"] = "connection"; data["ads_percentage_today"] = "to"; data["domains_being_blocked"] = "API"; // Adjust text @@ -406,19 +571,19 @@ function updateSummaryData(runOnce) { } } - ["ads_blocked_today", "dns_queries_today", "ads_percentage_today"].forEach(function(today) { - var todayElement = $("h3#" + today); + ["ads_blocked_today", "dns_queries_today", "ads_percentage_today", "unique_clients"].forEach(function(today) { + var todayElement = $("span#" + today); todayElement.text() !== data[today] && todayElement.text() !== data[today] + "%" && - todayElement.addClass("glow"); + $("span#" + today).addClass("glow"); }); window.setTimeout(function() { - ["ads_blocked_today", "dns_queries_today", "domains_being_blocked", "ads_percentage_today"].forEach(function(header, idx) { + ["ads_blocked_today", "dns_queries_today", "domains_being_blocked", "ads_percentage_today", "unique_clients"].forEach(function(header, idx) { var textData = (idx === 3 && data[header] !== "to") ? data[header] + "%" : data[header]; - $("h3#" + header).text(textData); + $("span#" + header).text(textData); }); - $("h3.statistic.glow").removeClass("glow"); + $("span.glow").removeClass("glow"); }, 500); }).done(function() { @@ -437,59 +602,130 @@ function updateSummaryData(runOnce) { $(document).ready(function() { - var isMobile = { - Windows: function() { - return /IEMobile/i.test(navigator.userAgent); - }, - Android: function() { - return /Android/i.test(navigator.userAgent); - }, - BlackBerry: function() { - return /BlackBerry/i.test(navigator.userAgent); - }, - iOS: function() { - return /iPhone|iPad|iPod/i.test(navigator.userAgent); - }, - any: function() { - return (isMobile.Android() || isMobile.BlackBerry() || isMobile.iOS() || isMobile.Windows()); - } - }; + var isMobile = { + Windows: function() { + return /IEMobile/i.test(navigator.userAgent); + }, + Android: function() { + return /Android/i.test(navigator.userAgent); + }, + BlackBerry: function() { + return /BlackBerry/i.test(navigator.userAgent); + }, + iOS: function() { + return /iPhone|iPad|iPod/i.test(navigator.userAgent); + }, + any: function() { + return (isMobile.Android() || isMobile.BlackBerry() || isMobile.iOS() || isMobile.Windows()); + } + }; - // Pull in data via AJAX + // Pull in data via AJAX - updateSummaryData(); + updateSummaryData(); - var ctx = document.getElementById("queryOverTimeChart").getContext("2d"); - timeLineChart = new Chart(ctx, { + var ctx = document.getElementById("queryOverTimeChart").getContext("2d"); + timeLineChart = new Chart(ctx, { + type: "line", + data: { + labels: [], + datasets: [ + { + label: "Total DNS Queries", + fill: true, + backgroundColor: "rgba(220,220,220,0.5)", + borderColor: "rgba(0, 166, 90,.8)", + pointBorderColor: "rgba(0, 166, 90,.8)", + pointRadius: 1, + pointHoverRadius: 5, + data: [], + pointHitRadius: 5, + cubicInterpolationMode: "monotone" + }, + { + label: "Blocked DNS Queries", + fill: true, + backgroundColor: "rgba(0,192,239,0.5)", + borderColor: "rgba(0,192,239,1)", + pointBorderColor: "rgba(0,192,239,1)", + pointRadius: 1, + pointHoverRadius: 5, + data: [], + pointHitRadius: 5, + cubicInterpolationMode: "monotone" + } + ] + }, + options: { + tooltips: { + enabled: true, + mode: "x-axis", + callbacks: { + title: function(tooltipItem, data) { + 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) { + if(tooltipItems.datasetIndex === 1) + { + var percentage = 0.0; + var total = parseInt(data.datasets[0].data[tooltipItems.index]); + var blocked = parseInt(data.datasets[1].data[tooltipItems.index]); + if(total > 0) + { + percentage = 100.0*blocked/total; + } + return data.datasets[tooltipItems.datasetIndex].label + ": " + tooltipItems.yLabel + " (" + percentage.toFixed(1) + "%)"; + } + else + { + return data.datasets[tooltipItems.datasetIndex].label + ": " + tooltipItems.yLabel; + } + } + } + }, + legend: { + display: false + }, + scales: { + xAxes: [{ + type: "time", + time: { + unit: "hour", + displayFormats: { + hour: "HH:mm" + }, + tooltipFormat: "HH:mm" + } + }], + yAxes: [{ + ticks: { + beginAtZero: true + } + }] + }, + maintainAspectRatio: false + } + }); + + // Pull in data via AJAX + + 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: [ - { - label: "Total DNS Queries", - fill: true, - backgroundColor: "rgba(220,220,220,0.5)", - borderColor: "rgba(0, 166, 90,.8)", - pointBorderColor: "rgba(0, 166, 90,.8)", - pointRadius: 1, - pointHoverRadius: 5, - data: [], - pointHitRadius: 5, - cubicInterpolationMode: "monotone" - }, - { - label: "Blocked DNS Queries", - fill: true, - backgroundColor: "rgba(0,192,239,0.5)", - borderColor: "rgba(0,192,239,1)", - pointBorderColor: "rgba(0,192,239,1)", - pointRadius: 1, - pointHoverRadius: 5, - data: [], - pointHitRadius: 5, - cubicInterpolationMode: "monotone" - } - ] + datasets: [{ data: [] }] }, options: { tooltips: { @@ -503,24 +739,73 @@ $(document).ready(function() { var m = parseInt(time[2], 10) || 0; var from = padNumber(h)+":"+padNumber(m-5)+":00"; var to = padNumber(h)+":"+padNumber(m+4)+":59"; - return "Queries from "+from+" to "+to; + return "Forward destinations from "+from+" to "+to; }, label: function(tooltipItems, data) { - if(tooltipItems.datasetIndex === 1) - { - var percentage = 0.0; - var total = parseInt(data.datasets[0].data[tooltipItems.index]); - var blocked = parseInt(data.datasets[1].data[tooltipItems.index]); - if(total > 0) - { - percentage = 100.0*blocked/total; - } - return data.datasets[tooltipItems.datasetIndex].label + ": " + tooltipItems.yLabel + " (" + percentage.toFixed(1) + "%)"; - } - else - { - return data.datasets[tooltipItems.datasetIndex].label + ": " + tooltipItems.yLabel; + 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, index, values) { + return Math.round(value*100) + " %"; } + }, + stacked: true + }] + }, + maintainAspectRatio: true + } + }); + + // Pull in data via AJAX + updateForwardedOverTime(); + } + + // Create / load "Top Clients over Time" only if authorized + if(document.getElementById("clientsChart")) + { + ctx = document.getElementById("clientsChart").getContext("2d"); + clientsChart = new Chart(ctx, { + type: "line", + data: { + labels: [], + datasets: [{ data: [] }] + }, + options: { + tooltips: { + enabled: true, + mode: "x-axis", + callbacks: { + title: function(tooltipItem, data) { + 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 "Client activity from "+from+" to "+to; + }, + label: function(tooltipItems, data) { + return data.datasets[tooltipItems.datasetIndex].label + ": " + tooltipItems.yLabel; } } }, @@ -541,186 +826,202 @@ $(document).ready(function() { yAxes: [{ ticks: { beginAtZero: true - } + }, + stacked: true }] }, - maintainAspectRatio: false + maintainAspectRatio: true } }); // Pull in data via AJAX + updateClientsOverTime(); + } - 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, data) { - 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) + "%"; - } - } + // Create / load "Query Types over Time" only if authorized + if(document.getElementById("queryTypeChart")) + { + ctx = document.getElementById("queryTypeChart").getContext("2d"); + queryTypeChart = new Chart(ctx, { + type: "line", + data: { + labels: [], + datasets: [ + { + label: "A: IPv4 queries", + pointRadius: 0, + pointHitRadius: 5, + pointHoverRadius: 5, + data: [], + cubicInterpolationMode: "monotone" }, - 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, index, values) { - return Math.round(value*100) + " %"; - } - }, - stacked: true - }] - }, - maintainAspectRatio: true - } - }); - - // Pull in data via AJAX - updateForwardedOverTime(); - } - - // Create / load "Query Types over Time" only if authorized - if(document.getElementById("queryTypeChart")) - { - ctx = document.getElementById("queryTypeChart").getContext("2d"); - queryTypeChart = new Chart(ctx, { - type: "line", - data: { - labels: [], - datasets: [ - { - label: "A: IPv4 queries", - pointRadius: 0, - pointHitRadius: 5, - pointHoverRadius: 5, - data: [] + { + label: "AAAA: IPv6 queries", + pointRadius: 0, + pointHitRadius: 5, + pointHoverRadius: 5, + data: [], + cubicInterpolationMode: "monotone" + } + ] + }, + options: { + tooltips: { + enabled: true, + mode: "x-axis", + callbacks: { + title: function(tooltipItem, data) { + 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 "Query types from "+from+" to "+to; }, - { - label: "AAAA: IPv6 queries", - pointRadius: 0, - pointHitRadius: 5, - pointHoverRadius: 5, - data: [] + label: function(tooltipItems, data) { + return data.datasets[tooltipItems.datasetIndex].label + ": " + (100.0*tooltipItems.yLabel).toFixed(1) + "%"; } - ] + } }, - options: { - tooltips: { - enabled: true, - mode: "x-axis", - callbacks: { - title: function(tooltipItem, data) { - 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 "Query types from "+from+" to "+to; + legend: { + display: false + }, + scales: { + xAxes: [{ + type: "time", + time: { + unit: "hour", + displayFormats: { + hour: "HH:mm" }, - label: function(tooltipItems, data) { - return data.datasets[tooltipItems.datasetIndex].label + ": " + (100.0*tooltipItems.yLabel).toFixed(1) + "%"; - } + tooltipFormat: "HH:mm" } - }, - 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, index, values) { + return Math.round(value*100) + " %"; } - }], - yAxes: [{ - ticks: { - mix: 0.0, - max: 1.0, - beginAtZero: true, - callback: function(value, index, values) { - return Math.round(value*100) + " %"; - } - }, - stacked: true - }] - }, - maintainAspectRatio: true - } - }); - - // Pull in data via AJAX - updateQueryTypesOverTime(); - } - - // Create / load "Top Domains" and "Top Advertisers" only if authorized - if(document.getElementById("domain-frequency") - && document.getElementById("ad-frequency")) - { - updateTopLists(); - } - - // Create / load "Top Clients" only if authorized - if(document.getElementById("client-frequency")) - { - updateTopClientsChart(); - } - - $("#queryOverTimeChart").click(function(evt){ - var activePoints = timeLineChart.getElementAtEvent(evt); - if(activePoints.length > 0) - { - //get the internal index of slice in pie chart - var clickedElementindex = activePoints[0]["_index"]; - - //get specific label by index - var label = timeLineChart.data.labels[clickedElementindex]; - - //get value by index - var from = label/1000 - 300; - var until = label/1000 + 300; - window.location.href = "queries.php?from="+from+"&until="+until; + }, + stacked: true + }] + }, + maintainAspectRatio: true } - return false; }); + + // Pull in data via AJAX + updateQueryTypesOverTime(); + } + + // Create / load "Top Domains" and "Top Advertisers" only if authorized + if(document.getElementById("domain-frequency") + && document.getElementById("ad-frequency")) + { + updateTopLists(); + } + + // Create / load "Top Clients" only if authorized + if(document.getElementById("client-frequency")) + { + updateTopClientsChart(); + } + + $("#queryOverTimeChart").click(function(evt){ + var activePoints = timeLineChart.getElementAtEvent(evt); + if(activePoints.length > 0) + { + //get the internal index of slice in pie chart + var clickedElementindex = activePoints[0]["_index"]; + + //get specific label by index + var label = timeLineChart.data.labels[clickedElementindex]; + + //get value by index + var from = label/1000 - 300; + var until = label/1000 + 300; + window.location.href = "queries.php?from="+from+"&until="+until; + } + return false; }); + + if(document.getElementById("queryTypePieChart")) + { + ctx = document.getElementById("queryTypePieChart").getContext("2d"); + queryTypePieChart = new Chart(ctx, { + type: "doughnut", + data: { + labels: [], + datasets: [{ data: [] }] + }, + options: { + legend: { + display: true, + position: "right" + }, + tooltips: { + enabled: true, + callbacks: { + title: function(tooltipItem, data) { + return "Query types"; + }, + label: function(tooltipItems, data) { + var dataset = data.datasets[tooltipItems.datasetIndex]; + var label = data.labels[tooltipItems.index]; + return label + ": " + dataset.data[tooltipItems.index].toFixed(1) + "%"; + } + } + }, + animation: { + duration: 750 + }, + cutoutPercentage: 0 + } + }); + + // Pull in data via AJAX + updateQueryTypesPie(); + } + + if(document.getElementById("forwardDestinationPieChart")) + { + ctx = document.getElementById("forwardDestinationPieChart").getContext("2d"); + forwardDestinationPieChart = new Chart(ctx, { + type: "doughnut", + data: { + labels: [], + datasets: [{ data: [] }] + }, + options: { + legend: { + display: true, + position: "right" + }, + tooltips: { + enabled: true, + callbacks: { + title: function(tooltipItem, data) { + return "Forward destinations"; + }, + label: function(tooltipItems, data) { + var dataset = data.datasets[tooltipItems.datasetIndex]; + var label = data.labels[tooltipItems.index]; + return label + ": " + dataset.data[tooltipItems.index].toFixed(1) + "%"; + } + } + }, + animation: { + duration: 750 + }, + cutoutPercentage: 0 + } + }); + + // Pull in data via AJAX + updateForwardDestinationsPie(); + } +}); diff --git a/scripts/pi-hole/js/list.js b/scripts/pi-hole/js/list.js index 1b01387c..e438f1b9 100644 --- a/scripts/pi-hole/js/list.js +++ b/scripts/pi-hole/js/list.js @@ -141,10 +141,9 @@ function add(arg) { $.ajax({ url: "scripts/pi-hole/php/add.php", method: "post", - data: {"domain":domain.val(), "list":locallistType, "token":token}, + data: {"domain":domain.val().trim(), "list":locallistType, "token":token}, success: function(response) { - if (response.indexOf("not a valid argument") >= 0 || - response.indexOf("is not a valid domain") >= 0) { + if (response.indexOf("] Pi-hole blocking is ") === -1) { alFailure.show(); err.html(response); alFailure.delay(4000).fadeOut(2000, function() { diff --git a/scripts/pi-hole/js/queries.js b/scripts/pi-hole/js/queries.js index 43952e47..72b4a002 100644 --- a/scripts/pi-hole/js/queries.js +++ b/scripts/pi-hole/js/queries.js @@ -3,7 +3,7 @@ * 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. */ +* Please see LICENSE file for your rights under this license. */ var tableApi; function escapeRegex(text) { @@ -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%" }, ], @@ -185,8 +185,33 @@ $(document).ready(function() { "targets": -1, "data": null, "defaultContent": "" - } ] + } ], + "initComplete": function () { + var api = this.api(); + // Query type IPv4 / IPv6 + api.$("td:eq(1)").click( function () { api.search( this.innerHTML ).draw(); $("#resetButton").show(); } ); + api.$("td:eq(1)").hover( + function () { this.title="Click to show only "+this.innerHTML+" queries"; this.style.color="#72afd2"; }, + function () { this.style.color=""; } + ); + api.$("td:eq(1)").css("cursor","pointer"); + // Domain + api.$("td:eq(2)").click( function () { api.search( this.innerHTML ).draw(); $("#resetButton").show(); } ); + api.$("td:eq(2)").hover( + function () { this.title="Click to show only queries with domain "+this.innerHTML; this.style.color="#72afd2"; }, + function () { this.style.color=""; } + ); + api.$("td:eq(2)").css("cursor","pointer"); + // Client + api.$("td:eq(3)").click( function () { api.search( this.innerHTML ).draw(); $("#resetButton").show(); } ); + api.$("td:eq(3)").hover( + function () { this.title="Click to show only queries made by "+this.innerHTML; this.style.color="#72afd2"; }, + function () { this.style.color=""; } + ); + api.$("td:eq(3)").css("cursor","pointer"); + } }); + $("#all-queries tbody").on( "click", "button", function () { var data = tableApi.row( $(this).parents("tr") ).data(); if (data[4] === "1" || data[4] === "5") @@ -198,6 +223,8 @@ $(document).ready(function() { add(data[2],"black"); } } ); + + $("#resetButton").click( function () { tableApi.search("").draw(); $("#resetButton").hide(); } ); } ); diff --git a/scripts/pi-hole/js/queryads.js b/scripts/pi-hole/js/queryads.js index 5d56068f..b86a0309 100644 --- a/scripts/pi-hole/js/queryads.js +++ b/scripts/pi-hole/js/queryads.js @@ -57,9 +57,10 @@ function httpGet(ta,quiet,theUrl) function eventsource() { var ta = $("#output"); - var domain = $("#domain"); + var domain = $("#domain").val().trim(); var q = $("#quiet"); - if(domain.val().length === 0) + + if(domain.length === 0) { return; } @@ -73,12 +74,12 @@ function eventsource() { // IE does not support EventSource - load whole content at once if (typeof EventSource !== "function") { - httpGet(ta,quiet,"/admin/scripts/pi-hole/php/queryads.php?domain="+domain.val().toLowerCase()+exact+"&IE"); + httpGet(ta,quiet,"/admin/scripts/pi-hole/php/queryads.php?domain="+domain.toLowerCase()+exact+"&IE"); return; } var host = window.location.host; - var source = new EventSource("/admin/scripts/pi-hole/php/queryads.php?domain="+domain.val().toLowerCase()+"&"+exact); + var source = new EventSource("/admin/scripts/pi-hole/php/queryads.php?domain="+domain.toLowerCase()+"&"+exact); // Reset and show field ta.empty(); diff --git a/scripts/pi-hole/js/settings.js b/scripts/pi-hole/js/settings.js index 74991352..0edae1bc 100644 --- a/scripts/pi-hole/js/settings.js +++ b/scripts/pi-hole/js/settings.js @@ -17,6 +17,22 @@ $(function () { $("input[name=\"AddMAC\"]").val(mac); }); }); +$(".confirm-poweroff").confirm({ + text: "Are you sure you want to send a poweroff command to your Pi-Hole?", + title: "Confirmation required", + confirm(button) { + $("#poweroffform").submit(); + }, + cancel(button) { + // nothing to do + }, + confirmButton: "Yes, poweroff", + cancelButton: "No, go back", + post: true, + confirmButtonClass: "btn-danger", + cancelButtonClass: "btn-success", + dialogClass: "modal-dialog modal-mg" // Bootstrap classes for mid-size modal +}); $(".confirm-reboot").confirm({ text: "Are you sure you want to send a reboot command to your Pi-Hole?", title: "Confirmation required", @@ -68,6 +84,23 @@ $(".confirm-flushlogs").confirm({ dialogClass: "modal-dialog modal-mg" }); +$(".confirm-disablelogging").confirm({ + text: "Note that disabling query logging will render graphs on the web user interface useless. Are you sure you want to disable your logging?", + title: "Confirmation required", + confirm(button) { + $("#disablelogsform").submit(); + }, + cancel(button) { + // nothing to do + }, + confirmButton: "Yes, disable logs", + cancelButton: "No, go back", + post: true, + confirmButtonClass: "btn-danger", + cancelButtonClass: "btn-success", + dialogClass: "modal-dialog modal-mg" +}); + $(".api-token").confirm({ text: "Make sure that nobody else can scan this code around you. They will have full access to the API without having to know the password. Note that the generation of the QR code will take some time.", title: "Confirmation required", @@ -102,9 +135,6 @@ $(document).ready(function() { "scrollY": "200px", "scrollX" : true }); - $("#leaseexpand").on( "click", function () { - setTimeout(function(){leasetable.draw();},100); - } ); } if(document.getElementById("DHCPStaticLeasesTable")) { @@ -116,10 +146,13 @@ $(document).ready(function() { "scrollY": "200px", "scrollX" : true }); - $("#leaseexpand").on( "click", function () { - setTimeout(function(){staticleasetable.draw();},100); - } ); } + //call draw() on each table... they don't render properly with scrollX and scrollY set... ¯\_(ツ)_/¯ + $("a[data-toggle=\"tab\"]").on("shown.bs.tab", function (e) { + leasetable.draw(); + staticleasetable.draw(); + }); + } ); // Handle hiding of alerts @@ -136,12 +169,39 @@ $(document).ready(function(){ // Handle list deletion $("button[id^='adlist-btn-']").on("click", function (e) { + var id = parseInt($(this).context.id.replace(/[^0-9\.]/g, ""), 10); e.preventDefault(); - var status = $(this).siblings("input[name^='adlist-del-']").is(":checked"); + var status = $("input[name=\"adlist-del-"+id+"\"]").is(":checked"); var textType = status ? "none" : "line-through"; - $(this).siblings("input[name^='adlist-del-']").prop("checked", !status); - $(this).siblings("input[name^='adlist-enable-']").prop("disabled", !status); - $(this).siblings("a").css("text-decoration", textType); + // Check hidden delete box (or reset) + $("input[name=\"adlist-del-"+id+"\"]").prop("checked", !status); + // Untick and disable check box (or reset) + $("input[name=\"adlist-enable-"+id+"\"]").prop("checked", status).prop("disabled", !status); + // Strink through text (or reset) + $("a[id=\"adlist-text-"+id+"\"]").css("text-decoration", textType); + // Highlight that the button has to be clicked in order to make the change live + $("button[id=\"blockinglistsaveupdate\"]").addClass("btn-danger").css("font-weight", "bold"); + +}); + +// Change "?tab=" parameter in URL for save and reload +$(".nav-tabs a").on("shown.bs.tab", function (e) { + window.history.pushState("", "", "?tab=" + e.target.hash.substring(1)); + window.scrollTo(0, 0); +}); + +// Auto dismissal for info and error notifications +$(document).ready(function(){ + var alInfo = $("#alInfo"); + var alError = $("#alError"); + if(alInfo.length) + { + alInfo.delay(3000).fadeOut(2000, function() { alInfo.hide(); }); + } + if(alError.length) + { + alError.delay(3000).fadeOut(2000, function() { alError.hide(); }); + } }); diff --git a/scripts/pi-hole/php/FTL.php b/scripts/pi-hole/php/FTL.php index 4b20e528..0c07ce5d 100644 --- a/scripts/pi-hole/php/FTL.php +++ b/scripts/pi-hole/php/FTL.php @@ -6,10 +6,19 @@ * This file is copyright under the latest version of the EUPL. * Please see LICENSE file for your rights under this license. */ -function testFTL() +function testFTL($address) { - $ret = shell_exec("pidof pihole-FTL"); - return intval($ret); + if($address === "127.0.0.1") + { + $ret = shell_exec("pidof pihole-FTL"); + return intval($ret); + } + // We cannot relly test for a distant FTL instance + // in the same way, so for any other IP address + // we simply return true here and rely on the API + // socket connection itself to fail if there is nothing + // on that address + return true; } function connectFTL($address, $port=4711, $quiet=true) 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/auth.php b/scripts/pi-hole/php/auth.php index a8a64e33..85d38f8f 100644 --- a/scripts/pi-hole/php/auth.php +++ b/scripts/pi-hole/php/auth.php @@ -119,7 +119,7 @@ function check_csrf($token) { function check_domain() { if(isset($_POST['domain'])){ - $domains = explode(" ",$_POST['domain']); + $domains = preg_split('\s+', $_POST['domain']); foreach($domains as $domain) { $validDomain = is_valid_domain_name($domain); @@ -131,7 +131,7 @@ function check_domain() { } function list_verify($type) { - global $pwhash, $wrongpassword; + global $pwhash, $wrongpassword, $auth; if(!isset($_POST['domain']) || !isset($_POST['list']) || !(isset($_POST['pw']) || isset($_POST['token']))) { log_and_die("Missing POST variables"); } @@ -144,11 +144,7 @@ function list_verify($type) { elseif(isset($_POST['pw'])) { require("password.php"); - if(strlen($pwhash) == 0) - { - log_and_die("No password set - ".htmlspecialchars($type)."listing with password not supported"); - } - elseif($wrongpassword) + if($wrongpassword || !$auth) { log_and_die("Wrong password - ".htmlspecialchars($type)."listing of ${_POST['domain']} not permitted"); } diff --git a/scripts/pi-hole/php/footer.php b/scripts/pi-hole/php/footer.php index 1f20eae0..d9ca6cec 100644 --- a/scripts/pi-hole/php/footer.php +++ b/scripts/pi-hole/php/footer.php @@ -11,17 +11,16 @@
' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + ' ' + + '' + + '
' + + '
' + + '
'; + + 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..709a1a67 100644 --- a/settings.php +++ b/settings.php @@ -5,931 +5,1212 @@ * * This file is copyright under the latest version of the EUPL. * Please see LICENSE file for your rights under this license. */ - require "scripts/pi-hole/php/header.php"; - require "scripts/pi-hole/php/savesettings.php"; - // Reread ini file as things might have been changed - $setupVars = parse_ini_file("/etc/pihole/setupVars.conf"); +require "scripts/pi-hole/php/header.php"; +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)))); +} + ?> refresh to the gravity page and start updating immediately -?> - - +if (isset($_POST["submit"])) { + if ($_POST["submit"] == "saveupdate") { + // If that is the case -> refresh to the gravity page and start updating immediately + ?> + + - - + + - 0){ ?> - + 0) { ?> + - 0){ ?> - + 0) { ?> + -
-
+ -
-
-

Networking

-
-
-
- -
-
- -
-
-
- -
-
- -
-
-
- -
-
- -
-

Warning: PHP has been compiled without IPv6 support.

-
-
- -
-
- -
-
-
-
+ -
-
-

Pi-hole DHCP Server

-
-
-
-
-
-
-
-
-
-

hidden>Make sure your router's DHCP server is disabled when using the Pi-hole DHCP server!

-
-
- -
-
-
-
-
From
- disabled> -
-
-
-
-
-
-
To
- disabled> -
-
-
-
- -
-
-
Router
- disabled> -
-
-
-
-
-
-

Advanced DHCP settings

-
-
-
-
-
-
-
-
-
- -
-
-
Domain
- disabled> -
-
-
-
- -
-
-
Lease time in hours
- disabled> -
-
-

Hint: 0 = infinite, 24 = one day, 168 = one week, 744 = one month, 8760 = one year

-
-
-
-
-unknown"; - } - - $clid = $line[4]; - if($clid == "*") - { - $clid = "unknown"; - } - - array_push($dhcp_leases,["TIME"=>$time, "hwaddr"=>strtoupper($line[1]), "IP"=>$line[2], "host"=>$host, "clid"=>$clid, "type"=>$type]); - } - } +$i = 1; +while (isset($setupVars["PIHOLE_DNS_" . $i])) { + if (isinserverlist($setupVars["PIHOLE_DNS_" . $i])) { + array_push($DNSactive, $setupVars["PIHOLE_DNS_" . $i]); + } elseif (strpos($setupVars["PIHOLE_DNS_" . $i], ".")) { + if (!isset($custom1)) { + $custom1 = $setupVars["PIHOLE_DNS_" . $i]; + } else { + $custom2 = $setupVars["PIHOLE_DNS_" . $i]; + } + } elseif (strpos($setupVars["PIHOLE_DNS_" . $i], ":")) { + if (!isset($custom3)) { + $custom3 = $setupVars["PIHOLE_DNS_" . $i]; + } else { + $custom4 = $setupVars["PIHOLE_DNS_" . $i]; + } + } + $i++; } -readStaticLeasesFile(); +if (isset($setupVars["DNS_FQDN_REQUIRED"])) { + if ($setupVars["DNS_FQDN_REQUIRED"]) { + $DNSrequiresFQDN = true; + } else { + $DNSrequiresFQDN = false; + } +} else { + $DNSrequiresFQDN = true; +} + +if (isset($setupVars["DNS_BOGUS_PRIV"])) { + if ($setupVars["DNS_BOGUS_PRIV"]) { + $DNSbogusPriv = true; + } else { + $DNSbogusPriv = false; + } +} else { + $DNSbogusPriv = true; +} + +if (isset($setupVars["DNSSEC"])) { + if ($setupVars["DNSSEC"]) { + $DNSSEC = true; + } else { + $DNSSEC = false; + } +} else { + $DNSSEC = false; +} + +if (isset($setupVars["DNSMASQ_LISTENING"])) { + if ($setupVars["DNSMASQ_LISTENING"] === "single") { + $DNSinterface = "single"; + } elseif ($setupVars["DNSMASQ_LISTENING"] === "all") { + $DNSinterface = "all"; + } else { + $DNSinterface = "local"; + } +} else { + $DNSinterface = "single"; +} ?> -
-
collapsed-box"> -
-

DHCP leases

-
-
-
-
- - - - - - - - - - - -
Remaining lease time:
DHCP UID: "> - -
MAC addressIP addressHostname

-
-
- - - - - - - - - - - - - - - - -
MAC addressIP addressHostname
0){ ?>
-

Specifying the MAC address is mandatory and only one entry per MAC address is allowed. If the IP address is omitted and a host name is given, the IP address will still be generated dynamically and the specified host name will be used. If the host name is omitted, only a static lease will be added.

-
-
-
-
-
- - -
-
-
-
-
-
-

Query Logging (size of log )

-
-
-

Current status: - - Enabled (recommended) - - Disabled -

- -

Note that disabling will render graphs on the web user interface useless

- -
- -
- -
-
-

Pi-Hole's Block Lists

-
-
-
-
-
- - $value) { ?> -
-
- - -
- - + + +
+
+ +
+
diff --git a/style/pi-hole.css b/style/pi-hole.css index 6f090058..3fc896c8 100644 --- a/style/pi-hole.css +++ b/style/pi-hole.css @@ -4,8 +4,10 @@ * * This file is copyright under the latest version of the EUPL. * Please see LICENSE file for your rights under this license. */ + .small-box { - -webkit-user-select: none; /* Chrome/Safari */ + cursor: default; + -webkit-user-select: none; /* Chrome/Safari */ -moz-user-select: none; /* Firefox */ -ms-user-select: none; /* IE10+ */ @@ -30,7 +32,21 @@ a.lookatme { -webkit-animation-name: Pulse; animation-name: Pulse; -webkit-animation-duration: 2s; - animation-duration: 2s; + animation-duration: 2s; -webkit-animation-iteration-count: infinite; animation-iteration-count: infinite; } +#all-queries { + table-layout: fixed; +} +#all-queries tbody { + word-wrap: break-word; +} +.main-header>.navbar { + height: 50px; +} + +#resetButton { + color: red; + font-weight: bold; +} diff --git a/style/vendor/daterangepicker.css b/style/vendor/daterangepicker.css new file mode 100644 index 00000000..dfce19cb --- /dev/null +++ b/style/vendor/daterangepicker.css @@ -0,0 +1,232 @@ +.daterangepicker { + position: absolute; + color: inherit; + background: #fff; + border-radius: 4px; + width: 278px; + padding: 4px; + margin-top: 1px; + top: 100px; + left: 20px; + /* Calendars */ } + .daterangepicker:before, .daterangepicker:after { + position: absolute; + display: inline-block; + border-bottom-color: rgba(0, 0, 0, 0.2); + content: ''; } + .daterangepicker:before { + top: -7px; + border-right: 7px solid transparent; + border-left: 7px solid transparent; + border-bottom: 7px solid #ccc; } + .daterangepicker:after { + top: -6px; + border-right: 6px solid transparent; + border-bottom: 6px solid #fff; + border-left: 6px solid transparent; } + .daterangepicker.opensleft:before { + right: 9px; } + .daterangepicker.opensleft:after { + right: 10px; } + .daterangepicker.openscenter:before { + left: 0; + right: 0; + width: 0; + margin-left: auto; + margin-right: auto; } + .daterangepicker.openscenter:after { + left: 0; + right: 0; + width: 0; + margin-left: auto; + margin-right: auto; } + .daterangepicker.opensright:before { + left: 9px; } + .daterangepicker.opensright:after { + left: 10px; } + .daterangepicker.dropup { + margin-top: -5px; } + .daterangepicker.dropup:before { + top: initial; + bottom: -7px; + border-bottom: initial; + border-top: 7px solid #ccc; } + .daterangepicker.dropup:after { + top: initial; + bottom: -6px; + border-bottom: initial; + border-top: 6px solid #fff; } + .daterangepicker.dropdown-menu { + max-width: none; + z-index: 3001; } + .daterangepicker.single .ranges, .daterangepicker.single .calendar { + float: none; } + .daterangepicker.show-calendar .calendar { + display: block; } + .daterangepicker .calendar { + display: none; + max-width: 270px; + margin: 4px; } + .daterangepicker .calendar.single .calendar-table { + border: none; } + .daterangepicker .calendar th, .daterangepicker .calendar td { + white-space: nowrap; + text-align: center; + min-width: 32px; } + .daterangepicker .calendar-table { + border: 1px solid #fff; + padding: 4px; + border-radius: 4px; + background: #fff; } + .daterangepicker table { + width: 100%; + margin: 0; } + .daterangepicker td, .daterangepicker th { + text-align: center; + width: 20px; + height: 20px; + border-radius: 4px; + border: 1px solid transparent; + white-space: nowrap; + cursor: pointer; } + .daterangepicker td.available:hover, .daterangepicker th.available:hover { + background: #eee; } + .daterangepicker td.week, .daterangepicker th.week { + font-size: 80%; + color: #ccc; } + .daterangepicker td.off, .daterangepicker td.off.in-range, .daterangepicker td.off.start-date, .daterangepicker td.off.end-date { + background-color: #fff; + border-color: transparent; + color: #999; } + .daterangepicker td.in-range { + background-color: #ebf4f8; + border-color: transparent; + color: #000; + border-radius: 0; } + .daterangepicker td.start-date { + border-radius: 4px 0 0 4px; } + .daterangepicker td.end-date { + border-radius: 0 4px 4px 0; } + .daterangepicker td.start-date.end-date { + border-radius: 4px; } + .daterangepicker td.active, .daterangepicker td.active:hover { + background-color: #357ebd; + border-color: transparent; + color: #fff; } + .daterangepicker th.month { + width: auto; } + .daterangepicker td.disabled, .daterangepicker option.disabled { + color: #999; + cursor: not-allowed; + text-decoration: line-through; } + .daterangepicker select.monthselect, .daterangepicker select.yearselect { + font-size: 12px; + padding: 1px; + height: auto; + margin: 0; + cursor: default; } + .daterangepicker select.monthselect { + margin-right: 2%; + width: 56%; } + .daterangepicker select.yearselect { + width: 40%; } + .daterangepicker select.hourselect, .daterangepicker select.minuteselect, .daterangepicker select.secondselect, .daterangepicker select.ampmselect { + width: 50px; + margin-bottom: 0; } + .daterangepicker .input-mini { + border: 1px solid #ccc; + border-radius: 4px; + color: #555; + height: 30px; + line-height: 30px; + display: block; + vertical-align: middle; + margin: 0 0 5px 0; + padding: 0 6px 0 28px; + width: 100%; } + .daterangepicker .input-mini.active { + border: 1px solid #08c; + border-radius: 4px; } + .daterangepicker .daterangepicker_input { + position: relative; } + .daterangepicker .daterangepicker_input i { + position: absolute; + left: 8px; + top: 8px; } + .daterangepicker .calendar-time { + text-align: center; + margin: 5px auto; + line-height: 30px; + position: relative; + padding-left: 28px; } + .daterangepicker .calendar-time select.disabled { + color: #ccc; + cursor: not-allowed; } + +.ranges { + font-size: 11px; + float: none; + margin: 4px; + text-align: left; } + .ranges ul { + list-style: none; + margin: 0 auto; + padding: 0; + width: 100%; } + .ranges li { + font-size: 13px; + background: #f5f5f5; + border: 1px solid #f5f5f5; + border-radius: 4px; + color: #08c; + padding: 3px 12px; + margin-bottom: 8px; + cursor: pointer; } + .ranges li:hover { + background: #08c; + border: 1px solid #08c; + color: #fff; } + .ranges li.active { + background: #08c; + border: 1px solid #08c; + color: #fff; } + +/* Larger Screen Styling */ +@media (min-width: 564px) { + .daterangepicker { + width: auto; } + .daterangepicker .ranges ul { + width: 160px; } + .daterangepicker.single .ranges ul { + width: 100%; } + .daterangepicker.single .calendar.left { + clear: none; } + .daterangepicker.single .ranges, .daterangepicker.single .calendar { + float: left; } + .daterangepicker .calendar.left { + clear: left; + margin-right: 0; } + .daterangepicker .calendar.left .calendar-table { + border-right: none; + border-top-right-radius: 0; + border-bottom-right-radius: 0; } + .daterangepicker .calendar.right { + margin-left: 0; } + .daterangepicker .calendar.right .calendar-table { + border-left: none; + border-top-left-radius: 0; + border-bottom-left-radius: 0; } + .daterangepicker .left .daterangepicker_input { + padding-right: 12px; } + .daterangepicker .calendar.left .calendar-table { + padding-right: 12px; } + .daterangepicker .ranges, .daterangepicker .calendar { + float: left; } } + +@media (min-width: 730px) { + .daterangepicker .ranges { + width: auto; + float: left; } + .daterangepicker .calendar.left { + clear: none; } } +