diff --git a/.codespellignore b/.codespellignore new file mode 100644 index 0000000000..57f7ed6a81 --- /dev/null +++ b/.codespellignore @@ -0,0 +1,3 @@ +ede +EDE +doubleclick diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..81d63a06e2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,39 @@ +# EditorConfig is awesome: https://editorconfig.org/ + +# top-most EditorConfig file +root = true + +# A newline ending every file +[*] +insert_final_newline = true +indent_style = space +indent_size = 4 +charset = utf-8 +trim_trailing_whitespace = true + +[*.css] +indent_size = 2 + +[*.js] +indent_size = 2 + +[package.json] +indent_size = 2 + +[.yamllint.conf] +indent_size = 2 + +[*.yml] +indent_size = 2 + +[*.md] +indent_size = 2 + +# Ignore paths +[**/vendor/**] +charset = unset +end_of_line = unset +insert_final_newline = unset +trim_trailing_whitespace = unset +indent_style = unset +indent_size = unset diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..176a458f94 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 6f277b2158..829de9da1c 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,33 +1,38 @@ -**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]:** -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. +**Expected behavior:** -**[BUG | ISSUE] Expected Behaviour:** +`{A detailed description of what you expect to see}` +**Actual behavior:** -**[BUG | ISSUE] Actual Behaviour:** +`{A detailed description and/or screenshots of what you do see}` +**Steps to reproduce:** -**[BUG | ISSUE] Steps to reproduce:** +`{Detailed steps of how we can reproduce this}` -- -- -- -- +**Debug token provided by [uploading `pihole -d` log](https://discourse.pi-hole.net/t/the-pihole-command-with-examples/738#debug):** -**(Optional) Debug token generated by `pihole -d`:** +`{Alphanumeric token}` -`` +**Troubleshooting undertaken, and/or other relevant information:** -_This template was created based on the work of [`udemy-dl`](https://github.com/nishad/udemy-dl/blob/master/LICENSE)._ +`{Steps of what you have done to fix this}` + +> - `{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 deleted file mode 100644 index 2c3914283b..0000000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,19 +0,0 @@ -**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_:** - -***Please submit all pull requests against the `development` branch. Failure to do so will delay or deny your request*** - -- [] 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}_ - ---- -_{replace this line with your pull request content}_ - - -_This template was created based on the work of [`udemy-dl`](https://github.com/nishad/udemy-dl/blob/master/LICENSE)._ diff --git a/.github/dco.yml b/.github/dco.yml new file mode 100644 index 0000000000..0c4b142e9a --- /dev/null +++ b/.github/dco.yml @@ -0,0 +1,2 @@ +require: + members: false diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..91eb587456 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,66 @@ +version: 2 +updates: +- package-ecosystem: npm + directory: "/" + schedule: + interval: weekly + day: saturday + time: "10:00" + open-pull-requests-limit: 10 + target-branch: devel + versioning-strategy: increase + reviewers: + - "pi-hole/web-maintainers" +- package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + day: saturday + time: "10:00" + open-pull-requests-limit: 10 + target-branch: devel + reviewers: + - "pi-hole/web-maintainers" +- package-ecosystem: composer + directory: "/" + schedule: + interval: weekly + day: saturday + time: "10:00" + open-pull-requests-limit: 10 + target-branch: devel + reviewers: + - "pi-hole/web-maintainers" + +# As above, but for development-v6 +- package-ecosystem: npm + directory: "/" + schedule: + interval: weekly + day: saturday + time: "10:00" + open-pull-requests-limit: 10 + target-branch: development-v6 + versioning-strategy: increase + reviewers: + - "pi-hole/web-maintainers" +- package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + day: saturday + time: "10:00" + open-pull-requests-limit: 10 + target-branch: development-v6 + reviewers: + - "pi-hole/web-maintainers" +- package-ecosystem: composer + directory: "/" + schedule: + interval: weekly + day: saturday + time: "10:00" + open-pull-requests-limit: 10 + target-branch: development-v6 + reviewers: + - "pi-hole/web-maintainers" diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000000..2e8776e999 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,7 @@ +changelog: + exclude: + labels: + - internal + authors: + - dependabot + - github-actions diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000000..6fff93e725 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,35 @@ +name: "CodeQL" + +on: + push: + branches: + - master + - devel + - "!dependabot/**" + pull_request: + # The branches below must be a subset of the branches above + branches: + - master + - devel + schedule: + - cron: "0 0 * * 0" + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4.1.1 + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: "javascript" + + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml new file mode 100644 index 0000000000..9e1e9258b8 --- /dev/null +++ b/.github/workflows/codespell.yml @@ -0,0 +1,20 @@ +name: Codespell + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + +jobs: + spell-check: + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + steps: + - + name: Checkout repository + uses: actions/checkout@v4.1.1 + - + name: Spell-Checking + uses: codespell-project/actions-codespell@master + with: + ignore_words_file: .codespellignore + skip: ./scripts/vendor,./style/vendor,./package.json,./package-lock.json,./composer.json,./composer.lock diff --git a/.github/workflows/editorconfig-checker b/.github/workflows/editorconfig-checker new file mode 100644 index 0000000000..2cb7bfea29 --- /dev/null +++ b/.github/workflows/editorconfig-checker @@ -0,0 +1,14 @@ +name: editorconfig-checker + +on: + pull_request: + push: + +jobs: + build: + name: editorconfig-checker + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + - uses: editorconfig-checker/action-editorconfig-checker@main + - run: editorconfig-checker diff --git a/.github/workflows/editorconfig-checker.yml b/.github/workflows/editorconfig-checker.yml new file mode 100644 index 0000000000..db28dc97a6 --- /dev/null +++ b/.github/workflows/editorconfig-checker.yml @@ -0,0 +1,14 @@ +name: editorconfig-checker + +on: + pull_request: + push: + +jobs: + build: + name: editorconfig-checker + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.1.1 + - uses: editorconfig-checker/action-editorconfig-checker@main + - run: editorconfig-checker diff --git a/.github/workflows/merge-conflict.yml b/.github/workflows/merge-conflict.yml new file mode 100644 index 0000000000..438b23c6d6 --- /dev/null +++ b/.github/workflows/merge-conflict.yml @@ -0,0 +1,21 @@ +name: "Check for merge conflicts" +on: + # So that PRs touching the same files as the push are updated + push: + # So that the `dirtyLabel` is removed if conflicts are resolve + # We recommend `pull_request_target` so that github secrets are available. + # In `pull_request` we wouldn't be able to change labels of fork PRs + pull_request_target: + types: [synchronize] + +jobs: + main: + runs-on: ubuntu-latest + steps: + - name: Check if PRs are have merge conflicts + uses: eps1lon/actions-label-merge-conflict@v2.1.0 + with: + dirtyLabel: "Merge Conflicts" + repoToken: "${{ secrets.GITHUB_TOKEN }}" + commentOnDirty: "This pull request has conflicts, please resolve those before we can evaluate the pull request." + commentOnClean: "Conflicts have been resolved." diff --git a/.github/workflows/php-cs-fixer.yml b/.github/workflows/php-cs-fixer.yml new file mode 100644 index 0000000000..35604906a7 --- /dev/null +++ b/.github/workflows/php-cs-fixer.yml @@ -0,0 +1,13 @@ +# .github/workflows/php-cs-fixer.yml +on: [push, pull_request] +name: Lint +jobs: + php-cs-fixer: + name: PHP-CS-Fixer + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.1.1 + - name: PHP-CS-Fixer + uses: docker://oskarstark/php-cs-fixer-ga + with: + args: --diff --dry-run diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml new file mode 100644 index 0000000000..23e257780b --- /dev/null +++ b/.github/workflows/phpstan.yml @@ -0,0 +1,21 @@ +name: PHPStan + +on: + push: + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.1.1 + + - name: Install composer + uses: php-actions/composer@v6 + + - name: Run PHPStan + uses: php-actions/phpstan@v3 + with: + configuration: phpstan.neon.dist + memory_limit: 256M + diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000000..055d59c519 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,48 @@ +name: Mark stale issues + +on: + schedule: + - cron: '0 8 * * *' + workflow_dispatch: + issue_comment: + +env: + stale_label: stale + +jobs: + stale_action: + if: github.event_name != 'issue_comment' + runs-on: ubuntu-latest + permissions: + issues: write + + steps: + - uses: actions/stale@v8.0.0 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + days-before-stale: 30 + days-before-close: 5 + stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Please comment or update this issue or it will be closed in 5 days.' + stale-issue-label: '${{ env.stale_label }}' + exempt-issue-labels: 'internal, Fixed In Next Release, Bug, never-stale' + exempt-all-issue-assignees: true + operations-per-run: 300 + close-issue-reason: 'not_planned' + + remove_stale: + # trigger "stale" removal immediately when stale issues are commented on + # we need to explicitly check that the trigger does not run on comment on a PR as + # 'issue_comment' triggers on issues AND PR comments + # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issue_comment-on-issues-only-or-pull-requests-only + if: ${{ !github.event.issue.pull_request && github.event_name != 'schedule' }} + permissions: + contents: read # for actions/checkout + issues: write # to edit issues label + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4.1.1 + - name: Remove 'stale' label + run: gh issue edit ${{ github.event.issue.number }} --remove-label ${{ env.stale_label }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/stale_pr.yml b/.github/workflows/stale_pr.yml new file mode 100644 index 0000000000..17cd31e380 --- /dev/null +++ b/.github/workflows/stale_pr.yml @@ -0,0 +1,35 @@ +name: Close stale PR +# This action will add a `stale` label and close immediately every PR that meets the following conditions: +# - it is already marked with "merge conflict" label +# - there was no update/comment on the PR in the last 30 days. + +on: + schedule: + - cron: '0 10 * * *' + workflow_dispatch: + +jobs: + stale: + + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + + steps: + - uses: actions/stale@v8.0.0 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + # Do not automatically mark PR/issue as stale + days-before-stale: -1 + # Override 'days-before-stale' for PR only + days-before-pr-stale: 30 + # Close PRs immediately, after marking them 'stale' + days-before-pr-close: 0 + # only run the action on merge conflict PR + any-of-labels: 'Merge Conflicts' + exempt-pr-labels: 'internal, never-stale, ON HOLD, WIP' + exempt-all-pr-assignees: true + operations-per-run: 300 + stale-pr-message: '' + close-pr-message: 'Existing merge conflicts have not been addressed. This PR is considered abandoned.' diff --git a/.github/workflows/sync-back-to-dev.yml b/.github/workflows/sync-back-to-dev.yml new file mode 100644 index 0000000000..cb53b2f69f --- /dev/null +++ b/.github/workflows/sync-back-to-dev.yml @@ -0,0 +1,18 @@ +name: Sync Back to Development + +on: + push: + branches: + - master + +jobs: + sync-branches: + runs-on: ubuntu-latest + name: Syncing branches + steps: + - name: Checkout + uses: actions/checkout@v4.1.1 + - name: Opening pull request + run: gh pr create -B devel -H master --title 'Sync master back into development' --body 'Created by Github action' --label 'internal' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000000..48e1c66339 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,34 @@ +name: Tests + +on: + push: + branches: + - devel + - master + pull_request: + branches: + - "**" + +env: + FORCE_COLOR: 2 + +jobs: + run: + name: Node + runs-on: ubuntu-latest + + steps: + - name: Clone repository + uses: actions/checkout@v4.1.1 + + - name: Set up Node.js + uses: actions/setup-node@v4.0.0 + with: + node-version: "16.x" + cache: npm + + - name: Install npm dependencies + run: npm ci + + - name: Run tests + run: npm run testpr diff --git a/.gitignore b/.gitignore index a4c3ecb8da..c8bdf0d6dd 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ TODO *.zip test.html *.log +.php-cs-fixer.cache # Intellij IDEA Project Files *.iml @@ -15,3 +16,6 @@ test.html # vim *.swp + +# Composer +/vendor/ diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000000..dfcb0c3ead --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,31 @@ +ignoreDotFiles(false) + ->ignoreVCSIgnored(true) + ->exclude('scripts/vendor') + ->in(__DIR__) +; + +$config = new PhpCsFixer\Config(); +$config + ->setRules(array( + '@Symfony' => true, + 'array_syntax' => array('syntax' => 'long'), + 'yoda_style' => array('equal' => false, 'identical' => false, 'less_and_greater' => false, 'always_move_variable' => false), + )) + ->setLineEnding(PHP_EOL) + ->setFinder($finder) +; + +return $config; diff --git a/.pullapprove.yml b/.pullapprove.yml deleted file mode 100644 index dfe836542c..0000000000 --- a/.pullapprove.yml +++ /dev/null @@ -1,38 +0,0 @@ -version: 2 - -always_pending: - title_regex: '(WIP|wip)' - labels: - - wip - explanation: 'This PR is a work in progress...' - -group_defaults: - reset_on_push: - enabled: true - reject_value: -2 - approve_regex: '^(Approved|:shipit:|:\+1:|Engage|:taco:)' - reject_regex: '^(Rejected|:-1:|Borg)' - author_approval: - auto: true - - -groups: - development: - approve_by_comment: - enabled: true - conditions: - branches: - - devel - required: 2 - teams: - - approvers - - master: - approve_by_comment: - enabled: true - conditions: - branches: - - master - required: 4 - teams: - - admin diff --git a/.stickler.yml b/.stickler.yml new file mode 100644 index 0000000000..92beabd940 --- /dev/null +++ b/.stickler.yml @@ -0,0 +1,9 @@ +--- +linters: + yamllint: + config: ./.yamllint.conf + remarklint: +files: + ignore: + - 'scripts/vendor/*' + - 'style/vendor/*' diff --git a/.user.php.ini b/.user.php.ini deleted file mode 100644 index 7660f85a01..0000000000 --- a/.user.php.ini +++ /dev/null @@ -1,2 +0,0 @@ -memory_limit = 256M -max_execution_time = 300 diff --git a/.yamllint.conf b/.yamllint.conf new file mode 100644 index 0000000000..d1b0953bdf --- /dev/null +++ b/.yamllint.conf @@ -0,0 +1,3 @@ +rules: + line-length: disable + document-start: disable diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bfb67b776d..4a308af579 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. - -* 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) +_This template was created based on the work of [`udemy-dl`](https://github.com/nishad/udemy-dl/blob/master/LICENSE)._ + +# 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/LICENSE b/LICENSE index 19da4218d1..a11a108791 100644 --- a/LICENSE +++ b/LICENSE @@ -6,8 +6,9 @@ The license is available in the 22 official languages of the EU. The English ver Please see https://joinup.ec.europa.eu/community/eupl/og_page/eupl for official translations of the other languages. This license applies to the whole project EXCEPT the files located under - - styles/vendor, and - - scripts/vendor + +- styles/vendor, and +- scripts/vendor whose licenses are located therein. ------------------------------------------------------------- diff --git a/README.md b/README.md index e3f3b257a4..6c28192c07 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,17 @@ -Pi-hole Admin Dashboard -============ -[![Codacy Badge](https://api.codacy.com/project/badge/Grade/938b4d9e61b7487da77cf63ba05c683d)](https://www.codacy.com/app/Pi-hole/AdminLTE?utm_source=github.com&utm_medium=referral&utm_content=pi-hole/AdminLTE&utm_campaign=badger) -[![Join the chat at https://gitter.im/pi-hole/AdminLTE](https://badges.gitter.im/pi-hole/AdminLTE.svg)](https://gitter.im/pi-hole/AdminLTE?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +
-[![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif "AdminLTE Presentation")](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=3J2L3Z4DHW9UY "Donate") +# Pi-hole Speedtest Modded Web -Using **[AdminLTE](https://almsaeedstudio.com)**, this project will create a Web interface for the ad-blocking Pi-hole: **a black hole for Internet advertisements**. +[![Join the chat at https://gitter.im/pihole-speedtest/community](https://badges.gitter.im/pihole-speedtest/community.svg)](https://gitter.im/pihole-speedtest/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Discord](https://badgen.net/badge/icon/discord?icon=discord&label)](https://discord.gg/TW9TfyM) -From this interface, you will be able to see stats on how well your Pi-hole is performing. You will also be able to update the lists used to block ads. +Test your connection speed directly in the Pi-hole web interface! -![Pi-hole Web interface](http://i.imgur.com/EgGZXbT.png) +
-## API -A read-only API can be accessed at `/admin/api.php`. With either no parameters or `api.php?summary` it returns the following JSON: -```JSON -{ - "domains_being_blocked": "136,708", - "dns_queries_today": "18,108", - "ads_blocked_today": "14,648", - "ads_percentage_today": "80.9" -} -``` +--- -There are many more parameters, such as `summaryRaw`, `overTimeData10mins`, `topItems`, ` topClients` or `getQuerySources`, `getQueryTypes`, `getForwardDestinations`, and finally `getAllQueries`. -Together with a token it is also possible to enable and disable (also with a set timeout) blocking via the API +Please go to the [main repository](https://github.com/arevindh/pihole-speedtest) for more information, including (un)installation instructions, pull requests, and issues. -The API returns more information (in a slighly different format if `FTL` is running) - it supports a fall-back to the "old" PHP API if `FTL` is not running. Test the type and/or version of the API by using the parameter `type` and `version`. +## Disclaimer -
-
-We use BrowserStack for multi-platform multi-browser testing. +We are not affiliated with or endorsed by [Pi-hole](https://github.com/pi-hole/web) diff --git a/api.php b/api.php index d6d68c344f..f8cdbb3725 100644 --- a/api.php +++ b/api.php @@ -4,98 +4,213 @@ * 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 +*/ $api = true; -header('Content-type: application/json'); -require("scripts/pi-hole/php/FTL.php"); -require("scripts/pi-hole/php/password.php"); -require("scripts/pi-hole/php/auth.php"); +require_once 'scripts/pi-hole/php/password.php'; +require_once 'scripts/pi-hole/php/FTL.php'; +require_once 'scripts/pi-hole/php/database.php'; +require_once 'scripts/pi-hole/php/auth.php'; check_cors(); - $data = array(); // Common API functions -if (isset($_GET['status']) && $auth) -{ - $pistatus = exec('sudo pihole status web'); - if ($pistatus == "1") - { - $data = array_merge($data, array("status" => "enabled")); - } - else - { - $data = array_merge($data, array("status" => "disabled")); - } -} -elseif (isset($_GET['enable']) && $auth) -{ - if(isset($_GET["auth"])) - { - if($_GET["auth"] !== $pwhash) - die("Not authorized!"); - } - else - { - // Skip token validation if explicit auth string is given - check_csrf($_GET['token']); - } - exec('sudo pihole enable'); - $data = array_merge($data, array("status" => "enabled")); - unlink("../custom_disable_timer"); -} -elseif (isset($_GET['disable']) && $auth) -{ - if(isset($_GET["auth"])) - { - if($_GET["auth"] !== $pwhash) - die("Not authorized!"); - } - else - { - // Skip token validation if explicit auth string is given - check_csrf($_GET['token']); - } - $disable = intval($_GET['disable']); - // intval returns the integer value on success, or 0 on failure - if($disable > 0) - { - $timestamp = time(); - exec("sudo pihole disable ".$disable."s"); - file_put_contents("../custom_disable_timer",($timestamp+$disable)*1000); - } - else - { - exec('sudo pihole disable'); - unlink("../custom_disable_timer"); - } - $data = array_merge($data, array("status" => "disabled")); +if (isset($_GET['enable']) && $auth) { + if (isset($_GET['auth'])) { + if ($_GET['auth'] !== $pwhash) { + exit('Not authorized!'); + } + } else { + // Skip token validation if explicit auth string is given + check_csrf($_GET['token']); + } + pihole_execute('enable'); + $data = array_merge($data, array('status' => 'enabled')); + if (file_exists('../custom_disable_timer')) { + unlink('../custom_disable_timer'); + } +} elseif (isset($_GET['disable']) && $auth) { + if (isset($_GET['auth'])) { + if ($_GET['auth'] !== $pwhash) { + exit('Not authorized!'); + } + } else { + // Skip token validation if explicit auth string is given + check_csrf($_GET['token']); + } + $disable = intval($_GET['disable']); + // intval returns the integer value on success, or 0 on failure + if ($disable > 0) { + $timestamp = time(); + pihole_execute('disable '.$disable.'s'); + file_put_contents('../custom_disable_timer', ($timestamp + $disable) * 1000); + } else { + pihole_execute('disable'); + if (file_exists('../custom_disable_timer')) { + unlink('../custom_disable_timer'); + } + } + $data = array_merge($data, array('status' => 'disabled')); +} elseif (isset($_GET['versions'])) { + // Determine if updates are available for Pi-hole + // using the same script that we use for the footer + // on the dashboard (update notifications are + // suppressed if on development branches) + require 'scripts/pi-hole/php/update_checker.php'; + $updates = array('core_update' => $core_update, + 'web_update' => $web_update, + 'FTL_update' => $FTL_update, ); + $current = array('core_current' => $core_current, + 'web_current' => $web_current, + 'FTL_current' => $FTL_current, ); + $latest = array('core_latest' => $core_latest, + 'web_latest' => $web_latest, + 'FTL_latest' => $FTL_latest, ); + $branches = array('core_branch' => $core_branch, + 'web_branch' => $web_branch, + 'FTL_branch' => $FTL_branch, ); + if (isset($versions['DOCKER_VERSION'])) { + // Docker info is available only inside containers + $updates['docker_update'] = $docker_update; + $current['docker_current'] = $docker_current; + $latest['docker_latest'] = $docker_latest; + } + + $data = array_merge($data, $updates); + $data = array_merge($data, $current); + $data = array_merge($data, $latest); + $data = array_merge($data, $branches); +} elseif (isset($_GET['setTempUnit'])) { + $unit = strtolower($_GET['setTempUnit']); + if ($unit == 'c' || $unit == 'f' || $unit == 'k') { + pihole_execute('-a -'.$unit); + $result = 'success'; + } else { + // invalid unit + $result = 'error'; + } + + $data = array_merge($data, array('result' => $result)); +} elseif (isset($_GET['list'])) { + if (!$auth) { + exit('Not authorized!'); + } + + if (!isset($_GET['list'])) { + exit('List has not been specified.'); + } + + switch ($_GET['list']) { + case 'black': + $_POST['type'] = LISTTYPE_BLACKLIST; + + break; + + case 'regex_black': + $_POST['type'] = LISTTYPE_REGEX_BLACKLIST; + + break; + + case 'white': + $_POST['type'] = LISTTYPE_WHITELIST; + + break; + + case 'regex_white': + $_POST['type'] = LISTTYPE_REGEX_WHITELIST; + + break; + + default: + exit('Invalid list [supported: black, regex_black, white, regex_white]'); + } + + if (isset($_GET['add'])) { + // Set POST parameters and invoke script to add domain to list + $_POST['domain'] = $_GET['add']; + $_POST['action'] = 'add_domain'; + require 'scripts/pi-hole/php/groups.php'; + } elseif (isset($_GET['sub'])) { + // Set POST parameters and invoke script to remove domain from list + $_POST['domain'] = $_GET['sub']; + $_POST['action'] = 'delete_domain_string'; + require 'scripts/pi-hole/php/groups.php'; + } else { + // Set POST parameters and invoke script to get all domains + $_POST['action'] = 'get_domains'; + require 'scripts/pi-hole/php/groups.php'; + } + + return; +} elseif (isset($_GET['customdns']) && $auth) { + if (isset($_GET['auth'])) { + if ($_GET['auth'] !== $pwhash) { + exit('Not authorized!'); + } + } else { + // Skip token validation if explicit auth string is given + check_csrf($_GET['token']); + } + + switch ($_GET['action']) { + case 'get': + $data = echoCustomDNSEntries(); + + break; + + case 'add': + $data = addCustomDNSEntry(); + + break; + + case 'delete': + $data = deleteCustomDNSEntry(); + + break; + + default: + exit('Wrong action'); + } +} elseif (isset($_GET['customcname']) && $auth) { + if (isset($_GET['auth'])) { + if ($_GET['auth'] !== $pwhash) { + exit('Not authorized!'); + } + } else { + // Skip token validation if explicit auth string is given + check_csrf($_GET['token']); + } + + switch ($_GET['action']) { + case 'get': + $data = echoCustomCNAMEEntries(); + + break; + + case 'add': + $data = addCustomCNAMEEntry(); + + break; + + case 'delete': + $data = deleteCustomCNAMEEntry(); + + break; + + default: + exit('Wrong action'); + } } // Other API functions -if(!testFTL() && !isset($_GET["PHP"])) -{ - $data = array_merge($data, array("FTLnotrunning" => true)); -} -else -{ - if(!isset($_GET["PHP"])) - { - require("api_FTL.php"); - } - else - { - require("api_PHP.php"); - } -} +require 'api_FTL.php'; +require 'api_speedtest.php'; -if(isset($_GET["jsonForceObject"])) -{ - echo json_encode($data, JSON_FORCE_OBJECT); -} -else -{ - echo json_encode($data); +header('Content-type: application/json'); +if (isset($_GET['jsonForceObject'])) { + echo json_encode($data, JSON_FORCE_OBJECT); +} else { + echo json_encode($data); } -?> diff --git a/api_FTL.php b/api_FTL.php index 55d8a17e81..927bbbba7b 100644 --- a/api_FTL.php +++ b/api_FTL.php @@ -4,304 +4,450 @@ * 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 +*/ - -if(!isset($api)) -{ - die("Direct call to api_FTL.php is not allowed!"); +if (!isset($api)) { + exit('Direct call to api_FTL.php is not allowed!'); } -$socket = connectFTL("127.0.0.1"); - if (isset($_GET['type'])) { - $data["type"] = "FTL"; + $data['type'] = 'FTL'; } if (isset($_GET['version'])) { - $data["version"] = 3; + $data['version'] = 3; +} + +if (isset($_GET['status']) && $auth) { + $return = callFTLAPI('stats'); + if (array_key_exists('FTLnotrunning', $return)) { + $data = array('FTLnotrunning' => true); + } else { + if (in_array('status enabled', $return)) { + $data = array_merge($data, array('status' => 'enabled')); + } else { + $data = array_merge($data, array('status' => 'disabled')); + } + } +} + +if ((isset($_GET['summary']) || isset($_GET['summaryRaw']) || !count($_GET)) && $auth) { + require_once 'scripts/pi-hole/php/gravity.php'; + + $return = callFTLAPI('stats'); + if (array_key_exists('FTLnotrunning', $return)) { + $data = array('FTLnotrunning' => true); + } else { + $stats = array(); + foreach ($return as $line) { + $tmp = explode(' ', $line); + + if ($tmp[0] === 'domains_being_blocked' && !is_numeric($tmp[1]) || $tmp[0] === 'status') { + // Expect string response + $stats[$tmp[0]] = $tmp[1]; + } elseif (isset($_GET['summary'])) { + // "summary" expects a formmated string response + if ($tmp[0] !== 'ads_percentage_today') { + $stats[$tmp[0]] = number_format($tmp[1]); + } else { + $stats[$tmp[0]] = number_format($tmp[1], 1, '.', ''); + } + } else { + // Expect float response + $stats[$tmp[0]] = floatval($tmp[1]); + } + } + $stats['gravity_last_updated'] = gravity_last_update(true); + $data = array_merge($data, $stats); + } +} + +if (isset($_GET['getMaxlogage']) && $auth) { + $maxlogage = getMaxlogage(); + + if ($maxlogage < 0) { + // FTL is offline + $data = array('FTLnotrunning' => true); + } else { + $data = array_merge($data, array('maxlogage' => $maxlogage)); + } +} + +if (isset($_GET['overTimeData10mins']) && $auth) { + $maxlogage = getMaxlogage(); + + $return = callFTLAPI('overTime'); + if (array_key_exists('FTLnotrunning', $return) || $maxlogage < 0) { + $data = array('FTLnotrunning' => true); + } else { + $domains_over_time = array(); + $ads_over_time = array(); + + // Use current time and maxlogage to limit the time range + $time_end = time(); + $time_start = $time_end - ($maxlogage * 3600); + + foreach ($return as $line) { + $tmp = explode(' ', $line); + $timeslot = intval($tmp[0]); + if ($timeslot >= $time_start && $timeslot <= $time_end) { + $domains_over_time[$timeslot] = intval($tmp[1]); + $ads_over_time[$timeslot] = intval($tmp[2]); + } + } + + $result = array( + 'domains_over_time' => $domains_over_time, + 'ads_over_time' => $ads_over_time, + ); + + $data = array_merge($data, $result); + } +} + +if (isset($_GET['topItems']) && $auth) { + if ($_GET['topItems'] === 'audit') { + $return = callFTLAPI('top-domains for audit'); + } elseif (is_numeric($_GET['topItems'])) { + $return = callFTLAPI('top-domains ('.$_GET['topItems'].')'); + } else { + $return = callFTLAPI('top-domains'); + } + + if (array_key_exists('FTLnotrunning', $return)) { + $data = array('FTLnotrunning' => true); + } else { + $top_queries = array(); + foreach ($return as $line) { + $tmp = explode(' ', $line); + if (count($tmp) == 2) { + $tmp[2] = ''; + } + $domain = utf8_encode($tmp[2]); + $top_queries[$domain] = intval($tmp[1]); + } + } + + if ($_GET['topItems'] === 'audit') { + $return = callFTLAPI('top-ads for audit'); + } elseif (is_numeric($_GET['topItems'])) { + $return = callFTLAPI('top-ads ('.$_GET['topItems'].')'); + } else { + $return = callFTLAPI('top-ads'); + } + + if (array_key_exists('FTLnotrunning', $return)) { + $data = array('FTLnotrunning' => true); + } else { + $top_ads = array(); + foreach ($return as $line) { + $tmp = explode(' ', $line); + $domain = utf8_encode($tmp[2]); + if (count($tmp) > 3) { + $top_ads[$domain.' ('.$tmp[3].')'] = intval($tmp[1]); + } else { + $top_ads[$domain] = intval($tmp[1]); + } + } + + $result = array( + 'top_queries' => $top_queries, + 'top_ads' => $top_ads, + ); + + $data = array_merge($data, $result); + } +} + +if ((isset($_GET['topClients']) || isset($_GET['getQuerySources'])) && $auth) { + if (isset($_GET['topClients'])) { + $number = $_GET['topClients']; + } elseif (isset($_GET['getQuerySources'])) { + $number = $_GET['getQuerySources']; + } + + if (is_numeric($number)) { + $return = callFTLAPI('top-clients ('.$number.')'); + } else { + $return = callFTLAPI('top-clients'); + } + if (array_key_exists('FTLnotrunning', $return)) { + $data = array('FTLnotrunning' => true); + } else { + $top_clients = array(); + foreach ($return as $line) { + $tmp = explode(' ', $line); + $clientip = utf8_encode($tmp[2]); + if (count($tmp) > 3 && strlen($tmp[3]) > 0) { + $clientname = utf8_encode($tmp[3]); + $top_clients[$clientname.'|'.$clientip] = intval($tmp[1]); + } else { + $top_clients[$clientip] = intval($tmp[1]); + } + } + + $result = array('top_sources' => $top_clients); + $data = array_merge($data, $result); + } } -if (isset($_GET['summary']) || isset($_GET['summaryRaw']) || !count($_GET)) -{ - sendRequestFTL("stats"); - $return = getResponseFTL(); - - $stats = []; - foreach($return as $line) - { - $tmp = explode(" ",$line); - - if(isset($_GET['summary'])) - { - if($tmp[0] !== "ads_percentage_today") - { - $stats[$tmp[0]] = number_format($tmp[1]); - } - else - { - $stats[$tmp[0]] = number_format($tmp[1], 1, '.', ''); - } - } - else - { - $stats[$tmp[0]] = floatval($tmp[1]); - } - } - $data = array_merge($data,$stats); +if (isset($_GET['topClientsBlocked']) && $auth) { + if (isset($_GET['topClientsBlocked'])) { + $number = $_GET['topClientsBlocked']; + } + + if (is_numeric($number)) { + $return = callFTLAPI('top-clients blocked ('.$number.')'); + } else { + $return = callFTLAPI('top-clients blocked'); + } + if (array_key_exists('FTLnotrunning', $return)) { + $data = array('FTLnotrunning' => true); + } else { + $top_clients = array(); + foreach ($return as $line) { + $tmp = explode(' ', $line); + $clientip = utf8_encode($tmp[2]); + if (count($tmp) > 3 && strlen($tmp[3]) > 0) { + $clientname = utf8_encode($tmp[3]); + $top_clients[$clientname.'|'.$clientip] = intval($tmp[1]); + } else { + $top_clients[$clientip] = intval($tmp[1]); + } + } + + $result = array('top_sources_blocked' => $top_clients); + $data = array_merge($data, $result); + } } -if (isset($_GET['overTimeData10mins'])) -{ - sendRequestFTL("overTime"); - $return = getResponseFTL(); - - $domains_over_time = array(); - $ads_over_time = array(); - foreach($return as $line) - { - $tmp = explode(" ",$line); - $domains_over_time[intval($tmp[0])] = intval($tmp[1]); - $ads_over_time[intval($tmp[0])] = intval($tmp[2]); - } - $result = array('domains_over_time' => $domains_over_time, - 'ads_over_time' => $ads_over_time); - $data = array_merge($data, $result); +if (isset($_GET['getForwardDestinations']) && $auth) { + if ($_GET['getForwardDestinations'] === 'unsorted') { + $return = callFTLAPI('forward-dest unsorted'); + } else { + $return = callFTLAPI('forward-dest'); + } + if (array_key_exists('FTLnotrunning', $return)) { + $data = array('FTLnotrunning' => true); + } else { + $forward_dest = array(); + foreach ($return as $line) { + $tmp = explode(' ', $line); + $forwardip = utf8_encode($tmp[2]); + if (count($tmp) > 3 && strlen($tmp[3]) > 0) { + $forwardname = utf8_encode($tmp[3]); + $forward_dest[$forwardname.'|'.$forwardip] = floatval($tmp[1]); + } else { + $forward_dest[$forwardip] = floatval($tmp[1]); + } + } + + $result = array('forward_destinations' => $forward_dest); + $data = array_merge($data, $result); + } } -if (isset($_GET['topItems']) && $auth) -{ - if($_GET['topItems'] === "audit") - { - sendRequestFTL("top-domains for audit"); - } - else if(is_numeric($_GET['topItems'])) - { - sendRequestFTL("top-domains (".$_GET['topItems'].")"); - } - else - { - sendRequestFTL("top-domains"); - } - - $return = getResponseFTL(); - $top_queries = array(); - foreach($return as $line) - { - $tmp = explode(" ",$line); - $top_queries[$tmp[2]] = intval($tmp[1]); - } - - if($_GET['topItems'] === "audit") - { - sendRequestFTL("top-ads for audit"); - } - else if(is_numeric($_GET['topItems'])) - { - sendRequestFTL("top-ads (".$_GET['topItems'].")"); - } - else - { - sendRequestFTL("top-ads"); - } - - $return = getResponseFTL(); - $top_ads = array(); - foreach($return as $line) - { - $tmp = explode(" ",$line); - 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, - 'top_ads' => $top_ads); - - $data = array_merge($data, $result); +if (isset($_GET['getQueryTypes']) && $auth) { + $return = callFTLAPI('querytypes'); + if (array_key_exists('FTLnotrunning', $return)) { + $data = array('FTLnotrunning' => true); + } else { + $querytypes = array(); + foreach ($return as $ret) { + $tmp = explode(': ', $ret); + // Reply cannot contain non-ASCII characters + $querytypes[$tmp[0]] = floatval($tmp[1]); + } + + $result = array('querytypes' => $querytypes); + $data = array_merge($data, $result); + } } -if ((isset($_GET['topClients']) || isset($_GET['getQuerySources'])) && $auth) -{ - - if(isset($_GET['topClients'])) - { - $number = $_GET['topClients']; - } - elseif(isset($_GET['getQuerySources'])) - { - $number = $_GET['getQuerySources']; - } - - if(is_numeric($number)) - { - sendRequestFTL("top-clients (".$number.")"); - } - else - { - sendRequestFTL("top-clients"); - } - - $return = getResponseFTL(); - $top_clients = array(); - foreach($return as $line) - { - $tmp = explode(" ",$line); - if(count($tmp) == 4) - { - $top_clients[$tmp[3]."|".$tmp[2]] = intval($tmp[1]); - } - else - { - $top_clients[$tmp[2]] = intval($tmp[1]); - } - } - - $result = array('top_sources' => $top_clients); - $data = array_merge($data, $result); +if (isset($_GET['getCacheInfo']) && $auth) { + $return = callFTLAPI('cacheinfo'); + if (array_key_exists('FTLnotrunning', $return)) { + $data = array('FTLnotrunning' => true); + } else { + $cacheinfo = array(); + foreach ($return as $ret) { + $tmp = explode(': ', $ret); + // Reply cannot contain non-ASCII characters + $cacheinfo[$tmp[0]] = floatval($tmp[1]); + } + + $result = array('cacheinfo' => $cacheinfo); + $data = array_merge($data, $result); + } } -if (isset($_GET['getForwardDestinations']) && $auth) -{ - if($_GET['getForwardDestinations'] === "unsorted") - { - sendRequestFTL("forward-dest unsorted"); - } - else - { - sendRequestFTL("forward-dest"); - } - $return = getResponseFTL(); - $forward_dest = array(); - foreach($return as $line) - { - $tmp = explode(" ",$line); - if(count($tmp) == 4) - { - $forward_dest[$tmp[3]."|".$tmp[2]] = intval($tmp[1]); - } - else - { - $forward_dest[$tmp[2]] = intval($tmp[1]); - } - } - - $result = array('forward_destinations' => $forward_dest); - $data = array_merge($data, $result); +if (isset($_GET['getAllQueries']) && $auth) { + if (isset($_GET['from'], $_GET['until'])) { + // Get limited time interval + $return = callFTLAPI('getallqueries-time '.$_GET['from'].' '.$_GET['until']); + } elseif (isset($_GET['domain'])) { + // Get specific domain only + $return = callFTLAPI('getallqueries-domain '.$_GET['domain']); + } elseif (isset($_GET['client']) && (isset($_GET['type']) && $_GET['type'] === 'blocked')) { + // Get specific client only + $return = callFTLAPI('getallqueries-client-blocked '.$_GET['client']); + } elseif (isset($_GET['client'])) { + // Get specific client only + $return = callFTLAPI('getallqueries-client '.$_GET['client']); + } elseif (isset($_GET['querytype'])) { + // Get specific query type only + $return = callFTLAPI('getallqueries-qtype '.$_GET['querytype']); + } elseif (isset($_GET['forwarddest'])) { + // Get specific forward destination only + $return = callFTLAPI('getallqueries-forward '.$_GET['forwarddest']); + } elseif (is_numeric($_GET['getAllQueries'])) { + $return = callFTLAPI('getallqueries ('.$_GET['getAllQueries'].')'); + } else { + // Get all queries + $return = callFTLAPI('getallqueries'); + } + + if (array_key_exists('FTLnotrunning', $return)) { + $data = array('FTLnotrunning' => true); + } else { + // Set the header + header('Content-type: application/json'); + + // Start the JSON string + echo '{"data":['; + $first = true; + + foreach ($return as $line) { + // Insert a comma before the next record (except on the first one) + if (!$first) { + echo ','; + } else { + $first = false; + } + + $row = str_getcsv($line, ' '); + // UTF-8 encode domain + $domain = utf8_encode(str_replace('~', ' ', $row[2])); + // UTF-8 encode client host name + $client = utf8_encode($row[3]); + + // Insert into array and output it in JSON format + // array: time type domain client status dnssecStatus reply response_time CNAMEDomain regexID upstream destination EDE + echo json_encode(array($row[0], $row[1], $domain, $client, $row[4], $row[5], $row[6], $row[7], $row[8], $row[9], $row[10], $row[11])); + } + // Finish the JSON string + echo ']}'; + // exit at the end + exit; + } } -if (isset($_GET['getQueryTypes']) && $auth) -{ - sendRequestFTL("querytypes"); - $return = getResponseFTL(); - $querytypes = array(); - foreach($return as $ret) - { - $tmp = explode(": ",$ret); - $querytypes[$tmp[0]] = intval($tmp[1]); - } - - $result = array('querytypes' => $querytypes); - $data = array_merge($data, $result); +if (isset($_GET['recentBlocked']) && $auth) { + exit(utf8_encode(callFTLAPI('recentBlocked')[0])); + unset($data); } -if (isset($_GET['getAllQueries']) && $auth) -{ - if(isset($_GET['from']) && isset($_GET['until'])) - { - // Get limited time interval - sendRequestFTL("getallqueries-time ".$_GET['from']." ".$_GET['until']); - } - else if(isset($_GET['domain'])) - { - // Get specific domain only - sendRequestFTL("getallqueries-domain ".$_GET['domain']); - } - else if(isset($_GET['client'])) - { - // Get specific client only - sendRequestFTL("getallqueries-client ".$_GET['client']); - } - else - { - // Get all queries - sendRequestFTL("getallqueries"); - } - $return = getResponseFTL(); - $allQueries = array(); - foreach($return as $line) - { - $tmp = explode(" ",$line); - array_push($allQueries,$tmp); - } - - $result = array('data' => $allQueries); - $data = array_merge($data, $result); +if (isset($_GET['getForwardDestinationNames']) && $auth) { + $return = callFTLAPI('forward-names'); + + if (array_key_exists('FTLnotrunning', $return)) { + $data = array('FTLnotrunning' => true); + } else { + $forward_dest = array(); + foreach ($return as $line) { + $tmp = explode(' ', $line); + $forwardip = utf8_encode($tmp[2]); + if (count($tmp) > 3) { + $forwardname = utf8_encode($tmp[3]); + $forward_dest[$forwardname.'|'.$forwardip] = floatval($tmp[1]); + } else { + $forward_dest[$forwardip] = floatval($tmp[1]); + } + } + + $result = array('forward_destinations' => $forward_dest); + $data = array_merge($data, $result); + } } -if(isset($_GET["recentBlocked"])) -{ - sendRequestFTL("recentBlocked"); - die(getResponseFTL()[0]); - unset($data); +if (isset($_GET['overTimeDataQueryTypes']) && $auth) { + $return = callFTLAPI('QueryTypesoverTime'); + if (array_key_exists('FTLnotrunning', $return)) { + $data = array('FTLnotrunning' => true); + } else { + $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); + $data = array_merge($data, $result); + } } -if (isset($_GET['overTimeDataForwards']) && $auth) -{ - sendRequestFTL("ForwardedoverTime"); - $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] = intval($tmp[$i+1]); - } - } - $result = array('over_time' => $over_time); - $data = array_merge($data, $result); +if (isset($_GET['getClientNames']) && $auth) { + $return = callFTLAPI('client-names'); + if (array_key_exists('FTLnotrunning', $return)) { + $data = array('FTLnotrunning' => true); + } else { + $client_names = array(); + foreach ($return as $line) { + $tmp = explode(' ', $line); + $client_names[] = array( + 'name' => utf8_encode($tmp[0]), + 'ip' => utf8_encode($tmp[1]), + ); + } + + $result = array('clients' => $client_names); + $data = array_merge($data, $result); + } } -if (isset($_GET['getForwardDestinationNames']) && $auth) -{ - sendRequestFTL("forward-names"); - $return = getResponseFTL(); - $forward_dest = array(); - foreach($return as $line) - { - $tmp = explode(" ",$line); - if(count($tmp) == 4) - { - $forward_dest[$tmp[3]."|".$tmp[2]] = intval($tmp[1]); - } - else - { - $forward_dest[$tmp[2]] = intval($tmp[1]); - } - } - - $result = array('forward_destinations' => $forward_dest); - $data = array_merge($data, $result); +if (isset($_GET['overTimeDataClients']) && $auth) { + $maxlogage = getMaxlogage(); + + $return = callFTLAPI('ClientsoverTime'); + if (array_key_exists('FTLnotrunning', $return) || $maxlogage < 0) { + $data = array('FTLnotrunning' => true); + } else { + $over_time = array(); + + // Use current time and maxlogage to limit the time range + $time_end = time(); + $time_start = $time_end - ($maxlogage * 3600); + + foreach ($return as $line) { + $tmp = explode(' ', $line); + for ($i = 0; $i < count($tmp) - 1; ++$i) { + $timeslot = intval($tmp[0]); + if ($timeslot >= $time_start && $timeslot <= $time_end) { + $over_time[$timeslot][$i] = floatval($tmp[$i + 1]); + } + } + } + $result = array('over_time' => $over_time); + $data = array_merge($data, $result); + } } -if (isset($_GET['overTimeDataQueryTypes']) && $auth) -{ - sendRequestFTL("QueryTypesoverTime"); - $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] = intval($tmp[$i+1]); - } - } - $result = array('over_time' => $over_time); - $data = array_merge($data, $result); +if (isset($_GET['delete_lease']) && $auth) { + $return = callFTLAPI('delete-lease '.$_GET['delete_lease']); + if (array_key_exists('FTLnotrunning', $return)) { + $data = array('FTLnotrunning' => true); + } else { + $data['delete_lease'] = $return[0]; + } } -disconnectFTL(); -?> +if (isset($_GET['dns-port']) && $auth) { + $return = callFTLAPI('dns-port'); + if (array_key_exists('FTLnotrunning', $return)) { + $data = array('FTLnotrunning' => true); + } else { + $data['dns-port'] = $return[0]; + } +} diff --git a/api_PHP.php b/api_PHP.php deleted file mode 100644 index dc7a1d2b91..0000000000 --- a/api_PHP.php +++ /dev/null @@ -1,93 +0,0 @@ -$value) { - if (is_array($value)) { - $outArray[htmlspecialchars($key)] = filterArray($value); - } else { - $outArray[htmlspecialchars($key)] = !is_numeric($value) ? htmlspecialchars($value) : $value; - } - } - return $outArray; - } - - $data = filterArray($data); -?> diff --git a/api_db.php b/api_db.php index a4de000232..f060d47bbb 100644 --- a/api_db.php +++ b/api_db.php @@ -4,213 +4,420 @@ * 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 +*/ $api = true; +require 'scripts/pi-hole/php/password.php'; + header('Content-type: application/json'); -require("scripts/pi-hole/php/password.php"); -require("scripts/pi-hole/php/auth.php"); +require 'scripts/pi-hole/php/database.php'; +require 'scripts/pi-hole/php/auth.php'; +require_once 'scripts/pi-hole/php/func.php'; check_cors(); +// Set maximum execution time to 10 minutes +ini_set('max_execution_time', '600'); + $data = array(); // Needs package php5-sqlite, e.g. // sudo apt-get install php5-sqlite -$db = new SQLite3('/etc/pihole/pihole-FTL.db'); -if(!$db) - die("Cannot access database"); - -// Long-Term API functions -// if (isset($_GET['test'])) -// { -// // $results = $db->query('SELECT * FROM QUERIES order by TIMESTAMP ASC LIMIT 2'); -// $results = $db->query('SELECT TIMESTAMP,TYPE,DOMAIN,CLIENT,STATUS FROM QUERIES order by TIMESTAMP ASC'); -// echo "N=".$result->numColumns; -// while ($row = $results->fetchArray()) -// var_dump($row); -// } - -if (isset($_GET['getAllQueries']) && $auth) -{ - if($_GET['getAllQueries'] === "empty") - { - $allQueries = array(); - } - else - { - $from = intval($_GET["from"]); - $until = intval($_GET["until"]); - $results = $db->query('SELECT timestamp,type,domain,client,status FROM queries WHERE timestamp >= '.$from.' AND timestamp <= '.$until.' ORDER BY timestamp ASC'); - $allQueries = array(); - while ($row = $results->fetchArray()) - { - $allQueries[] = [$row[0],$row[1] == 1 ? "IPv4" : "IPv6",$row[2],$row[3],$row[4]]; - } - } - $result = array('data' => $allQueries); - $data = array_merge($data, $result); +$QUERYDB = getQueriesDBFilename(); +$db = SQLite3_connect($QUERYDB); + +if (isset($_GET['network']) && $auth) { + $network = array(); + $results = $db->query('SELECT * FROM network'); + + while ($results !== false && $res = $results->fetchArray(SQLITE3_ASSOC)) { + $id = intval($res['id']); + + // Get IP addresses and host names for this device + $res['ip'] = array(); + $res['name'] = array(); + $network_addresses = $db->query("SELECT ip,name FROM network_addresses WHERE network_id = {$id} ORDER BY lastSeen DESC"); + while ($network_addresses !== false && $network_address = $network_addresses->fetchArray(SQLITE3_ASSOC)) { + array_push($res['ip'], $network_address['ip']); + if ($network_address['name'] !== null) { + array_push($res['name'], utf8_encode($network_address['name'])); + } else { + array_push($res['name'], ''); + } + } + $network_addresses->finalize(); + + // UTF-8 encode vendor + $res['macVendor'] = utf8_encode($res['macVendor']); + array_push($network, $res); + } + $results->finalize(); + + $data = array_merge($data, array('network' => $network)); } -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['getAllQueries']) && $auth) { + $allQueries = array(); + if ($_GET['getAllQueries'] !== 'empty') { + $from = intval($_GET['from']); + $until = intval($_GET['until']); + + // Use table "query_storage" + // - replace domain ID with domain + // - replace client ID with client name + // - replace forward ID with forward destination + $dbquery = 'SELECT timestamp, type,'; + $dbquery .= " CASE typeof(domain) WHEN 'integer' THEN (SELECT domain FROM domain_by_id d WHERE d.id = q.domain) ELSE domain END domain,"; + $dbquery .= " CASE typeof(client) WHEN 'integer' THEN ("; + $dbquery .= " SELECT CASE TRIM(name) WHEN '' THEN c.ip ELSE c.name END name FROM client_by_id c WHERE c.id = q.client"; + $dbquery .= ' ) ELSE client END client,'; + $dbquery .= " CASE typeof(forward) WHEN 'integer' THEN (SELECT forward FROM forward_by_id f WHERE f.id = q.forward) ELSE forward END forward,"; + $dbquery .= ' status, reply_type, reply_time, dnssec'; + $dbquery .= ' FROM query_storage q'; + $dbquery .= ' WHERE timestamp >= :from AND timestamp <= :until '; + if (isset($_GET['status'])) { + // if some query status should be excluded + $excludedStatus = $_GET['status']; + if (preg_match('/^[0-9]+(?:,[0-9]+)*$/', $excludedStatus) === 1) { + // Append selector to DB query. The used regex ensures + // that only numbers, separated by commas are accepted + // to avoid code injection and other malicious things + // We accept only valid lists like "1,2,3" + // We reject ",2,3", "1,2," and similar arguments + $dbquery .= 'AND status NOT IN ('.$excludedStatus.') '; + } else { + exit('Error. Selector status specified using an invalid format.'); + } + } + $dbquery .= 'ORDER BY timestamp ASC'; + $stmt = $db->prepare($dbquery); + $stmt->bindValue(':from', intval($from), SQLITE3_INTEGER); + $stmt->bindValue(':until', intval($until), SQLITE3_INTEGER); + $results = $stmt->execute(); + + // Start the JSON string + echo '{"data":['; + + if (!is_bool($results)) { + $first = true; + while ($row = $results->fetchArray(SQLITE3_ASSOC)) { + // Insert a comma before the next record (except on the first one) + if (!$first) { + echo ','; + } else { + $first = false; + } + + // Format, encode, transform each field (if necessary). + $time = $row['timestamp']; + $query_type = getQueryTypeStr($row['type']); // Convert query type ID to name + $domain = utf8_encode(str_replace('~', ' ', $row['domain'])); + $client = $row['client']; + $status = $row['status']; + $destination = utf8_encode($row['forward']); + $reply_type = $row['reply_type']; + $reply_time = $row['reply_time']; + $dnssec = $row['dnssec']; + + // Insert into array and output it in JSON format + echo json_encode(array($time, $query_type, $domain, $client, $status, $destination, $reply_type, $reply_time, $dnssec)); + } + } + + // Finish the JSON string + echo ']}'; + + // exit at the end + exit; + } + // only used if getAllQueries==empty + $result = array('data' => $allQueries); + $data = array_merge($data, $result); +} + +if (isset($_GET['topClients']) && $auth) { + // $from = intval($_GET["from"]); + $limit = ''; + if (isset($_GET['from'], $_GET['until'])) { + $limit = 'WHERE timestamp >= :from AND timestamp <= :until'; + } elseif (isset($_GET['from']) && !isset($_GET['until'])) { + $limit = 'WHERE timestamp >= :from'; + } elseif (!isset($_GET['from']) && isset($_GET['until'])) { + $limit = 'WHERE timestamp <= :until'; + } + $dbquery = "SELECT CASE typeof(client) WHEN 'integer' THEN ("; + $dbquery .= " SELECT CASE TRIM(name) WHEN '' THEN c.ip ELSE c.name END name FROM client_by_id c WHERE c.id = q.client)"; + $dbquery .= ' ELSE client END client, count(client) FROM query_storage q '.$limit.' GROUP BY client ORDER BY count(client) DESC LIMIT 20'; + + $stmt = $db->prepare($dbquery); + $stmt->bindValue(':from', intval($_GET['from']), SQLITE3_INTEGER); + $stmt->bindValue(':until', intval($_GET['until']), SQLITE3_INTEGER); + $results = $stmt->execute(); + + $clientnums = array(); + + if (!is_bool($results)) { + while ($row = $results->fetchArray()) { + // $row[0] is the client IP + + if (array_key_exists($row[0], $clientnums)) { + // Entry already exists, add to it (might appear multiple times due to mixed capitalization in the database) + $clientnums[$row[0]] += intval($row[1]); + } else { + // Entry does not yet exist + $clientnums[$row[0]] = intval($row[1]); + } + } + } + + // Sort by number of hits + arsort($clientnums); + + // Extract only the first ten entries + $clientnums = array_slice($clientnums, 0, 10); + + $result = array('top_sources' => $clientnums); + $data = array_merge($data, $result); +} + +if (isset($_GET['topDomains']) && $auth) { + $limit = ''; + + if (isset($_GET['from'], $_GET['until'])) { + $limit = ' AND timestamp >= :from AND timestamp <= :until'; + } elseif (isset($_GET['from']) && !isset($_GET['until'])) { + $limit = ' AND timestamp >= :from'; + } elseif (!isset($_GET['from']) && isset($_GET['until'])) { + $limit = ' AND timestamp <= :until'; + } + // Select top permitted domains only + $stmt = $db->prepare('SELECT domain,count(domain) FROM queries WHERE status IN (2,3,12,13,14,17)'.$limit.' GROUP by domain order by count(domain) desc limit 20'); + $stmt->bindValue(':from', intval($_GET['from']), SQLITE3_INTEGER); + $stmt->bindValue(':until', intval($_GET['until']), SQLITE3_INTEGER); + $results = $stmt->execute(); + + $domains = array(); + + if (!is_bool($results)) { + while ($row = $results->fetchArray()) { + // Convert domain to lower case UTF-8 + $c = utf8_encode(strtolower($row[0])); + if (array_key_exists($c, $domains)) { + // Entry already exists, add to it (might appear multiple times due to mixed capitalization in the database) + $domains[$c] += intval($row[1]); + } else { + // Entry does not yet exist + $domains[$c] = intval($row[1]); + } + } + } + + // Sort by number of hits + arsort($domains); + + // Extract only the first ten entries + $domains = array_slice($domains, 0, 10); + + $result = array('top_domains' => $domains); + $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'], $_GET['until'])) { + $limit = ' AND timestamp >= :from AND timestamp <= :until'; + } elseif (isset($_GET['from']) && !isset($_GET['until'])) { + $limit = ' AND timestamp >= :from'; + } elseif (!isset($_GET['from']) && isset($_GET['until'])) { + $limit = ' AND timestamp <= :until'; + } + $stmt = $db->prepare('SELECT domain,count(domain) FROM queries WHERE status IN (1,4,5,6,7,8,9,10,11)'.$limit.' GROUP by domain order by count(domain) desc limit 10'); + $stmt->bindValue(':from', intval($_GET['from']), SQLITE3_INTEGER); + $stmt->bindValue(':until', intval($_GET['until']), SQLITE3_INTEGER); + $results = $stmt->execute(); + + $addomains = array(); + + if (!is_bool($results)) { + while ($row = $results->fetchArray()) { + $addomains[utf8_encode($row[0])] = intval($row[1]); + } + } + $result = array('top_ads' => $addomains); + $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'); + + if (!is_bool($results)) { + $result = array('mintimestamp' => $results->fetchArray()[0]); + } else { + $result = array(); + } + + $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'); + + if (!is_bool($results)) { + $result = array('maxtimestamp' => $results->fetchArray()[0]); + } else { + $result = array(); + } + + $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'); + + if (!is_bool($results)) { + $result = array('count' => $results->fetchArray()[0]); + } else { + $result = array(); + } + + $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['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'], $_GET['until'])) { + $limit = 'timestamp >= :from AND timestamp <= :until'; + } elseif (isset($_GET['from']) && !isset($_GET['until'])) { + $limit = 'timestamp >= :from'; + } elseif (!isset($_GET['from']) && isset($_GET['until'])) { + $limit = 'timestamp <= :until'; + } + + $interval = 600; + + if (isset($_GET['interval'])) { + $q = intval($_GET['interval']); + if ($q >= 10) { + $interval = $q; + } + } + + // Round $from and $until to match the requested $interval + $from = intval((intval($_GET['from']) / $interval) * $interval); + $until = intval((intval($_GET['until']) / $interval) * $interval); + + // Count domains and blocked queries using the same intervals + $sqlcommand = " + SELECT + (timestamp / :interval) * :interval AS interval, + SUM(CASE + WHEN status !=0 THEN 1 + ELSE 0 + END) AS domains, + SUM(CASE + WHEN status IN (1,4,5,6,7,8,9,10,11,15,16) THEN 1 + ELSE 0 + END) AS blocked + FROM queries + WHERE $limit + GROUP BY interval + ORDER BY interval"; + + $stmt = $db->prepare($sqlcommand); + $stmt->bindValue(':from', $from, SQLITE3_INTEGER); + $stmt->bindValue(':until', $until, SQLITE3_INTEGER); + $stmt->bindValue(':interval', $interval, SQLITE3_INTEGER); + $results = $stmt->execute(); + + // Parse the DB result into graph data, filling in missing interval sections with zero + function parseDBData($results, $interval, $from, $until) + { + $domains = array(); + $blocked = array(); + $first_db_timestamp = -1; + + if (!is_bool($results)) { + // Read in the data + while ($row = $results->fetchArray()) { + $domains[$row['interval']] = intval($row['domains']); + $blocked[$row['interval']] = intval($row['blocked']); + if ($first_db_timestamp === -1) { + $first_db_timestamp = intval($row[0]); + } + } + } + + // It is unpredictable what the first timestamp returned by the database will be. + // This depends on live data. The bar graph can handle "gaps", but the Area graph can't. + // Hence, we filling the "missing" timeslots with 0 to avoid wrong graphic render. + // (https://github.com/pi-hole/AdminLTE/pull/2374#issuecomment-1261865428) + $aligned_from = $from + (($first_db_timestamp - $from) % $interval); + + // Fill gaps in returned data + for ($i = $aligned_from; $i < $until; $i += $interval) { + if (!array_key_exists($i, $domains)) { + $domains[$i] = 0; + $blocked[$i] = 0; + } + } + + return array('domains_over_time' => $domains, 'ads_over_time' => $blocked); + } + + $over_time = parseDBData($results, $interval, $from, $until); + $data = array_merge($data, $over_time); } -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['status']) && $auth) { + $extra = ';'; + if (isset($_GET['ignore']) && $_GET['ignore'] === 'DNSMASQ_WARN') { + $extra = "WHERE type != 'DNSMASQ_WARN';"; + } + $results = $db->query('SELECT COUNT(*) FROM message '.$extra); + + if (!is_bool($results)) { + $result = array('message_count' => $results->fetchArray()[0]); + } else { + $result = array(); + } + + $data = array_merge($data, $result); } -if(isset($_GET["jsonForceObject"])) -{ - echo json_encode($data, JSON_FORCE_OBJECT); +if (isset($_GET['messages']) && $auth) { + $extra = ';'; + if (isset($_GET['ignore']) && $_GET['ignore'] === 'DNSMASQ_WARN') { + $extra = "WHERE type != 'DNSMASQ_WARN';"; + } + + $messages = array(); + $results = $db->query('SELECT * FROM message '.$extra); + + while ($results !== false && $res = $results->fetchArray(SQLITE3_ASSOC)) { + // Convert string to to UTF-8 encoding to ensure php-json can handle it. + // Furthermore, convert special characters to HTML entities to prevent XSS attacks. + foreach ($res as $key => $value) { + if (is_string($value)) { + $res[$key] = htmlspecialchars(utf8_encode($value)); + } + } + array_push($messages, $res); + } + + $data = array_merge($data, array('messages' => $messages)); } -else -{ - echo json_encode($data); + +if (isset($_GET['jsonForceObject'])) { + echo json_encode($data, JSON_FORCE_OBJECT); +} else { + echo json_encode($data); } diff --git a/api_speedtest.php b/api_speedtest.php new file mode 100644 index 0000000000..2d29ba0907 --- /dev/null +++ b/api_speedtest.php @@ -0,0 +1,365 @@ +&1'; +$cmdRun = '[[ -f /tmp/speedtest.log ]] && cat /tmp/speedtest.log || { [[ -f /etc/pihole/speedtest.log ]] && cat /etc/pihole/speedtest.log || echo ""; }'; +$cmdServersCurl = "curl 'https://c.speedtest.net/speedtest-servers-static.php' --compressed -H 'Upgrade-Insecure-Requests: 1' -H 'DNT: 1' -H 'Sec-GPC: 1'"; +$cmdServersJSON = "curl 'https://www.speedtest.net/api/js/servers' --compressed -H 'Upgrade-Insecure-Requests: 1' -H 'DNT: 1' -H 'Sec-GPC: 1'"; + +if ($auth) { + if (isset($_GET['hasSpeedTestBackup'])) { + $data = array_merge($data, hasSpeedTestBackup($dbSpeedtestOld)); + } + if (isset($_GET['getSpeedData'])) { + $data = array_merge($data, getSpeedData($dbSpeedtest, $_GET['getSpeedData'])); + } + if (isset($_GET['getAllSpeedTestData'])) { + $data = array_merge($data, getAllSpeedTestData($dbSpeedtest)); + } + if (isset($_GET['getLatestLog'])) { + $data = array_merge($data, speedtestExecute($cmdLog)); + } + if (isset($_GET['getClosestServers'])) { + $data = array_merge($data, getServers($cmdServers)); + } + if (isset($_GET['getSpeedTestStatus'])) { + $data = array_merge($data, speedtestExecute(getStatusCmd())); + } + if (isset($_GET['getLatestRun'])) { + $data = array_merge($data, speedtestExecute($cmdRun)); + } + if (isset($_GET['curlClosestServers'])) { + $data = array_merge($data, curlServers($cmdServersCurl)); + } + if (isset($_GET['JSONClosestServers'])) { + $data = array_merge($data, JSONServers($cmdServersJSON)); + } + if (isset($_GET['getNumberOfDaysInDB'])) { + $data = array_merge($data, getNumberOfDaysInDB($dbSpeedtest)); + } + if (isset($_GET['whichSpeedtest'])) { + $data = array_merge($data, array('data' => whichSpeedtest())); + } +} + +function hasSpeedTestBackup($dbSpeedtestOld) +{ + $exists = file_exists($dbSpeedtestOld); + + if ($exists) { + $data = getAllSpeedTestData($dbSpeedtestOld); + } else { + $data = array(); + } + + return array('data' => !empty($data) && !empty($data['data']) ? true : false); +} + +function getAllSpeedTestData($dbSpeedtest) +{ + $data = getSpeedTestData($dbSpeedtest, -1); + if (isset($data['error'])) { + return array(); + } + $newarr = array(); + foreach ($data as $array) { + array_push($newarr, array_values($array)); + } + + return array('data' => $newarr); +} + +function getSpeedTestData($dbSpeedtest, $durationdays = '1') +{ + if (!file_exists($dbSpeedtest)) { + return array(); + } + $db = new SQLite3($dbSpeedtest); + if (!$db || !$db->querySingle('SELECT count(*) FROM sqlite_master WHERE type="table" AND name="speedtest"')) { + return array(); + } + + if ((int) $durationdays == -1) { + $sql = 'SELECT * from speedtest order by id asc'; + } else { + $curdate = new DateTime('now', new DateTimeZone('UTC')); + $daysago = new DateTime('now', new DateTimeZone('UTC')); + $daysago->modify('-'.$durationdays.' day'); + $curdate = $curdate->format('Y-m-d H:i:s'); + $daysago = $daysago->format('Y-m-d H:i:s'); + $sql = "SELECT * from speedtest where start_time between '{$daysago}' and '{$curdate}' order by id asc"; + } + + $dbResults = $db->query($sql); + $dataFromSpeedDB = array(); + if (!empty($dbResults)) { + while ($row = $dbResults->fetchArray(SQLITE3_ASSOC)) { + array_push($dataFromSpeedDB, $row); + } + } + $db->close(); + + return $dataFromSpeedDB; +} + +function getSpeedData($dbSpeedtest, $durationdays = '-2') +{ + global $setupVars; + if (isset($setupVars['SPEEDTEST_CHART_DAYS']) && $durationdays == '-2') { + $durationdays = $setupVars['SPEEDTEST_CHART_DAYS']; + } else { + $durationdays = (int) $durationdays < -1 ? '1' : $durationdays; + } + + $data = getSpeedTestData($dbSpeedtest, $durationdays); + if (isset($data['error'])) { + return array(); + } + + return $data; +} + +if (!empty($_GET['csv-export'])) { + exportData(); + exit; +} + +function exportData() +{ + // time for filename + $time = date('Y-m-d-H-i-s'); + + header('Content-type: text/csv'); + header("Content-Disposition: attachment; filename=speedtest-export-$time.csv"); + header('Pragma: no-cache'); + header('Expires: 0'); + + // DB Location + $speedtestDB = '/etc/pihole/speedtest.db'; + + // Connect to DB + $conn = new PDO('sqlite:'.$speedtestDB); + + // Query + $query = $conn->query('SELECT * FROM speedtest'); + + // Fetch the first row + $row = $query->fetch(PDO::FETCH_ASSOC); + + // If no results are found, echo a message and stop + if ($row == false) { + echo 'No results'; + exit; + } + + // Print the titles using the first line + print_titles($row); + // Iterate over the results and print each one in a line + while ($row != false) { + // Print the line + echo implode(',', array_values($row))."\n"; + // Fetch the next line + $row = $query->fetch(PDO::FETCH_ASSOC); + } + + // Close the connection + $conn = null; +} + +function print_titles($row) +{ + echo implode(',', array_keys($row))."\n"; +} + +function speedtestExecute($command) +{ + $output = array(); + $return_status = -1; + exec('/bin/bash -c \''.$command.'\'', $output, $return_status); + + if ($return_status !== 0) { + trigger_error("Executing {$command} failed.", E_USER_WARNING); + } + + return array('data' => implode("\n", $output)); +} + +function getServers($cmdServers) +{ + $array = speedtestExecute($cmdServers); + $servers = $array['data']; + + $output = explode("\n", $servers); + $output = array_filter($output); + if (count($output) > 1) { + array_shift($output); + } + $servers = implode("\n", $output); + + if ($servers === false) { + return array('error' => 'Error fetching servers'); + } else { + return array('data' => $servers); + } +} + +function curlServers($cmdServersCurl) +{ + $array = speedtestExecute($cmdServersCurl); + $xmlContent = $array['data']; + + if ($xmlContent === false) { + return array('error' => 'Error fetching XML'); + } else { + $xml = simplexml_load_string($xmlContent); + if ($xml === false) { + return array('error' => 'Error parsing XML'); + } + $serverList = array(); + foreach ($xml->servers->server as $server) { + $serverList[] = str_pad($server['id'], 5, ' ', STR_PAD_LEFT).') '.$server['sponsor'].' ('.$server['name'].', '.$server['cc'].') ('.$server['lat'].', '.$server['lon'].')'; + } + + return array('data' => implode("\n", $serverList)); + } +} + +function JSONServers($cmdServersJSON) +{ + $array = speedtestExecute($cmdServersJSON); + $jsonContent = $array['data']; + + if ($jsonContent === false) { + return array('error' => 'Error fetching JSON'); + } else { + $json = json_decode($jsonContent); + if ($json === false) { + return array('error' => 'Error parsing JSON'); + } + + $serverList = array(); + foreach ($json as $server) { + $serverList[] = str_pad($server->id, 5, ' ', STR_PAD_LEFT).') '.$server->sponsor.' ('.$server->name.', '.$server->cc.') [Distance '.$server->distance.']'; + } + + return array('data' => implode("\n", $serverList)); + } +} + +function getRemainingTime() +{ + $interval_seconds = -1; + + if (file_exists('/opt/pihole/speedtestmod/schedule_check.sh')) { + $interval_seconds = speedtestExecute("grep 'INTERVAL_SECONDS=' /opt/pihole/speedtestmod/schedule_check.sh | cut -d'=' -f2")['data']; + } + + // if interval_seconds is "nan", then schedule has never been set + if (strpos($interval_seconds, 'nan') !== false) { + return -1; + } + + $interval_seconds = (int) $interval_seconds; + + // if interval_seconds is less than 0, then schedule is disabled + if ($interval_seconds < 0) { + return -1; + } + + $last_run_time = -1; + if (file_exists('/etc/pihole/last_speedtest')) { + $last_run_time = file_get_contents('/etc/pihole/last_speedtest'); + $last_run_time = (int) $last_run_time; + } + + // if last_run_time is -1, then speedtest has never been run + if ($last_run_time == -1) { + return 0; + } + + return max(0, $interval_seconds - (time() - $last_run_time)); +} + +function getNumberOfDaysInDB($dbSpeedtest) +{ + $db = new SQLite3($dbSpeedtest); + if (!$db || !$db->querySingle('SELECT count(*) FROM sqlite_master WHERE type="table" AND name="speedtest"')) { + return array('data' => 0); + } + + $sql = 'SELECT start_time from speedtest order by id asc'; + $dbResults = $db->query($sql); + $dataFromSpeedDB = array(); + if (!empty($dbResults)) { + while ($row = $dbResults->fetchArray(SQLITE3_ASSOC)) { + array_push($dataFromSpeedDB, $row); + } + } + $db->close(); + + if (empty($dataFromSpeedDB)) { + return array('data' => 0); + } + + $first_date = new DateTime($dataFromSpeedDB[0]['start_time']); + $last_date = new DateTime('now', new DateTimeZone('UTC')); + $diff = $first_date->diff($last_date); + + return array('data' => $diff->days + 1); +} + +function getStatusCmd() +{ + $cmdStatus = 'echo ""'; + if (file_exists('/opt/pihole/speedtestmod/schedule_check.sh')) { + $remaining_seconds = getRemainingTime(); + if ($remaining_seconds >= 0) { + $remaining_date = sprintf('%dd %dh %dmin %ds', $remaining_seconds / 86400, $remaining_seconds / 3600 % 24, $remaining_seconds / 60 % 60, $remaining_seconds % 60); + $remaining_date = preg_replace('/^0d |(^|(?<= ))0h |(^|(?<= ))0min /', '', $remaining_date); // remove 0d 0h 0min + $remaining_date = preg_replace('/\s(\d+s)/', '', $remaining_date); // remove seconds if not needed + $cmdStatus = 'echo '.$remaining_date; + } + } elseif (file_exists('/bin/systemctl')) { + $cmdStatus = 'systemctl status pihole-speedtest.timer'; + } + + return $cmdStatus; +} + +function whichSpeedtest() +{ + if (file_exists('/usr/bin/speedtest')) { + $officialInstalled = speedtestExecute('. /opt/pihole/speedtestmod/lib.sh ; notInstalled speedtest && echo "false" || echo "true"')['data']; + + if ($officialInstalled === 'true') { + return 'official'; + } + + $version = speedtestExecute('/usr/bin/speedtest -h')['data']; + + if (strpos($version, 'sivel') !== false) { + return 'sivel\'s'; + } + + return 'librespeed'; + } + + return 'no'; +} diff --git a/auditlog.php b/auditlog.php index 96d7457728..3c311ac604 100644 --- a/auditlog.php +++ b/auditlog.php @@ -1,81 +1,86 @@ - - - +
-
-
-
-

Allowed queries

-
- -
-
- - - - - - - - -
DomainHitsActions
+
+
+
+

Allowed queries

+ +
+
+ + + + + + + + + + +
DomainHitsActions
+
+
+
+ +
+
-
- -
- -
- +
-
-
-
-

Blocked queries

-
- -
-
- - - - - - - - -
DomainHitsActions
+ +
+
+
+

Blocked queries

+ +
+
+ + + + + + + + + + +
DomainHitsActions
+
+
+
+ +
+
-
- -
- -
- -
-
-

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

+
+ + + - - diff --git a/cname_records.php b/cname_records.php new file mode 100644 index 0000000000..323ba4765b --- /dev/null +++ b/cname_records.php @@ -0,0 +1,86 @@ + + + + + + +
+
+
+ +
+

+ Add a new CNAME record +

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

+ List of local CNAME records +

+
+ +
+ + + + + + + + +
DomainTargetAction
+ +
+ +
+ +
+
+ + + + diff --git a/composer.json b/composer.json new file mode 100644 index 0000000000..e14afd814f --- /dev/null +++ b/composer.json @@ -0,0 +1,16 @@ +{ + "name": "pi-hole/adminlte", + "type": "project", + "description": "Pi-hole Dashboard for stats and more", + "require": { + "php": ">=5.4", + "phpstan/phpstan": "1.*" + }, + "license": "EUPL-1.2", + "minimum-stability": "stable", + "autoload": { + "files": [ + "scripts/vendor/qrcode.php" + ] + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000000..53abbd5e0f --- /dev/null +++ b/composer.lock @@ -0,0 +1,83 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "6b761e5cfc801bc1942ad8d0d675ddb9", + "packages": [ + { + "name": "phpstan/phpstan", + "version": "1.10.42", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "fc2316508de5453140b5cb3d3f8683a33e92f26a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/fc2316508de5453140b5cb3d3f8683a33e92f26a", + "reference": "fc2316508de5453140b5cb3d3f8683a33e92f26a", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", + "type": "tidelift" + } + ], + "time": "2023-11-17T15:26:57+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=5.4" + }, + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/db_graph.php b/db_graph.php index 9dbdba737c..2b05834508 100644 --- a/db_graph.php +++ b/db_graph.php @@ -1,67 +1,77 @@ - - - - - -
- -
- - -
-
- -
- +
+
+

+ Select date and time range +

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

Queries over the selected time period

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

+ Queries over the selected time period +

+
+
+
+
+
+ +
+
+
+
+
- -
+ + - - - - diff --git a/db_lists.php b/db_lists.php index 0ddc9ddab2..e89801aecb 100644 --- a/db_lists.php +++ b/db_lists.php @@ -1,142 +1,149 @@ - - - -
- -
- - -
-
- -
- +
+
+

+ Select date and time range +

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

Top Domains

-
- -
-
- - - - - - - - -
DomainHitsFrequency
+
+
+

Top Domains

+ +
+
+ + + + + + + + + + +
DomainHitsFrequency
+
+
+ +
- - -
- +
-
-
-

Top Blocked Domains

-
- -
-
- - - - - - - - -
DomainHitsFrequency
+
+
+

Top Blocked Domains

+ +
+
+ + + + + + + + + + +
DomainHitsFrequency
+
+
+ +
- - -
- +
-
-
-

Top Clients

-
- -
-
- - - - - - - - -
ClientRequestsFrequency
+
+
+

Top Clients

+ +
+
+ + + + + + + + + + +
ClientRequestsFrequency
+
+
+ +
- - -
- +
+ + + - - - - diff --git a/db_queries.php b/db_queries.php index 189c97cde0..6521a3c1fa 100644 --- a/db_queries.php +++ b/db_queries.php @@ -1,110 +1,182 @@ - - - -
- -
- - -
-
- -
- +
+
+

+ Select date and time range +

+
+
+
+
+   New options selected. Please reload the data or choose another time range. + +
+
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+
+
+

+
+
+
+
+
+

+
+
+
+
+

+
+
+
+
+

+

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

---

-

Queries Blocked

+

---

+

Total Queries

- +
-
+
-

---

-

Queries Blocked (Wildcards)

+

---

+

Queries Blocked

- +
-
+
-

---

-

Queries Total

+

---

+

Queries Blocked (Wildcards)

- +
-
+
-

---

-

Queries Blocked

+

---

+

Percentage Blocked

- +
+ + +
-
-
-

Recent Queries

-
- -
-
- +
+
+

Recent Queries

+
+ +
+
@@ -112,6 +184,7 @@ + @@ -122,24 +195,22 @@ +
TimeDomain Client StatusReply Action
Domain Client StatusReply Action
-
- -
- + +
+
- + + + - - - - diff --git a/debug.php b/debug.php index 20ffa7c876..52d9af4888 100644 --- a/debug.php +++ b/debug.php @@ -1,25 +1,43 @@ - - -

Upload debug log and provide token once finished

+
+

Options:

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

Once you click this button a debug log will be generated and can automatically be uploaded if we detect a working internet connection.

- - + + + + - - - diff --git a/dns_records.php b/dns_records.php new file mode 100644 index 0000000000..25870f4e57 --- /dev/null +++ b/dns_records.php @@ -0,0 +1,91 @@ + + + + + + +
+
+
+ +
+

+ Add a new domain/IP combination +

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

+ List of local DNS domains +

+
+ +
+ + + + + + + + +
DomainIPAction
+ +
+ +
+ +
+
+ + + + + diff --git a/gravity.php b/gravity.php index 5e7367255b..99f2636b15 100644 --- a/gravity.php +++ b/gravity.php @@ -1,33 +1,35 @@ - -