Compare commits
308 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
70df88b825 | ||
|
4d7254e74d | ||
|
4b2b0bf3c9 | ||
|
3943b2bc2c | ||
|
219fc58401 | ||
|
74503d542e | ||
|
11275a7796 | ||
|
c42640e21c | ||
|
1aad47f2af | ||
|
6bb9c8448b | ||
|
8f59b7c340 | ||
|
32ad39d0e1 | ||
|
77f617e984 | ||
|
81a802e3fc | ||
|
a6a97aa9c7 | ||
|
cab1105169 | ||
|
2eee0b87d5 | ||
|
aa198ed562 | ||
|
3f363b0175 | ||
|
8e867a5ace | ||
|
73dd5b80b5 | ||
|
839683b4e1 | ||
|
78614877f2 | ||
|
bf92944b95 | ||
|
fde2c4db1e | ||
|
96b9cce70c | ||
|
75a57ede07 | ||
|
a1adf60b30 | ||
|
5db72a9552 | ||
|
2a8519be30 | ||
|
03eeb3fad1 | ||
|
f688b88bd8 | ||
|
7164d066c3 | ||
|
4473f8ee1d | ||
|
6a24a785ee | ||
|
ee2d3726af | ||
|
c1d9373d55 | ||
|
a51fbb1b0e | ||
|
cada4efe1d | ||
|
0d2d5fff5d | ||
|
90e160094d | ||
|
877785c3ca | ||
|
d05ec08abf | ||
|
ddb8931e68 | ||
|
194b2eae74 | ||
|
966644baa0 | ||
|
ddc73a53fe | ||
|
cb5557cc2e | ||
|
c9ee9dcc8b | ||
|
dc03022e27 | ||
|
b03fe74f10 | ||
|
2600ad5a05 | ||
|
d512745767 | ||
|
d51be4f529 | ||
|
35ac5ac82f | ||
|
65796fd1a5 | ||
|
a9e1f0d1bc | ||
|
f9ff781df3 | ||
|
47df4da4b5 | ||
|
f22e5ac171 | ||
|
ef98d85dc5 | ||
|
57d9ae9351 | ||
|
ce477ef997 | ||
|
fb6627a9cc | ||
|
9bcd9931f7 | ||
|
fb600d6fc8 | ||
|
f5d599e7d2 | ||
|
5d521be5d9 | ||
|
0f6226ce51 | ||
|
194ddc33f3 | ||
|
b809c88fa5 | ||
|
7486697d41 | ||
|
afc93b8a21 | ||
|
b4d9f1f5e5 | ||
|
ad112e236e | ||
|
8a0b872337 | ||
|
2490089645 | ||
|
62d7491936 | ||
|
abc30d7da3 | ||
|
d62ceb8423 | ||
|
b2c524bc3e | ||
|
a9b675cd24 | ||
|
5c8be4428b | ||
|
7688c1a233 | ||
|
6d362ca5c7 | ||
|
94b4eb08a2 | ||
|
cded1e0272 | ||
|
ca80bb0caa | ||
|
9317d9217f | ||
|
7d01620316 | ||
|
739a5092cc | ||
|
2fcfeacd44 | ||
|
0e5630f33a | ||
|
470e2932ad | ||
|
797372ecaa | ||
|
788730cdc2 | ||
|
0d6901aaa2 | ||
|
5ecd4fe931 | ||
|
e575fad324 | ||
|
4c91667b6f | ||
|
3ec1f46fe8 | ||
|
73ab9f29a5 | ||
|
f5c47234de | ||
|
605338e998 | ||
|
9c4351a174 | ||
|
0048c2f9aa | ||
|
a58f70ca7e | ||
|
2a0ad8796c | ||
|
f7e3650728 | ||
|
69f845a047 | ||
|
809520ec70 | ||
|
b28fa86e33 | ||
|
5069838e69 | ||
|
c3634a5135 | ||
|
e72d8437f7 | ||
|
9984158ec1 | ||
|
0e711beca7 | ||
|
23402e27e1 | ||
|
d33e8241dc | ||
|
b2c048af92 | ||
|
7c5094d37b | ||
|
c6c9965335 | ||
|
4eafe0a5b0 | ||
|
070c327642 | ||
|
558a627a73 | ||
|
502067addc | ||
|
11099f7b1d | ||
|
4aa94a5d75 | ||
|
500942cb99 | ||
|
b393e68d1d | ||
|
63301efb28 | ||
|
e3394e29dd | ||
|
9ba73331aa | ||
|
33f56bb0cb | ||
|
fef280a0c9 | ||
|
df6aa59fbf | ||
|
3918c60d87 | ||
|
1af4566991 | ||
|
4dd2c581ac | ||
|
9cbd7bd9d3 | ||
|
2e3c647591 | ||
|
863cbb2b8d | ||
|
72e5a227c8 | ||
|
6d178342ee | ||
|
0b70962e0c | ||
|
ecb4277e69 | ||
|
09a0039a38 | ||
|
fc50359752 | ||
|
257e3f33ef | ||
|
4dd01cdfda | ||
|
74cb48086c | ||
|
ded787547a | ||
|
31f4c00aee | ||
|
f4b65be876 | ||
|
362b6a75c8 | ||
|
8c92b381a2 | ||
|
95be59eaab | ||
|
a2d5a23c43 | ||
|
d02a7d90b9 | ||
|
6d9df65d02 | ||
|
b745460a87 | ||
|
fd802aac06 | ||
|
dec6d80dda | ||
f6c0843183 | |||
|
c637eb28dd | ||
|
119437a07c | ||
|
84b5987ac5 | ||
|
3d8da1db58 | ||
|
634d179568 | ||
|
7bea6349a0 | ||
|
10a15e06e1 | ||
|
1867e7ad01 | ||
|
e16038bf28 | ||
|
b75ff0782d | ||
|
3e20788857 | ||
|
f73e4b9239 | ||
|
27051363ff | ||
|
15391379be | ||
|
9c96f0fd57 | ||
|
30d4337783 | ||
|
73f631b1f9 | ||
|
3c06519130 | ||
|
1d3e7c0255 | ||
|
a8afd49f84 | ||
|
79a4a17311 | ||
|
baffb5fc81 | ||
|
5a27d748d1 | ||
|
6f5f3d8ca7 | ||
|
0c5578937e | ||
|
de28e06d8f | ||
|
a768c1b5aa | ||
|
7f91de7399 | ||
|
e06ff85579 | ||
|
1f18e505ab | ||
|
257b23e89e | ||
|
e93507f148 | ||
|
3f40a6c485 | ||
|
24cc07c20a | ||
|
b742b6fc0d | ||
|
c91103a45b | ||
|
9ad1d60a47 | ||
|
3784d897d9 | ||
|
b73c14c7cc | ||
|
c766554eea | ||
|
ddf951de35 | ||
|
829903fb9c | ||
|
d1c9b7f803 | ||
|
7fe066b4ea | ||
|
c2ced23073 | ||
|
0a78c524fa | ||
|
26b560da1d | ||
|
cad1e2ab4d | ||
|
5189cdb072 | ||
|
f04c7c5557 | ||
|
190b684469 | ||
|
b96e3a0acb | ||
|
d8dcdc7455 | ||
|
1abd040428 | ||
|
591ed4a6d6 | ||
|
f154b5f2e2 | ||
|
6decab5a51 | ||
|
6763c2e99d | ||
|
d16ef6d011 | ||
|
2c9cf3ecc6 | ||
|
90441b2668 | ||
|
543f2b2a01 | ||
|
5a05bfb6de | ||
|
5118ddb8b8 | ||
|
999248d71b | ||
|
19e89de5d9 | ||
|
91002ec6be | ||
|
8f70236403 | ||
|
05c492bf82 | ||
|
782d4e160e | ||
|
771bf34ce9 | ||
|
aff7b6c72f | ||
|
284a8102c8 | ||
|
f7b8b30e9d | ||
|
d4e5984ccd | ||
|
1b7d3edd30 | ||
|
6a229eba5f | ||
|
e6bca2d35f | ||
|
ca782875c2 | ||
|
843632a22c | ||
|
79fb7531be | ||
|
ee6a27e541 | ||
|
87fbb04c59 | ||
|
ff885e4fde | ||
|
18688705be | ||
|
c1d8a0c625 | ||
|
a76d39ec86 | ||
|
9097c3ae23 | ||
|
20976f2ab9 | ||
|
c6716e6d46 | ||
|
60b36c3b19 | ||
|
334aabacb7 | ||
|
c89353cfec | ||
|
f18400b1f1 | ||
|
2dd86fcf97 | ||
|
002e2103ad | ||
|
b189ea3963 | ||
|
57a7bf6e95 | ||
|
1729324fbd | ||
|
10b60d9373 | ||
|
f12b0e62c5 | ||
|
27d978f232 | ||
|
46da74fe8a | ||
|
c3fd84b942 | ||
|
817b51eb48 | ||
|
5289b4ceb3 | ||
|
ed963933d9 | ||
|
b4e2add146 | ||
|
598b58a43d | ||
|
f093fd26c1 | ||
|
d1e0a06ebd | ||
|
c298b8447c | ||
|
5d86326ae6 | ||
|
118c9da813 | ||
|
4ad4ed5ff7 | ||
|
9f84a8ad83 | ||
|
ad2b2554c1 | ||
|
9320d8e230 | ||
|
648a540126 | ||
|
981c7d28f8 | ||
|
71070ee921 | ||
|
be3714f074 | ||
|
388f51cfb7 | ||
|
f34490d4f1 | ||
|
b44762d157 | ||
|
4f5ed37c0a | ||
|
14a5e63ad9 | ||
|
8d24f8abdd | ||
|
1a3790c7b1 | ||
|
8e91564600 | ||
|
a69ec74cfd | ||
|
694642ccb3 | ||
|
38c38a772f | ||
|
958faed1b6 | ||
|
13202cc6b1 | ||
|
68fdd55482 | ||
|
c41e0fc239 | ||
|
afd01820bb | ||
|
d894bd347d | ||
|
b21b4f4f57 | ||
|
bcb1d8ecc9 | ||
|
82ccace647 | ||
|
a6b4252210 | ||
|
83d19d7644 |
|
@ -1,2 +1,3 @@
|
||||||
.git/
|
.git/
|
||||||
venv/
|
venv/
|
||||||
|
test/
|
||||||
|
|
38
.github/ISSUE_TEMPLATE/new-theme.md
vendored
Normal file
38
.github/ISSUE_TEMPLATE/new-theme.md
vendored
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
---
|
||||||
|
name: New theme
|
||||||
|
about: Create a new theme for Whoogle
|
||||||
|
title: "[THEME] <your theme name>"
|
||||||
|
labels: theme
|
||||||
|
assignees: benbusby
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Use the following template to design your theme, replacing the blank spaces with the colors of your choice.
|
||||||
|
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
/* LIGHT THEME COLORS */
|
||||||
|
--whoogle-logo: #______;
|
||||||
|
--whoogle-page-bg: #______;
|
||||||
|
--whoogle-element-bg: #______;
|
||||||
|
--whoogle-text: #______;
|
||||||
|
--whoogle-contrast-text: #______;
|
||||||
|
--whoogle-secondary-text: #______;
|
||||||
|
--whoogle-result-bg: #______;
|
||||||
|
--whoogle-result-title: #______;
|
||||||
|
--whoogle-result-url: #______;
|
||||||
|
--whoogle-result-visited: #______;
|
||||||
|
|
||||||
|
/* DARK THEME COLORS */
|
||||||
|
--whoogle-dark-logo: #______;
|
||||||
|
--whoogle-dark-page-bg: #______;
|
||||||
|
--whoogle-dark-element-bg: #______;
|
||||||
|
--whoogle-dark-text: #______;
|
||||||
|
--whoogle-dark-contrast-text: #______;
|
||||||
|
--whoogle-dark-secondary-text: #______;
|
||||||
|
--whoogle-dark-result-bg: #______;
|
||||||
|
--whoogle-dark-result-title: #______;
|
||||||
|
--whoogle-dark-result-url: #______;
|
||||||
|
--whoogle-dark-result-visited: #______;
|
||||||
|
}
|
||||||
|
```
|
45
.github/workflows/buildx.yml
vendored
45
.github/workflows/buildx.yml
vendored
|
@ -1,13 +1,22 @@
|
||||||
name: buildx
|
name: buildx
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["docker_main"]
|
||||||
|
branches: [main]
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
push:
|
push:
|
||||||
branches: develop
|
tags:
|
||||||
|
- '*'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
on-success:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
- name: Wait for tests to succeed
|
||||||
|
if: ${{ github.event.workflow_run.conclusion != 'success' && startsWith(github.ref, 'refs/tags') != true }}
|
||||||
|
run: exit 1
|
||||||
- name: checkout code
|
- name: checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: install buildx
|
- name: install buildx
|
||||||
|
@ -15,14 +24,36 @@ jobs:
|
||||||
uses: crazy-max/ghaction-docker-buildx@v1
|
uses: crazy-max/ghaction-docker-buildx@v1
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
- name: log in to docker hub
|
- name: Login to Docker Hub
|
||||||
run: |
|
uses: docker/login-action@v1
|
||||||
echo "${{ secrets.DOCKER_PASSWORD }}" | \
|
with:
|
||||||
docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
- name: Login to ghcr.io
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: build and push the image
|
- name: build and push the image
|
||||||
|
if: startsWith(github.ref, 'refs/heads/main') && github.actor == 'benbusby'
|
||||||
run: |
|
run: |
|
||||||
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
||||||
docker buildx ls
|
docker buildx ls
|
||||||
docker buildx build --push \
|
docker buildx build --push \
|
||||||
--tag benbusby/whoogle-search:buildx-experimental \
|
--tag benbusby/whoogle-search:latest \
|
||||||
|
--platform linux/amd64,linux/arm/v7,linux/arm64 .
|
||||||
|
docker buildx build --push \
|
||||||
|
--tag ghcr.io/benbusby/whoogle-search:latest \
|
||||||
|
--platform linux/amd64,linux/arm/v7,linux/arm64 .
|
||||||
|
- name: build and push tag
|
||||||
|
if: startsWith(github.ref, 'refs/tags')
|
||||||
|
run: |
|
||||||
|
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
||||||
|
docker buildx ls
|
||||||
|
docker buildx build --push \
|
||||||
|
--tag benbusby/whoogle-search:${GITHUB_REF#refs/*/v}\
|
||||||
|
--platform linux/amd64,linux/arm/v7,linux/arm64 .
|
||||||
|
docker buildx build --push \
|
||||||
|
--tag ghcr.io/benbusby/whoogle-search:${GITHUB_REF#refs/*/v}\
|
||||||
--platform linux/amd64,linux/arm/v7,linux/arm64 .
|
--platform linux/amd64,linux/arm/v7,linux/arm64 .
|
||||||
|
|
28
.github/workflows/docker_main.yml
vendored
Normal file
28
.github/workflows/docker_main.yml
vendored
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
name: docker_main
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["tests"]
|
||||||
|
branches: [main]
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
|
||||||
|
# TODO: Needs refactoring to use reusable workflows and share w/ docker_tests
|
||||||
|
jobs:
|
||||||
|
on-success:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: build and test (docker)
|
||||||
|
run: |
|
||||||
|
docker build --tag whoogle-search:test .
|
||||||
|
docker run --publish 5000:5000 --detach --name whoogle-search-nocompose whoogle-search:test
|
||||||
|
sleep 15
|
||||||
|
docker exec whoogle-search-nocompose curl -f http://localhost:5000/healthz || exit 1
|
||||||
|
- name: build and test (docker-compose)
|
||||||
|
run: |
|
||||||
|
docker rm -f whoogle-search-nocompose
|
||||||
|
WHOOGLE_IMAGE="whoogle-search:test" docker-compose up --detach
|
||||||
|
sleep 15
|
||||||
|
docker exec whoogle-search curl -f http://localhost:5000/healthz || exit 1
|
26
.github/workflows/docker_tests.yml
vendored
Normal file
26
.github/workflows/docker_tests.yml
vendored
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
name: docker_tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: main
|
||||||
|
pull_request:
|
||||||
|
branches: main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docker:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: build and test (docker)
|
||||||
|
run: |
|
||||||
|
docker build --tag whoogle-search:test .
|
||||||
|
docker run --publish 5000:5000 --detach --name whoogle-search-nocompose whoogle-search:test
|
||||||
|
sleep 15
|
||||||
|
docker exec whoogle-search-nocompose curl -f http://localhost:5000/healthz || exit 1
|
||||||
|
- name: build and test (docker-compose)
|
||||||
|
run: |
|
||||||
|
docker rm -f whoogle-search-nocompose
|
||||||
|
WHOOGLE_IMAGE="whoogle-search:test" docker-compose up --detach
|
||||||
|
sleep 15
|
||||||
|
docker exec whoogle-search curl -f http://localhost:5000/healthz || exit 1
|
22
.github/workflows/pep8.yml
vendored
22
.github/workflows/pep8.yml
vendored
|
@ -1,22 +0,0 @@
|
||||||
name: pep8
|
|
||||||
|
|
||||||
on:
|
|
||||||
push
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v2
|
|
||||||
with:
|
|
||||||
python-version: '3.x'
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
pip install pycodestyle
|
|
||||||
- name: Run pycodestyle
|
|
||||||
run: |
|
|
||||||
pycodestyle --show-source --show-pep8 app/*
|
|
||||||
pycodestyle --show-source --show-pep8 test/*
|
|
68
.github/workflows/pypi.yml
vendored
Normal file
68
.github/workflows/pypi.yml
vendored
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
name: pypi
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: main
|
||||||
|
tags: v*
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-test:
|
||||||
|
name: Build and publish to TestPyPI
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Python 3.9
|
||||||
|
uses: actions/setup-python@v1
|
||||||
|
with:
|
||||||
|
python-version: 3.9
|
||||||
|
- name: Install pypa/build
|
||||||
|
run: >-
|
||||||
|
python -m
|
||||||
|
pip install
|
||||||
|
build
|
||||||
|
setuptools
|
||||||
|
--user
|
||||||
|
- name: Set dev timestamp
|
||||||
|
run: echo "DEV_BUILD=$(date +%s)" >> $GITHUB_ENV
|
||||||
|
- name: Build binary wheel and source tarball
|
||||||
|
run: >-
|
||||||
|
python -m
|
||||||
|
build
|
||||||
|
--sdist
|
||||||
|
--wheel
|
||||||
|
--outdir dist/
|
||||||
|
.
|
||||||
|
- name: Publish distribution to TestPyPI
|
||||||
|
uses: pypa/gh-action-pypi-publish@master
|
||||||
|
with:
|
||||||
|
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
|
||||||
|
repository_url: https://test.pypi.org/legacy/
|
||||||
|
publish:
|
||||||
|
name: Build and publish to PyPI
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Python 3.9
|
||||||
|
uses: actions/setup-python@v1
|
||||||
|
with:
|
||||||
|
python-version: 3.9
|
||||||
|
- name: Install pypa/build
|
||||||
|
run: >-
|
||||||
|
python -m
|
||||||
|
pip install
|
||||||
|
build
|
||||||
|
--user
|
||||||
|
- name: Build binary wheel and source tarball
|
||||||
|
run: >-
|
||||||
|
python -m
|
||||||
|
build
|
||||||
|
--sdist
|
||||||
|
--wheel
|
||||||
|
--outdir dist/
|
||||||
|
.
|
||||||
|
- name: Publish distribution to PyPI
|
||||||
|
if: startsWith(github.ref, 'refs/tags')
|
||||||
|
uses: pypa/gh-action-pypi-publish@master
|
||||||
|
with:
|
||||||
|
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||||
|
|
19
.github/workflows/scan.yml
vendored
Normal file
19
.github/workflows/scan.yml
vendored
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
name: scan
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * *'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
scan:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Build the container image
|
||||||
|
run: |
|
||||||
|
docker build --tag whoogle-search:test .
|
||||||
|
- name: Initiate grype scan
|
||||||
|
run: |
|
||||||
|
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b .
|
||||||
|
chmod +x ./grype
|
||||||
|
./grype whoogle-search:test --only-fixed
|
17
.github/workflows/tests.yml
vendored
Normal file
17
.github/workflows/tests.yml
vendored
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
name: tests
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pip install --upgrade pip && pip install -r requirements.txt
|
||||||
|
- name: Run tests
|
||||||
|
run: ./run test
|
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -4,6 +4,7 @@ __pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
*.pem
|
*.pem
|
||||||
*.conf
|
*.conf
|
||||||
|
*.key
|
||||||
config.json
|
config.json
|
||||||
test/static
|
test/static
|
||||||
flask_session/
|
flask_session/
|
||||||
|
@ -12,6 +13,9 @@ app/static/custom_config
|
||||||
app/static/bangs
|
app/static/bangs
|
||||||
|
|
||||||
# pip stuff
|
# pip stuff
|
||||||
build/
|
/build/
|
||||||
dist/
|
dist/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
|
|
||||||
|
# env
|
||||||
|
whoogle.env
|
||||||
|
|
6
.replit
6
.replit
|
@ -1,3 +1,3 @@
|
||||||
language = "python3"
|
language = "bash"
|
||||||
run = "./run"
|
run = "killall -q python3 > /dev/null 2>&1; pip install -r requirements.txt && ./run"
|
||||||
onBoot = "./run"
|
onBoot = "killall -q python3 > /dev/null 2>&1; pip install -r requirements.txt && ./run"
|
||||||
|
|
15
.travis.yml
15
.travis.yml
|
@ -1,15 +0,0 @@
|
||||||
language: python
|
|
||||||
python: 3.6
|
|
||||||
before_install:
|
|
||||||
- sudo apt-get -y install libgnutls28-dev
|
|
||||||
install:
|
|
||||||
- pip install -r requirements.txt
|
|
||||||
script:
|
|
||||||
- "./run test"
|
|
||||||
deploy:
|
|
||||||
provider: pypi
|
|
||||||
user: __token__
|
|
||||||
password:
|
|
||||||
secure: WNEH2Gg84MZF/AZEberFDGPPWb4cYyHAeD/XV8En94QRSI9Aznz6qiDKOvV4eVgjMAIEW5uB3TL1LHf6KU+Hrg6SmhF7JquqP1gsBOCDNFPTljO+k2Hc53uDdSnhi/HLgY7cnFNX4lc2nNrbyxZxMHuSA2oNz/tosyNGBEeyU+JA5va7uX0albGsLiNjimO4aeau83fsI0Hn2eN6ag68pewUMXNxzpyTeO2bRcCd5d5iILs07jMVwFoC2j7W11oNqrVuSWAs8CPe4+kwvNvXWxljUGiBGppNZ7RAsKNLwi6U6kGGUTWjQm09rY/2JBpJ2WEGmIWGIrno75iiFRbjnRp3mnXPvtVTyWhh+hQIUd7bJOVKM34i9eHotYTrkMJObgW1gnRzvI9VYldtgL/iP/Isn2Pv2EeMX8V+C9/8pxv0jkQkZMnFhE6gGlzpz37zTl04B2J7xyV5znM35Lx2Pn3zxdcmdCvD3yT8I4MuBbKqq2/v4emYCfPfOmfwnS0BEVSqr9lbx4xfUZV76tcvLcj4n86DJbx77pA2Ch8FRprpOOBcf0WuqTbZp8c3mb8prFp2EupUknXu7+C2VQ6sqrnzNuDeTGm/nyjjRQ81rlvlD4tqkwsEGEDDO44FF2eUTc5D2MvoHs4cnz095FWjy63gn5IxUjhMi31b5tGRz2Q=
|
|
||||||
on:
|
|
||||||
tags: true
|
|
92
Dockerfile
92
Dockerfile
|
@ -1,60 +1,73 @@
|
||||||
FROM python:3.8-slim as builder
|
FROM python:3.11.0a5-alpine as builder
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apk --update add \
|
||||||
build-essential \
|
build-base \
|
||||||
libxml2-dev \
|
libxml2-dev \
|
||||||
libxslt-dev \
|
libxslt-dev \
|
||||||
libssl-dev \
|
openssl-dev \
|
||||||
libffi-dev
|
libffi-dev
|
||||||
|
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
RUN pip install --upgrade pip
|
||||||
RUN pip install --prefix /install --no-warn-script-location --no-cache-dir -r requirements.txt
|
RUN pip install --prefix /install --no-warn-script-location --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
FROM python:3.8-slim
|
FROM python:3.11.0a5-alpine
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apk add --update --no-cache tor curl openrc libstdc++
|
||||||
libcurl4-openssl-dev \
|
# libcurl4-openssl-dev
|
||||||
tor \
|
|
||||||
curl \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
|
RUN apk -U upgrade
|
||||||
|
|
||||||
|
ARG DOCKER_USER=whoogle
|
||||||
|
ARG DOCKER_USERID=927
|
||||||
ARG config_dir=/config
|
ARG config_dir=/config
|
||||||
RUN mkdir -p $config_dir
|
RUN mkdir -p $config_dir
|
||||||
|
RUN chmod a+w $config_dir
|
||||||
VOLUME $config_dir
|
VOLUME $config_dir
|
||||||
ENV CONFIG_VOLUME=$config_dir
|
|
||||||
|
|
||||||
|
ARG url_prefix=''
|
||||||
ARG username=''
|
ARG username=''
|
||||||
ENV WHOOGLE_USER=$username
|
|
||||||
ARG password=''
|
ARG password=''
|
||||||
ENV WHOOGLE_PASS=$password
|
|
||||||
|
|
||||||
ARG proxyuser=''
|
ARG proxyuser=''
|
||||||
ENV WHOOGLE_PROXY_USER=$proxyuser
|
|
||||||
ARG proxypass=''
|
ARG proxypass=''
|
||||||
ENV WHOOGLE_PROXY_PASS=$proxypass
|
|
||||||
ARG proxytype=''
|
ARG proxytype=''
|
||||||
ENV WHOOGLE_PROXY_TYPE=$proxytype
|
|
||||||
ARG proxyloc=''
|
ARG proxyloc=''
|
||||||
ENV WHOOGLE_PROXY_LOC=$proxyloc
|
|
||||||
|
|
||||||
ARG whoogle_dotenv=''
|
ARG whoogle_dotenv=''
|
||||||
ENV WHOOGLE_DOTENV=$whoogle_dotenv
|
|
||||||
|
|
||||||
ARG use_https=''
|
ARG use_https=''
|
||||||
ENV HTTPS_ONLY=$use_https
|
|
||||||
|
|
||||||
ARG whoogle_port=5000
|
ARG whoogle_port=5000
|
||||||
ENV EXPOSE_PORT=$whoogle_port
|
ARG twitter_alt='farside.link/nitter'
|
||||||
|
ARG youtube_alt='farside.link/invidious'
|
||||||
|
ARG instagram_alt='farside.link/bibliogram/u'
|
||||||
|
ARG reddit_alt='farside.link/libreddit'
|
||||||
|
ARG medium_alt='farside.link/scribe'
|
||||||
|
ARG translate_alt='farside.link/lingva'
|
||||||
|
ARG imgur_alt='farside.link/rimgo'
|
||||||
|
ARG wikipedia_alt='farside.link/wikiless'
|
||||||
|
ARG imdb_alt='farside.link/libremdb'
|
||||||
|
ARG quora_alt='farside.link/quetre'
|
||||||
|
|
||||||
ARG twitter_alt='nitter.net'
|
ENV CONFIG_VOLUME=$config_dir \
|
||||||
ENV WHOOGLE_ALT_TW=$twitter_alt
|
WHOOGLE_URL_PREFIX=$url_prefix \
|
||||||
ARG youtube_alt='invidious.snopyta.org'
|
WHOOGLE_USER=$username \
|
||||||
ENV WHOOGLE_ALT_YT=$youtube_alt
|
WHOOGLE_PASS=$password \
|
||||||
ARG instagram_alt='bibliogram.art/u'
|
WHOOGLE_PROXY_USER=$proxyuser \
|
||||||
ENV WHOOGLE_ALT_IG=$instagram_alt
|
WHOOGLE_PROXY_PASS=$proxypass \
|
||||||
ARG reddit_alt='libredd.it'
|
WHOOGLE_PROXY_TYPE=$proxytype \
|
||||||
ENV WHOOGLE_ALT_RD=$reddit_alt
|
WHOOGLE_PROXY_LOC=$proxyloc \
|
||||||
|
WHOOGLE_DOTENV=$whoogle_dotenv \
|
||||||
|
HTTPS_ONLY=$use_https \
|
||||||
|
EXPOSE_PORT=$whoogle_port \
|
||||||
|
WHOOGLE_ALT_TW=$twitter_alt \
|
||||||
|
WHOOGLE_ALT_YT=$youtube_alt \
|
||||||
|
WHOOGLE_ALT_IG=$instagram_alt \
|
||||||
|
WHOOGLE_ALT_RD=$reddit_alt \
|
||||||
|
WHOOGLE_ALT_MD=$medium_alt \
|
||||||
|
WHOOGLE_ALT_TL=$translate_alt \
|
||||||
|
WHOOGLE_ALT_IMG=$imgur_alt \
|
||||||
|
WHOOGLE_ALT_WIKI=$wikipedia_alt \
|
||||||
|
WHOOGLE_ALT_IMDB=$imdb_alt \
|
||||||
|
WHOOGLE_ALT_QUORA=$quora_alt
|
||||||
|
|
||||||
WORKDIR /whoogle
|
WORKDIR /whoogle
|
||||||
|
|
||||||
|
@ -63,11 +76,22 @@ COPY misc/tor/torrc /etc/tor/torrc
|
||||||
COPY misc/tor/start-tor.sh misc/tor/start-tor.sh
|
COPY misc/tor/start-tor.sh misc/tor/start-tor.sh
|
||||||
COPY app/ app/
|
COPY app/ app/
|
||||||
COPY run .
|
COPY run .
|
||||||
COPY whoogle.env .
|
#COPY whoogle.env .
|
||||||
|
|
||||||
|
# Create user/group to run as
|
||||||
|
RUN adduser -D -g $DOCKER_USERID -u $DOCKER_USERID $DOCKER_USER
|
||||||
|
|
||||||
|
# Fix ownership / permissions
|
||||||
|
RUN chown -R ${DOCKER_USER}:${DOCKER_USER} /whoogle /var/lib/tor
|
||||||
|
|
||||||
|
# Allow writing symlinks to build dir
|
||||||
|
RUN chown $DOCKER_USERID:$DOCKER_USERID app/static/build
|
||||||
|
|
||||||
|
USER $DOCKER_USER:$DOCKER_USER
|
||||||
|
|
||||||
EXPOSE $EXPOSE_PORT
|
EXPOSE $EXPOSE_PORT
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=5s \
|
HEALTHCHECK --interval=30s --timeout=5s \
|
||||||
CMD curl -f http://localhost:${EXPOSE_PORT}/healthz || exit 1
|
CMD curl -f http://localhost:${EXPOSE_PORT}/healthz || exit 1
|
||||||
|
|
||||||
CMD misc/tor/start-tor.sh & ./run
|
CMD misc/tor/start-tor.sh & ./run
|
||||||
|
|
329
README.md
329
README.md
|
@ -2,11 +2,18 @@
|
||||||
|
|
||||||
[](https://github.com/benbusby/shoogle/releases)
|
[](https://github.com/benbusby/shoogle/releases)
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
[](https://travis-ci.com/benbusby/whoogle-search)
|
[](https://github.com/benbusby/whoogle-search/actions/workflows/tests.yml)
|
||||||
[](https://github.com/benbusby/whoogle-search/actions?query=workflow%3Apep8)
|
[](https://github.com/benbusby/whoogle-search/actions/workflows/buildx.yml)
|
||||||
[](https://codebeat.co/projects/github-com-benbusby-shoogle-master)
|
[](https://codebeat.co/projects/github-com-benbusby-shoogle-master)
|
||||||
[](https://hub.docker.com/r/benbusby/whoogle-search)
|
[](https://hub.docker.com/r/benbusby/whoogle-search)
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td><a href="https://sr.ht/~benbusby/whoogle-search">SourceHut</a></td>
|
||||||
|
<td><a href="https://github.com/benbusby/whoogle-search">GitHub</a></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
Get Google search results, but without any ads, javascript, AMP links, cookies, or IP address tracking. Easily deployable in one click as a Docker app, and customizable with a single config file. Quick and simple to implement as a primary search engine replacement on both desktop and mobile.
|
Get Google search results, but without any ads, javascript, AMP links, cookies, or IP address tracking. Easily deployable in one click as a Docker app, and customizable with a single config file. Quick and simple to implement as a primary search engine replacement on both desktop and mobile.
|
||||||
|
|
||||||
Contents
|
Contents
|
||||||
|
@ -21,25 +28,26 @@ Contents
|
||||||
6. [Manual](#f-manual)
|
6. [Manual](#f-manual)
|
||||||
7. [Docker](#g-manual-docker)
|
7. [Docker](#g-manual-docker)
|
||||||
8. [Arch/AUR](#arch-linux--arch-based-distributions)
|
8. [Arch/AUR](#arch-linux--arch-based-distributions)
|
||||||
|
9. [Helm/Kubernetes](#helm-chart-for-kubernetes)
|
||||||
4. [Environment Variables and Configuration](#environment-variables)
|
4. [Environment Variables and Configuration](#environment-variables)
|
||||||
5. [Usage](#usage)
|
5. [Usage](#usage)
|
||||||
6. [Extra Steps](#extra-steps)
|
6. [Extra Steps](#extra-steps)
|
||||||
1. [Set Primary Search Engine](#set-whoogle-as-your-primary-search-engine)
|
1. [Set Primary Search Engine](#set-whoogle-as-your-primary-search-engine)
|
||||||
2. [Prevent Downtime (Heroku Only)](#prevent-downtime-heroku-only)
|
2. [Prevent Downtime (Heroku Only)](#prevent-downtime-heroku-only)
|
||||||
3. [Manual HTTPS Enforcement](#https-enforcement)
|
3. [Manual HTTPS Enforcement](#https-enforcement)
|
||||||
|
4. [Using with Firefox Containers](#using-with-firefox-containers)
|
||||||
|
5. [Reverse Proxying](#reverse-proxying)
|
||||||
|
1. [Nginx](#nginx)
|
||||||
7. [Contributing](#contributing)
|
7. [Contributing](#contributing)
|
||||||
8. [FAQ](#faq)
|
8. [FAQ](#faq)
|
||||||
9. [Public Instances](#public-instances)
|
9. [Public Instances](#public-instances)
|
||||||
10. [Screenshots](#screenshots)
|
10. [Screenshots](#screenshots)
|
||||||
11. Mirrors (read-only)
|
|
||||||
1. [GitLab](https://gitlab.com/benbusby/whoogle-search)
|
|
||||||
2. [Gogs](https://gogs.benbusby.com/benbusby/whoogle-search)
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- No ads or sponsored content
|
- No ads or sponsored content
|
||||||
- No javascript
|
- No JavaScript\*
|
||||||
- No cookies
|
- No cookies\*\*
|
||||||
- No tracking/linking of your personal IP address\*
|
- No tracking/linking of your personal IP address\*\*\*
|
||||||
- No AMP links
|
- No AMP links
|
||||||
- No URL tracking tags (i.e. utm=%s)
|
- No URL tracking tags (i.e. utm=%s)
|
||||||
- No referrer header
|
- No referrer header
|
||||||
|
@ -47,14 +55,18 @@ Contents
|
||||||
- Autocomplete/search suggestions
|
- Autocomplete/search suggestions
|
||||||
- POST request search and suggestion queries (when possible)
|
- POST request search and suggestion queries (when possible)
|
||||||
- View images at full res without site redirect (currently mobile only)
|
- View images at full res without site redirect (currently mobile only)
|
||||||
- Dark mode
|
- Light/Dark/System theme modes (with support for [custom CSS theming](https://github.com/benbusby/whoogle-search/wiki/User-Contributed-CSS-Themes))
|
||||||
- Randomly generated User Agent
|
- Randomly generated User Agent
|
||||||
- Easy to install/deploy
|
- Easy to install/deploy
|
||||||
- DDG-style bang (i.e. `!<tag> <query>`) searches
|
- DDG-style bang (i.e. `!<tag> <query>`) searches
|
||||||
- Optional location-based searching (i.e. results near \<city\>)
|
- Optional location-based searching (i.e. results near \<city\>)
|
||||||
- Optional NoJS mode to disable all Javascript in results
|
- Optional NoJS mode to view search results in a separate window with JavaScript blocked
|
||||||
|
|
||||||
<sup>*If deployed to a remote server, or configured to send requests through a VPN, Tor, proxy, etc.</sup>
|
<sup>*No third party JavaScript. Whoogle can be used with JavaScript disabled, but if enabled, uses JavaScript for things like presenting search suggestions.</sup>
|
||||||
|
|
||||||
|
<sup>**No third party cookies. Whoogle uses server side cookies (sessions) to store non-sensitive configuration settings such as theme, language, etc. Just like with JavaScript, cookies can be disabled and not affect Whoogle's search functionality.</sup>
|
||||||
|
|
||||||
|
<sup>***If deployed to a remote server, or configured to send requests through a VPN, Tor, proxy, etc.</sup>
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
If using Heroku Quick Deploy, **you can skip this section**.
|
If using Heroku Quick Deploy, **you can skip this section**.
|
||||||
|
@ -71,15 +83,17 @@ If using Heroku Quick Deploy, **you can skip this section**.
|
||||||
There are a few different ways to begin using the app, depending on your preferences:
|
There are a few different ways to begin using the app, depending on your preferences:
|
||||||
|
|
||||||
### A) [Heroku Quick Deploy](https://heroku.com/about)
|
### A) [Heroku Quick Deploy](https://heroku.com/about)
|
||||||
[](https://heroku.com/deploy?template=https://github.com/benbusby/whoogle-search/tree/heroku-app-beta)
|
[](https://heroku.com/deploy?template=https://github.com/benbusby/whoogle-search/tree/main)
|
||||||
|
|
||||||
*Note: Requires a (free) Heroku account*
|
|
||||||
|
|
||||||
Provides:
|
Provides:
|
||||||
- Free deployment of app
|
- Free deployment of app
|
||||||
- Free HTTPS url (https://\<your app name\>.herokuapp.com)
|
- Free HTTPS url (https://\<your app name\>.herokuapp.com)
|
||||||
- Downtime after periods of inactivity \([solution](https://github.com/benbusby/whoogle-search#prevent-downtime-heroku-only)\)
|
- Downtime after periods of inactivity \([solution](https://github.com/benbusby/whoogle-search#prevent-downtime-heroku-only)\)
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Requires a (free) Heroku account
|
||||||
|
- Sometimes has issues with auto-redirecting to `https`. Make sure to navigate to the `https` version of your app before adding as a default search engine.
|
||||||
|
|
||||||
### B) [Repl.it](https://repl.it)
|
### B) [Repl.it](https://repl.it)
|
||||||
[](https://repl.it/github/benbusby/whoogle-search)
|
[](https://repl.it/github/benbusby/whoogle-search)
|
||||||
|
|
||||||
|
@ -93,30 +107,19 @@ Provides:
|
||||||
|
|
||||||
### C) [Fly.io](https://fly.io)
|
### C) [Fly.io](https://fly.io)
|
||||||
|
|
||||||
You will need a [Fly.io](https://fly.io) account to do this. Fly requires a credit card to deploy anything, but you can have up to 3 shared-CPU VMs running full-time each month for free.
|
You will need a **PAID** [Fly.io](https://fly.io) account to deploy Whoogle.
|
||||||
|
|
||||||
#### Install the CLI:
|
#### Install the CLI: https://fly.io/docs/hands-on/installing/
|
||||||
|
|
||||||
|
#### Deploy the app
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -L https://fly.io/install.sh | sh
|
flyctl auth login
|
||||||
```
|
flyctl launch --image benbusby/whoogle-search:latest
|
||||||
|
|
||||||
#### Deploy your app
|
|
||||||
|
|
||||||
```bash
|
|
||||||
fly apps create --org personal --port 5000
|
|
||||||
# Choose a name and the Image builder
|
|
||||||
# Enter `benbusby/whoogle-search:latest` as the image name
|
|
||||||
fly deploy
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Your app is now available at `https://<app-name>.fly.dev`.
|
Your app is now available at `https://<app-name>.fly.dev`.
|
||||||
|
|
||||||
You can customize the `fly.toml`:
|
|
||||||
- Remove the non-https service
|
|
||||||
- Add environment variables under the `[env]` key
|
|
||||||
- Use `fly secrets set NAME=value` for more sensitive values like `WHOOGLE_PASS` and `WHOOGLE_PROXY_PASS`.
|
|
||||||
|
|
||||||
### D) [pipx](https://github.com/pipxproject/pipx#install-pipx)
|
### D) [pipx](https://github.com/pipxproject/pipx#install-pipx)
|
||||||
Persistent install:
|
Persistent install:
|
||||||
|
|
||||||
|
@ -155,7 +158,7 @@ See the [available environment variables](#environment-variables) for additional
|
||||||
|
|
||||||
### F) Manual
|
### F) Manual
|
||||||
|
|
||||||
*Note: `Content-Security-Policy` headers are already sent by Whoogle -- you don't/shouldn't need to apply a CSP header yourself*
|
*Note: `Content-Security-Policy` headers can be sent by Whoogle if you set `WHOOGLE_CSP`.*
|
||||||
|
|
||||||
Clone the repo and run the following commands to start the app in a local-only environment:
|
Clone the repo and run the following commands to start the app in a local-only environment:
|
||||||
|
|
||||||
|
@ -170,9 +173,9 @@ pip install -r requirements.txt
|
||||||
See the [available environment variables](#environment-variables) for additional configuration.
|
See the [available environment variables](#environment-variables) for additional configuration.
|
||||||
|
|
||||||
#### systemd Configuration
|
#### systemd Configuration
|
||||||
After building the virtual environment, you can add the following to `/lib/systemd/system/whoogle.service` to set up a Whoogle Search systemd service:
|
After building the virtual environment, you can add something like the following to `/lib/systemd/system/whoogle.service` to set up a Whoogle Search systemd service:
|
||||||
|
|
||||||
```
|
```ini
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Whoogle
|
Description=Whoogle
|
||||||
|
|
||||||
|
@ -187,17 +190,29 @@ Description=Whoogle
|
||||||
#Environment=WHOOGLE_PROXY_LOC=<proxy host/ip>
|
#Environment=WHOOGLE_PROXY_LOC=<proxy host/ip>
|
||||||
# Site alternative configurations, uncomment to enable
|
# Site alternative configurations, uncomment to enable
|
||||||
# Note: If not set, the feature will still be available
|
# Note: If not set, the feature will still be available
|
||||||
# with default values.
|
# with default values.
|
||||||
#Environment=WHOOGLE_ALT_TW=nitter.net
|
#Environment=WHOOGLE_ALT_TW=farside.link/nitter
|
||||||
#Environment=WHOOGLE_ALT_YT=invidious.snopyta.org
|
#Environment=WHOOGLE_ALT_YT=farside.link/invidious
|
||||||
#Environment=WHOOGLE_ALT_IG=bibliogram.art/u
|
#Environment=WHOOGLE_ALT_IG=farside.link/bibliogram/u
|
||||||
#Environment=WHOOGLE_ALT_RD=libredd.it
|
#Environment=WHOOGLE_ALT_RD=farside.link/libreddit
|
||||||
|
#Environment=WHOOGLE_ALT_MD=farside.link/scribe
|
||||||
|
#Environment=WHOOGLE_ALT_TL=farside.link/lingva
|
||||||
|
#Environment=WHOOGLE_ALT_IMG=farside.link/rimgo
|
||||||
|
#Environment=WHOOGLE_ALT_WIKI=farside.link/wikiless
|
||||||
|
#Environment=WHOOGLE_ALT_IMDB=farside.link/libremdb
|
||||||
|
#Environment=WHOOGLE_ALT_QUORA=farside.link/quetre
|
||||||
# Load values from dotenv only
|
# Load values from dotenv only
|
||||||
#Environment=WHOOGLE_DOTENV=1
|
#Environment=WHOOGLE_DOTENV=1
|
||||||
Type=simple
|
Type=simple
|
||||||
User=root
|
User=<username>
|
||||||
WorkingDirectory=<whoogle_directory>
|
# If installed as a package, add:
|
||||||
ExecStart=<whoogle_directory>/venv/bin/python3 -um app --host 0.0.0.0 --port 5000
|
ExecStart=<python_install_dir>/python3 <whoogle_install_dir>/whoogle-search --host 127.0.0.1 --port 5000
|
||||||
|
# For example:
|
||||||
|
# ExecStart=/usr/bin/python3 /home/my_username/.local/bin/whoogle-search --host 127.0.0.1 --port 5000
|
||||||
|
# Otherwise if running the app from source, add:
|
||||||
|
ExecStart=<whoogle_repo_dir>/run
|
||||||
|
# For example:
|
||||||
|
# ExecStart=/var/www/whoogle-search/run
|
||||||
ExecReload=/bin/kill -HUP $MAINPID
|
ExecReload=/bin/kill -HUP $MAINPID
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=3
|
RestartSec=3
|
||||||
|
@ -213,6 +228,51 @@ sudo systemctl enable whoogle
|
||||||
sudo systemctl start whoogle
|
sudo systemctl start whoogle
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Tor Configuration *optional*
|
||||||
|
If routing your request through Tor you will need to make the following adjustments.
|
||||||
|
Due to the nature of interacting with Google through Tor we will need to be able to send signals to Tor and therefore authenticate with it.
|
||||||
|
|
||||||
|
There are two authentication methods, password and cookie. You will need to make changes to your torrc:
|
||||||
|
* Cookie
|
||||||
|
1. Uncomment or add the following lines in your torrc:
|
||||||
|
- `ControlPort 9051`
|
||||||
|
- `CookieAuthentication 1`
|
||||||
|
- `DataDirectoryGroupReadable 1`
|
||||||
|
- `CookieAuthFileGroupReadable 1`
|
||||||
|
|
||||||
|
2. Make the tor auth cookie readable:
|
||||||
|
- This is assuming that you are using a dedicated user to run whoogle. If you are using a different user replace `whoogle` with that user.
|
||||||
|
|
||||||
|
1. `chmod tor:whoogle /var/lib/tor`
|
||||||
|
2. `chmod tor:whoogle /var/lib/tor/control_auth_cookie`
|
||||||
|
|
||||||
|
3. Restart the tor service:
|
||||||
|
- `systemctl restart tor`
|
||||||
|
|
||||||
|
4. Set the Tor environment variable to 1, `WHOOGLE_CONFIG_TOR`. Refer to the [Environment Variables](#environment-variables) section for more details.
|
||||||
|
- This may be added in the systemd unit file or env file `WHOOGLE_CONFIG_TOR=1`
|
||||||
|
|
||||||
|
* Password
|
||||||
|
1. Run this command:
|
||||||
|
- `tor --hash-password {Your Password Here}`; put your password in place of `{Your Password Here}`.
|
||||||
|
- Keep the output of this command, you will be placing it in your torrc.
|
||||||
|
- Keep the password input of this command, you will be using it later.
|
||||||
|
|
||||||
|
2. Uncomment or add the following lines in your torrc:
|
||||||
|
- `ControlPort 9051`
|
||||||
|
- `HashedControlPassword {Place output here}`; put the output of the previous command in place of `{Place output here}`.
|
||||||
|
|
||||||
|
3. Now take the password from the first step and place it in the control.conf file within the whoogle working directory, ie. [misc/tor/control.conf](misc/tor/control.conf)
|
||||||
|
- If you want to place your password file in a different location set this location with the `WHOOGLE_TOR_CONF` environment variable. Refer to the [Environment Variables](#environment-variables) section for more details.
|
||||||
|
|
||||||
|
4. Heavily restrict access to control.conf to only be readable by the user running whoogle:
|
||||||
|
- `chmod 400 control.conf`
|
||||||
|
|
||||||
|
5. Finally set the Tor environment variable and use password variable to 1, `WHOOGLE_CONFIG_TOR` and `WHOOGLE_TOR_USE_PASS`. Refer to the [Environment Variables](#environment-variables) section for more details.
|
||||||
|
- These may be added to the systemd unit file or env file:
|
||||||
|
- `WHOOGLE_CONFIG_TOR=1`
|
||||||
|
- `WHOOGLE_TOR_USE_PASS=1`
|
||||||
|
|
||||||
### G) Manual (Docker)
|
### G) Manual (Docker)
|
||||||
1. Ensure the Docker daemon is running, and is accessible by your user account
|
1. Ensure the Docker daemon is running, and is accessible by your user account
|
||||||
- To add user permissions, you can execute `sudo usermod -aG docker yourusername`
|
- To add user permissions, you can execute `sudo usermod -aG docker yourusername`
|
||||||
|
@ -221,8 +281,6 @@ sudo systemctl start whoogle
|
||||||
|
|
||||||
#### Docker CLI
|
#### Docker CLI
|
||||||
|
|
||||||
***Note:** For ARM machines, use the `buildx-experimental` Docker tag.*
|
|
||||||
|
|
||||||
Through Docker Hub:
|
Through Docker Hub:
|
||||||
```bash
|
```bash
|
||||||
docker pull benbusby/whoogle-search
|
docker pull benbusby/whoogle-search
|
||||||
|
@ -279,6 +337,13 @@ You may also edit environment variables from your app’s Settings tab in the He
|
||||||
#### Arch Linux & Arch-based Distributions
|
#### Arch Linux & Arch-based Distributions
|
||||||
There is an [AUR package available](https://aur.archlinux.org/packages/whoogle-git/), as well as a pre-built and daily updated package available at [Chaotic-AUR](https://chaotic.cx).
|
There is an [AUR package available](https://aur.archlinux.org/packages/whoogle-git/), as well as a pre-built and daily updated package available at [Chaotic-AUR](https://chaotic.cx).
|
||||||
|
|
||||||
|
#### Helm chart for Kubernetes
|
||||||
|
To use the Kubernetes Helm Chart:
|
||||||
|
1. Ensure you have [Helm](https://helm.sh/docs/intro/install/) `>=3.0.0` installed
|
||||||
|
2. Clone this repository
|
||||||
|
3. Update [charts/whoogle/values.yaml](./charts/whoogle/values.yaml) as desired
|
||||||
|
4. Run `helm install whoogle ./charts/whoogle`
|
||||||
|
|
||||||
#### Using your own server, or alternative container deployment
|
#### Using your own server, or alternative container deployment
|
||||||
There are other methods for deploying docker containers that are well outlined in [this article](https://rollout.io/blog/the-shortlist-of-docker-hosting/), but there are too many to describe set up for each here. Generally it should be about the same amount of effort as the Heroku deployment.
|
There are other methods for deploying docker containers that are well outlined in [this article](https://rollout.io/blog/the-shortlist-of-docker-hosting/), but there are too many to describe set up for each here. Generally it should be about the same amount of effort as the Heroku deployment.
|
||||||
|
|
||||||
|
@ -295,41 +360,58 @@ There are a few optional environment variables available for customizing a Whoog
|
||||||
- With `docker-compose`: Uncomment the `env_file` option
|
- With `docker-compose`: Uncomment the `env_file` option
|
||||||
- With `docker build/run`: Add `--env-file ./whoogle.env` to your command
|
- With `docker build/run`: Add `--env-file ./whoogle.env` to your command
|
||||||
|
|
||||||
| Variable | Description |
|
| Variable | Description |
|
||||||
| ------------------ | ----------------------------------------------------------------------------------------- |
|
| -------------------- | ----------------------------------------------------------------------------------------- |
|
||||||
| WHOOGLE_DOTENV | Load environment variables in `whoogle.env` |
|
| WHOOGLE_URL_PREFIX | The URL prefix to use for the whoogle instance (i.e. "/whoogle") |
|
||||||
| WHOOGLE_USER | The username for basic auth. WHOOGLE_PASS must also be set if used. |
|
| WHOOGLE_DOTENV | Load environment variables in `whoogle.env` |
|
||||||
| WHOOGLE_PASS | The password for basic auth. WHOOGLE_USER must also be set if used. |
|
| WHOOGLE_USER | The username for basic auth. WHOOGLE_PASS must also be set if used. |
|
||||||
| WHOOGLE_PROXY_USER | The username of the proxy server. |
|
| WHOOGLE_PASS | The password for basic auth. WHOOGLE_USER must also be set if used. |
|
||||||
| WHOOGLE_PROXY_PASS | The password of the proxy server. |
|
| WHOOGLE_PROXY_USER | The username of the proxy server. |
|
||||||
| WHOOGLE_PROXY_TYPE | The type of the proxy server. Can be "socks5", "socks4", or "http". |
|
| WHOOGLE_PROXY_PASS | The password of the proxy server. |
|
||||||
| WHOOGLE_PROXY_LOC | The location of the proxy server (host or ip). |
|
| WHOOGLE_PROXY_TYPE | The type of the proxy server. Can be "socks5", "socks4", or "http". |
|
||||||
| EXPOSE_PORT | The port where Whoogle will be exposed. |
|
| WHOOGLE_PROXY_LOC | The location of the proxy server (host or ip). |
|
||||||
| HTTPS_ONLY | Enforce HTTPS. (See [here](https://github.com/benbusby/whoogle-search#https-enforcement)) |
|
| EXPOSE_PORT | The port where Whoogle will be exposed. |
|
||||||
| WHOOGLE_ALT_TW | The twitter.com alternative to use when site alternatives are enabled in the config. |
|
| HTTPS_ONLY | Enforce HTTPS. (See [here](https://github.com/benbusby/whoogle-search#https-enforcement)) |
|
||||||
| WHOOGLE_ALT_YT | The youtube.com alternative to use when site alternatives are enabled in the config. |
|
| WHOOGLE_ALT_TW | The twitter.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
|
||||||
| WHOOGLE_ALT_IG | The instagram.com alternative to use when site alternatives are enabled in the config. |
|
| WHOOGLE_ALT_YT | The youtube.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
|
||||||
| WHOOGLE_ALT_RD | The reddit.com alternative to use when site alternatives are enabled in the config. |
|
| WHOOGLE_ALT_IG | The instagram.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
|
||||||
|
| WHOOGLE_ALT_RD | The reddit.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
|
||||||
|
| WHOOGLE_ALT_TL | The Google Translate alternative to use. This is used for all "translate ____" searches. Set to "" to disable. |
|
||||||
|
| WHOOGLE_ALT_MD | The medium.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
|
||||||
|
| WHOOGLE_ALT_IMG | The imgur.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
|
||||||
|
| WHOOGLE_ALT_WIKI | The wikipedia.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
|
||||||
|
| WHOOGLE_ALT_IMDB | The imdb.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
|
||||||
|
| WHOOGLE_ALT_QUORA | The quora.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
|
||||||
|
| WHOOGLE_AUTOCOMPLETE | Controls visibility of autocomplete/search suggestions. Default on -- use '0' to disable. |
|
||||||
|
| WHOOGLE_MINIMAL | Remove everything except basic result cards from all search queries. |
|
||||||
|
| WHOOGLE_CSP | Sets a default set of 'Content-Security-Policy' headers |
|
||||||
|
| WHOOGLE_RESULTS_PER_PAGE | Set the number of results per page |
|
||||||
|
| WHOOGLE_TOR_SERVICE | Enable/disable the Tor service on startup. Default on -- use '0' to disable. |
|
||||||
|
| WHOOGLE_TOR_USE_PASS | Use password authentication for tor control port. |
|
||||||
|
| WHOOGLE_TOR_CONF | The absolute path to the config file containing the password for the tor control port. Default: ./misc/tor/control.conf WHOOGLE_TOR_PASS must be 1 for this to work.|
|
||||||
|
|
||||||
### Config Environment Variables
|
### Config Environment Variables
|
||||||
These environment variables allow setting default config values, but can be overwritten manually by using the home page config menu. These allow a shortcut for destroying/rebuilding an instance to the same config state every time.
|
These environment variables allow setting default config values, but can be overwritten manually by using the home page config menu. These allow a shortcut for destroying/rebuilding an instance to the same config state every time.
|
||||||
|
|
||||||
| Variable | Description |
|
| Variable | Description |
|
||||||
| ------------------------------ | --------------------------------------------------------------- |
|
| ------------------------------------ | --------------------------------------------------------------- |
|
||||||
| WHOOGLE_CONFIG_DISABLE | Hide config from UI and disallow changes to config by client |
|
| WHOOGLE_CONFIG_DISABLE | Hide config from UI and disallow changes to config by client |
|
||||||
| WHOOGLE_CONFIG_COUNTRY | Filter results by hosting country |
|
| WHOOGLE_CONFIG_COUNTRY | Filter results by hosting country |
|
||||||
| WHOOGLE_CONFIG_LANGUAGE | Set interface language |
|
| WHOOGLE_CONFIG_LANGUAGE | Set interface language |
|
||||||
| WHOOGLE_CONFIG_SEARCH_LANGUAGE | Set search result language |
|
| WHOOGLE_CONFIG_SEARCH_LANGUAGE | Set search result language |
|
||||||
| WHOOGLE_CONFIG_BLOCK | Block websites from search results (use comma-separated list) |
|
| WHOOGLE_CONFIG_BLOCK | Block websites from search results (use comma-separated list) |
|
||||||
| WHOOGLE_CONFIG_DARK | Enable dark theme |
|
| WHOOGLE_CONFIG_THEME | Set theme mode (light, dark, or system) |
|
||||||
| WHOOGLE_CONFIG_SAFE | Enable safe searches |
|
| WHOOGLE_CONFIG_SAFE | Enable safe searches |
|
||||||
| WHOOGLE_CONFIG_ALTS | Use social media site alternatives (nitter, invidious, etc) |
|
| WHOOGLE_CONFIG_ALTS | Use social media site alternatives (nitter, invidious, etc) |
|
||||||
| WHOOGLE_CONFIG_TOR | Use Tor routing (if available) |
|
| WHOOGLE_CONFIG_NEAR | Restrict results to only those near a particular city |
|
||||||
| WHOOGLE_CONFIG_NEW_TAB | Always open results in new tab |
|
| WHOOGLE_CONFIG_TOR | Use Tor routing (if available) |
|
||||||
| WHOOGLE_CONFIG_VIEW_IMAGE | Enable View Image option |
|
| WHOOGLE_CONFIG_NEW_TAB | Always open results in new tab |
|
||||||
| WHOOGLE_CONFIG_GET_ONLY | Search using GET requests only |
|
| WHOOGLE_CONFIG_VIEW_IMAGE | Enable View Image option |
|
||||||
| WHOOGLE_CONFIG_URL | The root url of the instance (`https://<your url>/`) |
|
| WHOOGLE_CONFIG_GET_ONLY | Search using GET requests only |
|
||||||
| WHOOGLE_CONFIG_STYLE | The custom CSS to use for styling (should be single line) |
|
| WHOOGLE_CONFIG_URL | The root url of the instance (`https://<your url>/`) |
|
||||||
|
| WHOOGLE_CONFIG_STYLE | The custom CSS to use for styling (should be single line) |
|
||||||
|
| WHOOGLE_CONFIG_PREFERENCES_ENCRYPTED | Encrypt preferences token, requires preferences key |
|
||||||
|
| WHOOGLE_CONFIG_PREFERENCES_KEY | Key to encrypt preferences in URL (REQUIRED to show url) |
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
Same as most search engines, with the exception of filtering by time range.
|
Same as most search engines, with the exception of filtering by time range.
|
||||||
|
@ -342,7 +424,12 @@ To filter by a range of time, append ":past <time>" to the end of your search, w
|
||||||
|
|
||||||
Browser settings:
|
Browser settings:
|
||||||
- Firefox (Desktop)
|
- Firefox (Desktop)
|
||||||
- Navigate to your app's url, and click the 3 dot menu in the address bar. At the bottom, there should be an option to "Add Search Engine". Once you've clicked this, open your Firefox Preferences menu, click "Search" in the left menu, and use the available dropdown to select "Whoogle" from the list.
|
- Version 89+
|
||||||
|
- Navigate to your app's url, right click the address bar, and select "Add Search Engine".
|
||||||
|
- Previous versions
|
||||||
|
- Navigate to your app's url, and click the 3 dot menu in the address bar. At the bottom, there should be an option to "Add Search Engine".
|
||||||
|
- Once you've added the new search engine, open your Firefox Preferences menu, click "Search" in the left menu, and use the available dropdown to select "Whoogle" from the list.
|
||||||
|
- **Note**: If your Whoogle instance uses Firefox Containers, you'll need to [go through the steps here](#using-with-firefox-containers) to get it working properly.
|
||||||
- Firefox (iOS)
|
- Firefox (iOS)
|
||||||
- In the mobile app Settings page, tap "Search" within the "General" section. There should be an option titled "Add Search Engine" to select. It should prompt you to enter a title and search query url - use the following elements to fill out the form:
|
- In the mobile app Settings page, tap "Search" within the "General" section. There should be an option titled "Add Search Engine" to select. It should prompt you to enter a title and search query url - use the following elements to fill out the form:
|
||||||
- Title: "Whoogle"
|
- Title: "Whoogle"
|
||||||
|
@ -372,7 +459,7 @@ Browser settings:
|
||||||
2. Go to `Default Results` and click the `Setup fallback results` button. Click `+` and add Whoogle, then drag it to the top.
|
2. Go to `Default Results` and click the `Setup fallback results` button. Click `+` and add Whoogle, then drag it to the top.
|
||||||
- Chrome/Chromium-based Browsers
|
- Chrome/Chromium-based Browsers
|
||||||
- Automatic
|
- Automatic
|
||||||
- Visit the home page of your Whoogle Search instance -- this may automatically add the search engine to your list of search engines. If not, you can add it manually.
|
- Visit the home page of your Whoogle Search instance -- this will automatically add the search engine if the [requirements](https://www.chromium.org/tab-to-search/) are met (GET request, no OnSubmit script, no path). If not, you can add it manually.
|
||||||
- Manual
|
- Manual
|
||||||
- Under search engines > manage search engines > add, manually enter your Whoogle instance details with a `<whoogle url>/search?q=%s` formatted search URL.
|
- Under search engines > manage search engines > add, manually enter your Whoogle instance details with a `<whoogle url>/search?q=%s` formatted search URL.
|
||||||
|
|
||||||
|
@ -396,6 +483,40 @@ Note: You should have your own domain name and [an https certificate](https://le
|
||||||
- Pip/Pipx: Add the `--https-only` flag to the end of the `whoogle-search` command
|
- Pip/Pipx: Add the `--https-only` flag to the end of the `whoogle-search` command
|
||||||
- Default `run` script: Modify the script locally to include the `--https-only` flag at the end of the python run command
|
- Default `run` script: Modify the script locally to include the `--https-only` flag at the end of the python run command
|
||||||
|
|
||||||
|
### Using with Firefox Containers
|
||||||
|
Unfortunately, Firefox Containers do not currently pass through `POST` requests (the default) to the engine, and Firefox caches the opensearch template on initial page load. To get around this, you can take the following steps to get it working as expected:
|
||||||
|
|
||||||
|
1. Remove any existing Whoogle search engines from Firefox settings
|
||||||
|
2. Enable `GET Requests Only` in Whoogle config
|
||||||
|
3. Clear Firefox cache
|
||||||
|
4. Restart Firefox
|
||||||
|
5. Navigate to Whoogle instance and [re-add the engine](#set-whoogle-as-your-primary-search-engine)
|
||||||
|
|
||||||
|
### Reverse Proxying
|
||||||
|
|
||||||
|
#### Nginx
|
||||||
|
|
||||||
|
Here is a sample Nginx config for Whoogle:
|
||||||
|
|
||||||
|
```
|
||||||
|
server {
|
||||||
|
server_name your_domain_name.com;
|
||||||
|
access_log /dev/null;
|
||||||
|
error_log /dev/null;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-NginX-Proxy true;
|
||||||
|
proxy_pass http://localhost:5000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can then add SSL support using LetsEncrypt by following a guide such as [this one](https://www.nginx.com/blog/using-free-ssltls-certificates-from-lets-encrypt-with-nginx/).
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Under the hood, Whoogle is a basic Flask app with the following structure:
|
Under the hood, Whoogle is a basic Flask app with the following structure:
|
||||||
|
@ -416,12 +537,12 @@ Under the hood, Whoogle is a basic Flask app with the following structure:
|
||||||
- `search.html`: An iframe-able search page
|
- `search.html`: An iframe-able search page
|
||||||
- `logo.html`: A template consisting mostly of the Whoogle logo as an SVG (separated to help keep `index.html` a bit cleaner)
|
- `logo.html`: A template consisting mostly of the Whoogle logo as an SVG (separated to help keep `index.html` a bit cleaner)
|
||||||
- `opensearch.xml`: A template used for supporting [OpenSearch](https://developer.mozilla.org/en-US/docs/Web/OpenSearch).
|
- `opensearch.xml`: A template used for supporting [OpenSearch](https://developer.mozilla.org/en-US/docs/Web/OpenSearch).
|
||||||
- `imageresults.html`: An "exprimental" template used for supporting the "Full Size" image feature on desktop.
|
- `imageresults.html`: An "experimental" template used for supporting the "Full Size" image feature on desktop.
|
||||||
- `static/<css|js>`
|
- `static/<css|js>`
|
||||||
- CSS/Javascript files, should be self-explanatory
|
- CSS/Javascript files, should be self-explanatory
|
||||||
- `static/settings`
|
- `static/settings`
|
||||||
- Key-value JSON files for establishing valid configuration values
|
- Key-value JSON files for establishing valid configuration values
|
||||||
|
|
||||||
|
|
||||||
If you're new to the project, the easiest way to get started would be to try fixing [an open bug report](https://github.com/benbusby/whoogle-search/issues?q=is%3Aissue+is%3Aopen+label%3Abug). If there aren't any open, or if the open ones are too stale, try taking on a [feature request](https://github.com/benbusby/whoogle-search/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement). Generally speaking, if you can write something that has any potential of breaking down in the future, you should write a test for it.
|
If you're new to the project, the easiest way to get started would be to try fixing [an open bug report](https://github.com/benbusby/whoogle-search/issues?q=is%3Aissue+is%3Aopen+label%3Abug). If there aren't any open, or if the open ones are too stale, try taking on a [feature request](https://github.com/benbusby/whoogle-search/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement). Generally speaking, if you can write something that has any potential of breaking down in the future, you should write a test for it.
|
||||||
|
|
||||||
|
@ -440,7 +561,7 @@ def contains(x: list, y: int) -> bool:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return y in x
|
return y in x
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Translating
|
#### Translating
|
||||||
|
|
||||||
|
@ -461,19 +582,45 @@ A lot of the app currently piggybacks on Google's existing support for fetching
|
||||||
|
|
||||||
## Public Instances
|
## Public Instances
|
||||||
|
|
||||||
*Note: Use public instances at your own discretion. Maintainers of Whoogle do not personally validate the integrity of these instances, and popular public instances are more likely to be rate-limited or blocked.*
|
*Note: Use public instances at your own discretion. The maintainers of Whoogle do not personally validate the integrity of any other instances. Popular public instances are more likely to be rate-limited or blocked.*
|
||||||
|
|
||||||
|
| Website | Country | Language | Cloudflare |
|
||||||
|
|-|-|-|-|
|
||||||
|
| [https://search.albony.xyz](https://search.albony.xyz/) | 🇮🇳 IN | Multi-choice | |
|
||||||
|
| [https://search.garudalinux.org](https://search.garudalinux.org) | 🇫🇮 FI | Multi-choice | ✅ |
|
||||||
|
| [https://search.dr460nf1r3.org](https://search.dr460nf1r3.org) | 🇩🇪 DE | Multi-choice | ✅ |
|
||||||
|
| [https://s.tokhmi.xyz](https://s.tokhmi.xyz) | 🇺🇸 US | Multi-choice | ✅ |
|
||||||
|
| [https://www.whooglesearch.ml](https://www.whooglesearch.ml) | 🇺🇸 US | English | |
|
||||||
|
| [https://search.sethforprivacy.com](https://search.sethforprivacy.com) | 🇩🇪 DE | English | |
|
||||||
|
| [https://whoogle.dcs0.hu](https://whoogle.dcs0.hu) | 🇭🇺 HU | Multi-choice | |
|
||||||
|
| [https://whoogle.esmailelbob.xyz](https://whoogle.esmailelbob.xyz) | 🇨🇦 CA | Multi-choice | |
|
||||||
|
| [https://gowogle.voring.me](https://gowogle.voring.me) | 🇺🇸 US | Multi-choice | |
|
||||||
|
| [https://whoogle.lunar.icu](https://whoogle.lunar.icu) | 🇩🇪 DE | Multi-choice | ✅ |
|
||||||
|
| [https://whoogle.privacydev.net](https://whoogle.privacydev.net) | 🇺🇸 US | Multi-choice | |
|
||||||
|
| [https://search.wef.lol](https://search.wef.lol) | 🇮🇸 IC | Multi-choice | |
|
||||||
|
| [https://wg.vern.cc](https://wg.vern.cc) | 🇺🇸 US | English | |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
* A checkmark in the "Cloudflare" category here refers to the use of the reverse proxy, [Cloudflare](https://cloudflare.com). The checkmark will not be listed for a site which uses Cloudflare DNS but rather the proxying service which grants Cloudflare the ability to monitor traffic to the website.
|
||||||
|
|
||||||
|
#### Onion Instances
|
||||||
|
|
||||||
|
| Website | Country | Language |
|
||||||
|
|-|-|-|
|
||||||
|
| [http://whoglqjdkgt2an4tdepberwqz3hk7tjo4kqgdnuj77rt7nshw2xqhqad.onion](http://whoglqjdkgt2an4tdepberwqz3hk7tjo4kqgdnuj77rt7nshw2xqhqad.onion) | 🇺🇸 US | Multi-choice
|
||||||
|
| [http://nuifgsnbb2mcyza74o7illtqmuaqbwu4flam3cdmsrnudwcmkqur37qd.onion](http://nuifgsnbb2mcyza74o7illtqmuaqbwu4flam3cdmsrnudwcmkqur37qd.onion) | 🇩🇪 DE | English
|
||||||
|
| [http://whoogle.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion](http://whoogle.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion/) | 🇺🇸 US | English |
|
||||||
|
|
||||||
|
#### I2P Instances
|
||||||
|
|
||||||
|
| Website | Country | Language |
|
||||||
|
|-|-|-|
|
||||||
|
| [http://verneks7rfjptpz5fpii7n7nrxilsidi2qxepeuuf66c3tsf4nhq.b32.i2p](http://verneks7rfjptpz5fpii7n7nrxilsidi2qxepeuuf66c3tsf4nhq.b32.i2p) | 🇺🇸 US | English |
|
||||||
|
|
||||||
- [https://whoogle.sdf.org](https://whoogle.sdf.org)
|
|
||||||
- [https://whoogle.himiko.cloud](https://whoogle.himiko.cloud)
|
|
||||||
- [https://whoogle.kavin.rocks](https://whoogle.kavin.rocks) or [http://whoogledq5f5wly5p4i2ohnvjwlihnlg4oajjum2oeddfwqdwupbuhqd.onion](http://whoogledq5f5wly5p4i2ohnvjwlihnlg4oajjum2oeddfwqdwupbuhqd.onion)
|
|
||||||
- [https://search.garudalinux.org](https://search.garudalinux.org)
|
|
||||||
- [https://whooglesearch.net/](https://whooglesearch.net/)
|
|
||||||
- [https://search.flawcra.cc/](https://search.flawcra.cc/)
|
|
||||||
- [https://search.exonip.de/](https://search.exonip.de/)
|
|
||||||
- [https://whoogle.silkky.cloud/](https://whoogle.silkky.cloud/)
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
#### Desktop
|
#### Desktop
|
||||||

|

|
||||||
|
|
||||||
#### Mobile
|
#### Mobile
|
||||||

|

|
||||||
|
|
69
app.json
69
app.json
|
@ -15,6 +15,11 @@
|
||||||
],
|
],
|
||||||
"stack": "container",
|
"stack": "container",
|
||||||
"env": {
|
"env": {
|
||||||
|
"WHOOGLE_URL_PREFIX": {
|
||||||
|
"description": "The URL prefix to use for the whoogle instance (i.e. \"/whoogle\")",
|
||||||
|
"value": "",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
"WHOOGLE_USER": {
|
"WHOOGLE_USER": {
|
||||||
"description": "The username for basic auth. WHOOGLE_PASS must also be set if used. Leave empty to disable.",
|
"description": "The username for basic auth. WHOOGLE_PASS must also be set if used. Leave empty to disable.",
|
||||||
"value": "",
|
"value": "",
|
||||||
|
@ -47,24 +52,59 @@
|
||||||
},
|
},
|
||||||
"WHOOGLE_ALT_TW": {
|
"WHOOGLE_ALT_TW": {
|
||||||
"description": "The site to use as a replacement for twitter.com when site alternatives are enabled in the config.",
|
"description": "The site to use as a replacement for twitter.com when site alternatives are enabled in the config.",
|
||||||
"value": "nitter.net",
|
"value": "farside.link/nitter",
|
||||||
"required": false
|
"required": false
|
||||||
},
|
},
|
||||||
"WHOOGLE_ALT_YT": {
|
"WHOOGLE_ALT_YT": {
|
||||||
"description": "The site to use as a replacement for youtube.com when site alternatives are enabled in the config.",
|
"description": "The site to use as a replacement for youtube.com when site alternatives are enabled in the config.",
|
||||||
"value": "invidious.snopyta.org",
|
"value": "farside.link/invidious",
|
||||||
"required": false
|
"required": false
|
||||||
},
|
},
|
||||||
"WHOOGLE_ALT_IG": {
|
"WHOOGLE_ALT_IG": {
|
||||||
"description": "The site to use as a replacement for instagram.com when site alternatives are enabled in the config.",
|
"description": "The site to use as a replacement for instagram.com when site alternatives are enabled in the config.",
|
||||||
"value": "bibliogram.art/u",
|
"value": "farside.link/bibliogram/u",
|
||||||
"required": false
|
"required": false
|
||||||
},
|
},
|
||||||
"WHOOGLE_ALT_RD": {
|
"WHOOGLE_ALT_RD": {
|
||||||
"description": "The site to use as a replacement for reddit.com when site alternatives are enabled in the config.",
|
"description": "The site to use as a replacement for reddit.com when site alternatives are enabled in the config.",
|
||||||
"value": "libredd.it",
|
"value": "farside.link/libreddit",
|
||||||
"required": false
|
"required": false
|
||||||
},
|
},
|
||||||
|
"WHOOGLE_ALT_MD": {
|
||||||
|
"description": "The site to use as a replacement for medium.com when site alternatives are enabled in the config.",
|
||||||
|
"value": "farside.link/scribe",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_ALT_TL": {
|
||||||
|
"description": "The Google Translate alternative to use for all searches following the 'translate ___' structure.",
|
||||||
|
"value": "farside.link/lingva",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_ALT_IMG": {
|
||||||
|
"description": "The site to use as a replacement for imgur.com when site alternatives are enabled in the config.",
|
||||||
|
"value": "farside.link/rimgo",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_ALT_WIKI": {
|
||||||
|
"description": "The site to use as a replacement for wikipedia.com when site alternatives are enabled in the config.",
|
||||||
|
"value": "farside.link/wikiless",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_ALT_IMDB": {
|
||||||
|
"description": "The site to use as a replacement for imdb.com when site alternatives are enabled in the config.",
|
||||||
|
"value": "farside.link/libremdb",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_ALT_QUORA": {
|
||||||
|
"description": "The site to use as a replacement for quora.com when site alternatives are enabled in the config.",
|
||||||
|
"value": "farside.link/quetre",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_MINIMAL": {
|
||||||
|
"description": "Remove everything except basic result cards from all search queries (set to 1 or leave blank)",
|
||||||
|
"value": "",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
"WHOOGLE_CONFIG_COUNTRY": {
|
"WHOOGLE_CONFIG_COUNTRY": {
|
||||||
"description": "[CONFIG] The country to use for restricting search results (use values from https://raw.githubusercontent.com/benbusby/whoogle-search/develop/app/static/settings/countries.json)",
|
"description": "[CONFIG] The country to use for restricting search results (use values from https://raw.githubusercontent.com/benbusby/whoogle-search/develop/app/static/settings/countries.json)",
|
||||||
"value": "",
|
"value": "",
|
||||||
|
@ -90,9 +130,9 @@
|
||||||
"value": "",
|
"value": "",
|
||||||
"required": false
|
"required": false
|
||||||
},
|
},
|
||||||
"WHOOGLE_CONFIG_DARK": {
|
"WHOOGLE_CONFIG_THEME": {
|
||||||
"description": "[CONFIG] Enable dark mode (set to 1 or leave blank)",
|
"description": "[CONFIG] Set theme to 'dark', 'light', or 'system'",
|
||||||
"value": "",
|
"value": "system",
|
||||||
"required": false
|
"required": false
|
||||||
},
|
},
|
||||||
"WHOOGLE_CONFIG_SAFE": {
|
"WHOOGLE_CONFIG_SAFE": {
|
||||||
|
@ -105,6 +145,11 @@
|
||||||
"value": "",
|
"value": "",
|
||||||
"required": false
|
"required": false
|
||||||
},
|
},
|
||||||
|
"WHOOGLE_CONFIG_NEAR": {
|
||||||
|
"description": "[CONFIG] Restrict results to only those near a particular city",
|
||||||
|
"value": "",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
"WHOOGLE_CONFIG_TOR": {
|
"WHOOGLE_CONFIG_TOR": {
|
||||||
"description": "[CONFIG] Use Tor, if available (set to 1 or leave blank)",
|
"description": "[CONFIG] Use Tor, if available (set to 1 or leave blank)",
|
||||||
"value": "",
|
"value": "",
|
||||||
|
@ -127,8 +172,18 @@
|
||||||
},
|
},
|
||||||
"WHOOGLE_CONFIG_STYLE": {
|
"WHOOGLE_CONFIG_STYLE": {
|
||||||
"description": "[CONFIG] Custom CSS styling (paste in CSS or leave blank)",
|
"description": "[CONFIG] Custom CSS styling (paste in CSS or leave blank)",
|
||||||
|
"value": ":root { /* LIGHT THEME COLORS */ --whoogle-background: #d8dee9; --whoogle-accent: #2e3440; --whoogle-text: #3B4252; --whoogle-contrast-text: #eceff4; --whoogle-secondary-text: #70757a; --whoogle-result-bg: #fff; --whoogle-result-title: #4c566a; --whoogle-result-url: #81a1c1; --whoogle-result-visited: #a3be8c; /* DARK THEME COLORS */ --whoogle-dark-background: #222; --whoogle-dark-accent: #685e79; --whoogle-dark-text: #fff; --whoogle-dark-contrast-text: #000; --whoogle-dark-secondary-text: #bbb; --whoogle-dark-result-bg: #000; --whoogle-dark-result-title: #1967d2; --whoogle-dark-result-url: #4b11a8; --whoogle-dark-result-visited: #bbbbff; }",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_CONFIG_PREFERENCES_ENCRYPTED": {
|
||||||
|
"description": "[CONFIG] Encrypt preferences token, requires WHOOGLE_CONFIG_PREFERENCES_KEY to be set",
|
||||||
"value": "",
|
"value": "",
|
||||||
"required": false
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_CONFIG_PREFERENCES_KEY": {
|
||||||
|
"description": "[CONFIG] Key to encrypt preferences",
|
||||||
|
"value": "NEEDS_TO_BE_MODIFIED",
|
||||||
|
"required": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
145
app/__init__.py
145
app/__init__.py
|
@ -2,81 +2,170 @@ from app.filter import clean_query
|
||||||
from app.request import send_tor_signal
|
from app.request import send_tor_signal
|
||||||
from app.utils.session import generate_user_key
|
from app.utils.session import generate_user_key
|
||||||
from app.utils.bangs import gen_bangs_json
|
from app.utils.bangs import gen_bangs_json
|
||||||
|
from app.utils.misc import gen_file_hash, read_config_bool
|
||||||
|
from base64 import b64encode
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask_session import Session
|
|
||||||
import json
|
import json
|
||||||
import logging.config
|
import logging.config
|
||||||
import os
|
import os
|
||||||
from stem import Signal
|
from stem import Signal
|
||||||
|
import threading
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
|
|
||||||
|
from app.utils.misc import read_config_bool
|
||||||
|
|
||||||
app = Flask(__name__, static_folder=os.path.dirname(
|
app = Flask(__name__, static_folder=os.path.dirname(
|
||||||
os.path.abspath(__file__)) + '/static')
|
os.path.abspath(__file__)) + '/static')
|
||||||
|
|
||||||
|
app.wsgi_app = ProxyFix(app.wsgi_app)
|
||||||
|
|
||||||
|
dot_env_path = (
|
||||||
|
os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||||
|
'../whoogle.env'))
|
||||||
|
|
||||||
# Load .env file if enabled
|
# Load .env file if enabled
|
||||||
if os.getenv("WHOOGLE_DOTENV", ''):
|
if read_config_bool('WHOOGLE_DOTENV'):
|
||||||
dotenv_path = '../whoogle.env'
|
load_dotenv(dot_env_path)
|
||||||
load_dotenv(os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
|
||||||
dotenv_path))
|
|
||||||
|
|
||||||
app.default_key = generate_user_key()
|
app.default_key = generate_user_key()
|
||||||
app.no_cookie_ips = []
|
|
||||||
app.config['SECRET_KEY'] = os.urandom(32)
|
if read_config_bool('HTTPS_ONLY'):
|
||||||
app.config['SESSION_TYPE'] = 'filesystem'
|
app.config['SESSION_COOKIE_NAME'] = '__Secure-session'
|
||||||
app.config['VERSION_NUMBER'] = '0.5.4'
|
app.config['SESSION_COOKIE_SECURE'] = True
|
||||||
|
|
||||||
|
app.config['VERSION_NUMBER'] = '0.7.4'
|
||||||
app.config['APP_ROOT'] = os.getenv(
|
app.config['APP_ROOT'] = os.getenv(
|
||||||
'APP_ROOT',
|
'APP_ROOT',
|
||||||
os.path.dirname(os.path.abspath(__file__)))
|
os.path.dirname(os.path.abspath(__file__)))
|
||||||
app.config['STATIC_FOLDER'] = os.getenv(
|
app.config['STATIC_FOLDER'] = os.getenv(
|
||||||
'STATIC_FOLDER',
|
'STATIC_FOLDER',
|
||||||
os.path.join(app.config['APP_ROOT'], 'static'))
|
os.path.join(app.config['APP_ROOT'], 'static'))
|
||||||
|
app.config['BUILD_FOLDER'] = os.path.join(
|
||||||
|
app.config['STATIC_FOLDER'], 'build')
|
||||||
|
app.config['CACHE_BUSTING_MAP'] = {}
|
||||||
app.config['LANGUAGES'] = json.load(open(
|
app.config['LANGUAGES'] = json.load(open(
|
||||||
os.path.join(app.config['STATIC_FOLDER'], 'settings/languages.json')))
|
os.path.join(app.config['STATIC_FOLDER'], 'settings/languages.json'),
|
||||||
|
encoding='utf-8'))
|
||||||
app.config['COUNTRIES'] = json.load(open(
|
app.config['COUNTRIES'] = json.load(open(
|
||||||
os.path.join(app.config['STATIC_FOLDER'], 'settings/countries.json')))
|
os.path.join(app.config['STATIC_FOLDER'], 'settings/countries.json'),
|
||||||
|
encoding='utf-8'))
|
||||||
app.config['TRANSLATIONS'] = json.load(open(
|
app.config['TRANSLATIONS'] = json.load(open(
|
||||||
os.path.join(app.config['STATIC_FOLDER'], 'settings/translations.json')))
|
os.path.join(app.config['STATIC_FOLDER'], 'settings/translations.json'),
|
||||||
|
encoding='utf-8'))
|
||||||
|
app.config['THEMES'] = json.load(open(
|
||||||
|
os.path.join(app.config['STATIC_FOLDER'], 'settings/themes.json'),
|
||||||
|
encoding='utf-8'))
|
||||||
|
app.config['HEADER_TABS'] = json.load(open(
|
||||||
|
os.path.join(app.config['STATIC_FOLDER'], 'settings/header_tabs.json'),
|
||||||
|
encoding='utf-8'))
|
||||||
app.config['CONFIG_PATH'] = os.getenv(
|
app.config['CONFIG_PATH'] = os.getenv(
|
||||||
'CONFIG_VOLUME',
|
'CONFIG_VOLUME',
|
||||||
os.path.join(app.config['STATIC_FOLDER'], 'config'))
|
os.path.join(app.config['STATIC_FOLDER'], 'config'))
|
||||||
app.config['DEFAULT_CONFIG'] = os.path.join(
|
app.config['DEFAULT_CONFIG'] = os.path.join(
|
||||||
app.config['CONFIG_PATH'],
|
app.config['CONFIG_PATH'],
|
||||||
'config.json')
|
'config.json')
|
||||||
app.config['CONFIG_DISABLE'] = os.getenv('WHOOGLE_CONFIG_DISABLE', '')
|
app.config['CONFIG_DISABLE'] = read_config_bool('WHOOGLE_CONFIG_DISABLE')
|
||||||
app.config['SESSION_FILE_DIR'] = os.path.join(
|
app.config['SESSION_FILE_DIR'] = os.path.join(
|
||||||
app.config['CONFIG_PATH'],
|
app.config['CONFIG_PATH'],
|
||||||
'session')
|
'session')
|
||||||
|
app.config['MAX_SESSION_SIZE'] = 4000 # Sessions won't exceed 4KB
|
||||||
app.config['BANG_PATH'] = os.getenv(
|
app.config['BANG_PATH'] = os.getenv(
|
||||||
'CONFIG_VOLUME',
|
'CONFIG_VOLUME',
|
||||||
os.path.join(app.config['STATIC_FOLDER'], 'bangs'))
|
os.path.join(app.config['STATIC_FOLDER'], 'bangs'))
|
||||||
app.config['BANG_FILE'] = os.path.join(
|
app.config['BANG_FILE'] = os.path.join(
|
||||||
app.config['BANG_PATH'],
|
app.config['BANG_PATH'],
|
||||||
'bangs.json')
|
'bangs.json')
|
||||||
app.config['CSP'] = 'default-src \'none\';' \
|
|
||||||
'manifest-src \'self\';' \
|
|
||||||
'img-src \'self\' data:;' \
|
|
||||||
'style-src \'self\' \'unsafe-inline\';' \
|
|
||||||
'script-src \'self\';' \
|
|
||||||
'media-src \'self\';' \
|
|
||||||
'connect-src \'self\';' \
|
|
||||||
'form-action \'self\';'
|
|
||||||
|
|
||||||
# Templating functions
|
|
||||||
app.jinja_env.globals.update(clean_query=clean_query)
|
|
||||||
|
|
||||||
|
# Ensure all necessary directories exist
|
||||||
if not os.path.exists(app.config['CONFIG_PATH']):
|
if not os.path.exists(app.config['CONFIG_PATH']):
|
||||||
os.makedirs(app.config['CONFIG_PATH'])
|
os.makedirs(app.config['CONFIG_PATH'])
|
||||||
|
|
||||||
if not os.path.exists(app.config['SESSION_FILE_DIR']):
|
if not os.path.exists(app.config['SESSION_FILE_DIR']):
|
||||||
os.makedirs(app.config['SESSION_FILE_DIR'])
|
os.makedirs(app.config['SESSION_FILE_DIR'])
|
||||||
|
|
||||||
# Generate DDG bang filter, and create path if it doesn't exist yet
|
|
||||||
if not os.path.exists(app.config['BANG_PATH']):
|
if not os.path.exists(app.config['BANG_PATH']):
|
||||||
os.makedirs(app.config['BANG_PATH'])
|
os.makedirs(app.config['BANG_PATH'])
|
||||||
if not os.path.exists(app.config['BANG_FILE']):
|
|
||||||
gen_bangs_json(app.config['BANG_FILE'])
|
|
||||||
|
|
||||||
Session(app)
|
if not os.path.exists(app.config['BUILD_FOLDER']):
|
||||||
|
os.makedirs(app.config['BUILD_FOLDER'])
|
||||||
|
|
||||||
|
# Session values
|
||||||
|
app_key_path = os.path.join(app.config['CONFIG_PATH'], 'whoogle.key')
|
||||||
|
if os.path.exists(app_key_path):
|
||||||
|
app.config['SECRET_KEY'] = open(app_key_path, 'r').read()
|
||||||
|
else:
|
||||||
|
app.config['SECRET_KEY'] = str(b64encode(os.urandom(32)))
|
||||||
|
with open(app_key_path, 'w') as key_file:
|
||||||
|
key_file.write(app.config['SECRET_KEY'])
|
||||||
|
key_file.close()
|
||||||
|
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=365)
|
||||||
|
|
||||||
|
# NOTE: SESSION_COOKIE_SAMESITE must be set to 'lax' to allow the user's
|
||||||
|
# previous session to persist when accessing the instance from an external
|
||||||
|
# link. Setting this value to 'strict' causes Whoogle to revalidate a new
|
||||||
|
# session, and fail, resulting in cookies being disabled.
|
||||||
|
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
|
||||||
|
|
||||||
|
# Config fields that are used to check for updates
|
||||||
|
app.config['RELEASES_URL'] = 'https://github.com/' \
|
||||||
|
'benbusby/whoogle-search/releases'
|
||||||
|
app.config['LAST_UPDATE_CHECK'] = datetime.now() - timedelta(hours=24)
|
||||||
|
app.config['HAS_UPDATE'] = ''
|
||||||
|
|
||||||
|
# The alternative to Google Translate is treated a bit differently than other
|
||||||
|
# social media site alternatives, in that it is used for any translation
|
||||||
|
# related searches.
|
||||||
|
translate_url = os.getenv('WHOOGLE_ALT_TL', 'https://farside.link/lingva')
|
||||||
|
if not translate_url.startswith('http'):
|
||||||
|
translate_url = 'https://' + translate_url
|
||||||
|
app.config['TRANSLATE_URL'] = translate_url
|
||||||
|
|
||||||
|
app.config['CSP'] = 'default-src \'none\';' \
|
||||||
|
'frame-src ' + translate_url + ';' \
|
||||||
|
'manifest-src \'self\';' \
|
||||||
|
'img-src \'self\' data:;' \
|
||||||
|
'style-src \'self\' \'unsafe-inline\';' \
|
||||||
|
'script-src \'self\';' \
|
||||||
|
'media-src \'self\';' \
|
||||||
|
'connect-src \'self\';'
|
||||||
|
|
||||||
|
# Generate DDG bang filter
|
||||||
|
if not os.path.exists(app.config['BANG_FILE']):
|
||||||
|
json.dump({}, open(app.config['BANG_FILE'], 'w'))
|
||||||
|
bangs_thread = threading.Thread(
|
||||||
|
target=gen_bangs_json,
|
||||||
|
args=(app.config['BANG_FILE'],))
|
||||||
|
bangs_thread.start()
|
||||||
|
|
||||||
|
# Build new mapping of static files for cache busting
|
||||||
|
cache_busting_dirs = ['css', 'js']
|
||||||
|
for cb_dir in cache_busting_dirs:
|
||||||
|
full_cb_dir = os.path.join(app.config['STATIC_FOLDER'], cb_dir)
|
||||||
|
for cb_file in os.listdir(full_cb_dir):
|
||||||
|
# Create hash from current file state
|
||||||
|
full_cb_path = os.path.join(full_cb_dir, cb_file)
|
||||||
|
cb_file_link = gen_file_hash(full_cb_dir, cb_file)
|
||||||
|
build_path = os.path.join(app.config['BUILD_FOLDER'], cb_file_link)
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.symlink(full_cb_path, build_path)
|
||||||
|
except FileExistsError:
|
||||||
|
# Symlink hasn't changed, ignore
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Create mapping for relative path urls
|
||||||
|
map_path = build_path.replace(app.config['APP_ROOT'], '')
|
||||||
|
if map_path.startswith('/'):
|
||||||
|
map_path = map_path[1:]
|
||||||
|
app.config['CACHE_BUSTING_MAP'][cb_file] = map_path
|
||||||
|
|
||||||
|
# Templating functions
|
||||||
|
app.jinja_env.globals.update(clean_query=clean_query)
|
||||||
|
app.jinja_env.globals.update(
|
||||||
|
cb_url=lambda f: app.config['CACHE_BUSTING_MAP'][f])
|
||||||
|
|
||||||
# Attempt to acquire tor identity, to determine if Tor config is available
|
# Attempt to acquire tor identity, to determine if Tor config is available
|
||||||
send_tor_signal(Signal.HEARTBEAT)
|
send_tor_signal(Signal.HEARTBEAT)
|
||||||
|
|
447
app/filter.py
447
app/filter.py
|
@ -1,12 +1,35 @@
|
||||||
from app.request import VALID_PARAMS, MAPS_URL
|
import cssutils
|
||||||
from app.utils.results import *
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from bs4.element import ResultSet, Tag
|
from bs4.element import ResultSet, Tag
|
||||||
from cryptography.fernet import Fernet
|
from cryptography.fernet import Fernet
|
||||||
from flask import render_template
|
from flask import render_template
|
||||||
import re
|
|
||||||
import urllib.parse as urlparse
|
import urllib.parse as urlparse
|
||||||
from urllib.parse import parse_qs
|
from urllib.parse import parse_qs
|
||||||
|
import re
|
||||||
|
|
||||||
|
from app.models.g_classes import GClasses
|
||||||
|
from app.request import VALID_PARAMS, MAPS_URL
|
||||||
|
from app.utils.misc import get_abs_url, read_config_bool
|
||||||
|
from app.utils.results import (
|
||||||
|
BLANK_B64, GOOG_IMG, GOOG_STATIC, G_M_LOGO_URL, LOGO_URL, SITE_ALTS,
|
||||||
|
has_ad_content, filter_link_args, append_anon_view, get_site_alt,
|
||||||
|
)
|
||||||
|
from app.models.endpoint import Endpoint
|
||||||
|
from app.models.config import Config
|
||||||
|
|
||||||
|
|
||||||
|
MAPS_ARGS = ['q', 'daddr']
|
||||||
|
|
||||||
|
minimal_mode_sections = ['Top stories', 'Images', 'People also ask']
|
||||||
|
unsupported_g_pages = [
|
||||||
|
'support.google.com',
|
||||||
|
'accounts.google.com',
|
||||||
|
'policies.google.com',
|
||||||
|
'google.com/preferences',
|
||||||
|
'google.com/intl',
|
||||||
|
'advanced_search',
|
||||||
|
'tbm=shop'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def extract_q(q_str: str, href: str) -> str:
|
def extract_q(q_str: str, href: str) -> str:
|
||||||
|
@ -24,6 +47,28 @@ def extract_q(q_str: str, href: str) -> str:
|
||||||
return parse_qs(q_str)['q'][0] if ('&q=' in href or '?q=' in href) else ''
|
return parse_qs(q_str)['q'][0] if ('&q=' in href or '?q=' in href) else ''
|
||||||
|
|
||||||
|
|
||||||
|
def build_map_url(href: str) -> str:
|
||||||
|
"""Tries to extract known args that explain the location in the url. If a
|
||||||
|
location is found, returns the default url with it. Otherwise, returns the
|
||||||
|
url unchanged.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
href: The full url to check.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The parsed url, or the url unchanged.
|
||||||
|
"""
|
||||||
|
# parse the url
|
||||||
|
parsed_url = parse_qs(href)
|
||||||
|
# iterate through the known parameters and try build the url
|
||||||
|
for param in MAPS_ARGS:
|
||||||
|
if param in parsed_url:
|
||||||
|
return MAPS_URL + "?q=" + parsed_url[param][0]
|
||||||
|
|
||||||
|
# query could not be extracted returning unchanged url
|
||||||
|
return href
|
||||||
|
|
||||||
|
|
||||||
def clean_query(query: str) -> str:
|
def clean_query(query: str) -> str:
|
||||||
"""Strips the blocked site list from the query, if one is being
|
"""Strips the blocked site list from the query, if one is being
|
||||||
used.
|
used.
|
||||||
|
@ -37,20 +82,53 @@ def clean_query(query: str) -> str:
|
||||||
return query[:query.find('-site:')] if '-site:' in query else query
|
return query[:query.find('-site:')] if '-site:' in query else query
|
||||||
|
|
||||||
|
|
||||||
class Filter:
|
def clean_css(css: str, page_url: str) -> str:
|
||||||
def __init__(self, user_key: str, mobile=False, config=None) -> None:
|
"""Removes all remote URLs from a CSS string.
|
||||||
if config is None:
|
|
||||||
config = {}
|
|
||||||
|
|
||||||
self.near = config['near'] if 'near' in config else ''
|
Args:
|
||||||
self.dark = config['dark'] if 'dark' in config else False
|
css: The CSS string
|
||||||
self.nojs = config['nojs'] if 'nojs' in config else False
|
|
||||||
self.new_tab = config['new_tab'] if 'new_tab' in config else False
|
Returns:
|
||||||
self.alt_redirect = config['alts'] if 'alts' in config else False
|
str: The filtered CSS, with URLs proxied through Whoogle
|
||||||
|
"""
|
||||||
|
sheet = cssutils.parseString(css)
|
||||||
|
urls = cssutils.getUrls(sheet)
|
||||||
|
|
||||||
|
for url in urls:
|
||||||
|
abs_url = get_abs_url(url, page_url)
|
||||||
|
if abs_url.startswith('data:'):
|
||||||
|
continue
|
||||||
|
css = css.replace(
|
||||||
|
url,
|
||||||
|
f'{Endpoint.element}?type=image/png&url={abs_url}'
|
||||||
|
)
|
||||||
|
|
||||||
|
return css
|
||||||
|
|
||||||
|
|
||||||
|
class Filter:
|
||||||
|
# Limit used for determining if a result is a "regular" result or a list
|
||||||
|
# type result (such as "people also asked", "related searches", etc)
|
||||||
|
RESULT_CHILD_LIMIT = 7
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
user_key: str,
|
||||||
|
config: Config,
|
||||||
|
root_url='',
|
||||||
|
page_url='',
|
||||||
|
query='',
|
||||||
|
mobile=False) -> None:
|
||||||
|
self.config = config
|
||||||
self.mobile = mobile
|
self.mobile = mobile
|
||||||
self.user_key = user_key
|
self.user_key = user_key
|
||||||
|
self.page_url = page_url
|
||||||
|
self.query = query
|
||||||
self.main_divs = ResultSet('')
|
self.main_divs = ResultSet('')
|
||||||
self._elements = 0
|
self._elements = 0
|
||||||
|
self._av = set()
|
||||||
|
|
||||||
|
self.root_url = root_url[:-1] if root_url.endswith('/') else root_url
|
||||||
|
|
||||||
def __getitem__(self, name):
|
def __getitem__(self, name):
|
||||||
return getattr(self, name)
|
return getattr(self, name)
|
||||||
|
@ -59,16 +137,6 @@ class Filter:
|
||||||
def elements(self):
|
def elements(self):
|
||||||
return self._elements
|
return self._elements
|
||||||
|
|
||||||
def reskin(self, page: str) -> str:
|
|
||||||
# Aesthetic only re-skinning
|
|
||||||
if self.dark:
|
|
||||||
page = page.replace(
|
|
||||||
'fff', '000').replace(
|
|
||||||
'202124', 'ddd').replace(
|
|
||||||
'1967D2', '3b85ea')
|
|
||||||
|
|
||||||
return page
|
|
||||||
|
|
||||||
def encrypt_path(self, path, is_element=False) -> str:
|
def encrypt_path(self, path, is_element=False) -> str:
|
||||||
# Encrypts path to avoid plaintext results in logs
|
# Encrypts path to avoid plaintext results in logs
|
||||||
if is_element:
|
if is_element:
|
||||||
|
@ -83,8 +151,12 @@ class Filter:
|
||||||
def clean(self, soup) -> BeautifulSoup:
|
def clean(self, soup) -> BeautifulSoup:
|
||||||
self.main_divs = soup.find('div', {'id': 'main'})
|
self.main_divs = soup.find('div', {'id': 'main'})
|
||||||
self.remove_ads()
|
self.remove_ads()
|
||||||
self.fix_question_section()
|
self.remove_block_titles()
|
||||||
|
self.remove_block_url()
|
||||||
|
self.collapse_sections()
|
||||||
|
self.update_css(soup)
|
||||||
self.update_styling(soup)
|
self.update_styling(soup)
|
||||||
|
self.remove_block_tabs(soup)
|
||||||
|
|
||||||
for img in [_ for _ in soup.find_all('img') if 'src' in _.attrs]:
|
for img in [_ for _ in soup.find_all('img') if 'src' in _.attrs]:
|
||||||
self.update_element_src(img, 'image/png')
|
self.update_element_src(img, 'image/png')
|
||||||
|
@ -97,7 +169,9 @@ class Filter:
|
||||||
|
|
||||||
input_form = soup.find('form')
|
input_form = soup.find('form')
|
||||||
if input_form is not None:
|
if input_form is not None:
|
||||||
input_form['method'] = 'POST'
|
input_form['method'] = 'GET' if self.config.get_only else 'POST'
|
||||||
|
# Use a relative URI for submissions
|
||||||
|
input_form['action'] = 'search'
|
||||||
|
|
||||||
# Ensure no extra scripts passed through
|
# Ensure no extra scripts passed through
|
||||||
for script in soup('script'):
|
for script in soup('script'):
|
||||||
|
@ -113,9 +187,20 @@ class Filter:
|
||||||
header = soup.find('header')
|
header = soup.find('header')
|
||||||
if header:
|
if header:
|
||||||
header.decompose()
|
header.decompose()
|
||||||
|
self.remove_site_blocks(soup)
|
||||||
return soup
|
return soup
|
||||||
|
|
||||||
|
def remove_site_blocks(self, soup) -> None:
|
||||||
|
if not self.config.block or not soup.body:
|
||||||
|
return
|
||||||
|
search_string = ' '.join(['-site:' +
|
||||||
|
_ for _ in self.config.block.split(',')])
|
||||||
|
selected = soup.body.findAll(text=re.compile(search_string))
|
||||||
|
|
||||||
|
for result in selected:
|
||||||
|
result.string.replace_with(result.string.replace(
|
||||||
|
search_string, ''))
|
||||||
|
|
||||||
def remove_ads(self) -> None:
|
def remove_ads(self) -> None:
|
||||||
"""Removes ads found in the list of search result divs
|
"""Removes ads found in the list of search result divs
|
||||||
|
|
||||||
|
@ -130,43 +215,124 @@ class Filter:
|
||||||
if has_ad_content(_.text)]
|
if has_ad_content(_.text)]
|
||||||
_ = div.decompose() if len(div_ads) else None
|
_ = div.decompose() if len(div_ads) else None
|
||||||
|
|
||||||
def fix_question_section(self) -> None:
|
def remove_block_titles(self) -> None:
|
||||||
"""Collapses the "People Also Asked" section into a "details" element
|
if not self.main_divs or not self.config.block_title:
|
||||||
|
return
|
||||||
|
block_title = re.compile(self.block_title)
|
||||||
|
for div in [_ for _ in self.main_divs.find_all('div', recursive=True)]:
|
||||||
|
block_divs = [_ for _ in div.find_all('h3', recursive=True)
|
||||||
|
if block_title.search(_.text) is not None]
|
||||||
|
_ = div.decompose() if len(block_divs) else None
|
||||||
|
|
||||||
|
def remove_block_url(self) -> None:
|
||||||
|
if not self.main_divs or not self.config.block_url:
|
||||||
|
return
|
||||||
|
block_url = re.compile(self.block_url)
|
||||||
|
for div in [_ for _ in self.main_divs.find_all('div', recursive=True)]:
|
||||||
|
block_divs = [_ for _ in div.find_all('a', recursive=True)
|
||||||
|
if block_url.search(_.attrs['href']) is not None]
|
||||||
|
_ = div.decompose() if len(block_divs) else None
|
||||||
|
|
||||||
|
def remove_block_tabs(self, soup) -> None:
|
||||||
|
if self.main_divs:
|
||||||
|
for div in self.main_divs.find_all(
|
||||||
|
'div',
|
||||||
|
attrs={'class': f'{GClasses.main_tbm_tab}'}
|
||||||
|
):
|
||||||
|
_ = div.decompose()
|
||||||
|
else:
|
||||||
|
# when in images tab
|
||||||
|
for div in soup.find_all(
|
||||||
|
'div',
|
||||||
|
attrs={'class': f'{GClasses.images_tbm_tab}'}
|
||||||
|
):
|
||||||
|
_ = div.decompose()
|
||||||
|
|
||||||
|
def collapse_sections(self) -> None:
|
||||||
|
"""Collapses long result sections ("people also asked", "related
|
||||||
|
searches", etc) into "details" elements
|
||||||
|
|
||||||
These sections are typically the only sections in the results page that
|
These sections are typically the only sections in the results page that
|
||||||
are structured as <div><h2>Title</h2><div>...</div></div>, so they are
|
have more than ~5 child divs within a primary result div.
|
||||||
extracted by checking all result divs for h2 children.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
None (The soup object is modified directly)
|
None (The soup object is modified directly)
|
||||||
"""
|
"""
|
||||||
|
minimal_mode = read_config_bool('WHOOGLE_MINIMAL')
|
||||||
|
|
||||||
|
def pull_child_divs(result_div: BeautifulSoup):
|
||||||
|
try:
|
||||||
|
return result_div.findChildren(
|
||||||
|
'div', recursive=False
|
||||||
|
)[0].findChildren(
|
||||||
|
'div', recursive=False)
|
||||||
|
except IndexError:
|
||||||
|
return []
|
||||||
|
|
||||||
if not self.main_divs:
|
if not self.main_divs:
|
||||||
return
|
return
|
||||||
|
|
||||||
question_divs = [_ for _ in self.main_divs.find_all(
|
# Loop through results and check for the number of child divs in each
|
||||||
'div', recursive=False
|
for result in self.main_divs.find_all():
|
||||||
) if len(_.find_all('h2')) > 0]
|
result_children = pull_child_divs(result)
|
||||||
|
if minimal_mode:
|
||||||
|
if any(f">{x}</span" in str(s) for s in result_children
|
||||||
|
for x in minimal_mode_sections):
|
||||||
|
result.decompose()
|
||||||
|
continue
|
||||||
|
for s in result_children:
|
||||||
|
if ('Twitter ›' in str(s)):
|
||||||
|
result.decompose()
|
||||||
|
continue
|
||||||
|
if len(result_children) < self.RESULT_CHILD_LIMIT:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
if len(result_children) < self.RESULT_CHILD_LIMIT:
|
||||||
|
continue
|
||||||
|
|
||||||
if len(question_divs) == 0:
|
# Find and decompose the first element with an inner HTML text val.
|
||||||
return
|
# This typically extracts the title of the section (i.e. "Related
|
||||||
|
# Searches", "People also ask", etc)
|
||||||
|
# If there are more than one child tags with text
|
||||||
|
# parenthesize the rest except the first
|
||||||
|
label = 'Collapsed Results'
|
||||||
|
subtitle = None
|
||||||
|
for elem in result_children:
|
||||||
|
if elem.text:
|
||||||
|
content = list(elem.strings)
|
||||||
|
label = content[0]
|
||||||
|
if len(content) > 1:
|
||||||
|
subtitle = '<span> (' + \
|
||||||
|
''.join(content[1:]) + ')</span>'
|
||||||
|
elem.decompose()
|
||||||
|
break
|
||||||
|
|
||||||
# Wrap section in details element to allow collapse/expand
|
# Create the new details element to wrap around the result's
|
||||||
details = BeautifulSoup(features='html.parser').new_tag('details')
|
# first parent
|
||||||
summary = BeautifulSoup(features='html.parser').new_tag('summary')
|
parent = None
|
||||||
summary.string = question_divs[0].find('h2').text
|
idx = 0
|
||||||
question_divs[0].find('h2').decompose()
|
while not parent and idx < len(result_children):
|
||||||
details.append(summary)
|
parent = result_children[idx].parent
|
||||||
question_divs[0].wrap(details)
|
idx += 1
|
||||||
|
|
||||||
for question_div in question_divs:
|
details = BeautifulSoup(features='html.parser').new_tag('details')
|
||||||
questions = [_ for _ in question_div.find_all(
|
summary = BeautifulSoup(features='html.parser').new_tag('summary')
|
||||||
'div', recursive=True
|
summary.string = label
|
||||||
) if _.text.endswith('?')]
|
|
||||||
|
|
||||||
for question in questions:
|
if subtitle:
|
||||||
question['style'] = 'padding: 10px; font-style: italic;'
|
soup = BeautifulSoup(subtitle, 'html.parser')
|
||||||
|
summary.append(soup)
|
||||||
|
|
||||||
def update_element_src(self, element: Tag, mime: str) -> None:
|
details.append(summary)
|
||||||
|
|
||||||
|
if parent and not minimal_mode:
|
||||||
|
parent.wrap(details)
|
||||||
|
elif parent and minimal_mode:
|
||||||
|
# Remove parent element from document if "minimal mode" is
|
||||||
|
# enabled
|
||||||
|
parent.decompose()
|
||||||
|
|
||||||
|
def update_element_src(self, element: Tag, mime: str, attr='src') -> None:
|
||||||
"""Encrypts the original src of an element and rewrites the element src
|
"""Encrypts the original src of an element and rewrites the element src
|
||||||
to use the "/element?src=" pass-through.
|
to use the "/element?src=" pass-through.
|
||||||
|
|
||||||
|
@ -174,26 +340,56 @@ class Filter:
|
||||||
None (The soup element is modified directly)
|
None (The soup element is modified directly)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
src = element['src']
|
src = element[attr].split(' ')[0]
|
||||||
|
|
||||||
if src.startswith('//'):
|
if src.startswith('//'):
|
||||||
src = 'https:' + src
|
src = 'https:' + src
|
||||||
|
elif src.startswith('data:'):
|
||||||
|
return
|
||||||
|
|
||||||
if src.startswith(LOGO_URL):
|
if src.startswith(LOGO_URL):
|
||||||
# Re-brand with Whoogle logo
|
# Re-brand with Whoogle logo
|
||||||
element.replace_with(BeautifulSoup(
|
element.replace_with(BeautifulSoup(
|
||||||
render_template('logo.html', dark=self.dark),
|
render_template('logo.html'),
|
||||||
features='html.parser'))
|
features='html.parser'))
|
||||||
return
|
return
|
||||||
|
elif src.startswith(G_M_LOGO_URL):
|
||||||
|
# Re-brand with single-letter Whoogle logo
|
||||||
|
element['src'] = 'static/img/favicon/apple-icon.png'
|
||||||
|
element.parent['href'] = 'home'
|
||||||
|
return
|
||||||
elif src.startswith(GOOG_IMG) or GOOG_STATIC in src:
|
elif src.startswith(GOOG_IMG) or GOOG_STATIC in src:
|
||||||
element['src'] = BLANK_B64
|
element['src'] = BLANK_B64
|
||||||
return
|
return
|
||||||
|
|
||||||
element['src'] = 'element?url=' + self.encrypt_path(
|
element[attr] = f'{self.root_url}/{Endpoint.element}?url=' + (
|
||||||
src,
|
self.encrypt_path(
|
||||||
is_element=True) + '&type=' + urlparse.quote(mime)
|
src,
|
||||||
|
is_element=True
|
||||||
|
) + '&type=' + urlparse.quote(mime)
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_css(self, soup) -> None:
|
||||||
|
"""Updates URLs used in inline styles to be proxied by Whoogle
|
||||||
|
using the /element endpoint.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None (The soup element is modified directly)
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Filter all <style> tags
|
||||||
|
for style in soup.find_all('style'):
|
||||||
|
style.string = clean_css(style.string, self.page_url)
|
||||||
|
|
||||||
|
# TODO: Convert remote stylesheets to style tags and proxy all
|
||||||
|
# remote requests
|
||||||
|
# for link in soup.find_all('link', attrs={'rel': 'stylesheet'}):
|
||||||
|
# print(link)
|
||||||
|
|
||||||
def update_styling(self, soup) -> None:
|
def update_styling(self, soup) -> None:
|
||||||
|
# Update CSS classes for result divs
|
||||||
|
soup = GClasses.replace_css_classes(soup)
|
||||||
|
|
||||||
# Remove unnecessary button(s)
|
# Remove unnecessary button(s)
|
||||||
for button in soup.find_all('button'):
|
for button in soup.find_all('button'):
|
||||||
button.decompose()
|
button.decompose()
|
||||||
|
@ -216,6 +412,26 @@ class Filter:
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Fix body max width on images tab
|
||||||
|
style = soup.find('style')
|
||||||
|
div = soup.find('div', attrs={'class': f'{GClasses.images_tbm_tab}'})
|
||||||
|
if style and div and not self.mobile:
|
||||||
|
css = style.string
|
||||||
|
css_html_tag = (
|
||||||
|
'html{'
|
||||||
|
'font-family: Roboto, Helvetica Neue, Arial, sans-serif;'
|
||||||
|
'font-size: 14px;'
|
||||||
|
'line-height: 20px;'
|
||||||
|
'text-size-adjust: 100%;'
|
||||||
|
'word-wrap: break-word;'
|
||||||
|
'}'
|
||||||
|
)
|
||||||
|
css = f"{css_html_tag}{css}"
|
||||||
|
css = re.sub('body{(.*?)}',
|
||||||
|
'body{padding:0 8px;margin:0 auto;max-width:736px;}',
|
||||||
|
css)
|
||||||
|
style.string = css
|
||||||
|
|
||||||
def update_link(self, link: Tag) -> None:
|
def update_link(self, link: Tag) -> None:
|
||||||
"""Update internal link paths with encrypted path, otherwise remove
|
"""Update internal link paths with encrypted path, otherwise remove
|
||||||
unnecessary redirects and/or marketing params from the url
|
unnecessary redirects and/or marketing params from the url
|
||||||
|
@ -227,23 +443,57 @@ class Filter:
|
||||||
None (the tag is updated directly)
|
None (the tag is updated directly)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Replace href with only the intended destination (no "utm" type tags)
|
parsed_link = urlparse.urlparse(link['href'])
|
||||||
href = link['href'].replace('https://www.google.com', '')
|
link_netloc = ''
|
||||||
if 'advanced_search' in href or 'tbm=shop' in href:
|
if '/url?q=' in link['href']:
|
||||||
|
link_netloc = extract_q(parsed_link.query, link['href'])
|
||||||
|
else:
|
||||||
|
link_netloc = parsed_link.netloc
|
||||||
|
|
||||||
|
# Remove any elements that direct to unsupported Google pages
|
||||||
|
if any(url in link_netloc for url in unsupported_g_pages):
|
||||||
# FIXME: The "Shopping" tab requires further filtering (see #136)
|
# FIXME: The "Shopping" tab requires further filtering (see #136)
|
||||||
# Temporarily removing all links to that tab for now.
|
# Temporarily removing all links to that tab for now.
|
||||||
link.decompose()
|
|
||||||
return
|
# Replaces the /url google unsupported link to the direct url
|
||||||
elif self.new_tab:
|
link['href'] = link_netloc
|
||||||
link['target'] = '_blank'
|
parent = link.parent
|
||||||
|
|
||||||
|
if 'google.com/preferences?hl=' in link_netloc:
|
||||||
|
# Handle case where a search is performed in a different
|
||||||
|
# language than what is configured. This usually returns a
|
||||||
|
# div with the same classes as normal search results, but with
|
||||||
|
# a link to configure language preferences through Google.
|
||||||
|
# Since we want all language config done through Whoogle, we
|
||||||
|
# can safely decompose this element.
|
||||||
|
while parent:
|
||||||
|
p_cls = parent.attrs.get('class') or []
|
||||||
|
if f'{GClasses.result_class_a}' in p_cls:
|
||||||
|
parent.decompose()
|
||||||
|
break
|
||||||
|
parent = parent.parent
|
||||||
|
else:
|
||||||
|
# Remove cases where google links appear in the footer
|
||||||
|
while parent:
|
||||||
|
p_cls = parent.attrs.get('class') or []
|
||||||
|
if parent.name == 'footer' or f'{GClasses.footer}' in p_cls:
|
||||||
|
link.decompose()
|
||||||
|
parent = parent.parent
|
||||||
|
return
|
||||||
|
|
||||||
|
# Replace href with only the intended destination (no "utm" type tags)
|
||||||
|
href = link['href'].replace('https://www.google.com', '')
|
||||||
result_link = urlparse.urlparse(href)
|
result_link = urlparse.urlparse(href)
|
||||||
q = extract_q(result_link.query, href)
|
q = extract_q(result_link.query, href)
|
||||||
|
|
||||||
if q.startswith('/'):
|
if q.startswith('/') and q not in self.query and 'spell=1' not in href:
|
||||||
# Internal google links (i.e. mail, maps, etc) should still
|
# Internal google links (i.e. mail, maps, etc) should still
|
||||||
# be forwarded to Google
|
# be forwarded to Google
|
||||||
link['href'] = 'https://google.com' + q
|
link['href'] = 'https://google.com' + q
|
||||||
|
elif q.startswith('https://accounts.google.com'):
|
||||||
|
# Remove Sign-in link
|
||||||
|
link.decompose()
|
||||||
|
return
|
||||||
elif '/search?q=' in href:
|
elif '/search?q=' in href:
|
||||||
# "li:1" implies the query should be interpreted verbatim,
|
# "li:1" implies the query should be interpreted verbatim,
|
||||||
# which is accomplished by wrapping the query in double quotes
|
# which is accomplished by wrapping the query in double quotes
|
||||||
|
@ -262,18 +512,39 @@ class Filter:
|
||||||
# Strip unneeded arguments
|
# Strip unneeded arguments
|
||||||
link['href'] = filter_link_args(q)
|
link['href'] = filter_link_args(q)
|
||||||
|
|
||||||
# Add no-js option
|
# Add alternate viewing options for results,
|
||||||
if self.nojs:
|
# if the result doesn't already have an AV link
|
||||||
append_nojs(link)
|
netloc = urlparse.urlparse(link['href']).netloc
|
||||||
|
if self.config.anon_view and netloc not in self._av:
|
||||||
|
self._av.add(netloc)
|
||||||
|
append_anon_view(link, self.config)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if href.startswith(MAPS_URL):
|
if href.startswith(MAPS_URL):
|
||||||
# Maps links don't work if a site filter is applied
|
# Maps links don't work if a site filter is applied
|
||||||
link['href'] = MAPS_URL + "?q=" + clean_query(q)
|
link['href'] = build_map_url(link['href'])
|
||||||
|
elif (href.startswith('/?') or href.startswith('/search?') or
|
||||||
|
href.startswith('/imgres?')):
|
||||||
|
# make sure that tags can be clicked as relative URLs
|
||||||
|
link['href'] = href[1:]
|
||||||
|
elif href.startswith('/intl/'):
|
||||||
|
# do nothing, keep original URL for ToS
|
||||||
|
pass
|
||||||
|
elif href.startswith('/preferences'):
|
||||||
|
# there is no config specific URL, remove this
|
||||||
|
link.decompose()
|
||||||
|
return
|
||||||
else:
|
else:
|
||||||
link['href'] = href
|
link['href'] = href
|
||||||
|
|
||||||
|
if self.config.new_tab and (
|
||||||
|
link["href"].startswith("http")
|
||||||
|
or link["href"].startswith("imgres?")
|
||||||
|
):
|
||||||
|
link["target"] = "_blank"
|
||||||
|
|
||||||
# Replace link location if "alts" config is enabled
|
# Replace link location if "alts" config is enabled
|
||||||
if self.alt_redirect:
|
if self.config.alts:
|
||||||
# Search and replace all link descriptions
|
# Search and replace all link descriptions
|
||||||
# with alternative location
|
# with alternative location
|
||||||
link['href'] = get_site_alt(link['href'])
|
link['href'] = get_site_alt(link['href'])
|
||||||
|
@ -282,8 +553,15 @@ class Filter:
|
||||||
if len(link_desc) == 0:
|
if len(link_desc) == 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Replace link destination
|
# Replace link description
|
||||||
link_desc[0].replace_with(get_site_alt(link_desc[0]))
|
link_desc = link_desc[0]
|
||||||
|
for site, alt in SITE_ALTS.items():
|
||||||
|
if site not in link_desc or not alt:
|
||||||
|
continue
|
||||||
|
new_desc = BeautifulSoup(features='html.parser').new_tag('div')
|
||||||
|
new_desc.string = str(link_desc).replace(site, alt)
|
||||||
|
link_desc.replace_with(new_desc)
|
||||||
|
break
|
||||||
|
|
||||||
def view_image(self, soup) -> BeautifulSoup:
|
def view_image(self, soup) -> BeautifulSoup:
|
||||||
"""Replaces the soup with a new one that handles mobile results and
|
"""Replaces the soup with a new one that handles mobile results and
|
||||||
|
@ -297,11 +575,8 @@ class Filter:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# get some tags that are unchanged between mobile and pc versions
|
# get some tags that are unchanged between mobile and pc versions
|
||||||
search_input = soup.find_all('td', attrs={'class': "O4cRJf"})[0]
|
|
||||||
search_options = soup.find_all('div', attrs={'class': "M7pB2"})[0]
|
|
||||||
cor_suggested = soup.find_all('table', attrs={'class': "By0U9"})
|
cor_suggested = soup.find_all('table', attrs={'class': "By0U9"})
|
||||||
next_pages = soup.find_all('table', attrs={'class': "uZgmoc"})[0]
|
next_pages = soup.find_all('table', attrs={'class': "uZgmoc"})[0]
|
||||||
information = soup.find_all('div', attrs={'class': "TuS8Ad"})[0]
|
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
# find results div
|
# find results div
|
||||||
|
@ -312,13 +587,25 @@ class Filter:
|
||||||
for item in results_all:
|
for item in results_all:
|
||||||
urls = item.find('a')['href'].split('&imgrefurl=')
|
urls = item.find('a')['href'].split('&imgrefurl=')
|
||||||
|
|
||||||
img_url = urlparse.unquote(urls[0].replace('/imgres?imgurl=', ''))
|
# Skip urls that are not two-element lists
|
||||||
webpage = urlparse.unquote(urls[1].split('&')[0])
|
if len(urls) != 2:
|
||||||
|
continue
|
||||||
|
|
||||||
|
img_url = urlparse.unquote(urls[0].replace(
|
||||||
|
f'/{Endpoint.imgres}?imgurl=', ''))
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Try to strip out only the necessary part of the web page link
|
||||||
|
web_page = urlparse.unquote(urls[1].split('&')[0])
|
||||||
|
except IndexError:
|
||||||
|
web_page = urlparse.unquote(urls[1])
|
||||||
|
|
||||||
img_tbn = urlparse.unquote(item.find('a').find('img')['src'])
|
img_tbn = urlparse.unquote(item.find('a').find('img')['src'])
|
||||||
|
|
||||||
results.append({
|
results.append({
|
||||||
'domain': urlparse.urlparse(webpage).netloc,
|
'domain': urlparse.urlparse(web_page).netloc,
|
||||||
'img_url': img_url,
|
'img_url': img_url,
|
||||||
'webpage': webpage,
|
'web_page': web_page,
|
||||||
'img_tbn': img_tbn
|
'img_tbn': img_tbn
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -327,12 +614,7 @@ class Filter:
|
||||||
results=results,
|
results=results,
|
||||||
view_label="View Image"),
|
view_label="View Image"),
|
||||||
features='html.parser')
|
features='html.parser')
|
||||||
# replace search input object
|
|
||||||
soup.find_all('td',
|
|
||||||
attrs={'class': "O4cRJf"})[0].replaceWith(search_input)
|
|
||||||
# replace search options object (All, Images, Videos, etc.)
|
|
||||||
soup.find_all('div',
|
|
||||||
attrs={'class': "M7pB2"})[0].replaceWith(search_options)
|
|
||||||
# replace correction suggested by google object if exists
|
# replace correction suggested by google object if exists
|
||||||
if len(cor_suggested):
|
if len(cor_suggested):
|
||||||
soup.find_all(
|
soup.find_all(
|
||||||
|
@ -342,7 +624,4 @@ class Filter:
|
||||||
# replace next page object at the bottom of the page
|
# replace next page object at the bottom of the page
|
||||||
soup.find_all('table',
|
soup.find_all('table',
|
||||||
attrs={'class': "uZgmoc"})[0].replaceWith(next_pages)
|
attrs={'class': "uZgmoc"})[0].replaceWith(next_pages)
|
||||||
# replace information about user connection at the bottom of the page
|
|
||||||
soup.find_all('div',
|
|
||||||
attrs={'class': "TuS8Ad"})[0].replaceWith(information)
|
|
||||||
return soup
|
return soup
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
|
from inspect import Attribute
|
||||||
|
from app.utils.misc import read_config_bool
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
from base64 import urlsafe_b64encode, urlsafe_b64decode
|
||||||
|
import pickle
|
||||||
|
from cryptography.fernet import Fernet
|
||||||
|
import hashlib
|
||||||
|
import brotli
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
def read_config_bool(var: str) -> bool:
|
|
||||||
val = os.getenv(var, '0')
|
|
||||||
if val.isdigit():
|
|
||||||
return bool(int(val))
|
|
||||||
return False
|
|
||||||
|
|
||||||
app_config = current_app.config
|
app_config = current_app.config
|
||||||
self.url = os.getenv('WHOOGLE_CONFIG_URL', '')
|
self.url = os.getenv('WHOOGLE_CONFIG_URL', '')
|
||||||
self.lang_search = os.getenv('WHOOGLE_CONFIG_SEARCH_LANGUAGE', '')
|
self.lang_search = os.getenv('WHOOGLE_CONFIG_SEARCH_LANGUAGE', '')
|
||||||
|
@ -19,9 +21,12 @@ class Config:
|
||||||
open(os.path.join(app_config['STATIC_FOLDER'],
|
open(os.path.join(app_config['STATIC_FOLDER'],
|
||||||
'css/variables.css')).read())
|
'css/variables.css')).read())
|
||||||
self.block = os.getenv('WHOOGLE_CONFIG_BLOCK', '')
|
self.block = os.getenv('WHOOGLE_CONFIG_BLOCK', '')
|
||||||
self.ctry = os.getenv('WHOOGLE_CONFIG_COUNTRY', '')
|
self.block_title = os.getenv('WHOOGLE_CONFIG_BLOCK_TITLE', '')
|
||||||
|
self.block_url = os.getenv('WHOOGLE_CONFIG_BLOCK_URL', '')
|
||||||
|
self.country = os.getenv('WHOOGLE_CONFIG_COUNTRY', '')
|
||||||
|
self.theme = os.getenv('WHOOGLE_CONFIG_THEME', 'system')
|
||||||
self.safe = read_config_bool('WHOOGLE_CONFIG_SAFE')
|
self.safe = read_config_bool('WHOOGLE_CONFIG_SAFE')
|
||||||
self.dark = read_config_bool('WHOOGLE_CONFIG_DARK')
|
self.dark = read_config_bool('WHOOGLE_CONFIG_DARK') # deprecated
|
||||||
self.alts = read_config_bool('WHOOGLE_CONFIG_ALTS')
|
self.alts = read_config_bool('WHOOGLE_CONFIG_ALTS')
|
||||||
self.nojs = read_config_bool('WHOOGLE_CONFIG_NOJS')
|
self.nojs = read_config_bool('WHOOGLE_CONFIG_NOJS')
|
||||||
self.tor = read_config_bool('WHOOGLE_CONFIG_TOR')
|
self.tor = read_config_bool('WHOOGLE_CONFIG_TOR')
|
||||||
|
@ -29,12 +34,25 @@ class Config:
|
||||||
self.new_tab = read_config_bool('WHOOGLE_CONFIG_NEW_TAB')
|
self.new_tab = read_config_bool('WHOOGLE_CONFIG_NEW_TAB')
|
||||||
self.view_image = read_config_bool('WHOOGLE_CONFIG_VIEW_IMAGE')
|
self.view_image = read_config_bool('WHOOGLE_CONFIG_VIEW_IMAGE')
|
||||||
self.get_only = read_config_bool('WHOOGLE_CONFIG_GET_ONLY')
|
self.get_only = read_config_bool('WHOOGLE_CONFIG_GET_ONLY')
|
||||||
|
self.anon_view = read_config_bool('WHOOGLE_CONFIG_ANON_VIEW')
|
||||||
|
self.preferences_encrypted = read_config_bool('WHOOGLE_CONFIG_PREFERENCES_ENCRYPTED')
|
||||||
|
self.preferences_key = os.getenv('WHOOGLE_CONFIG_PREFERENCES_KEY', '')
|
||||||
|
|
||||||
|
self.accept_language = False
|
||||||
|
|
||||||
self.safe_keys = [
|
self.safe_keys = [
|
||||||
'lang_search',
|
'lang_search',
|
||||||
'lang_interface',
|
'lang_interface',
|
||||||
'ctry',
|
'country',
|
||||||
'dark'
|
'theme',
|
||||||
|
'alts',
|
||||||
|
'new_tab',
|
||||||
|
'view_image',
|
||||||
|
'block',
|
||||||
|
'safe',
|
||||||
|
'nojs',
|
||||||
|
'anon_view',
|
||||||
|
'preferences_encrypted'
|
||||||
]
|
]
|
||||||
|
|
||||||
# Skip setting custom config if there isn't one
|
# Skip setting custom config if there isn't one
|
||||||
|
@ -63,6 +81,24 @@ class Config:
|
||||||
if not name.startswith("__")
|
if not name.startswith("__")
|
||||||
and (type(attr) is bool or type(attr) is str)}
|
and (type(attr) is bool or type(attr) is str)}
|
||||||
|
|
||||||
|
def get_attrs(self):
|
||||||
|
return {name: attr for name, attr in self.__dict__.items()
|
||||||
|
if not name.startswith("__")
|
||||||
|
and (type(attr) is bool or type(attr) is str)}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def preferences(self) -> str:
|
||||||
|
# if encryption key is not set will uncheck preferences encryption
|
||||||
|
if self.preferences_encrypted:
|
||||||
|
self.preferences_encrypted = bool(self.preferences_key)
|
||||||
|
|
||||||
|
# add a tag for visibility if preferences token startswith 'e' it means
|
||||||
|
# the token is encrypted, 'u' means the token is unencrypted and can be
|
||||||
|
# used by other whoogle instances
|
||||||
|
encrypted_flag = "e" if self.preferences_encrypted else 'u'
|
||||||
|
preferences_digest = self._encode_preferences()
|
||||||
|
return f"{encrypted_flag}{preferences_digest}"
|
||||||
|
|
||||||
def is_safe_key(self, key) -> bool:
|
def is_safe_key(self, key) -> bool:
|
||||||
"""Establishes a group of config options that are safe to set
|
"""Establishes a group of config options that are safe to set
|
||||||
in the url.
|
in the url.
|
||||||
|
@ -101,8 +137,74 @@ class Config:
|
||||||
Returns:
|
Returns:
|
||||||
Config -- a modified config object
|
Config -- a modified config object
|
||||||
"""
|
"""
|
||||||
|
if 'preferences' in params:
|
||||||
|
params_new = self._decode_preferences(params['preferences'])
|
||||||
|
# if preferences leads to an empty dictionary it means preferences
|
||||||
|
# parameter was not decrypted successfully
|
||||||
|
if len(params_new):
|
||||||
|
params = params_new
|
||||||
|
|
||||||
for param_key in params.keys():
|
for param_key in params.keys():
|
||||||
if not self.is_safe_key(param_key):
|
if not self.is_safe_key(param_key):
|
||||||
continue
|
continue
|
||||||
self[param_key] = params.get(param_key)
|
param_val = params.get(param_key)
|
||||||
|
|
||||||
|
if param_val == 'off':
|
||||||
|
param_val = False
|
||||||
|
elif isinstance(param_val, str):
|
||||||
|
if param_val.isdigit():
|
||||||
|
param_val = int(param_val)
|
||||||
|
|
||||||
|
self[param_key] = param_val
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def to_params(self) -> str:
|
||||||
|
"""Generates a set of safe params for using in Whoogle URLs
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str -- a set of URL parameters
|
||||||
|
"""
|
||||||
|
param_str = ''
|
||||||
|
for safe_key in self.safe_keys:
|
||||||
|
if not self[safe_key]:
|
||||||
|
continue
|
||||||
|
param_str = param_str + f'&{safe_key}={self[safe_key]}'
|
||||||
|
|
||||||
|
return param_str
|
||||||
|
|
||||||
|
def _get_fernet_key(self, password: str) -> bytes:
|
||||||
|
hash_object = hashlib.md5(password.encode())
|
||||||
|
key = urlsafe_b64encode(hash_object.hexdigest().encode())
|
||||||
|
return key
|
||||||
|
|
||||||
|
def _encode_preferences(self) -> str:
|
||||||
|
encoded_preferences = brotli.compress(pickle.dumps(self.get_attrs()))
|
||||||
|
if self.preferences_encrypted:
|
||||||
|
if self.preferences_key != '':
|
||||||
|
key = self._get_fernet_key(self.preferences_key)
|
||||||
|
encoded_preferences = Fernet(key).encrypt(encoded_preferences)
|
||||||
|
encoded_preferences = brotli.compress(encoded_preferences)
|
||||||
|
|
||||||
|
return urlsafe_b64encode(encoded_preferences).decode()
|
||||||
|
|
||||||
|
def _decode_preferences(self, preferences: str) -> dict:
|
||||||
|
mode = preferences[0]
|
||||||
|
preferences = preferences[1:]
|
||||||
|
if mode == 'e': # preferences are encrypted
|
||||||
|
try:
|
||||||
|
key = self._get_fernet_key(self.preferences_key)
|
||||||
|
|
||||||
|
config = Fernet(key).decrypt(
|
||||||
|
brotli.decompress(urlsafe_b64decode(preferences.encode()))
|
||||||
|
)
|
||||||
|
|
||||||
|
config = pickle.loads(brotli.decompress(config))
|
||||||
|
except Exception:
|
||||||
|
config = {}
|
||||||
|
elif mode == 'u': # preferences are not encrypted
|
||||||
|
config = pickle.loads(
|
||||||
|
brotli.decompress(urlsafe_b64decode(preferences.encode()))
|
||||||
|
)
|
||||||
|
else: # preferences are incorrectly formatted
|
||||||
|
config = {}
|
||||||
|
return config
|
||||||
|
|
22
app/models/endpoint.py
Normal file
22
app/models/endpoint.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class Endpoint(Enum):
|
||||||
|
autocomplete = 'autocomplete'
|
||||||
|
home = 'home'
|
||||||
|
healthz = 'healthz'
|
||||||
|
config = 'config'
|
||||||
|
opensearch = 'opensearch.xml'
|
||||||
|
search = 'search'
|
||||||
|
search_html = 'search.html'
|
||||||
|
url = 'url'
|
||||||
|
imgres = 'imgres'
|
||||||
|
element = 'element'
|
||||||
|
window = 'window'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
def in_path(self, path: str) -> bool:
|
||||||
|
return path.startswith(self.value) or \
|
||||||
|
path.startswith(f'/{self.value}')
|
46
app/models/g_classes.py
Normal file
46
app/models/g_classes.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
|
||||||
|
class GClasses:
|
||||||
|
"""A class for tracking obfuscated class names used in Google results that
|
||||||
|
are directly referenced in Whoogle's filtering code.
|
||||||
|
|
||||||
|
Note: Using these should be a last resort. It is always preferred to filter
|
||||||
|
results using structural cues instead of referencing class names, as these
|
||||||
|
are liable to change at any moment.
|
||||||
|
"""
|
||||||
|
main_tbm_tab = 'KP7LCb'
|
||||||
|
images_tbm_tab = 'n692Zd'
|
||||||
|
footer = 'TuS8Ad'
|
||||||
|
result_class_a = 'ZINbbc'
|
||||||
|
result_class_b = 'luh4td'
|
||||||
|
|
||||||
|
result_classes = {
|
||||||
|
result_class_a: ['Gx5Zad'],
|
||||||
|
result_class_b: ['fP1Qef']
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def replace_css_classes(cls, soup: BeautifulSoup) -> BeautifulSoup:
|
||||||
|
"""Replace updated Google classes with the original class names that
|
||||||
|
Whoogle relies on for styling.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
soup: The result page as a BeautifulSoup object
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BeautifulSoup: The new BeautifulSoup
|
||||||
|
"""
|
||||||
|
result_divs = soup.find_all('div', {
|
||||||
|
'class': [_ for c in cls.result_classes.values() for _ in c]
|
||||||
|
})
|
||||||
|
|
||||||
|
for div in result_divs:
|
||||||
|
new_class = ' '.join(div['class'])
|
||||||
|
for key, val in cls.result_classes.items():
|
||||||
|
new_class = ' '.join(new_class.replace(_, key) for _ in val)
|
||||||
|
div['class'] = new_class.split(' ')
|
||||||
|
return soup
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.value
|
114
app/request.py
114
app/request.py
|
@ -1,15 +1,17 @@
|
||||||
from app.models.config import Config
|
from app.models.config import Config
|
||||||
|
from app.utils.misc import read_config_bool
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import xml.etree.ElementTree as ET
|
from defusedxml import ElementTree as ET
|
||||||
import random
|
import random
|
||||||
import requests
|
import requests
|
||||||
from requests import Response, ConnectionError
|
from requests import Response, ConnectionError
|
||||||
import urllib.parse as urlparse
|
import urllib.parse as urlparse
|
||||||
import os
|
import os
|
||||||
from stem import Signal, SocketError
|
from stem import Signal, SocketError
|
||||||
|
from stem.connection import AuthenticationFailure
|
||||||
from stem.control import Controller
|
from stem.control import Controller
|
||||||
|
from stem.connection import authenticate_cookie, authenticate_password
|
||||||
|
|
||||||
SEARCH_URL = 'https://www.google.com/search?gbv=1&q='
|
|
||||||
MAPS_URL = 'https://maps.google.com/maps'
|
MAPS_URL = 'https://maps.google.com/maps'
|
||||||
AUTOCOMPLETE_URL = ('https://suggestqueries.google.com/'
|
AUTOCOMPLETE_URL = ('https://suggestqueries.google.com/'
|
||||||
'complete/search?client=toolbar&')
|
'complete/search?client=toolbar&')
|
||||||
|
@ -38,13 +40,33 @@ class TorError(Exception):
|
||||||
|
|
||||||
|
|
||||||
def send_tor_signal(signal: Signal) -> bool:
|
def send_tor_signal(signal: Signal) -> bool:
|
||||||
|
use_pass = read_config_bool('WHOOGLE_TOR_USE_PASS')
|
||||||
|
|
||||||
|
confloc = './misc/tor/control.conf'
|
||||||
|
# Check that the custom location of conf is real.
|
||||||
|
temp = os.getenv('WHOOGLE_TOR_CONF', '')
|
||||||
|
if os.path.isfile(temp):
|
||||||
|
confloc = temp
|
||||||
|
|
||||||
|
# Attempt to authenticate and send signal.
|
||||||
try:
|
try:
|
||||||
with Controller.from_port(port=9051) as c:
|
with Controller.from_port(port=9051) as c:
|
||||||
c.authenticate()
|
if use_pass:
|
||||||
|
with open(confloc, "r") as conf:
|
||||||
|
# Scan for the last line of the file.
|
||||||
|
for line in conf:
|
||||||
|
pass
|
||||||
|
secret = line.strip('\n')
|
||||||
|
authenticate_password(c, password=secret)
|
||||||
|
else:
|
||||||
|
cookie_path = '/var/lib/tor/control_auth_cookie'
|
||||||
|
authenticate_cookie(c, cookie_path=cookie_path)
|
||||||
c.signal(signal)
|
c.signal(signal)
|
||||||
os.environ['TOR_AVAILABLE'] = '1'
|
os.environ['TOR_AVAILABLE'] = '1'
|
||||||
return True
|
return True
|
||||||
except (SocketError, ConnectionRefusedError, ConnectionError):
|
except (SocketError, AuthenticationFailure,
|
||||||
|
ConnectionRefusedError, ConnectionError):
|
||||||
|
# TODO: Handle Tor authentication (password and cookie)
|
||||||
os.environ['TOR_AVAILABLE'] = '0'
|
os.environ['TOR_AVAILABLE'] = '0'
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
@ -60,7 +82,7 @@ def gen_user_agent(is_mobile) -> str:
|
||||||
return DESKTOP_UA.format("Mozilla", linux, firefox)
|
return DESKTOP_UA.format("Mozilla", linux, firefox)
|
||||||
|
|
||||||
|
|
||||||
def gen_query(query, args, config, near_city=None) -> str:
|
def gen_query(query, args, config) -> str:
|
||||||
param_dict = {key: '' for key in VALID_PARAMS}
|
param_dict = {key: '' for key in VALID_PARAMS}
|
||||||
|
|
||||||
# Use :past(hour/day/week/month/year) if available
|
# Use :past(hour/day/week/month/year) if available
|
||||||
|
@ -97,8 +119,8 @@ def gen_query(query, args, config, near_city=None) -> str:
|
||||||
param_dict['start'] = '&start=' + args.get('start')
|
param_dict['start'] = '&start=' + args.get('start')
|
||||||
|
|
||||||
# Search for results near a particular city, if available
|
# Search for results near a particular city, if available
|
||||||
if near_city:
|
if config.near:
|
||||||
param_dict['near'] = '&near=' + urlparse.quote(near_city)
|
param_dict['near'] = '&near=' + urlparse.quote(config.near)
|
||||||
|
|
||||||
# Set language for results (lr) if source isn't set, otherwise use the
|
# Set language for results (lr) if source isn't set, otherwise use the
|
||||||
# result language param provided in the results
|
# result language param provided in the results
|
||||||
|
@ -108,19 +130,25 @@ def gen_query(query, args, config, near_city=None) -> str:
|
||||||
[_ for _ in lang if not _.isdigit()]
|
[_ for _ in lang if not _.isdigit()]
|
||||||
)) if lang else ''
|
)) if lang else ''
|
||||||
else:
|
else:
|
||||||
param_dict['lr'] = '&lr=' + (
|
param_dict['lr'] = (
|
||||||
config.lang_search if config.lang_search else ''
|
'&lr=' + config.lang_search
|
||||||
)
|
) if config.lang_search else ''
|
||||||
|
|
||||||
# 'nfpr' defines the exclusion of results from an auto-corrected query
|
# 'nfpr' defines the exclusion of results from an auto-corrected query
|
||||||
if 'nfpr' in args:
|
if 'nfpr' in args:
|
||||||
param_dict['nfpr'] = '&nfpr=' + args.get('nfpr')
|
param_dict['nfpr'] = '&nfpr=' + args.get('nfpr')
|
||||||
|
|
||||||
param_dict['cr'] = ('&cr=' + config.ctry) if config.ctry else ''
|
# 'chips' is used in image tabs to pass the optional 'filter' to add to the
|
||||||
param_dict['hl'] = '&hl=' + (
|
# given search term
|
||||||
config.lang_interface.replace('lang_', '')
|
if 'chips' in args:
|
||||||
if config.lang_interface else ''
|
param_dict['chips'] = '&chips=' + args.get('chips')
|
||||||
)
|
|
||||||
|
param_dict['gl'] = (
|
||||||
|
'&gl=' + config.country
|
||||||
|
) if config.country else ''
|
||||||
|
param_dict['hl'] = (
|
||||||
|
'&hl=' + config.lang_interface.replace('lang_', '')
|
||||||
|
) if config.lang_interface else ''
|
||||||
param_dict['safe'] = '&safe=' + ('active' if config.safe else 'off')
|
param_dict['safe'] = '&safe=' + ('active' if config.safe else 'off')
|
||||||
|
|
||||||
# Block all sites specified in the user config
|
# Block all sites specified in the user config
|
||||||
|
@ -150,6 +178,8 @@ class Request:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, normal_ua, root_path, config: Config):
|
def __init__(self, normal_ua, root_path, config: Config):
|
||||||
|
self.search_url = 'https://www.google.com/search?gbv=1&num=' + str(
|
||||||
|
os.getenv('WHOOGLE_RESULTS_PER_PAGE', 10)) + '&q='
|
||||||
# Send heartbeat to Tor, used in determining if the user can or cannot
|
# Send heartbeat to Tor, used in determining if the user can or cannot
|
||||||
# enable Tor for future requests
|
# enable Tor for future requests
|
||||||
send_tor_signal(Signal.HEARTBEAT)
|
send_tor_signal(Signal.HEARTBEAT)
|
||||||
|
@ -157,7 +187,14 @@ class Request:
|
||||||
self.language = (
|
self.language = (
|
||||||
config.lang_search if config.lang_search else ''
|
config.lang_search if config.lang_search else ''
|
||||||
)
|
)
|
||||||
self.mobile = 'Android' in normal_ua or 'iPhone' in normal_ua
|
|
||||||
|
# For setting Accept-language Header
|
||||||
|
self.lang_interface = ''
|
||||||
|
if config.accept_language:
|
||||||
|
self.lang_interface = config.lang_interface
|
||||||
|
|
||||||
|
self.mobile = bool(normal_ua) and ('Android' in normal_ua
|
||||||
|
or 'iPhone' in normal_ua)
|
||||||
self.modified_user_agent = gen_user_agent(self.mobile)
|
self.modified_user_agent = gen_user_agent(self.mobile)
|
||||||
if not self.mobile:
|
if not self.mobile:
|
||||||
self.modified_user_agent_mobile = gen_user_agent(True)
|
self.modified_user_agent_mobile = gen_user_agent(True)
|
||||||
|
@ -205,18 +242,25 @@ class Request:
|
||||||
list: The list of matches for possible search suggestions
|
list: The list of matches for possible search suggestions
|
||||||
|
|
||||||
"""
|
"""
|
||||||
ac_query = dict(hl=self.language, q=query)
|
ac_query = dict(q=query)
|
||||||
|
if self.language:
|
||||||
|
ac_query['hl'] = self.language
|
||||||
|
|
||||||
response = self.send(base_url=AUTOCOMPLETE_URL,
|
response = self.send(base_url=AUTOCOMPLETE_URL,
|
||||||
query=urlparse.urlencode(ac_query)).text
|
query=urlparse.urlencode(ac_query)).text
|
||||||
|
|
||||||
if not response:
|
if not response:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
root = ET.fromstring(response)
|
try:
|
||||||
return [_.attrib['data'] for _ in
|
root = ET.fromstring(response)
|
||||||
root.findall('.//suggestion/[@data]')]
|
return [_.attrib['data'] for _ in
|
||||||
|
root.findall('.//suggestion/[@data]')]
|
||||||
|
except ET.ParseError:
|
||||||
|
# Malformed XML response
|
||||||
|
return []
|
||||||
|
|
||||||
def send(self, base_url=SEARCH_URL, query='', attempt=0,
|
def send(self, base_url='', query='', attempt=0,
|
||||||
force_mobile=False) -> Response:
|
force_mobile=False) -> Response:
|
||||||
"""Sends an outbound request to a URL. Optionally sends the request
|
"""Sends an outbound request to a URL. Optionally sends the request
|
||||||
using Tor, if enabled by the user.
|
using Tor, if enabled by the user.
|
||||||
|
@ -242,7 +286,12 @@ class Request:
|
||||||
'User-Agent': modified_user_agent
|
'User-Agent': modified_user_agent
|
||||||
}
|
}
|
||||||
|
|
||||||
# FIXME: Should investigate this further to ensure the consent
|
# Adding the Accept-Language to the Header if possible
|
||||||
|
if self.lang_interface:
|
||||||
|
headers.update({'Accept-Language':
|
||||||
|
self.lang_interface.replace('lang_', '')
|
||||||
|
+ ';q=1.0'})
|
||||||
|
|
||||||
# view is suppressed correctly
|
# view is suppressed correctly
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
cookies = {
|
cookies = {
|
||||||
|
@ -261,18 +310,23 @@ class Request:
|
||||||
|
|
||||||
# Make sure that the tor connection is valid, if enabled
|
# Make sure that the tor connection is valid, if enabled
|
||||||
if self.tor:
|
if self.tor:
|
||||||
tor_check = requests.get('https://check.torproject.org/',
|
try:
|
||||||
proxies=self.proxies, headers=headers)
|
tor_check = requests.get('https://check.torproject.org/',
|
||||||
self.tor_valid = 'Congratulations' in tor_check.text
|
proxies=self.proxies, headers=headers)
|
||||||
|
self.tor_valid = 'Congratulations' in tor_check.text
|
||||||
|
|
||||||
if not self.tor_valid:
|
if not self.tor_valid:
|
||||||
|
raise TorError(
|
||||||
|
"Tor connection succeeded, but the connection could "
|
||||||
|
"not be validated by torproject.org",
|
||||||
|
disable=True)
|
||||||
|
except ConnectionError:
|
||||||
raise TorError(
|
raise TorError(
|
||||||
"Tor connection succeeded, but the connection could not "
|
"Error raised during Tor connection validation",
|
||||||
"be validated by torproject.org",
|
|
||||||
disable=True)
|
disable=True)
|
||||||
|
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
base_url + query,
|
(base_url or self.search_url) + query,
|
||||||
proxies=self.proxies,
|
proxies=self.proxies,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
cookies=cookies)
|
cookies=cookies)
|
||||||
|
@ -282,6 +336,6 @@ class Request:
|
||||||
attempt += 1
|
attempt += 1
|
||||||
if attempt > 10:
|
if attempt > 10:
|
||||||
raise TorError("Tor query failed -- max attempts exceeded 10")
|
raise TorError("Tor query failed -- max attempts exceeded 10")
|
||||||
return self.send(base_url, query, attempt)
|
return self.send((base_url or self.search_url), query, attempt)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
386
app/routes.py
386
app/routes.py
|
@ -2,25 +2,46 @@ import argparse
|
||||||
import base64
|
import base64
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import pickle
|
import pickle
|
||||||
import urllib.parse as urlparse
|
import urllib.parse as urlparse
|
||||||
import uuid
|
import uuid
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
import waitress
|
import waitress
|
||||||
from flask import jsonify, make_response, request, redirect, render_template, \
|
|
||||||
send_file, session, url_for
|
|
||||||
from requests import exceptions
|
|
||||||
|
|
||||||
from app import app
|
from app import app
|
||||||
from app.models.config import Config
|
from app.models.config import Config
|
||||||
|
from app.models.endpoint import Endpoint
|
||||||
from app.request import Request, TorError
|
from app.request import Request, TorError
|
||||||
from app.utils.bangs import resolve_bang
|
from app.utils.bangs import resolve_bang
|
||||||
|
from app.utils.misc import get_proxy_host_url
|
||||||
|
from app.filter import Filter
|
||||||
|
from app.utils.misc import read_config_bool, get_client_ip, get_request_url, \
|
||||||
|
check_for_update
|
||||||
|
from app.utils.results import add_ip_card, bold_search_terms,\
|
||||||
|
add_currency_card, check_currency, get_tabs_content
|
||||||
|
from app.utils.search import Search, needs_https, has_captcha
|
||||||
from app.utils.session import generate_user_key, valid_user_session
|
from app.utils.session import generate_user_key, valid_user_session
|
||||||
from app.utils.search import *
|
from bs4 import BeautifulSoup as bsoup
|
||||||
|
from flask import jsonify, make_response, request, redirect, render_template, \
|
||||||
|
send_file, session, url_for, g
|
||||||
|
from requests import exceptions
|
||||||
|
from requests.models import PreparedRequest
|
||||||
|
from cryptography.fernet import Fernet, InvalidToken
|
||||||
|
from cryptography.exceptions import InvalidSignature
|
||||||
|
|
||||||
# Load DDG bang json files only on init
|
# Load DDG bang json files only on init
|
||||||
bang_json = json.load(open(app.config['BANG_FILE']))
|
bang_json = json.load(open(app.config['BANG_FILE'])) or {}
|
||||||
|
|
||||||
|
ac_var = 'WHOOGLE_AUTOCOMPLETE'
|
||||||
|
autocomplete_enabled = os.getenv(ac_var, '1')
|
||||||
|
|
||||||
|
|
||||||
|
def get_search_name(tbm):
|
||||||
|
for tab in app.config['HEADER_TABS'].values():
|
||||||
|
if tab['tbm'] == tbm:
|
||||||
|
return tab['name']
|
||||||
|
|
||||||
|
|
||||||
def auth_required(f):
|
def auth_required(f):
|
||||||
|
@ -43,63 +64,110 @@ def auth_required(f):
|
||||||
return decorated
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
|
def session_required(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated(*args, **kwargs):
|
||||||
|
if (valid_user_session(session)):
|
||||||
|
g.session_key = session['key']
|
||||||
|
else:
|
||||||
|
session.pop('_permanent', None)
|
||||||
|
g.session_key = app.default_key
|
||||||
|
|
||||||
|
# Clear out old sessions
|
||||||
|
invalid_sessions = []
|
||||||
|
for user_session in os.listdir(app.config['SESSION_FILE_DIR']):
|
||||||
|
file_path = os.path.join(
|
||||||
|
app.config['SESSION_FILE_DIR'],
|
||||||
|
user_session)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Ignore files that are larger than the max session file size
|
||||||
|
if os.path.getsize(file_path) > app.config['MAX_SESSION_SIZE']:
|
||||||
|
continue
|
||||||
|
|
||||||
|
with open(file_path, 'rb') as session_file:
|
||||||
|
_ = pickle.load(session_file)
|
||||||
|
data = pickle.load(session_file)
|
||||||
|
if isinstance(data, dict) and 'valid' in data:
|
||||||
|
continue
|
||||||
|
invalid_sessions.append(file_path)
|
||||||
|
except Exception:
|
||||||
|
# Broad exception handling here due to how instances installed
|
||||||
|
# with pip seem to have issues storing unrelated files in the
|
||||||
|
# same directory as sessions
|
||||||
|
pass
|
||||||
|
|
||||||
|
for invalid_session in invalid_sessions:
|
||||||
|
try:
|
||||||
|
os.remove(invalid_session)
|
||||||
|
except FileNotFoundError:
|
||||||
|
# Don't throw error if the invalid session has been removed
|
||||||
|
pass
|
||||||
|
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
@app.before_request
|
@app.before_request
|
||||||
def before_request_func():
|
def before_request_func():
|
||||||
|
global bang_json
|
||||||
|
session.permanent = True
|
||||||
|
|
||||||
|
# Check for latest version if needed
|
||||||
|
now = datetime.now()
|
||||||
|
if now - timedelta(hours=24) > app.config['LAST_UPDATE_CHECK']:
|
||||||
|
app.config['LAST_UPDATE_CHECK'] = now
|
||||||
|
app.config['HAS_UPDATE'] = check_for_update(
|
||||||
|
app.config['RELEASES_URL'],
|
||||||
|
app.config['VERSION_NUMBER'])
|
||||||
|
|
||||||
g.request_params = (
|
g.request_params = (
|
||||||
request.args if request.method == 'GET' else request.form
|
request.args if request.method == 'GET' else request.form
|
||||||
)
|
)
|
||||||
g.cookies_disabled = False
|
|
||||||
|
default_config = json.load(open(app.config['DEFAULT_CONFIG'])) \
|
||||||
|
if os.path.exists(app.config['DEFAULT_CONFIG']) else {}
|
||||||
|
|
||||||
# Generate session values for user if unavailable
|
# Generate session values for user if unavailable
|
||||||
if not valid_user_session(session):
|
if (not valid_user_session(session)):
|
||||||
session['config'] = json.load(open(app.config['DEFAULT_CONFIG'])) \
|
session['config'] = default_config
|
||||||
if os.path.exists(app.config['DEFAULT_CONFIG']) else {}
|
|
||||||
session['uuid'] = str(uuid.uuid4())
|
session['uuid'] = str(uuid.uuid4())
|
||||||
session['key'] = generate_user_key(True)
|
session['key'] = generate_user_key()
|
||||||
|
|
||||||
# Flag cookies as possibly disabled in order to prevent against
|
|
||||||
# unnecessary session directory expansion
|
|
||||||
g.cookies_disabled = True
|
|
||||||
|
|
||||||
# Handle https upgrade
|
|
||||||
if needs_https(request.url):
|
|
||||||
return redirect(
|
|
||||||
request.url.replace('http://', 'https://', 1),
|
|
||||||
code=308)
|
|
||||||
|
|
||||||
|
# Establish config values per user session
|
||||||
g.user_config = Config(**session['config'])
|
g.user_config = Config(**session['config'])
|
||||||
|
|
||||||
if not g.user_config.url:
|
if not g.user_config.url:
|
||||||
g.user_config.url = request.url_root.replace(
|
g.user_config.url = get_request_url(request.url_root)
|
||||||
'http://',
|
|
||||||
'https://') if os.getenv('HTTPS_ONLY', False) else request.url_root
|
|
||||||
|
|
||||||
g.user_request = Request(
|
g.user_request = Request(
|
||||||
request.headers.get('User-Agent'),
|
request.headers.get('User-Agent'),
|
||||||
request.url_root,
|
get_request_url(request.url_root),
|
||||||
config=g.user_config)
|
config=g.user_config)
|
||||||
|
|
||||||
g.app_location = g.user_config.url
|
g.app_location = g.user_config.url
|
||||||
|
|
||||||
|
# Attempt to reload bangs json if not generated yet
|
||||||
|
if not bang_json and os.path.getsize(app.config['BANG_FILE']) > 4:
|
||||||
|
try:
|
||||||
|
bang_json = json.load(open(app.config['BANG_FILE']))
|
||||||
|
except json.decoder.JSONDecodeError:
|
||||||
|
# Ignore decoding error, can occur if file is still
|
||||||
|
# being written
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@app.after_request
|
@app.after_request
|
||||||
def after_request_func(resp):
|
def after_request_func(resp):
|
||||||
# Check if address consistently has cookies blocked,
|
resp.headers['X-Content-Type-Options'] = 'nosniff'
|
||||||
# in which case start removing session files after creation.
|
resp.headers['X-Frame-Options'] = 'DENY'
|
||||||
#
|
|
||||||
# Note: This is primarily done to prevent overpopulation of session
|
|
||||||
# directories, since browsers that block cookies will still trigger
|
|
||||||
# Flask's session creation routine with every request.
|
|
||||||
if g.cookies_disabled and request.remote_addr not in app.no_cookie_ips:
|
|
||||||
app.no_cookie_ips.append(request.remote_addr)
|
|
||||||
elif g.cookies_disabled and request.remote_addr in app.no_cookie_ips:
|
|
||||||
session_list = list(session.keys())
|
|
||||||
for key in session_list:
|
|
||||||
session.pop(key)
|
|
||||||
|
|
||||||
resp.headers['Content-Security-Policy'] = app.config['CSP']
|
if os.getenv('WHOOGLE_CSP', False):
|
||||||
if os.environ.get('HTTPS_ONLY', False):
|
resp.headers['Content-Security-Policy'] = app.config['CSP']
|
||||||
resp.headers['Content-Security-Policy'] += 'upgrade-insecure-requests'
|
if os.environ.get('HTTPS_ONLY', False):
|
||||||
|
resp.headers['Content-Security-Policy'] += \
|
||||||
|
'upgrade-insecure-requests'
|
||||||
|
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
@ -110,39 +178,45 @@ def unknown_page(e):
|
||||||
return redirect(g.app_location)
|
return redirect(g.app_location)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/healthz', methods=['GET'])
|
@app.route(f'/{Endpoint.healthz}', methods=['GET'])
|
||||||
def healthz():
|
def healthz():
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
|
||||||
@app.route('/', methods=['GET'])
|
@app.route('/', methods=['GET'])
|
||||||
|
@app.route(f'/{Endpoint.home}', methods=['GET'])
|
||||||
@auth_required
|
@auth_required
|
||||||
def index():
|
def index():
|
||||||
# Reset keys
|
|
||||||
session['key'] = generate_user_key(g.cookies_disabled)
|
|
||||||
|
|
||||||
# Redirect if an error was raised
|
# Redirect if an error was raised
|
||||||
if 'error_message' in session and session['error_message']:
|
if 'error_message' in session and session['error_message']:
|
||||||
error_message = session['error_message']
|
error_message = session['error_message']
|
||||||
session['error_message'] = ''
|
session['error_message'] = ''
|
||||||
return render_template('error.html', error_message=error_message)
|
return render_template('error.html', error_message=error_message)
|
||||||
|
|
||||||
|
# Update user config if specified in search args
|
||||||
|
g.user_config = g.user_config.from_params(g.request_params)
|
||||||
|
|
||||||
return render_template('index.html',
|
return render_template('index.html',
|
||||||
|
has_update=app.config['HAS_UPDATE'],
|
||||||
languages=app.config['LANGUAGES'],
|
languages=app.config['LANGUAGES'],
|
||||||
countries=app.config['COUNTRIES'],
|
countries=app.config['COUNTRIES'],
|
||||||
|
themes=app.config['THEMES'],
|
||||||
|
autocomplete_enabled=autocomplete_enabled,
|
||||||
translation=app.config['TRANSLATIONS'][
|
translation=app.config['TRANSLATIONS'][
|
||||||
g.user_config.get_localization_lang()
|
g.user_config.get_localization_lang()
|
||||||
],
|
],
|
||||||
logo=render_template(
|
logo=render_template(
|
||||||
'logo.html',
|
'logo.html',
|
||||||
dark=g.user_config.dark),
|
dark=g.user_config.dark),
|
||||||
config_disabled=app.config['CONFIG_DISABLE'],
|
config_disabled=(
|
||||||
|
app.config['CONFIG_DISABLE'] or
|
||||||
|
not valid_user_session(session)),
|
||||||
config=g.user_config,
|
config=g.user_config,
|
||||||
tor_available=int(os.environ.get('TOR_AVAILABLE')),
|
tor_available=int(os.environ.get('TOR_AVAILABLE')),
|
||||||
version_number=app.config['VERSION_NUMBER'])
|
version_number=app.config['VERSION_NUMBER'])
|
||||||
|
|
||||||
|
|
||||||
@app.route('/opensearch.xml', methods=['GET'])
|
@app.route(f'/{Endpoint.opensearch}', methods=['GET'])
|
||||||
def opensearch():
|
def opensearch():
|
||||||
opensearch_url = g.app_location
|
opensearch_url = g.app_location
|
||||||
if opensearch_url.endswith('/'):
|
if opensearch_url.endswith('/'):
|
||||||
|
@ -158,11 +232,14 @@ def opensearch():
|
||||||
return render_template(
|
return render_template(
|
||||||
'opensearch.xml',
|
'opensearch.xml',
|
||||||
main_url=opensearch_url,
|
main_url=opensearch_url,
|
||||||
request_type='' if get_only else 'method="post"'
|
request_type='' if get_only else 'method="post"',
|
||||||
), 200, {'Content-Disposition': 'attachment; filename="opensearch.xml"'}
|
search_type=request.args.get('tbm'),
|
||||||
|
search_name=get_search_name(request.args.get('tbm')),
|
||||||
|
preferences=g.user_config.preferences
|
||||||
|
), 200, {'Content-Type': 'application/xml'}
|
||||||
|
|
||||||
|
|
||||||
@app.route('/search.html', methods=['GET'])
|
@app.route(f'/{Endpoint.search_html}', methods=['GET'])
|
||||||
def search_html():
|
def search_html():
|
||||||
search_url = g.app_location
|
search_url = g.app_location
|
||||||
if search_url.endswith('/'):
|
if search_url.endswith('/'):
|
||||||
|
@ -170,8 +247,11 @@ def search_html():
|
||||||
return render_template('search.html', url=search_url)
|
return render_template('search.html', url=search_url)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/autocomplete', methods=['GET', 'POST'])
|
@app.route(f'/{Endpoint.autocomplete}', methods=['GET', 'POST'])
|
||||||
def autocomplete():
|
def autocomplete():
|
||||||
|
if os.getenv(ac_var) and not read_config_bool(ac_var):
|
||||||
|
return jsonify({})
|
||||||
|
|
||||||
q = g.request_params.get('q')
|
q = g.request_params.get('q')
|
||||||
if not q:
|
if not q:
|
||||||
# FF will occasionally (incorrectly) send the q field without a
|
# FF will occasionally (incorrectly) send the q field without a
|
||||||
|
@ -199,23 +279,23 @@ def autocomplete():
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
@app.route('/search', methods=['GET', 'POST'])
|
@app.route(f'/{Endpoint.search}', methods=['GET', 'POST'])
|
||||||
|
@session_required
|
||||||
@auth_required
|
@auth_required
|
||||||
def search():
|
def search():
|
||||||
# Update user config if specified in search args
|
# Update user config if specified in search args
|
||||||
g.user_config = g.user_config.from_params(g.request_params)
|
g.user_config = g.user_config.from_params(g.request_params)
|
||||||
|
|
||||||
search_util = Search(request, g.user_config, session,
|
search_util = Search(request, g.user_config, g.session_key)
|
||||||
cookies_disabled=g.cookies_disabled)
|
|
||||||
query = search_util.new_search_query()
|
query = search_util.new_search_query()
|
||||||
|
|
||||||
bang = resolve_bang(query=query, bangs_dict=bang_json)
|
bang = resolve_bang(query, bang_json)
|
||||||
if bang != '':
|
if bang:
|
||||||
return redirect(bang)
|
return redirect(bang)
|
||||||
|
|
||||||
# Redirect to home if invalid/blank search
|
# Redirect to home if invalid/blank search
|
||||||
if not query:
|
if not query:
|
||||||
return redirect('/')
|
return redirect(url_for('.index'))
|
||||||
|
|
||||||
# Generate response and number of external elements from the page
|
# Generate response and number of external elements from the page
|
||||||
try:
|
try:
|
||||||
|
@ -230,33 +310,88 @@ def search():
|
||||||
if search_util.feeling_lucky:
|
if search_util.feeling_lucky:
|
||||||
return redirect(response, code=303)
|
return redirect(response, code=303)
|
||||||
|
|
||||||
|
# If the user is attempting to translate a string, determine the correct
|
||||||
|
# string for formatting the lingva.ml url
|
||||||
|
localization_lang = g.user_config.get_localization_lang()
|
||||||
|
translation = app.config['TRANSLATIONS'][localization_lang]
|
||||||
|
translate_to = localization_lang.replace('lang_', '')
|
||||||
|
|
||||||
# Return 503 if temporarily blocked by captcha
|
# Return 503 if temporarily blocked by captcha
|
||||||
resp_code = 503 if has_captcha(str(response)) else 200
|
if has_captcha(str(response)):
|
||||||
|
return render_template(
|
||||||
|
'error.html',
|
||||||
|
blocked=True,
|
||||||
|
error_message=translation['ratelimit'],
|
||||||
|
translation=translation,
|
||||||
|
farside='https://farside.link',
|
||||||
|
config=g.user_config,
|
||||||
|
query=urlparse.unquote(query),
|
||||||
|
params=g.user_config.to_params()), 503
|
||||||
|
response = bold_search_terms(response, query)
|
||||||
|
|
||||||
|
# Feature to display IP address
|
||||||
|
if search_util.check_kw_ip():
|
||||||
|
html_soup = bsoup(str(response), 'html.parser')
|
||||||
|
response = add_ip_card(html_soup, get_client_ip(request))
|
||||||
|
|
||||||
|
# Update tabs content
|
||||||
|
tabs = get_tabs_content(app.config['HEADER_TABS'],
|
||||||
|
search_util.full_query,
|
||||||
|
search_util.search_type,
|
||||||
|
g.user_config.preferences,
|
||||||
|
translation)
|
||||||
|
|
||||||
|
# Feature to display currency_card
|
||||||
|
conversion = check_currency(str(response))
|
||||||
|
if conversion:
|
||||||
|
html_soup = bsoup(str(response), 'html.parser')
|
||||||
|
response = add_currency_card(html_soup, conversion)
|
||||||
|
|
||||||
|
preferences = g.user_config.preferences
|
||||||
|
home_url = f"home?preferences={preferences}" if preferences else "home"
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'display.html',
|
'display.html',
|
||||||
|
has_update=app.config['HAS_UPDATE'],
|
||||||
query=urlparse.unquote(query),
|
query=urlparse.unquote(query),
|
||||||
search_type=search_util.search_type,
|
search_type=search_util.search_type,
|
||||||
|
search_name=get_search_name(search_util.search_type),
|
||||||
config=g.user_config,
|
config=g.user_config,
|
||||||
translation=app.config['TRANSLATIONS'][
|
autocomplete_enabled=autocomplete_enabled,
|
||||||
g.user_config.get_localization_lang()
|
lingva_url=app.config['TRANSLATE_URL'],
|
||||||
],
|
translation=translation,
|
||||||
|
translate_to=translate_to,
|
||||||
|
translate_str=query.replace(
|
||||||
|
'translate', ''
|
||||||
|
).replace(
|
||||||
|
translation['translate'], ''
|
||||||
|
),
|
||||||
|
is_translation=any(
|
||||||
|
_ in query.lower() for _ in [translation['translate'], 'translate']
|
||||||
|
) and not search_util.search_type, # Standard search queries only
|
||||||
response=response,
|
response=response,
|
||||||
version_number=app.config['VERSION_NUMBER'],
|
version_number=app.config['VERSION_NUMBER'],
|
||||||
search_header=(render_template(
|
search_header=render_template(
|
||||||
'header.html',
|
'header.html',
|
||||||
|
home_url=home_url,
|
||||||
config=g.user_config,
|
config=g.user_config,
|
||||||
|
translation=translation,
|
||||||
|
languages=app.config['LANGUAGES'],
|
||||||
|
countries=app.config['COUNTRIES'],
|
||||||
logo=render_template('logo.html', dark=g.user_config.dark),
|
logo=render_template('logo.html', dark=g.user_config.dark),
|
||||||
query=urlparse.unquote(query),
|
query=urlparse.unquote(query),
|
||||||
search_type=search_util.search_type,
|
search_type=search_util.search_type,
|
||||||
mobile=g.user_request.mobile)
|
mobile=g.user_request.mobile,
|
||||||
if 'isch' not in search_util.search_type else '')), resp_code
|
tabs=tabs))
|
||||||
|
|
||||||
|
|
||||||
@app.route('/config', methods=['GET', 'POST', 'PUT'])
|
@app.route(f'/{Endpoint.config}', methods=['GET', 'POST', 'PUT'])
|
||||||
|
@session_required
|
||||||
@auth_required
|
@auth_required
|
||||||
def config():
|
def config():
|
||||||
config_disabled = app.config['CONFIG_DISABLE']
|
config_disabled = (
|
||||||
|
app.config['CONFIG_DISABLE'] or
|
||||||
|
not valid_user_session(session))
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
return json.dumps(g.user_config.__dict__)
|
return json.dumps(g.user_config.__dict__)
|
||||||
elif request.method == 'PUT' and not config_disabled:
|
elif request.method == 'PUT' and not config_disabled:
|
||||||
|
@ -283,43 +418,33 @@ def config():
|
||||||
app.config['CONFIG_PATH'],
|
app.config['CONFIG_PATH'],
|
||||||
request.args.get('name')), 'wb'))
|
request.args.get('name')), 'wb'))
|
||||||
|
|
||||||
# Overwrite default config if user has cookies disabled
|
|
||||||
if g.cookies_disabled:
|
|
||||||
open(app.config['DEFAULT_CONFIG'], 'w').write(
|
|
||||||
json.dumps(config_data, indent=4))
|
|
||||||
|
|
||||||
session['config'] = config_data
|
session['config'] = config_data
|
||||||
return redirect(config_data['url'])
|
return redirect(config_data['url'])
|
||||||
else:
|
else:
|
||||||
return redirect(url_for('.index'), code=403)
|
return redirect(url_for('.index'), code=403)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/url', methods=['GET'])
|
@app.route(f'/{Endpoint.imgres}')
|
||||||
@auth_required
|
@session_required
|
||||||
def url():
|
|
||||||
if 'url' in request.args:
|
|
||||||
return redirect(request.args.get('url'))
|
|
||||||
|
|
||||||
q = request.args.get('q')
|
|
||||||
if len(q) > 0 and 'http' in q:
|
|
||||||
return redirect(q)
|
|
||||||
else:
|
|
||||||
return render_template(
|
|
||||||
'error.html',
|
|
||||||
error_message='Unable to resolve query: ' + q)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/imgres')
|
|
||||||
@auth_required
|
@auth_required
|
||||||
def imgres():
|
def imgres():
|
||||||
return redirect(request.args.get('imgurl'))
|
return redirect(request.args.get('imgurl'))
|
||||||
|
|
||||||
|
|
||||||
@app.route('/element')
|
@app.route(f'/{Endpoint.element}')
|
||||||
|
@session_required
|
||||||
@auth_required
|
@auth_required
|
||||||
def element():
|
def element():
|
||||||
cipher_suite = Fernet(session['key'])
|
element_url = src_url = request.args.get('url')
|
||||||
src_url = cipher_suite.decrypt(request.args.get('url').encode()).decode()
|
if element_url.startswith('gAAAAA'):
|
||||||
|
try:
|
||||||
|
cipher_suite = Fernet(g.session_key)
|
||||||
|
src_url = cipher_suite.decrypt(element_url.encode()).decode()
|
||||||
|
except (InvalidSignature, InvalidToken) as e:
|
||||||
|
return render_template(
|
||||||
|
'error.html',
|
||||||
|
error_message=str(e)), 401
|
||||||
|
|
||||||
src_type = request.args.get('type')
|
src_type = request.args.get('type')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -337,21 +462,71 @@ def element():
|
||||||
return send_file(io.BytesIO(empty_gif), mimetype='image/gif')
|
return send_file(io.BytesIO(empty_gif), mimetype='image/gif')
|
||||||
|
|
||||||
|
|
||||||
@app.route('/window')
|
@app.route(f'/{Endpoint.window}')
|
||||||
|
@session_required
|
||||||
@auth_required
|
@auth_required
|
||||||
def window():
|
def window():
|
||||||
get_body = g.user_request.send(base_url=request.args.get('location')).text
|
target_url = request.args.get('location')
|
||||||
get_body = get_body.replace('src="/',
|
if target_url.startswith('gAAAAA'):
|
||||||
'src="' + request.args.get('location') + '"')
|
cipher_suite = Fernet(g.session_key)
|
||||||
get_body = get_body.replace('href="/',
|
target_url = cipher_suite.decrypt(target_url.encode()).decode()
|
||||||
'href="' + request.args.get('location') + '"')
|
|
||||||
|
content_filter = Filter(
|
||||||
|
g.session_key,
|
||||||
|
root_url=request.url_root,
|
||||||
|
config=g.user_config)
|
||||||
|
target = urlparse.urlparse(target_url)
|
||||||
|
host_url = f'{target.scheme}://{target.netloc}'
|
||||||
|
|
||||||
|
get_body = g.user_request.send(base_url=target_url).text
|
||||||
|
|
||||||
results = bsoup(get_body, 'html.parser')
|
results = bsoup(get_body, 'html.parser')
|
||||||
|
src_attrs = ['src', 'href', 'srcset', 'data-srcset', 'data-src']
|
||||||
|
|
||||||
for script in results('script'):
|
# Parse HTML response and replace relative links w/ absolute
|
||||||
script.decompose()
|
for element in results.find_all():
|
||||||
|
for attr in src_attrs:
|
||||||
|
if not element.has_attr(attr) or not element[attr].startswith('/'):
|
||||||
|
continue
|
||||||
|
|
||||||
return render_template('display.html', response=results)
|
element[attr] = host_url + element[attr]
|
||||||
|
|
||||||
|
# Replace or remove javascript sources
|
||||||
|
for script in results.find_all('script', {'src': True}):
|
||||||
|
if 'nojs' in request.args:
|
||||||
|
script.decompose()
|
||||||
|
else:
|
||||||
|
content_filter.update_element_src(script, 'application/javascript')
|
||||||
|
|
||||||
|
# Replace all possible image attributes
|
||||||
|
img_sources = ['src', 'data-src', 'data-srcset', 'srcset']
|
||||||
|
for img in results.find_all('img'):
|
||||||
|
_ = [
|
||||||
|
content_filter.update_element_src(img, 'image/png', attr=_)
|
||||||
|
for _ in img_sources if img.has_attr(_)
|
||||||
|
]
|
||||||
|
|
||||||
|
# Replace all stylesheet sources
|
||||||
|
for link in results.find_all('link', {'href': True}):
|
||||||
|
content_filter.update_element_src(link, 'text/css', attr='href')
|
||||||
|
|
||||||
|
# Use anonymous view for all links on page
|
||||||
|
for a in results.find_all('a', {'href': True}):
|
||||||
|
a['href'] = f'{Endpoint.window}?location=' + a['href'] + (
|
||||||
|
'&nojs=1' if 'nojs' in request.args else '')
|
||||||
|
|
||||||
|
# Remove all iframes -- these are commonly used inside of <noscript> tags
|
||||||
|
# to enforce loading Google Analytics
|
||||||
|
for iframe in results.find_all('iframe'):
|
||||||
|
iframe.decompose()
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
'display.html',
|
||||||
|
response=results,
|
||||||
|
translation=app.config['TRANSLATIONS'][
|
||||||
|
g.user_config.get_localization_lang()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def run_app() -> None:
|
def run_app() -> None:
|
||||||
|
@ -367,6 +542,11 @@ def run_app() -> None:
|
||||||
default='127.0.0.1',
|
default='127.0.0.1',
|
||||||
metavar='<ip address>',
|
metavar='<ip address>',
|
||||||
help='Specifies the host address to use (default 127.0.0.1)')
|
help='Specifies the host address to use (default 127.0.0.1)')
|
||||||
|
parser.add_argument(
|
||||||
|
'--unix-socket',
|
||||||
|
default='',
|
||||||
|
metavar='</path/to/unix.sock>',
|
||||||
|
help='Listen for app on unix socket instead of host:port')
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--debug',
|
'--debug',
|
||||||
default=False,
|
default=False,
|
||||||
|
@ -412,9 +592,15 @@ def run_app() -> None:
|
||||||
os.environ['WHOOGLE_PROXY_TYPE'] = args.proxytype
|
os.environ['WHOOGLE_PROXY_TYPE'] = args.proxytype
|
||||||
os.environ['WHOOGLE_PROXY_LOC'] = args.proxyloc
|
os.environ['WHOOGLE_PROXY_LOC'] = args.proxyloc
|
||||||
|
|
||||||
os.environ['HTTPS_ONLY'] = '1' if args.https_only else ''
|
if args.https_only:
|
||||||
|
os.environ['HTTPS_ONLY'] = '1'
|
||||||
|
|
||||||
if args.debug:
|
if args.debug:
|
||||||
app.run(host=args.host, port=args.port, debug=args.debug)
|
app.run(host=args.host, port=args.port, debug=args.debug)
|
||||||
|
elif args.unix_socket:
|
||||||
|
waitress.serve(app, unix_socket=args.unix_socket)
|
||||||
else:
|
else:
|
||||||
waitress.serve(app, listen="{}:{}".format(args.host, args.port))
|
waitress.serve(
|
||||||
|
app,
|
||||||
|
listen="{}:{}".format(args.host, args.port),
|
||||||
|
url_prefix=os.environ.get('WHOOGLE_URL_PREFIX', ''))
|
||||||
|
|
2
app/static/build/.gitignore
vendored
Normal file
2
app/static/build/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
|
@ -22,6 +22,11 @@ li {
|
||||||
color: var(--whoogle-dark-text) !important;
|
color: var(--whoogle-dark-text) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.anon-view {
|
||||||
|
color: var(--whoogle-dark-text) !important;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
background: var(--whoogle-dark-page-bg) !important;
|
background: var(--whoogle-dark-page-bg) !important;
|
||||||
color: var(--whoogle-dark-text) !important;
|
color: var(--whoogle-dark-text) !important;
|
||||||
|
@ -58,17 +63,31 @@ select {
|
||||||
}
|
}
|
||||||
|
|
||||||
.ZINbbc {
|
.ZINbbc {
|
||||||
background-color: var(--whoogle-dark-result-bg) !important;
|
overflow: hidden;
|
||||||
|
box-shadow: 0 0 0 0 !important;
|
||||||
|
background-color: var(--whoogle-dark-result-bg) !important;
|
||||||
|
margin-bottom: 10px !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bRsWnc {
|
.KP7LCb {
|
||||||
background-color: var(--whoogle-dark-result-bg) !important;
|
box-shadow: 0 0 0 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.BVG0Nb {
|
.BVG0Nb {
|
||||||
|
box-shadow: 0 0 0 0 !important;
|
||||||
background-color: var(--whoogle-dark-page-bg) !important;
|
background-color: var(--whoogle-dark-page-bg) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ZINbbc.luh4tb {
|
||||||
|
background: var(--whoogle-dark-result-bg) !important;
|
||||||
|
margin-bottom: 24px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bRsWnc {
|
||||||
|
background-color: var(--whoogle-dark-result-bg) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.x54gtf {
|
.x54gtf {
|
||||||
background-color: var(--whoogle-dark-divider) !important;
|
background-color: var(--whoogle-dark-divider) !important;
|
||||||
}
|
}
|
||||||
|
@ -81,9 +100,19 @@ select {
|
||||||
background-color: var(--whoogle-dark-divider) !important;
|
background-color: var(--whoogle-dark-divider) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.home-search {
|
||||||
|
border-color: var(--whoogle-dark-element-bg) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sa1toc {
|
||||||
|
background: var(--whoogle-dark-page-bg) !important;
|
||||||
|
}
|
||||||
|
|
||||||
#search-bar {
|
#search-bar {
|
||||||
border-color: var(--whoogle-dark-element-bg) !important;
|
border-color: var(--whoogle-dark-element-bg) !important;
|
||||||
color: var(--whoogle-dark-text) !important;
|
color: var(--whoogle-dark-text) !important;
|
||||||
|
background-color: var(--whoogle-dark-result-bg) !important;
|
||||||
|
border-bottom: 2px solid var(--whoogle-dark-element-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
#search-bar:focus {
|
#search-bar:focus {
|
||||||
|
@ -102,11 +131,11 @@ select {
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapsible {
|
.collapsible {
|
||||||
color: var(--whoogle-dark-text);
|
color: var(--whoogle-dark-text) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapsible:after {
|
.collapsible:after {
|
||||||
color: var(--whoogle-dark-text);
|
color: var(--whoogle-dark-text) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.active {
|
.active {
|
||||||
|
@ -114,7 +143,7 @@ select {
|
||||||
color: var(--whoogle-dark-contrast-text) !important;
|
color: var(--whoogle-dark-contrast-text) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content, .result-config {
|
||||||
background-color: var(--whoogle-dark-element-bg) !important;
|
background-color: var(--whoogle-dark-element-bg) !important;
|
||||||
color: var(--whoogle-contrast-text) !important;
|
color: var(--whoogle-contrast-text) !important;
|
||||||
}
|
}
|
||||||
|
@ -123,10 +152,14 @@ select {
|
||||||
color: var(--whoogle-dark-contrast-text) !important;
|
color: var(--whoogle-dark-contrast-text) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#gh-link {
|
.link {
|
||||||
color: var(--whoogle-dark-contrast-text);
|
color: var(--whoogle-dark-contrast-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.link-color {
|
||||||
|
color: var(--whoogle-dark-result-url) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.autocomplete-items {
|
.autocomplete-items {
|
||||||
border: 1px solid var(--whoogle-dark-element-bg);
|
border: 1px solid var(--whoogle-dark-element-bg);
|
||||||
}
|
}
|
||||||
|
@ -146,3 +179,40 @@ select {
|
||||||
background-color: var(--whoogle-dark-element-bg) !important;
|
background-color: var(--whoogle-dark-element-bg) !important;
|
||||||
color: var(--whoogle-dark-contrast-text) !important;
|
color: var(--whoogle-dark-contrast-text) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
color: var(--whoogle-dark-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
path {
|
||||||
|
fill: var(--whoogle-dark-logo);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-div {
|
||||||
|
background-color: var(--whoogle-dark-result-bg) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-reset {
|
||||||
|
color: var(--whoogle-dark-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-search-bar {
|
||||||
|
background-color: var(--whoogle-dark-result-bg) !important;
|
||||||
|
color: var(--whoogle-dark-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar-desktop {
|
||||||
|
color: var(--whoogle-dark-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ip-text-div, .update_available, .cb_label, .cb {
|
||||||
|
color: var(--whoogle-dark-secondary-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb:focus {
|
||||||
|
color: var(--whoogle-dark-contrast-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-header, .mobile-header {
|
||||||
|
background-color: var(--whoogle-dark-result-bg) !important;
|
||||||
|
}
|
||||||
|
|
9
app/static/css/error.css
Normal file
9
app/static/css/error.css
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
html {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
html {
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,9 +13,18 @@ header {
|
||||||
border-radius: 2px 0 0 0;
|
border-radius: 2px 0 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.result-config {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.mobile-logo {
|
.mobile-logo {
|
||||||
font: 22px/36px Futura, Arial, sans-serif;
|
font: 22px/36px Futura, Arial, sans-serif;
|
||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-div {
|
.logo-div {
|
||||||
|
@ -71,3 +80,171 @@ header {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 0 6px 1px #2375e8;
|
box-shadow: 0 0 6px 1px #2375e8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#mobile-header-logo {
|
||||||
|
height: 1.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-input-div {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-search-bar {
|
||||||
|
display: block;
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 0 0 0 8px;
|
||||||
|
padding-right: 0px;
|
||||||
|
-webkit-box-flex: 1;
|
||||||
|
height: 35px;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
width: 100%;
|
||||||
|
-webkit-tap-highlight-color: rgba(0,0,0,.00);
|
||||||
|
overflow: hidden;
|
||||||
|
border: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-mobile{
|
||||||
|
display: -webkit-box;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-header-logo {
|
||||||
|
height: 1.65em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-autocomplete {
|
||||||
|
width: 100%;
|
||||||
|
flex: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #1967D2;
|
||||||
|
text-decoration: none;
|
||||||
|
tap-highlight-color: rgba(0, 0, 0, .10);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-tab-div {
|
||||||
|
border-radius: 0 0 8px 8px;
|
||||||
|
box-shadow: 0 2px 3px rgba(32, 33, 36, 0.18);
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-tab-div-2 {
|
||||||
|
border-top: 1px solid #dadce0;
|
||||||
|
height: 39px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-tab-div-3 {
|
||||||
|
height: 51px;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-header {
|
||||||
|
height: 39px;
|
||||||
|
display: box;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-tab {
|
||||||
|
box-pack: justify;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 37px;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-header a, .desktop-header span {
|
||||||
|
color: #70757a;
|
||||||
|
display: block;
|
||||||
|
flex: none;
|
||||||
|
padding: 0 16px;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.header-tab-span {
|
||||||
|
border-bottom: 2px solid #4285f4;
|
||||||
|
color: #4285f4;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-header {
|
||||||
|
height: 39px;
|
||||||
|
display: box;
|
||||||
|
display: flex;
|
||||||
|
overflow-x: scroll;
|
||||||
|
width: 100%;
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-header a, .mobile-header span {
|
||||||
|
color: #70757a;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
/* padding: 8px 12px 8px 12px; */
|
||||||
|
}
|
||||||
|
|
||||||
|
span.mobile-tab-span {
|
||||||
|
border-bottom: 2px solid #202124;
|
||||||
|
color: #202124;
|
||||||
|
height: 26px;
|
||||||
|
/* margin: 0 12px; */
|
||||||
|
/* padding: 0; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-header input {
|
||||||
|
margin: 2px 4px 2px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.header-tab-a:visited {
|
||||||
|
color: #70757a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-tab-div-end {
|
||||||
|
border-left: 1px solid rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.adv-search {
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adv-search:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#adv-search-toggle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-collapsible {
|
||||||
|
max-height: 0px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height .25s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar-input {
|
||||||
|
display: block;
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 0 0 0 8px;
|
||||||
|
flex: 1;
|
||||||
|
height: 35px;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
width: 100%;
|
||||||
|
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#result-country {
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 801px) {
|
||||||
|
.header-tab-div {
|
||||||
|
margin-bottom: 10px !important
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -12,3 +12,30 @@
|
||||||
height: 40px;
|
height: 40px;
|
||||||
width: 50px;
|
width: 50px;
|
||||||
}
|
}
|
||||||
|
.ZINbbc.xpd.O9g5cc.uUPGi input::-webkit-outer-spin-button,
|
||||||
|
input::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb {
|
||||||
|
width: 40%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: left;
|
||||||
|
line-height: 28px;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #5f6368;
|
||||||
|
font-size: 14px !important;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 0 0 12px;
|
||||||
|
margin: 10px 10px 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversion_box {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ZINbbc.xpd.O9g5cc.uUPGi input:focus-visible {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
|
@ -22,6 +22,11 @@ li {
|
||||||
color: var(--whoogle-text) !important;
|
color: var(--whoogle-text) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.anon-view {
|
||||||
|
color: var(--whoogle-text) !important;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
background: var(--whoogle-page-bg) !important;
|
background: var(--whoogle-page-bg) !important;
|
||||||
color: var(--whoogle-text) !important;
|
color: var(--whoogle-text) !important;
|
||||||
|
@ -33,11 +38,24 @@ select {
|
||||||
}
|
}
|
||||||
|
|
||||||
.ZINbbc {
|
.ZINbbc {
|
||||||
background-color: var(--whoogle-result-bg) !important;
|
overflow: hidden;
|
||||||
|
background-color: var(--whoogle-result-bg) !important;
|
||||||
|
margin-bottom: 10px !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
box-shadow: 0 1px 6px rgba(32,33,36,0.28) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.BVG0Nb {
|
||||||
|
background-color: var(--whoogle-result-bg) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ZINbbc.luh4tb {
|
||||||
|
background: var(--whoogle-result-bg) !important;
|
||||||
|
margin-bottom: 24px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bRsWnc {
|
.bRsWnc {
|
||||||
background-color: var(--whoogle-result-bg) !important;
|
background-color: var(--whoogle-result-bg) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.x54gtf {
|
.x54gtf {
|
||||||
|
@ -80,7 +98,7 @@ input {
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-search {
|
.home-search {
|
||||||
border: 3px solid var(--whoogle-element-bg) !important;
|
border-color: var(--whoogle-element-bg) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-container {
|
.search-container {
|
||||||
|
@ -111,7 +129,7 @@ input {
|
||||||
color: var(--whoogle-contrast-text) !important;
|
color: var(--whoogle-contrast-text) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content, .result-config {
|
||||||
background-color: var(--whoogle-element-bg) !important;
|
background-color: var(--whoogle-element-bg) !important;
|
||||||
color: var(--whoogle-contrast-text) !important;
|
color: var(--whoogle-contrast-text) !important;
|
||||||
}
|
}
|
||||||
|
@ -120,10 +138,14 @@ input {
|
||||||
color: var(--whoogle-contrast-text);
|
color: var(--whoogle-contrast-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
#gh-link {
|
.link {
|
||||||
color: var(--whoogle-element-bg);
|
color: var(--whoogle-element-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.link-color {
|
||||||
|
color: var(--whoogle-result-url) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.autocomplete-items {
|
.autocomplete-items {
|
||||||
border: 1px solid var(--whoogle-element-bg);
|
border: 1px solid var(--whoogle-element-bg);
|
||||||
}
|
}
|
||||||
|
@ -142,3 +164,42 @@ input {
|
||||||
background-color: var(--whoogle-element-bg) !important;
|
background-color: var(--whoogle-element-bg) !important;
|
||||||
color: var(--whoogle-contrast-text) !important;
|
color: var(--whoogle-contrast-text) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
color: var(--whoogle-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
path {
|
||||||
|
fill: var(--whoogle-logo);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-div {
|
||||||
|
background-color: var(--whoogle-result-bg) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-reset {
|
||||||
|
color: var(--whoogle-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-search-bar {
|
||||||
|
background-color: var(--whoogle-result-bg) !important;
|
||||||
|
color: var(--whoogle-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar-desktop {
|
||||||
|
background-color: var(--whoogle-result-bg) !important;
|
||||||
|
color: var(--whoogle-text);
|
||||||
|
border-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ip-text-div, .update_available, .cb_label, .cb {
|
||||||
|
color: var(--whoogle-secondary-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb:focus {
|
||||||
|
color: var(--whoogle-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-header, .mobile-header {
|
||||||
|
background-color: var(--whoogle-result-bg) !important;
|
||||||
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ a {
|
||||||
|
|
||||||
@media (max-width: 1000px) {
|
@media (max-width: 1000px) {
|
||||||
svg {
|
svg {
|
||||||
margin-top: .7em;
|
margin-top: .3em;
|
||||||
|
height: 70%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,11 @@ body {
|
||||||
max-height: 500px;
|
max-height: 500px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.home-search {
|
||||||
|
background: transparent !important;
|
||||||
|
border: 3px solid;
|
||||||
|
}
|
||||||
|
|
||||||
.search-container {
|
.search-container {
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
width: 80%;
|
width: 80%;
|
||||||
|
@ -56,6 +61,15 @@ body {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.config-options {
|
||||||
|
max-height: 370px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-buttons {
|
||||||
|
max-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
.config-div {
|
.config-div {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
@ -130,6 +144,7 @@ footer {
|
||||||
|
|
||||||
.whoogle-svg {
|
.whoogle-svg {
|
||||||
width: 80%;
|
width: 80%;
|
||||||
|
height: initial;
|
||||||
display: block;
|
display: block;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
|
@ -162,3 +177,14 @@ details summary {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile styles */
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-bar {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,12 @@
|
||||||
|
body {
|
||||||
|
display: block !important;
|
||||||
|
margin: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vvjwJb {
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
.autocomplete {
|
.autocomplete {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -22,6 +31,41 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
details summary {
|
details summary {
|
||||||
padding: 10px;
|
margin-bottom: 20px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
details summary span {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
#lingva-iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 650px;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ip-address-div {
|
||||||
|
padding-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ip-text-div {
|
||||||
|
padding-top: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 801px) {
|
||||||
|
body {
|
||||||
|
min-width: 736px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 801px) {
|
||||||
|
details summary {
|
||||||
|
margin-bottom: 10px !important
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
/* LIGHT THEME COLORS */
|
/* LIGHT THEME COLORS */
|
||||||
--whoogle-logo: #685e79;
|
--whoogle-logo: #685e79;
|
||||||
--whoogle-page-bg: #ffffff;
|
--whoogle-page-bg: #ffffff;
|
||||||
--whoogle-element-bg: #685e79;
|
--whoogle-element-bg: #4285f4;
|
||||||
--whoogle-text: #000000;
|
--whoogle-text: #000000;
|
||||||
--whoogle-contrast-text: #ffffff;
|
--whoogle-contrast-text: #ffffff;
|
||||||
--whoogle-secondary-text: #70757a;
|
--whoogle-secondary-text: #70757a;
|
||||||
|
@ -11,18 +11,44 @@
|
||||||
--whoogle-result-title: #1967d2;
|
--whoogle-result-title: #1967d2;
|
||||||
--whoogle-result-url: #0d652d;
|
--whoogle-result-url: #0d652d;
|
||||||
--whoogle-result-visited: #4b11a8;
|
--whoogle-result-visited: #4b11a8;
|
||||||
--whoogle-divider: #dfe1e5;
|
|
||||||
|
|
||||||
/* DARK THEME COLORS */
|
/* DARK THEME COLORS */
|
||||||
--whoogle-dark-logo: #888888;
|
--whoogle-dark-logo: #685e79;
|
||||||
--whoogle-dark-page-bg: #080808;
|
--whoogle-dark-page-bg: #101020;
|
||||||
--whoogle-dark-element-bg: #111111;
|
--whoogle-dark-element-bg: #4285f4;
|
||||||
--whoogle-dark-text: #dddddd;
|
--whoogle-dark-text: #ffffff;
|
||||||
--whoogle-dark-contrast-text: #aaaaaa;
|
--whoogle-dark-contrast-text: #ffffff;
|
||||||
--whoogle-dark-secondary-text: #8a8b8c;
|
--whoogle-dark-secondary-text: #bbbbbb;
|
||||||
--whoogle-dark-result-bg: #111111;
|
--whoogle-dark-result-bg: #212131;
|
||||||
--whoogle-dark-result-title: #dddddd;
|
--whoogle-dark-result-title: #64a7f6;
|
||||||
--whoogle-dark-result-url: #eceff4;
|
--whoogle-dark-result-url: #34a853;
|
||||||
--whoogle-dark-result-visited: #959595;
|
--whoogle-dark-result-visited: #bbbbff;
|
||||||
--whoogle-dark-divider: #111111;
|
}
|
||||||
|
|
||||||
|
#whoogle-w {
|
||||||
|
fill: #4285f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
#whoogle-h {
|
||||||
|
fill: #ea4335;
|
||||||
|
}
|
||||||
|
|
||||||
|
#whoogle-o-1 {
|
||||||
|
fill: #fbbc05;
|
||||||
|
}
|
||||||
|
|
||||||
|
#whoogle-o-2 {
|
||||||
|
fill: #4285f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
#whoogle-g {
|
||||||
|
fill: #34a853;
|
||||||
|
}
|
||||||
|
|
||||||
|
#whoogle-l {
|
||||||
|
fill: #ea4335;
|
||||||
|
}
|
||||||
|
|
||||||
|
#whoogle-e {
|
||||||
|
fill: #fbbc05;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
const handleUserInput = searchBar => {
|
let searchInput;
|
||||||
|
let currentFocus;
|
||||||
|
let originalSearch;
|
||||||
|
let autocompleteResults;
|
||||||
|
|
||||||
|
const handleUserInput = () => {
|
||||||
let xhrRequest = new XMLHttpRequest();
|
let xhrRequest = new XMLHttpRequest();
|
||||||
xhrRequest.open("POST", "autocomplete");
|
xhrRequest.open("POST", "autocomplete");
|
||||||
xhrRequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
|
xhrRequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
|
||||||
|
@ -9,118 +14,119 @@ const handleUserInput = searchBar => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fill autocomplete with fetched results
|
// Fill autocomplete with fetched results
|
||||||
let autocompleteResults = JSON.parse(xhrRequest.responseText);
|
autocompleteResults = JSON.parse(xhrRequest.responseText)[1];
|
||||||
autocomplete(searchBar, autocompleteResults[1]);
|
updateAutocompleteList();
|
||||||
};
|
};
|
||||||
|
|
||||||
xhrRequest.send('q=' + searchBar.value);
|
xhrRequest.send('q=' + searchInput.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const autocomplete = (searchInput, autocompleteResults) => {
|
const closeAllLists = el => {
|
||||||
let currentFocus;
|
// Close all autocomplete suggestions
|
||||||
let originalSearch;
|
let suggestions = document.getElementsByClassName("autocomplete-items");
|
||||||
|
for (let i = 0; i < suggestions.length; i++) {
|
||||||
searchInput.addEventListener("input", function () {
|
if (el !== suggestions[i] && el !== searchInput) {
|
||||||
let autocompleteList, autocompleteItem, i, val = this.value;
|
suggestions[i].parentNode.removeChild(suggestions[i]);
|
||||||
closeAllLists();
|
|
||||||
|
|
||||||
if (!val || !autocompleteResults) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
currentFocus = -1;
|
const removeActive = suggestion => {
|
||||||
autocompleteList = document.createElement("div");
|
// Remove "autocomplete-active" class from previously active suggestion
|
||||||
autocompleteList.setAttribute("id", this.id + "-autocomplete-list");
|
for (let i = 0; i < suggestion.length; i++) {
|
||||||
autocompleteList.setAttribute("class", "autocomplete-items");
|
suggestion[i].classList.remove("autocomplete-active");
|
||||||
this.parentNode.appendChild(autocompleteList);
|
}
|
||||||
|
};
|
||||||
|
|
||||||
for (i = 0; i < autocompleteResults.length; i++) {
|
const addActive = (suggestion) => {
|
||||||
if (autocompleteResults[i].substr(0, val.length).toUpperCase() === val.toUpperCase()) {
|
// Handle navigation outside of suggestion list
|
||||||
autocompleteItem = document.createElement("div");
|
if (!suggestion || !suggestion[currentFocus]) {
|
||||||
autocompleteItem.innerHTML = "<strong>" + autocompleteResults[i].substr(0, val.length) + "</strong>";
|
if (currentFocus >= suggestion.length) {
|
||||||
autocompleteItem.innerHTML += autocompleteResults[i].substr(val.length);
|
// Move selection back to the beginning
|
||||||
autocompleteItem.innerHTML += "<input type=\"hidden\" value=\"" + autocompleteResults[i] + "\">";
|
currentFocus = 0;
|
||||||
autocompleteItem.addEventListener("click", function () {
|
} else if (currentFocus < 0) {
|
||||||
searchInput.value = this.getElementsByTagName("input")[0].value;
|
// Retrieve original search and remove active suggestion selection
|
||||||
closeAllLists();
|
currentFocus = -1;
|
||||||
document.getElementById("search-form").submit();
|
searchInput.value = originalSearch;
|
||||||
});
|
removeActive(suggestion);
|
||||||
autocompleteList.appendChild(autocompleteItem);
|
return;
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
searchInput.addEventListener("keydown", function (e) {
|
|
||||||
let suggestion = document.getElementById(this.id + "-autocomplete-list");
|
|
||||||
if (suggestion) suggestion = suggestion.getElementsByTagName("div");
|
|
||||||
if (e.keyCode === 40) { // down
|
|
||||||
e.preventDefault();
|
|
||||||
currentFocus++;
|
|
||||||
addActive(suggestion);
|
|
||||||
} else if (e.keyCode === 38) { //up
|
|
||||||
e.preventDefault();
|
|
||||||
currentFocus--;
|
|
||||||
addActive(suggestion);
|
|
||||||
} else if (e.keyCode === 13) { // enter
|
|
||||||
e.preventDefault();
|
|
||||||
if (currentFocus > -1) {
|
|
||||||
if (suggestion) suggestion[currentFocus].click();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
originalSearch = document.getElementById("search-bar").value;
|
return;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
const addActive = suggestion => {
|
removeActive(suggestion);
|
||||||
let searchBar = document.getElementById("search-bar");
|
suggestion[currentFocus].classList.add("autocomplete-active");
|
||||||
|
|
||||||
// Handle navigation outside of suggestion list
|
// Autofill search bar with suggestion content (minus the "bang name" if using a bang operator)
|
||||||
if (!suggestion || !suggestion[currentFocus]) {
|
let searchContent = suggestion[currentFocus].textContent;
|
||||||
if (currentFocus >= suggestion.length) {
|
if (searchContent.indexOf('(') > 0) {
|
||||||
// Move selection back to the beginning
|
searchInput.value = searchContent.substring(0, searchContent.indexOf('('));
|
||||||
currentFocus = 0;
|
} else {
|
||||||
} else if (currentFocus < 0) {
|
searchInput.value = searchContent;
|
||||||
// Retrieve original search and remove active suggestion selection
|
}
|
||||||
currentFocus = -1;
|
|
||||||
searchBar.value = originalSearch;
|
searchInput.focus();
|
||||||
removeActive(suggestion);
|
};
|
||||||
return;
|
|
||||||
} else {
|
const autocompleteInput = (e) => {
|
||||||
return;
|
// Handle navigation between autocomplete suggestions
|
||||||
}
|
let suggestion = document.getElementById(this.id + "-autocomplete-list");
|
||||||
|
if (suggestion) suggestion = suggestion.getElementsByTagName("div");
|
||||||
|
if (e.keyCode === 40) { // down
|
||||||
|
e.preventDefault();
|
||||||
|
currentFocus++;
|
||||||
|
addActive(suggestion);
|
||||||
|
} else if (e.keyCode === 38) { //up
|
||||||
|
e.preventDefault();
|
||||||
|
currentFocus--;
|
||||||
|
addActive(suggestion);
|
||||||
|
} else if (e.keyCode === 13) { // enter
|
||||||
|
e.preventDefault();
|
||||||
|
if (currentFocus > -1) {
|
||||||
|
if (suggestion) suggestion[currentFocus].click();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
originalSearch = searchInput.value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
removeActive(suggestion);
|
const updateAutocompleteList = () => {
|
||||||
suggestion[currentFocus].classList.add("autocomplete-active");
|
let autocompleteList, autocompleteItem, i;
|
||||||
|
let val = originalSearch;
|
||||||
|
closeAllLists();
|
||||||
|
|
||||||
// Autofill search bar with suggestion content (minus the "bang name" if using a bang operator)
|
if (!val || !autocompleteResults) {
|
||||||
let searchContent = suggestion[currentFocus].textContent;
|
return false;
|
||||||
if (searchContent.indexOf('(') > 0) {
|
}
|
||||||
searchBar.value = searchContent.substring(0, searchContent.indexOf('('));
|
|
||||||
} else {
|
currentFocus = -1;
|
||||||
searchBar.value = searchContent;
|
autocompleteList = document.createElement("div");
|
||||||
|
autocompleteList.setAttribute("id", this.id + "-autocomplete-list");
|
||||||
|
autocompleteList.setAttribute("class", "autocomplete-items");
|
||||||
|
searchInput.parentNode.appendChild(autocompleteList);
|
||||||
|
|
||||||
|
for (i = 0; i < autocompleteResults.length; i++) {
|
||||||
|
if (autocompleteResults[i].substr(0, val.length).toUpperCase() === val.toUpperCase()) {
|
||||||
|
autocompleteItem = document.createElement("div");
|
||||||
|
autocompleteItem.innerHTML = "<strong>" + autocompleteResults[i].substr(0, val.length) + "</strong>";
|
||||||
|
autocompleteItem.innerHTML += autocompleteResults[i].substr(val.length);
|
||||||
|
autocompleteItem.innerHTML += "<input type=\"hidden\" value=\"" + autocompleteResults[i] + "\">";
|
||||||
|
autocompleteItem.addEventListener("click", function () {
|
||||||
|
searchInput.value = this.getElementsByTagName("input")[0].value;
|
||||||
|
closeAllLists();
|
||||||
|
document.getElementById("search-form").submit();
|
||||||
|
});
|
||||||
|
autocompleteList.appendChild(autocompleteItem);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
searchBar.focus();
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
};
|
searchInput = document.getElementById("search-bar");
|
||||||
|
searchInput.addEventListener("keydown", (event) => autocompleteInput(event));
|
||||||
|
|
||||||
const removeActive = suggestion => {
|
|
||||||
for (let i = 0; i < suggestion.length; i++) {
|
|
||||||
suggestion[i].classList.remove("autocomplete-active");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeAllLists = el => {
|
|
||||||
let suggestions = document.getElementsByClassName("autocomplete-items");
|
|
||||||
for (let i = 0; i < suggestions.length; i++) {
|
|
||||||
if (el !== suggestions[i] && el !== searchInput) {
|
|
||||||
suggestions[i].parentNode.removeChild(suggestions[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Close lists and search when user selects a suggestion
|
|
||||||
document.addEventListener("click", function (e) {
|
document.addEventListener("click", function (e) {
|
||||||
closeAllLists(e.target);
|
closeAllLists(e.target);
|
||||||
});
|
});
|
||||||
};
|
});
|
|
@ -2,6 +2,8 @@ const setupSearchLayout = () => {
|
||||||
// Setup search field
|
// Setup search field
|
||||||
const searchBar = document.getElementById("search-bar");
|
const searchBar = document.getElementById("search-bar");
|
||||||
const searchBtn = document.getElementById("search-submit");
|
const searchBtn = document.getElementById("search-submit");
|
||||||
|
const arrowKeys = [37, 38, 39, 40];
|
||||||
|
let searchValue = searchBar.value;
|
||||||
|
|
||||||
// Automatically focus on search field
|
// Automatically focus on search field
|
||||||
searchBar.focus();
|
searchBar.focus();
|
||||||
|
@ -11,8 +13,9 @@ const setupSearchLayout = () => {
|
||||||
if (event.keyCode === 13) {
|
if (event.keyCode === 13) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
searchBtn.click();
|
searchBtn.click();
|
||||||
} else {
|
} else if (searchBar.value !== searchValue && !arrowKeys.includes(event.keyCode)) {
|
||||||
handleUserInput(searchBar);
|
searchValue = searchBar.value;
|
||||||
|
handleUserInput();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -26,7 +29,7 @@ const setupConfigLayout = () => {
|
||||||
if (content.style.maxHeight) {
|
if (content.style.maxHeight) {
|
||||||
content.style.maxHeight = null;
|
content.style.maxHeight = null;
|
||||||
} else {
|
} else {
|
||||||
content.style.maxHeight = content.scrollHeight + "px";
|
content.style.maxHeight = "400px";
|
||||||
}
|
}
|
||||||
|
|
||||||
content.classList.toggle("open");
|
content.classList.toggle("open");
|
||||||
|
|
9
app/static/js/currency.js
Normal file
9
app/static/js/currency.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
const convert = (n1, n2, conversionFactor) => {
|
||||||
|
// id's for currency input boxes
|
||||||
|
let id1 = "cb" + n1;
|
||||||
|
let id2 = "cb" + n2;
|
||||||
|
// getting the value of the input box that just got filled
|
||||||
|
let inputBox = document.getElementById(id1).value;
|
||||||
|
// updating the other input box after conversion
|
||||||
|
document.getElementById(id2).value = ((inputBox * conversionFactor).toFixed(2));
|
||||||
|
}
|
|
@ -1,11 +1,46 @@
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const advSearchToggle = document.getElementById("adv-search-toggle");
|
||||||
|
const advSearchDiv = document.getElementById("adv-search-div");
|
||||||
const searchBar = document.getElementById("search-bar");
|
const searchBar = document.getElementById("search-bar");
|
||||||
|
const countrySelect = document.getElementById("result-country");
|
||||||
|
const arrowKeys = [37, 38, 39, 40];
|
||||||
|
let searchValue = searchBar.value;
|
||||||
|
|
||||||
searchBar.addEventListener("keyup", function (event) {
|
countrySelect.onchange = () => {
|
||||||
if (event.keyCode !== 13) {
|
let str = window.location.href;
|
||||||
handleUserInput(searchBar);
|
n = str.lastIndexOf("/search");
|
||||||
|
if (n > 0) {
|
||||||
|
str = str.substring(0, n) +
|
||||||
|
`/search?q=${searchBar.value}&country=${countrySelect.value}`;
|
||||||
|
window.location.href = str;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleAdvancedSearch = on => {
|
||||||
|
if (on) {
|
||||||
|
advSearchDiv.style.maxHeight = "70px";
|
||||||
} else {
|
} else {
|
||||||
|
advSearchDiv.style.maxHeight = "0px";
|
||||||
|
}
|
||||||
|
localStorage.advSearchToggled = on;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
toggleAdvancedSearch(JSON.parse(localStorage.advSearchToggled));
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Did not recover advanced search toggle state");
|
||||||
|
}
|
||||||
|
|
||||||
|
advSearchToggle.onclick = () => {
|
||||||
|
toggleAdvancedSearch(advSearchToggle.checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
searchBar.addEventListener("keyup", function(event) {
|
||||||
|
if (event.keyCode === 13) {
|
||||||
document.getElementById("search-form").submit();
|
document.getElementById("search-form").submit();
|
||||||
|
} else if (searchBar.value !== searchValue && !arrowKeys.includes(event.keyCode)) {
|
||||||
|
searchValue = searchBar.value;
|
||||||
|
handleUserInput();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,44 +1,58 @@
|
||||||
(function () {
|
(function () {
|
||||||
let searchBar, results;
|
let searchBar, results;
|
||||||
const keymap = {
|
let shift = false;
|
||||||
ArrowUp: goUp,
|
const keymap = {
|
||||||
ArrowDown: goDown,
|
ArrowUp: goUp,
|
||||||
k: goUp,
|
ArrowDown: goDown,
|
||||||
j: goDown,
|
ShiftTab: goUp,
|
||||||
'/': focusSearch,
|
Tab: goDown,
|
||||||
};
|
k: goUp,
|
||||||
let activeIdx = -1;
|
j: goDown,
|
||||||
|
'/': focusSearch,
|
||||||
|
};
|
||||||
|
let activeIdx = -1;
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
searchBar = document.querySelector('#search-bar');
|
searchBar = document.querySelector('#search-bar');
|
||||||
results = document.querySelectorAll('#main>div>div>div>a');
|
results = document.querySelectorAll('#main>div>div>div>a');
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
if (e.target.tagName === 'INPUT') return true;
|
if (e.key === 'Shift') {
|
||||||
if (typeof keymap[e.key] === 'function') {
|
shift = true;
|
||||||
e.preventDefault();
|
}
|
||||||
keymap[e.key]();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function goUp () {
|
if (e.target.tagName === 'INPUT') return true;
|
||||||
if (activeIdx > 0) focusResult(activeIdx - 1);
|
if (typeof keymap[e.key] === 'function') {
|
||||||
else focusSearch();
|
e.preventDefault();
|
||||||
}
|
|
||||||
|
|
||||||
function goDown () {
|
keymap[`${shift && e.key == 'Tab' ? 'Shift' : ''}${e.key}`]();
|
||||||
if (activeIdx < results.length - 1) focusResult(activeIdx + 1);
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
function focusResult (idx) {
|
document.addEventListener('keyup', (e) => {
|
||||||
activeIdx = idx;
|
if (e.key === 'Shift') {
|
||||||
results[activeIdx].scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
|
shift = false;
|
||||||
results[activeIdx].focus();
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
function focusSearch () {
|
function goUp () {
|
||||||
activeIdx = -1;
|
if (activeIdx > 0) focusResult(activeIdx - 1);
|
||||||
searchBar.focus();
|
else focusSearch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function goDown () {
|
||||||
|
if (activeIdx < results.length - 1) focusResult(activeIdx + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusResult (idx) {
|
||||||
|
activeIdx = idx;
|
||||||
|
results[activeIdx].scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
|
||||||
|
results[activeIdx].focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusSearch () {
|
||||||
|
activeIdx = -1;
|
||||||
|
searchBar.focus();
|
||||||
|
}
|
||||||
}());
|
}());
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
const checkForTracking = () => {
|
const checkForTracking = () => {
|
||||||
const mainDiv = document.getElementById("main");
|
const mainDiv = document.getElementById("main");
|
||||||
const query = document.getElementById("search-bar").value.replace(/\s+/g, '');
|
const searchBar = document.getElementById("search-bar");
|
||||||
|
// some pages (e.g. images) do not have these
|
||||||
|
if (!mainDiv || !searchBar)
|
||||||
|
return;
|
||||||
|
const query = searchBar.value.replace(/\s+/g, '');
|
||||||
|
|
||||||
// Note: regex functions for checking for tracking queries were derived
|
// Note: regex functions for checking for tracking queries were derived
|
||||||
// from here -- https://stackoverflow.com/questions/619977
|
// from here -- https://stackoverflow.com/questions/619977
|
||||||
|
@ -59,11 +63,14 @@ document.addEventListener("DOMContentLoaded", function() {
|
||||||
checkForTracking();
|
checkForTracking();
|
||||||
|
|
||||||
// Clear input if reset button tapped
|
// Clear input if reset button tapped
|
||||||
const search = document.getElementById("search-bar");
|
const searchBar = document.getElementById("search-bar");
|
||||||
const resetBtn = document.getElementById("search-reset");
|
const resetBtn = document.getElementById("search-reset");
|
||||||
|
// some pages (e.g. images) do not have these
|
||||||
|
if (!searchBar || !resetBtn)
|
||||||
|
return;
|
||||||
resetBtn.addEventListener("click", event => {
|
resetBtn.addEventListener("click", event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
search.value = "";
|
searchBar.value = "";
|
||||||
search.focus();
|
searchBar.focus();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,248 +1,247 @@
|
||||||
[
|
[
|
||||||
{"name": "-------", "value": ""},
|
{"name": "-------", "value": ""},
|
||||||
{"name": "Afghanistan", "value": "countryAF"},
|
{"name": "Afghanistan", "value": "AF"},
|
||||||
{"name": "Albania", "value": "countryAL"},
|
{"name": "Albania", "value": "AL"},
|
||||||
{"name": "Algeria", "value": "countryDZ"},
|
{"name": "Algeria", "value": "DZ"},
|
||||||
{"name": "American Samoa", "value": "countryAS"},
|
{"name": "American Samoa", "value": "AS"},
|
||||||
{"name": "Andorra", "value": "countryAD"},
|
{"name": "Andorra", "value": "AD"},
|
||||||
{"name": "Angola", "value": "countryAO"},
|
{"name": "Angola", "value": "AO"},
|
||||||
{"name": "Anguilla", "value": "countryAI"},
|
{"name": "Anguilla", "value": "AI"},
|
||||||
{"name": "Antarctica", "value": "countryAQ"},
|
{"name": "Antarctica", "value": "AQ"},
|
||||||
{"name": "Antigua and Barbuda", "value": "countryAG"},
|
{"name": "Antigua and Barbuda", "value": "AG"},
|
||||||
{"name": "Argentina", "value": "countryAR"},
|
{"name": "Argentina", "value": "AR"},
|
||||||
{"name": "Armenia", "value": "countryAM"},
|
{"name": "Armenia", "value": "AM"},
|
||||||
{"name": "Aruba", "value": "countryAW"},
|
{"name": "Aruba", "value": "AW"},
|
||||||
{"name": "Australia", "value": "countryAU"},
|
{"name": "Australia", "value": "AU"},
|
||||||
{"name": "Austria", "value": "countryAT"},
|
{"name": "Austria", "value": "AT"},
|
||||||
{"name": "Azerbaijan", "value": "countryAZ"},
|
{"name": "Azerbaijan", "value": "AZ"},
|
||||||
{"name": "Bahamas", "value": "countryBS"},
|
{"name": "Bahamas", "value": "BS"},
|
||||||
{"name": "Bahrain", "value": "countryBH"},
|
{"name": "Bahrain", "value": "BH"},
|
||||||
{"name": "Bangladesh", "value": "countryBD"},
|
{"name": "Bangladesh", "value": "BD"},
|
||||||
{"name": "Barbados", "value": "countryBB"},
|
{"name": "Barbados", "value": "BB"},
|
||||||
{"name": "Belarus", "value": "countryBY"},
|
{"name": "Belarus", "value": "BY"},
|
||||||
{"name": "Belgium", "value": "countryBE"},
|
{"name": "Belgium", "value": "BE"},
|
||||||
{"name": "Belize", "value": "countryBZ"},
|
{"name": "Belize", "value": "BZ"},
|
||||||
{"name": "Benin", "value": "countryBJ"},
|
{"name": "Benin", "value": "BJ"},
|
||||||
{"name": "Bermuda", "value": "countryBM"},
|
{"name": "Bermuda", "value": "BM"},
|
||||||
{"name": "Bhutan", "value": "countryBT"},
|
{"name": "Bhutan", "value": "BT"},
|
||||||
{"name": "Bolivia", "value": "countryBO"},
|
{"name": "Bolivia", "value": "BO"},
|
||||||
{"name": "Bosnia and Herzegovina", "value": "countryBA"},
|
{"name": "Bosnia and Herzegovina", "value": "BA"},
|
||||||
{"name": "Botswana", "value": "countryBW"},
|
{"name": "Botswana", "value": "BW"},
|
||||||
{"name": "Bouvet Island", "value": "countryBV"},
|
{"name": "Bouvet Island", "value": "BV"},
|
||||||
{"name": "Brazil", "value": "countryBR"},
|
{"name": "Brazil", "value": "BR"},
|
||||||
{"name": "British Indian Ocean Territory", "value": "countryIO"},
|
{"name": "British Indian Ocean Territory", "value": "IO"},
|
||||||
{"name": "Brunei Darussalam", "value": "countryBN"},
|
{"name": "Brunei Darussalam", "value": "BN"},
|
||||||
{"name": "Bulgaria", "value": "countryBG"},
|
{"name": "Bulgaria", "value": "BG"},
|
||||||
{"name": "Burkina Faso", "value": "countryBF"},
|
{"name": "Burkina Faso", "value": "BF"},
|
||||||
{"name": "Burundi", "value": "countryBI"},
|
{"name": "Burundi", "value": "BI"},
|
||||||
{"name": "Cambodia", "value": "countryKH"},
|
{"name": "Cambodia", "value": "KH"},
|
||||||
{"name": "Cameroon", "value": "countryCM"},
|
{"name": "Cameroon", "value": "CM"},
|
||||||
{"name": "Canada", "value": "countryCA"},
|
{"name": "Canada", "value": "CA"},
|
||||||
{"name": "Cape Verde", "value": "countryCV"},
|
{"name": "Cape Verde", "value": "CV"},
|
||||||
{"name": "Cayman Islands", "value": "countryKY"},
|
{"name": "Cayman Islands", "value": "KY"},
|
||||||
{"name": "Central African Republic", "value": "countryCF"},
|
{"name": "Central African Republic", "value": "CF"},
|
||||||
{"name": "Chad", "value": "countryTD"},
|
{"name": "Chad", "value": "TD"},
|
||||||
{"name": "Chile", "value": "countryCL"},
|
{"name": "Chile", "value": "CL"},
|
||||||
{"name": "China", "value": "countryCN"},
|
{"name": "China", "value": "CN"},
|
||||||
{"name": "Christmas Island", "value": "countryCX"},
|
{"name": "Christmas Island", "value": "CX"},
|
||||||
{"name": "Cocos (Keeling) Islands", "value": "countryCC"},
|
{"name": "Cocos (Keeling) Islands", "value": "CC"},
|
||||||
{"name": "Colombia", "value": "countryCO"},
|
{"name": "Colombia", "value": "CO"},
|
||||||
{"name": "Comoros", "value": "countryKM"},
|
{"name": "Comoros", "value": "KM"},
|
||||||
{"name": "Congo", "value": "countryCG"},
|
{"name": "Congo", "value": "CG"},
|
||||||
{"name": "Congo, Democratic Republic of the", "value": "countryCD"},
|
{"name": "Congo, Democratic Republic of the", "value": "CD"},
|
||||||
{"name": "Cook Islands", "value": "countryCK"},
|
{"name": "Cook Islands", "value": "CK"},
|
||||||
{"name": "Costa Rica", "value": "countryCR"},
|
{"name": "Costa Rica", "value": "CR"},
|
||||||
{"name": "Cote D\"ivoire", "value": "countryCI"},
|
{"name": "Cote D'ivoire", "value": "CI"},
|
||||||
{"name": "Croatia (Hrvatska)", "value": "countryHR"},
|
{"name": "Croatia (Hrvatska)", "value": "HR"},
|
||||||
{"name": "Cuba", "value": "countryCU"},
|
{"name": "Cuba", "value": "CU"},
|
||||||
{"name": "Cyprus", "value": "countryCY"},
|
{"name": "Cyprus", "value": "CY"},
|
||||||
{"name": "Czech Republic", "value": "countryCZ"},
|
{"name": "Czech Republic", "value": "CZ"},
|
||||||
{"name": "Denmark", "value": "countryDK"},
|
{"name": "Denmark", "value": "DK"},
|
||||||
{"name": "Djibouti", "value": "countryDJ"},
|
{"name": "Djibouti", "value": "DJ"},
|
||||||
{"name": "Dominica", "value": "countryDM"},
|
{"name": "Dominica", "value": "DM"},
|
||||||
{"name": "Dominican Republic", "value": "countryDO"},
|
{"name": "Dominican Republic", "value": "DO"},
|
||||||
{"name": "East Timor", "value": "countryTP"},
|
{"name": "East Timor", "value": "TP"},
|
||||||
{"name": "Ecuador", "value": "countryEC"},
|
{"name": "Ecuador", "value": "EC"},
|
||||||
{"name": "Egypt", "value": "countryEG"},
|
{"name": "Egypt", "value": "EG"},
|
||||||
{"name": "El Salvador", "value": "countrySV"},
|
{"name": "El Salvador", "value": "SV"},
|
||||||
{"name": "Equatorial Guinea", "value": "countryGQ"},
|
{"name": "Equatorial Guinea", "value": "GQ"},
|
||||||
{"name": "Eritrea", "value": "countryER"},
|
{"name": "Eritrea", "value": "ER"},
|
||||||
{"name": "Estonia", "value": "countryEE"},
|
{"name": "Estonia", "value": "EE"},
|
||||||
{"name": "Ethiopia", "value": "countryET"},
|
{"name": "Ethiopia", "value": "ET"},
|
||||||
{"name": "European Union", "value": "countryEU"},
|
{"name": "European Union", "value": "EU"},
|
||||||
{"name": "Falkland Islands (Malvinas)", "value": "countryFK"},
|
{"name": "Falkland Islands (Malvinas)", "value": "FK"},
|
||||||
{"name": "Faroe Islands", "value": "countryFO"},
|
{"name": "Faroe Islands", "value": "FO"},
|
||||||
{"name": "Fiji", "value": "countryFJ"},
|
{"name": "Fiji", "value": "FJ"},
|
||||||
{"name": "Finland", "value": "countryFI"},
|
{"name": "Finland", "value": "FI"},
|
||||||
{"name": "France", "value": "countryFR"},
|
{"name": "France", "value": "FR"},
|
||||||
{"name": "France, Metropolitan", "value": "countryFX"},
|
{"name": "France, Metropolitan", "value": "FX"},
|
||||||
{"name": "French Guiana", "value": "countryGF"},
|
{"name": "French Guiana", "value": "GF"},
|
||||||
{"name": "French Polynesia", "value": "countryPF"},
|
{"name": "French Polynesia", "value": "PF"},
|
||||||
{"name": "French Southern Territories", "value": "countryTF"},
|
{"name": "French Southern Territories", "value": "TF"},
|
||||||
{"name": "Gabon", "value": "countryGA"},
|
{"name": "Gabon", "value": "GA"},
|
||||||
{"name": "Gambia", "value": "countryGM"},
|
{"name": "Gambia", "value": "GM"},
|
||||||
{"name": "Georgia", "value": "countryGE"},
|
{"name": "Georgia", "value": "GE"},
|
||||||
{"name": "Germany", "value": "countryDE"},
|
{"name": "Germany", "value": "DE"},
|
||||||
{"name": "Ghana", "value": "countryGH"},
|
{"name": "Ghana", "value": "GH"},
|
||||||
{"name": "Gibraltar", "value": "countryGI"},
|
{"name": "Gibraltar", "value": "GI"},
|
||||||
{"name": "Greece", "value": "countryGR"},
|
{"name": "Greece", "value": "GR"},
|
||||||
{"name": "Greenland", "value": "countryGL"},
|
{"name": "Greenland", "value": "GL"},
|
||||||
{"name": "Grenada", "value": "countryGD"},
|
{"name": "Grenada", "value": "GD"},
|
||||||
{"name": "Guadeloupe", "value": "countryGP"},
|
{"name": "Guadeloupe", "value": "GP"},
|
||||||
{"name": "Guam", "value": "countryGU"},
|
{"name": "Guam", "value": "GU"},
|
||||||
{"name": "Guatemala", "value": "countryGT"},
|
{"name": "Guatemala", "value": "GT"},
|
||||||
{"name": "Guinea", "value": "countryGN"},
|
{"name": "Guinea", "value": "GN"},
|
||||||
{"name": "Guinea-Bissau", "value": "countryGW"},
|
{"name": "Guinea-Bissau", "value": "GW"},
|
||||||
{"name": "Guyana", "value": "countryGY"},
|
{"name": "Guyana", "value": "GY"},
|
||||||
{"name": "Haiti", "value": "countryHT"},
|
{"name": "Haiti", "value": "HT"},
|
||||||
{"name": "Heard Island and Mcdonald Islands", "value": "countryHM"},
|
{"name": "Heard Island and Mcdonald Islands", "value": "HM"},
|
||||||
{"name": "Holy See (Vatican City State)", "value": "countryVA"},
|
{"name": "Holy See (Vatican City State)", "value": "VA"},
|
||||||
{"name": "Honduras", "value": "countryHN"},
|
{"name": "Honduras", "value": "HN"},
|
||||||
{"name": "Hong Kong", "value": "countryHK"},
|
{"name": "Hong Kong", "value": "HK"},
|
||||||
{"name": "Hungary", "value": "countryHU"},
|
{"name": "Hungary", "value": "HU"},
|
||||||
{"name": "Iceland", "value": "countryIS"},
|
{"name": "Iceland", "value": "IS"},
|
||||||
{"name": "India", "value": "countryIN"},
|
{"name": "India", "value": "IN"},
|
||||||
{"name": "Indonesia", "value": "countryID"},
|
{"name": "Indonesia", "value": "ID"},
|
||||||
{"name": "Iran, Islamic Republic of", "value": "countryIR"},
|
{"name": "Iran, Islamic Republic of", "value": "IR"},
|
||||||
{"name": "Iraq", "value": "countryIQ"},
|
{"name": "Iraq", "value": "IQ"},
|
||||||
{"name": "Ireland", "value": "countryIE"},
|
{"name": "Ireland", "value": "IE"},
|
||||||
{"name": "Israel", "value": "countryIL"},
|
{"name": "Israel", "value": "IL"},
|
||||||
{"name": "Italy", "value": "countryIT"},
|
{"name": "Italy", "value": "IT"},
|
||||||
{"name": "Jamaica", "value": "countryJM"},
|
{"name": "Jamaica", "value": "JM"},
|
||||||
{"name": "Japan", "value": "countryJP"},
|
{"name": "Japan", "value": "JP"},
|
||||||
{"name": "Jordan", "value": "countryJO"},
|
{"name": "Jordan", "value": "JO"},
|
||||||
{"name": "Kazakhstan", "value": "countryKZ"},
|
{"name": "Kazakhstan", "value": "KZ"},
|
||||||
{"name": "Kenya", "value": "countryKE"},
|
{"name": "Kenya", "value": "KE"},
|
||||||
{"name": "Kiribati", "value": "countryKI"},
|
{"name": "Kiribati", "value": "KI"},
|
||||||
{"name": "Korea, Democratic People\"s Republic of",
|
{"name": "Korea, Democratic People's Republic of", "value": "KP"},
|
||||||
"value": "countryKP"},
|
{"name": "Korea, Republic of", "value": "KR"},
|
||||||
{"name": "Korea, Republic of", "value": "countryKR"},
|
{"name": "Kuwait", "value": "KW"},
|
||||||
{"name": "Kuwait", "value": "countryKW"},
|
{"name": "Kyrgyzstan", "value": "KG"},
|
||||||
{"name": "Kyrgyzstan", "value": "countryKG"},
|
{"name": "Lao People's Democratic Republic", "value": "LA"},
|
||||||
{"name": "Lao People\"s Democratic Republic", "value": "countryLA"},
|
{"name": "Latvia", "value": "LV"},
|
||||||
{"name": "Latvia", "value": "countryLV"},
|
{"name": "Lebanon", "value": "LB"},
|
||||||
{"name": "Lebanon", "value": "countryLB"},
|
{"name": "Lesotho", "value": "LS"},
|
||||||
{"name": "Lesotho", "value": "countryLS"},
|
{"name": "Liberia", "value": "LR"},
|
||||||
{"name": "Liberia", "value": "countryLR"},
|
{"name": "Libyan Arab Jamahiriya", "value": "LY"},
|
||||||
{"name": "Libyan Arab Jamahiriya", "value": "countryLY"},
|
{"name": "Liechtenstein", "value": "LI"},
|
||||||
{"name": "Liechtenstein", "value": "countryLI"},
|
{"name": "Lithuania", "value": "LT"},
|
||||||
{"name": "Lithuania", "value": "countryLT"},
|
{"name": "Luxembourg", "value": "LU"},
|
||||||
{"name": "Luxembourg", "value": "countryLU"},
|
{"name": "Macao", "value": "MO"},
|
||||||
{"name": "Macao", "value": "countryMO"},
|
|
||||||
{"name": "Macedonia, the Former Yugosalv Republic of",
|
{"name": "Macedonia, the Former Yugosalv Republic of",
|
||||||
"value": "countryMK"},
|
"value": "MK"},
|
||||||
{"name": "Madagascar", "value": "countryMG"},
|
{"name": "Madagascar", "value": "MG"},
|
||||||
{"name": "Malawi", "value": "countryMW"},
|
{"name": "Malawi", "value": "MW"},
|
||||||
{"name": "Malaysia", "value": "countryMY"},
|
{"name": "Malaysia", "value": "MY"},
|
||||||
{"name": "Maldives", "value": "countryMV"},
|
{"name": "Maldives", "value": "MV"},
|
||||||
{"name": "Mali", "value": "countryML"},
|
{"name": "Mali", "value": "ML"},
|
||||||
{"name": "Malta", "value": "countryMT"},
|
{"name": "Malta", "value": "MT"},
|
||||||
{"name": "Marshall Islands", "value": "countryMH"},
|
{"name": "Marshall Islands", "value": "MH"},
|
||||||
{"name": "Martinique", "value": "countryMQ"},
|
{"name": "Martinique", "value": "MQ"},
|
||||||
{"name": "Mauritania", "value": "countryMR"},
|
{"name": "Mauritania", "value": "MR"},
|
||||||
{"name": "Mauritius", "value": "countryMU"},
|
{"name": "Mauritius", "value": "MU"},
|
||||||
{"name": "Mayotte", "value": "countryYT"},
|
{"name": "Mayotte", "value": "YT"},
|
||||||
{"name": "Mexico", "value": "countryMX"},
|
{"name": "Mexico", "value": "MX"},
|
||||||
{"name": "Micronesia, Federated States of", "value": "countryFM"},
|
{"name": "Micronesia, Federated States of", "value": "FM"},
|
||||||
{"name": "Moldova, Republic of", "value": "countryMD"},
|
{"name": "Moldova, Republic of", "value": "MD"},
|
||||||
{"name": "Monaco", "value": "countryMC"},
|
{"name": "Monaco", "value": "MC"},
|
||||||
{"name": "Mongolia", "value": "countryMN"},
|
{"name": "Mongolia", "value": "MN"},
|
||||||
{"name": "Montserrat", "value": "countryMS"},
|
{"name": "Montserrat", "value": "MS"},
|
||||||
{"name": "Morocco", "value": "countryMA"},
|
{"name": "Morocco", "value": "MA"},
|
||||||
{"name": "Mozambique", "value": "countryMZ"},
|
{"name": "Mozambique", "value": "MZ"},
|
||||||
{"name": "Myanmar", "value": "countryMM"},
|
{"name": "Myanmar", "value": "MM"},
|
||||||
{"name": "Namibia", "value": "countryNA"},
|
{"name": "Namibia", "value": "NA"},
|
||||||
{"name": "Nauru", "value": "countryNR"},
|
{"name": "Nauru", "value": "NR"},
|
||||||
{"name": "Nepal", "value": "countryNP"},
|
{"name": "Nepal", "value": "NP"},
|
||||||
{"name": "Netherlands", "value": "countryNL"},
|
{"name": "Netherlands", "value": "NL"},
|
||||||
{"name": "Netherlands Antilles", "value": "countryAN"},
|
{"name": "Netherlands Antilles", "value": "AN"},
|
||||||
{"name": "New Caledonia", "value": "countryNC"},
|
{"name": "New Caledonia", "value": "NC"},
|
||||||
{"name": "New Zealand", "value": "countryNZ"},
|
{"name": "New Zealand", "value": "NZ"},
|
||||||
{"name": "Nicaragua", "value": "countryNI"},
|
{"name": "Nicaragua", "value": "NI"},
|
||||||
{"name": "Niger", "value": "countryNE"},
|
{"name": "Niger", "value": "NE"},
|
||||||
{"name": "Nigeria", "value": "countryNG"},
|
{"name": "Nigeria", "value": "NG"},
|
||||||
{"name": "Niue", "value": "countryNU"},
|
{"name": "Niue", "value": "NU"},
|
||||||
{"name": "Norfolk Island", "value": "countryNF"},
|
{"name": "Norfolk Island", "value": "NF"},
|
||||||
{"name": "Northern Mariana Islands", "value": "countryMP"},
|
{"name": "Northern Mariana Islands", "value": "MP"},
|
||||||
{"name": "Norway", "value": "countryNO"},
|
{"name": "Norway", "value": "NO"},
|
||||||
{"name": "Oman", "value": "countryOM"},
|
{"name": "Oman", "value": "OM"},
|
||||||
{"name": "Pakistan", "value": "countryPK"},
|
{"name": "Pakistan", "value": "PK"},
|
||||||
{"name": "Palau", "value": "countryPW"},
|
{"name": "Palau", "value": "PW"},
|
||||||
{"name": "Palestinian Territory", "value": "countryPS"},
|
{"name": "Palestinian Territory", "value": "PS"},
|
||||||
{"name": "Panama", "value": "countryPA"},
|
{"name": "Panama", "value": "PA"},
|
||||||
{"name": "Papua New Guinea", "value": "countryPG"},
|
{"name": "Papua New Guinea", "value": "PG"},
|
||||||
{"name": "Paraguay", "value": "countryPY"},
|
{"name": "Paraguay", "value": "PY"},
|
||||||
{"name": "Peru", "value": "countryPE"},
|
{"name": "Peru", "value": "PE"},
|
||||||
{"name": "Philippines", "value": "countryPH"},
|
{"name": "Philippines", "value": "PH"},
|
||||||
{"name": "Pitcairn", "value": "countryPN"},
|
{"name": "Pitcairn", "value": "PN"},
|
||||||
{"name": "Poland", "value": "countryPL"},
|
{"name": "Poland", "value": "PL"},
|
||||||
{"name": "Portugal", "value": "countryPT"},
|
{"name": "Portugal", "value": "PT"},
|
||||||
{"name": "Puerto Rico", "value": "countryPR"},
|
{"name": "Puerto Rico", "value": "PR"},
|
||||||
{"name": "Qatar", "value": "countryQA"},
|
{"name": "Qatar", "value": "QA"},
|
||||||
{"name": "Reunion", "value": "countryRE"},
|
{"name": "Reunion", "value": "RE"},
|
||||||
{"name": "Romania", "value": "countryRO"},
|
{"name": "Romania", "value": "RO"},
|
||||||
{"name": "Russian Federation", "value": "countryRU"},
|
{"name": "Russian Federation", "value": "RU"},
|
||||||
{"name": "Rwanda", "value": "countryRW"},
|
{"name": "Rwanda", "value": "RW"},
|
||||||
{"name": "Saint Helena", "value": "countrySH"},
|
{"name": "Saint Helena", "value": "SH"},
|
||||||
{"name": "Saint Kitts and Nevis", "value": "countryKN"},
|
{"name": "Saint Kitts and Nevis", "value": "KN"},
|
||||||
{"name": "Saint Lucia", "value": "countryLC"},
|
{"name": "Saint Lucia", "value": "LC"},
|
||||||
{"name": "Saint Pierre and Miquelon", "value": "countryPM"},
|
{"name": "Saint Pierre and Miquelon", "value": "PM"},
|
||||||
{"name": "Saint Vincent and the Grenadines", "value": "countryVC"},
|
{"name": "Saint Vincent and the Grenadines", "value": "VC"},
|
||||||
{"name": "Samoa", "value": "countryWS"},
|
{"name": "Samoa", "value": "WS"},
|
||||||
{"name": "San Marino", "value": "countrySM"},
|
{"name": "San Marino", "value": "SM"},
|
||||||
{"name": "Sao Tome and Principe", "value": "countryST"},
|
{"name": "Sao Tome and Principe", "value": "ST"},
|
||||||
{"name": "Saudi Arabia", "value": "countrySA"},
|
{"name": "Saudi Arabia", "value": "SA"},
|
||||||
{"name": "Senegal", "value": "countrySN"},
|
{"name": "Senegal", "value": "SN"},
|
||||||
{"name": "Serbia and Montenegro", "value": "countryCS"},
|
{"name": "Serbia and Montenegro", "value": "CS"},
|
||||||
{"name": "Seychelles", "value": "countrySC"},
|
{"name": "Seychelles", "value": "SC"},
|
||||||
{"name": "Sierra Leone", "value": "countrySL"},
|
{"name": "Sierra Leone", "value": "SL"},
|
||||||
{"name": "Singapore", "value": "countrySG"},
|
{"name": "Singapore", "value": "SG"},
|
||||||
{"name": "Slovakia", "value": "countrySK"},
|
{"name": "Slovakia", "value": "SK"},
|
||||||
{"name": "Slovenia", "value": "countrySI"},
|
{"name": "Slovenia", "value": "SI"},
|
||||||
{"name": "Solomon Islands", "value": "countrySB"},
|
{"name": "Solomon Islands", "value": "SB"},
|
||||||
{"name": "Somalia", "value": "countrySO"},
|
{"name": "Somalia", "value": "SO"},
|
||||||
{"name": "South Africa", "value": "countryZA"},
|
{"name": "South Africa", "value": "ZA"},
|
||||||
{"name": "South Georgia and the South Sandwich Islands",
|
{"name": "South Georgia and the South Sandwich Islands",
|
||||||
"value": "countryGS"},
|
"value": "GS"},
|
||||||
{"name": "Spain", "value": "countryES"},
|
{"name": "Spain", "value": "ES"},
|
||||||
{"name": "Sri Lanka", "value": "countryLK"},
|
{"name": "Sri Lanka", "value": "LK"},
|
||||||
{"name": "Sudan", "value": "countrySD"},
|
{"name": "Sudan", "value": "SD"},
|
||||||
{"name": "Suriname", "value": "countrySR"},
|
{"name": "Suriname", "value": "SR"},
|
||||||
{"name": "Svalbard and Jan Mayen", "value": "countrySJ"},
|
{"name": "Svalbard and Jan Mayen", "value": "SJ"},
|
||||||
{"name": "Swaziland", "value": "countrySZ"},
|
{"name": "Swaziland", "value": "SZ"},
|
||||||
{"name": "Sweden", "value": "countrySE"},
|
{"name": "Sweden", "value": "SE"},
|
||||||
{"name": "Switzerland", "value": "countryCH"},
|
{"name": "Switzerland", "value": "CH"},
|
||||||
{"name": "Syrian Arab Republic", "value": "countrySY"},
|
{"name": "Syrian Arab Republic", "value": "SY"},
|
||||||
{"name": "Taiwan, Province of China", "value": "countryTW"},
|
{"name": "Taiwan", "value": "TW"},
|
||||||
{"name": "Tajikistan", "value": "countryTJ"},
|
{"name": "Tajikistan", "value": "TJ"},
|
||||||
{"name": "Tanzania, United Republic of", "value": "countryTZ"},
|
{"name": "Tanzania, United Republic of", "value": "TZ"},
|
||||||
{"name": "Thailand", "value": "countryTH"},
|
{"name": "Thailand", "value": "TH"},
|
||||||
{"name": "Togo", "value": "countryTG"},
|
{"name": "Togo", "value": "TG"},
|
||||||
{"name": "Tokelau", "value": "countryTK"},
|
{"name": "Tokelau", "value": "TK"},
|
||||||
{"name": "Tonga", "value": "countryTO"},
|
{"name": "Tonga", "value": "TO"},
|
||||||
{"name": "Trinidad and Tobago", "value": "countryTT"},
|
{"name": "Trinidad and Tobago", "value": "TT"},
|
||||||
{"name": "Tunisia", "value": "countryTN"},
|
{"name": "Tunisia", "value": "TN"},
|
||||||
{"name": "Turkey", "value": "countryTR"},
|
{"name": "Turkey", "value": "TR"},
|
||||||
{"name": "Turkmenistan", "value": "countryTM"},
|
{"name": "Turkmenistan", "value": "TM"},
|
||||||
{"name": "Turks and Caicos Islands", "value": "countryTC"},
|
{"name": "Turks and Caicos Islands", "value": "TC"},
|
||||||
{"name": "Tuvalu", "value": "countryTV"},
|
{"name": "Tuvalu", "value": "TV"},
|
||||||
{"name": "Uganda", "value": "countryUG"},
|
{"name": "Uganda", "value": "UG"},
|
||||||
{"name": "Ukraine", "value": "countryUA"},
|
{"name": "Ukraine", "value": "UA"},
|
||||||
{"name": "United Arab Emirates", "value": "countryAE"},
|
{"name": "United Arab Emirates", "value": "AE"},
|
||||||
{"name": "United Kingdom", "value": "countryUK"},
|
{"name": "United Kingdom", "value": "UK"},
|
||||||
{"name": "United States", "value": "countryUS"},
|
{"name": "United States", "value": "US"},
|
||||||
{"name": "United States Minor Outlying Islands", "value": "countryUM"},
|
{"name": "United States Minor Outlying Islands", "value": "UM"},
|
||||||
{"name": "Uruguay", "value": "countryUY"},
|
{"name": "Uruguay", "value": "UY"},
|
||||||
{"name": "Uzbekistan", "value": "countryUZ"},
|
{"name": "Uzbekistan", "value": "UZ"},
|
||||||
{"name": "Vanuatu", "value": "countryVU"},
|
{"name": "Vanuatu", "value": "VU"},
|
||||||
{"name": "Venezuela", "value": "countryVE"},
|
{"name": "Venezuela", "value": "VE"},
|
||||||
{"name": "Vietnam", "value": "countryVN"},
|
{"name": "Vietnam", "value": "VN"},
|
||||||
{"name": "Virgin Islands, British", "value": "countryVG"},
|
{"name": "Virgin Islands, British", "value": "VG"},
|
||||||
{"name": "Virgin Islands, U.S.", "value": "countryVI"},
|
{"name": "Virgin Islands, U.S.", "value": "VI"},
|
||||||
{"name": "Wallis and Futuna", "value": "countryWF"},
|
{"name": "Wallis and Futuna", "value": "WF"},
|
||||||
{"name": "Western Sahara", "value": "countryEH"},
|
{"name": "Western Sahara", "value": "EH"},
|
||||||
{"name": "Yemen", "value": "countryYE"},
|
{"name": "Yemen", "value": "YE"},
|
||||||
{"name": "Yugoslavia", "value": "countryYU"},
|
{"name": "Yugoslavia", "value": "YU"},
|
||||||
{"name": "Zambia", "value": "countryZM"},
|
{"name": "Zambia", "value": "ZM"},
|
||||||
{"name": "Zimbabwe", "value": "countryZW"}
|
{"name": "Zimbabwe", "value": "ZW"}
|
||||||
]
|
]
|
||||||
|
|
32
app/static/settings/header_tabs.json
Normal file
32
app/static/settings/header_tabs.json
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"all": {
|
||||||
|
"tbm": null,
|
||||||
|
"href": "search?q={query}",
|
||||||
|
"name": "All",
|
||||||
|
"selected": true
|
||||||
|
},
|
||||||
|
"images": {
|
||||||
|
"tbm": "isch",
|
||||||
|
"href": "search?q={query}",
|
||||||
|
"name": "Images",
|
||||||
|
"selected": false
|
||||||
|
},
|
||||||
|
"maps": {
|
||||||
|
"tbm": null,
|
||||||
|
"href": "https://maps.google.com/maps?q={query}",
|
||||||
|
"name": "Maps",
|
||||||
|
"selected": false
|
||||||
|
},
|
||||||
|
"videos": {
|
||||||
|
"tbm": "vid",
|
||||||
|
"href": "search?q={query}",
|
||||||
|
"name": "Videos",
|
||||||
|
"selected": false
|
||||||
|
},
|
||||||
|
"news": {
|
||||||
|
"tbm": "nws",
|
||||||
|
"href": "search?q={query}",
|
||||||
|
"name": "News",
|
||||||
|
"selected": false
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,7 +8,7 @@
|
||||||
{"name": "Bulgarian (български)", "value": "lang_bg"},
|
{"name": "Bulgarian (български)", "value": "lang_bg"},
|
||||||
{"name": "Catalan (Català)", "value": "lang_ca"},
|
{"name": "Catalan (Català)", "value": "lang_ca"},
|
||||||
{"name": "Chinese, Simplified (简体中文)", "value": "lang_zh-CN"},
|
{"name": "Chinese, Simplified (简体中文)", "value": "lang_zh-CN"},
|
||||||
{"name": "Chinese, Traditional (繁体中文)", "value": "lang_zh-TW"},
|
{"name": "Chinese, Traditional (正體中文)", "value": "lang_zh-TW"},
|
||||||
{"name": "Croatian (Hrvatski)", "value": "lang_hr"},
|
{"name": "Croatian (Hrvatski)", "value": "lang_hr"},
|
||||||
{"name": "Czech (čeština)", "value": "lang_cs"},
|
{"name": "Czech (čeština)", "value": "lang_cs"},
|
||||||
{"name": "Danish (Dansk)", "value": "lang_da"},
|
{"name": "Danish (Dansk)", "value": "lang_da"},
|
||||||
|
@ -28,15 +28,17 @@
|
||||||
{"name": "Italian (Italiano)", "value": "lang_it"},
|
{"name": "Italian (Italiano)", "value": "lang_it"},
|
||||||
{"name": "Japanese (日本語)", "value": "lang_ja"},
|
{"name": "Japanese (日本語)", "value": "lang_ja"},
|
||||||
{"name": "Korean (한국어)", "value": "lang_ko"},
|
{"name": "Korean (한국어)", "value": "lang_ko"},
|
||||||
|
{"name": "Kurdish (Kurdî)", "value": "lang_ku"},
|
||||||
{"name": "Latvian (Latvietis)", "value": "lang_lv"},
|
{"name": "Latvian (Latvietis)", "value": "lang_lv"},
|
||||||
{"name": "Lithuanian (Lietuvis)", "value": "lang_lt"},
|
{"name": "Lithuanian (Lietuvis)", "value": "lang_lt"},
|
||||||
{"name": "Norwegian (Norwegian)", "value": "lang_no"},
|
{"name": "Norwegian (Norwegian)", "value": "lang_no"},
|
||||||
{"name": "Persian (فارسی)", "value": "lang_fa"},
|
{"name": "Persian (فارسی)", "value": "lang_fa"},
|
||||||
{"name": "Polish (Polskie)", "value": "lang_pl"},
|
{"name": "Polish (Polskie)", "value": "lang_pl"},
|
||||||
{"name": "Portugese (Português)", "value": "lang_pt"},
|
{"name": "Portuguese (Português)", "value": "lang_pt"},
|
||||||
{"name": "Romanian (Română)", "value": "lang_ro"},
|
{"name": "Romanian (Română)", "value": "lang_ro"},
|
||||||
{"name": "Russian (русский)", "value": "lang_ru"},
|
{"name": "Russian (русский)", "value": "lang_ru"},
|
||||||
{"name": "Serbian (Српски)", "value": "lang_sr"},
|
{"name": "Serbian (Српски)", "value": "lang_sr"},
|
||||||
|
{"name": "Sinhala (සිංහල)", "value": "lang_si"},
|
||||||
{"name": "Slovak (Slovák)", "value": "lang_sk"},
|
{"name": "Slovak (Slovák)", "value": "lang_sk"},
|
||||||
{"name": "Slovenian (Slovenščina)", "value": "lang_sl"},
|
{"name": "Slovenian (Slovenščina)", "value": "lang_sl"},
|
||||||
{"name": "Spanish (Español)", "value": "lang_es"},
|
{"name": "Spanish (Español)", "value": "lang_es"},
|
||||||
|
@ -44,6 +46,8 @@
|
||||||
{"name": "Swedish (Svenska)", "value": "lang_sv"},
|
{"name": "Swedish (Svenska)", "value": "lang_sv"},
|
||||||
{"name": "Thai (ไทย)", "value": "lang_th"},
|
{"name": "Thai (ไทย)", "value": "lang_th"},
|
||||||
{"name": "Turkish (Türk)", "value": "lang_tr"},
|
{"name": "Turkish (Türk)", "value": "lang_tr"},
|
||||||
{"name": "Ukranian (Український)", "value": "lang_uk"},
|
{"name": "Ukrainian (Український)", "value": "lang_uk"},
|
||||||
{"name": "Vietnamese (Tiếng Việt)", "value": "lang_vi"}
|
{"name": "Vietnamese (Tiếng Việt)", "value": "lang_vi"},
|
||||||
|
{"name": "Xhosa (isiXhosa)", "value": "lang_xh"},
|
||||||
|
{"name": "Zulu (isiZulu)", "value": "lang_zu"}
|
||||||
]
|
]
|
||||||
|
|
5
app/static/settings/themes.json
Normal file
5
app/static/settings/themes.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
[
|
||||||
|
"light",
|
||||||
|
"dark",
|
||||||
|
"system"
|
||||||
|
]
|
File diff suppressed because it is too large
Load Diff
|
@ -1,29 +1,48 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<link rel="shortcut icon" href="static/img/favicon.ico" type="image/x-icon">
|
<link rel="shortcut icon" href="static/img/favicon.ico" type="image/x-icon">
|
||||||
<link rel="icon" href="static/img/favicon.ico" type="image/x-icon">
|
<link rel="icon" href="static/img/favicon.ico" type="image/x-icon">
|
||||||
|
{% if not search_type %}
|
||||||
<link rel="search" href="opensearch.xml" type="application/opensearchdescription+xml" title="Whoogle Search">
|
<link rel="search" href="opensearch.xml" type="application/opensearchdescription+xml" title="Whoogle Search">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
{% else %}
|
||||||
<meta name="referrer" content="no-referrer">
|
<link rel="search" href="opensearch.xml?tbm={{ search_type }}" type="application/opensearchdescription+xml" title="Whoogle Search ({{ search_name }})">
|
||||||
<link rel="stylesheet" href="static/css/input.css">
|
{% endif %}
|
||||||
<link rel="stylesheet" href="static/css/search.css">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link rel="stylesheet" href="static/css/variables.css">
|
<meta name="referrer" content="no-referrer">
|
||||||
<link rel="stylesheet" href="static/css/header.css">
|
<link rel="stylesheet" href="{{ cb_url('logo.css') }}">
|
||||||
<link rel="stylesheet" href="static/css/{{ 'dark' if config.dark else 'light' }}-theme.css"/>
|
<link rel="stylesheet" href="{{ cb_url('input.css') }}">
|
||||||
<style>{{ config.style }}</style>
|
<link rel="stylesheet" href="{{ cb_url('search.css') }}">
|
||||||
<title>{{ clean_query(query) }} - Whoogle Search</title>
|
<link rel="stylesheet" href="{{ cb_url('header.css') }}">
|
||||||
</head>
|
{% if config.theme %}
|
||||||
<body>
|
{% if config.theme == 'system' %}
|
||||||
{{ search_header|safe }}
|
<style>
|
||||||
{{ response|safe }}
|
@import "{{ cb_url('light-theme.css') }}" screen;
|
||||||
</body>
|
@import "{{ cb_url('dark-theme.css') }}" screen and (prefers-color-scheme: dark);
|
||||||
<footer>
|
</style>
|
||||||
<p style="color: {{ 'var(--whoogle-dark-text)' if config.dark else 'var(--whoogle-text)' }};">
|
{% else %}
|
||||||
Whoogle Search v{{ version_number }} ||
|
<link rel="stylesheet" href="{{ cb_url(config.theme + '-theme.css') }}"/>
|
||||||
<a id="gh-link" href="https://github.com/benbusby/whoogle-search">{{ translation['github-link'] }}</a>
|
{% endif %}
|
||||||
</p>
|
{% else %}
|
||||||
</footer>
|
<link rel="stylesheet" href="{{ cb_url(('dark' if config.dark else 'light') + '-theme.css') }}"/>
|
||||||
<script src="static/js/autocomplete.js"></script>
|
{% endif %}
|
||||||
<script src="static/js/utils.js"></script>
|
<style>{{ config.style }}</style>
|
||||||
<script src="static/js/keyboard.js"></script>
|
<title>{{ clean_query(query) }} - Whoogle Search</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{{ search_header|safe }}
|
||||||
|
{% if is_translation %}
|
||||||
|
<iframe
|
||||||
|
id="lingva-iframe"
|
||||||
|
src="{{ lingva_url }}/auto/{{ translate_to }}/{{ translate_str }}">
|
||||||
|
</iframe>
|
||||||
|
{% endif %}
|
||||||
|
{{ response|safe }}
|
||||||
|
</body>
|
||||||
|
{% include 'footer.html' %}
|
||||||
|
{% if autocomplete_enabled == '1' %}
|
||||||
|
<script src="{{ cb_url('autocomplete.js') }}"></script>
|
||||||
|
{% endif %}
|
||||||
|
<script src="{{ cb_url('utils.js') }}"></script>
|
||||||
|
<script src="{{ cb_url('keyboard.js') }}"></script>
|
||||||
|
<script src="{{ cb_url('currency.js') }}"></script>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,6 +1,40 @@
|
||||||
<h1>Error</h1>
|
{% if config.theme %}
|
||||||
<hr>
|
{% if config.theme == 'system' %}
|
||||||
<p>
|
<style>
|
||||||
Error: "{{ error_message|safe }}"
|
@import "{{ cb_url('light-theme.css') }}" screen;
|
||||||
</p>
|
@import "{{ cb_url('dark-theme.css') }}" screen and (prefers-color-scheme: dark);
|
||||||
<a href="/">Return Home</a>
|
</style>
|
||||||
|
{% else %}
|
||||||
|
<link rel="stylesheet" href="{{ cb_url(config.theme + '-theme.css') }}"/>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<link rel="stylesheet" href="{{ cb_url(('dark' if config.dark else 'light') + '-theme.css') }}"/>
|
||||||
|
{% endif %}
|
||||||
|
<link rel="stylesheet" href="{{ cb_url('main.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ cb_url('error.css') }}">
|
||||||
|
<style>{{ config.style }}</style>
|
||||||
|
<div>
|
||||||
|
<h1>Error</h1>
|
||||||
|
<p>
|
||||||
|
{{ error_message }}
|
||||||
|
</p>
|
||||||
|
<hr>
|
||||||
|
<p>
|
||||||
|
{% if blocked is defined %}
|
||||||
|
<h4><a class="link" href="https://farside.link">{{ translation['continue-search'] }}</a></h4>
|
||||||
|
Whoogle:
|
||||||
|
<br>
|
||||||
|
<a class="link-color" href="{{farside}}/whoogle/search?q={{query}}{{params}}">
|
||||||
|
{{farside}}/whoogle/search?q={{query}}{{params}}
|
||||||
|
</a>
|
||||||
|
<br><br>
|
||||||
|
Searx:
|
||||||
|
<br>
|
||||||
|
<a class="link-color" href="{{farside}}/searx/search?q={{query}}">
|
||||||
|
{{farside}}/searx/search?q={{query}}
|
||||||
|
</a>
|
||||||
|
<hr>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
<a class="link" href="home">Return Home</a>
|
||||||
|
</div>
|
||||||
|
|
9
app/templates/footer.html
Normal file
9
app/templates/footer.html
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<footer>
|
||||||
|
<p class="footer">
|
||||||
|
Whoogle Search v{{ version_number }} ||
|
||||||
|
<a class="link" href="https://github.com/benbusby/whoogle-search">{{ translation['github-link'] }}</a>
|
||||||
|
{% if has_update %}
|
||||||
|
|| <span class="update_available">Update Available 🟢</span>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</footer>
|
|
@ -1,64 +1,94 @@
|
||||||
{% if mobile %}
|
{% if mobile %}
|
||||||
<header>
|
<header>
|
||||||
<div style="background-color: {{ 'var(--whoogle-dark-result-bg)' if config.dark else 'var(--whoogle-result-bg)' }} !important;" class="bz1lBb">
|
<div class="header-div">
|
||||||
<form class="search-form Pg70bf" id="search-form" method="POST">
|
<form class="search-form header"
|
||||||
<a class="logo-link mobile-logo"
|
id="search-form"
|
||||||
href="/"
|
method="{{ 'GET' if config.get_only else 'POST' }}">
|
||||||
style="display:flex; justify-content:center; align-items:center;">
|
<a class="logo-link mobile-logo" href="{{ home_url }}">
|
||||||
<div style="height: 1.75em;">
|
<div id="mobile-header-logo">
|
||||||
{{ logo|safe }}
|
{{ logo|safe }}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<div class="H0PQec" style="width: 100%;">
|
<div class="H0PQec mobile-input-div">
|
||||||
<div class="sbc esbc autocomplete">
|
<div class="autocomplete-mobile esbc autocomplete">
|
||||||
<input
|
{% if config.preferences %}
|
||||||
id="search-bar"
|
<input type="hidden" name="preferences" value="{{ config.preferences }}" />
|
||||||
autocapitalize="none"
|
{% endif %}
|
||||||
autocomplete="off"
|
<input
|
||||||
autocorrect="off"
|
id="search-bar"
|
||||||
spellcheck="false"
|
class="mobile-search-bar"
|
||||||
class="noHIxc"
|
autocapitalize="none"
|
||||||
name="q"
|
autocomplete="off"
|
||||||
style="background-color: {{ 'var(--whoogle-dark-result-bg)' if config.dark else 'var(--whoogle-result-bg)' }} !important;
|
autocorrect="off"
|
||||||
color: {{ 'var(--whoogle-dark-text)' if config.dark else 'var(--whoogle-text)' }};"
|
spellcheck="false"
|
||||||
type="text"
|
class="search-bar-input"
|
||||||
value="{{ clean_query(query) }}">
|
name="q"
|
||||||
<input style="color: {{ 'var(--whoogle-dark-text)' if config.dark else 'var(--whoogle-text)' }}" id="search-reset" type="reset" value="x">
|
type="text"
|
||||||
|
value="{{ clean_query(query) }}"
|
||||||
|
dir="auto">
|
||||||
|
<input id="search-reset" type="reset" value="x">
|
||||||
<input name="tbm" value="{{ search_type }}" style="display: none">
|
<input name="tbm" value="{{ search_type }}" style="display: none">
|
||||||
|
<input name="country" value="{{ config.country }}" style="display: none;">
|
||||||
<input type="submit" style="display: none;">
|
<input type="submit" style="display: none;">
|
||||||
<div class="sc"></div>
|
<div class="sc"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="header-tab-div">
|
||||||
|
<div class="header-tab-div-2">
|
||||||
|
<div class="header-tab-div-3">
|
||||||
|
<div class="mobile-header header-tab">
|
||||||
|
{% for tab_id, tab_content in tabs.items() %}
|
||||||
|
{% if tab_content['selected'] %}
|
||||||
|
<span class="mobile-tab-span">{{ tab_content['name'] }}</span>
|
||||||
|
{% else %}
|
||||||
|
<a class="header-tab-a" href="{{ tab_content['href'] }}">{{ tab_content['name'] }}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
<label for="adv-search-toggle" id="adv-search-label" class="adv-search">⚙</label>
|
||||||
|
<input id="adv-search-toggle" type="checkbox">
|
||||||
|
<div class="header-tab-div-end"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="" id="s">
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{% else %}
|
{% else %}
|
||||||
<header>
|
<header>
|
||||||
<div class="logo-div">
|
<div class="logo-div">
|
||||||
<a class="logo-link" href="/">
|
<a class="logo-link" href="{{ home_url }}">
|
||||||
<div style="height: 1.65em;">
|
<div class="desktop-header-logo">
|
||||||
{{ logo|safe }}
|
{{ logo|safe }}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="search-div">
|
<div class="search-div">
|
||||||
<form id="search-form" class="search-form" id="sf" method="POST">
|
<form id="search-form"
|
||||||
<div class="autocomplete" style="width: 100%; flex: 1">
|
class="search-form"
|
||||||
|
id="sf"
|
||||||
|
method="{{ 'GET' if config.get_only else 'POST' }}">
|
||||||
|
<div class="autocomplete header-autocomplete">
|
||||||
<div style="width: 100%; display: flex">
|
<div style="width: 100%; display: flex">
|
||||||
<input
|
{% if config.preferences %}
|
||||||
id="search-bar"
|
<input type="hidden" name="preferences" value="{{ config.preferences }}" />
|
||||||
autocapitalize="none"
|
{% endif %}
|
||||||
autocomplete="off"
|
<input
|
||||||
autocorrect="off"
|
id="search-bar"
|
||||||
class="search-bar-desktop noHIxc"
|
autocapitalize="none"
|
||||||
name="q"
|
autocomplete="off"
|
||||||
spellcheck="false"
|
autocorrect="off"
|
||||||
type="text"
|
class="search-bar-desktop search-bar-input"
|
||||||
value="{{ clean_query(query) }}"
|
name="q"
|
||||||
style="background-color: {{ 'var(--whoogle-dark-result-bg)' if config.dark else 'var(--whoogle-result-bg)' }} !important;
|
spellcheck="false"
|
||||||
color: {{ 'var(--whoogle-dark-text)' if config.dark else 'var(--whoogle-text)' }};
|
type="text"
|
||||||
border-bottom: {{ '2px solid var(--whoogle-dark-element-bg)' if config.dark else '0px' }};">
|
value="{{ clean_query(query) }}"
|
||||||
|
dir="auto">
|
||||||
<input name="tbm" value="{{ search_type }}" style="display: none">
|
<input name="tbm" value="{{ search_type }}" style="display: none">
|
||||||
|
<input name="country" value="{{ config.country }}" style="display: none;">
|
||||||
<input type="submit" style="display: none;">
|
<input type="submit" style="display: none;">
|
||||||
<div class="sc"></div>
|
<div class="sc"></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -66,6 +96,46 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
<div>
|
||||||
|
<div class="header-tab-div">
|
||||||
|
<div class="header-tab-div-2">
|
||||||
|
<div class="header-tab-div-3">
|
||||||
|
<div class="desktop-header header-tab">
|
||||||
|
{% for tab_id, tab_content in tabs.items() %}
|
||||||
|
{% if tab_content['selected'] %}
|
||||||
|
<span class="header-tab-span">{{ tab_content['name'] }}</span>
|
||||||
|
{% else %}
|
||||||
|
<a class="header-tab-a" href="{{ tab_content['href'] }}">{{ tab_content['name'] }}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
<label for="adv-search-toggle" id="adv-search-label" class="adv-search">⚙</label>
|
||||||
|
<input id="adv-search-toggle" type="checkbox">
|
||||||
|
<div class="header-tab-div-end"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="" id="s">
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<div class="result-collapsible" id="adv-search-div">
|
||||||
|
<div class="result-config">
|
||||||
|
<label for="config-country">{{ translation['config-country'] }}: </label>
|
||||||
|
<select name="country" id="result-country">
|
||||||
|
{% for country in countries %}
|
||||||
|
<option value="{{ country.value }}"
|
||||||
|
{% if (
|
||||||
|
config.country != '' and config.country in country.value
|
||||||
|
) or (
|
||||||
|
config.country == '' and country.value == '')
|
||||||
|
%}
|
||||||
|
selected
|
||||||
|
{% endif %}>
|
||||||
|
{{ country.name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script type="text/javascript" src="static/js/header.js"></script>
|
<script type="text/javascript" src="{{ cb_url('header.js') }}"></script>
|
||||||
|
|
|
@ -1,116 +1,390 @@
|
||||||
<!DOCTYPE html>
|
<div>
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta content="application/xhtml+xml; charset=utf-8" http-equiv="Content-Type"/>
|
|
||||||
<meta content="no-cache" name="Cache-Control"/>
|
|
||||||
<title>
|
|
||||||
</title>
|
|
||||||
<style>
|
<style>
|
||||||
a{text-decoration:none;color:inherit}a:hover{text-decoration:underline}a img{border:0}body{font-family:Roboto,Helvetica,Arial,sans-serif;padding:8px;margin:0 auto;max-width:700px;min-width:240px;}.FbhRzb{border-left:thin solid #dadce0;border-right:thin solid #dadce0;border-top:thin solid #dadce0;height:40px;overflow:hidden}.n692Zd{margin-bottom:10px}.cvifge{height:40px;border-spacing:0;width:100%;}.QvGUP{height:40px;padding:0 8px 0 8px;vertical-align:top}.O4cRJf{height:40px;width:100%;padding:0;padding-right:16px}.O1ePr{height:40px;padding:0;vertical-align:top}.kgJEQe{height:36px;width:98px;vertical-align:top;margin-top:4px}.lXLRf{vertical-align:top}.MhzMZd{border:0;vertical-align:middle;font-size:14px;height:40px;padding:0;width:100%;padding-left:16px}.xB0fq{height:40px;border:none;font-size:14px;background-color:#4285f4;color:#fff;padding:0 16px;margin:0;vertical-align:top;cursor:pointer}.xB0fq:focus{border:1px solid #000}.M7pB2{border:thin solid #dadce0;margin:0 0 3px 0;font-size:13px;font-weight:500;height:40px}.euZec{width:100%;height:40px;text-align:center;border-spacing:0}table.euZec td{padding:0;width:25%}.QIqI7{display:inline-block;padding-top:4px;font-weight:bold;color:#4285f4}.EY24We{border-bottom:2px solid #4285f4}.CsQyDc{display:inline-block;color:#70757a}.TuS8Ad{font-size:14px}.HddGcc{padding:8px;color:#70757a}.dzp8ae{font-weight:bold;color:#3c4043}.rEM8G{color:#70757a}.bookcf{table-layout:fixed;width:100%;border-spacing:0}.InWNIe{text-align:center}.uZgmoc{border:thin solid #dadce0;color:#70757a;font-size:14px;text-align:center;table-layout:fixed;width:100%}.frGj1b{display:block;padding:12px 0 12px 0;width:100%}.BnJWBc{text-align:center;padding:6px 0 13px 0;height:35px}.e3goi{vertical-align:top;padding:0;height:180px}.GpQGbf{margin:auto;border-collapse:collapse;border-spacing:0;width:100%}
|
html {
|
||||||
|
font-family: Roboto, Helvetica Neue, Arial, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
text-size-adjust: 100%;
|
||||||
|
color: #3c4043;
|
||||||
|
word-wrap: break-word;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
padding: 0 8px;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 736px;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
a img {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.FbhRzb {
|
||||||
|
border-left: thin solid #dadce0;
|
||||||
|
border-right: thin solid #dadce0;
|
||||||
|
border-top: thin solid #dadce0;
|
||||||
|
height: 40px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.n692Zd {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.cvifge {
|
||||||
|
height: 40px;
|
||||||
|
border-spacing: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.QvGUP {
|
||||||
|
height: 40px;
|
||||||
|
padding: 0 8px 0 8px;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
.O4cRJf {
|
||||||
|
height: 40px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
padding-right: 16px;
|
||||||
|
}
|
||||||
|
.O1ePr {
|
||||||
|
height: 40px;
|
||||||
|
padding: 0;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
.kgJEQe {
|
||||||
|
height: 36px;
|
||||||
|
width: 98px;
|
||||||
|
vertical-align: top;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.lXLRf {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
.MhzMZd {
|
||||||
|
border: 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
font-size: 14px;
|
||||||
|
height: 40px;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
padding-left: 16px;
|
||||||
|
}
|
||||||
|
.xB0fq {
|
||||||
|
height: 40px;
|
||||||
|
border: none;
|
||||||
|
font-size: 14px;
|
||||||
|
background-color: #4285f4;
|
||||||
|
color: #fff;
|
||||||
|
padding: 0 16px;
|
||||||
|
margin: 0;
|
||||||
|
vertical-align: top;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.xB0fq:focus {
|
||||||
|
border: 1px solid #000;
|
||||||
|
}
|
||||||
|
.M7pB2 {
|
||||||
|
border: thin solid #dadce0;
|
||||||
|
margin: 0 0 3px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
.euZec {
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
text-align: center;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
table.euZec td {
|
||||||
|
padding: 0;
|
||||||
|
width: 25%;
|
||||||
|
}
|
||||||
|
.QIqI7 {
|
||||||
|
display: inline-block;
|
||||||
|
padding-top: 4px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #4285f4;
|
||||||
|
}
|
||||||
|
.EY24We {
|
||||||
|
border-bottom: 2px solid #4285f4;
|
||||||
|
}
|
||||||
|
.CsQyDc {
|
||||||
|
display: inline-block;
|
||||||
|
color: #70757a;
|
||||||
|
}
|
||||||
|
.TuS8Ad {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.HddGcc {
|
||||||
|
padding: 8px;
|
||||||
|
color: #70757a;
|
||||||
|
}
|
||||||
|
.dzp8ae {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #3c4043;
|
||||||
|
}
|
||||||
|
.rEM8G {
|
||||||
|
color: #70757a;
|
||||||
|
}
|
||||||
|
.bookcf {
|
||||||
|
table-layout: fixed;
|
||||||
|
width: 100%;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
.InWNIe {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.uZgmoc {
|
||||||
|
border: thin solid #dadce0;
|
||||||
|
color: #70757a;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
table-layout: fixed;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.frGj1b {
|
||||||
|
display: block;
|
||||||
|
padding: 12px 0 12px 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.BnJWBc {
|
||||||
|
text-align: center;
|
||||||
|
padding: 6px 0 13px 0;
|
||||||
|
height: 35px;
|
||||||
|
}
|
||||||
|
.e3goi {
|
||||||
|
vertical-align: top;
|
||||||
|
padding: 0;
|
||||||
|
height: 180px;
|
||||||
|
}
|
||||||
|
.GpQGbf {
|
||||||
|
margin: auto;
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.X6ZCif {
|
||||||
|
color: #202124;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 16px;
|
||||||
|
display: inline-block;
|
||||||
|
padding-top: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.TwVfHd {
|
||||||
|
border-radius: 16px;
|
||||||
|
border: thin solid #dadce0;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 8px 8px;
|
||||||
|
margin-right: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.yekiAe {
|
||||||
|
background-color: #dadce0;
|
||||||
|
}
|
||||||
|
.svla5d {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.ezO2md {
|
||||||
|
border: thin solid #dadce0;
|
||||||
|
padding: 12px 16px 12px 16px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-family: Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.TxbwNb {
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
.K35ahc {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.owohpf {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.RAyV4b {
|
||||||
|
width: 162px;
|
||||||
|
height: 140px;
|
||||||
|
line-height: 140px;
|
||||||
|
overflow: "hidden";
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.t0fcAb {
|
||||||
|
text-align: center;
|
||||||
|
margin: auto;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
.Tor4Ec {
|
||||||
|
padding-top: 2px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
.fYyStc {
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.ynsChf {
|
||||||
|
display: block;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.Fj3V3b {
|
||||||
|
color: #1967d2;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
.FrIlee {
|
||||||
|
color: #202124;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
.F9iS2e {
|
||||||
|
color: #70757a;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
.WMQ2Le {
|
||||||
|
color: #70757a;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
.x3G5ab {
|
||||||
|
color: #202124;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
.fuLhoc {
|
||||||
|
color: #1967d2;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
.epoveb {
|
||||||
|
font-size: 32px;
|
||||||
|
line-height: 40px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #202124;
|
||||||
|
}
|
||||||
|
.dXDvrc {
|
||||||
|
color: #0d652d;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
.dloBPe {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.YVIcad {
|
||||||
|
color: #70757a;
|
||||||
|
}
|
||||||
|
.JkVVdd {
|
||||||
|
color: #ea4335;
|
||||||
|
}
|
||||||
|
.oXZRFd {
|
||||||
|
color: #ea4335;
|
||||||
|
}
|
||||||
|
.MQHtg {
|
||||||
|
color: #fbbc04;
|
||||||
|
}
|
||||||
|
.pyMRrb {
|
||||||
|
color: #1e8e3e;
|
||||||
|
}
|
||||||
|
.EtTZid {
|
||||||
|
color: #1e8e3e;
|
||||||
|
}
|
||||||
|
.M3vVJe {
|
||||||
|
color: #1967d2;
|
||||||
|
}
|
||||||
|
.qXLe6d {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.NHQNef {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.Cb8Z7c {
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
a.ZWRArf {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
a .CVA68e:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<style>
|
|
||||||
.X6ZCif{color:#202124;font-size:11px;line-height:16px;display:inline-block;padding-top:2px;overflow:hidden;padding-bottom:4px;width:100%}.TwVfHd{border-radius:16px;border:thin solid #dadce0;display:inline-block;padding:8px 8px;margin-right:8px;margin-bottom:4px}.yekiAe{background-color:#dadce0}.svla5d{width:100%}.ezO2md{border:thin solid #dadce0;padding:12px 16px 12px 16px;margin-bottom:10px;font-family:Roboto,Helvetica,Arial,sans-serif}.lIMUZd{font-family:Roboto,Helvetica,Arial,sans-serif}.TxbwNb{border-spacing:0}.K35ahc{width:100%}.owohpf{text-align:center}.RAyV4b{width:162px;height:140px;line-height:140px;overflow:'hidden';text-align:center;}.t0fcAb{text-align:center;margin:auto;vertical-align:middle;width:100%;height:100%;object-fit: contain}.Tor4Ec{padding-top:2px;padding-bottom:8px;}.fYyStc{word-break:break-word}.ynsChf{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.Fj3V3b{color:#1967D2;font-size:14px;line-height:20px}.FrIlee{color:#202124;font-size:11px;line-height:16px}.F9iS2e{color:#70757a;font-size:11px;line-height:16px}.WMQ2Le{color:#70757a;font-size:12px;line-height:16px}.x3G5ab{color:#202124;font-size:12px;line-height:16px}.fuLhoc{color:#1967D2;font-size:18px;line-height:24px}.epoveb{font-size:32px;line-height:40px;font-weight:400;color:#202124}.dXDvrc{color:#0d652d;font-size:14px;line-height:20px;word-wrap:break-word}.dloBPe{font-weight:bold}.YVIcad{color:#70757a}.JkVVdd{color:#ea4335}.oXZRFd{color:#ea4335}.MQHtg{color:#fbbc04}.pyMRrb{color:#1e8e3e}.EtTZid{color:#1e8e3e}.M3vVJe{color:#1967D2}.qXLe6d{display:block}.NHQNef{font-style:italic}.Cb8Z7c{white-space:pre}a.ZWRArf{text-decoration:none}a .CVA68e:hover{text-decoration:underline}
|
|
||||||
</style>
|
|
||||||
<div class="n692Zd">
|
|
||||||
<div class="BnJWBc">
|
|
||||||
<a class="lXLRf" href="/?safe=off&gbv=1&output=images&ie=UTF-8&tbm=isch&sa=X&ved=0ahUKEwjhh7TZyd_vAhWShf0HHeYzCmsQPAgC">
|
|
||||||
<img alt="Google" class="kgJEQe" src="/images/branding/searchlogo/1x/googlelogo_desk_heirloom_color_150x55dp.gif"/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="FbhRzb">
|
|
||||||
<form action="/search">
|
|
||||||
<input name="safe" type="hidden" value="off"/>
|
|
||||||
<input name="gbv" type="hidden" value="1"/>
|
|
||||||
<input name="ie" type="hidden" value="ISO-8859-1"/>
|
|
||||||
<input name="tbm" type="hidden" value="isch"/>
|
|
||||||
<input name="oq" type="hidden"/>
|
|
||||||
<input name="aqs" type="hidden"/>
|
|
||||||
<table class="cvifge">
|
|
||||||
<tr>
|
|
||||||
<td class="O4cRJf">
|
|
||||||
<!-- search input -->
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="M7pB2">
|
|
||||||
<!-- search options -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- <div class="X6ZCif"> Not present in mobile
|
|
||||||
</div> -->
|
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<div class="lIMUZd">
|
<div class="lIMUZd">
|
||||||
<table class="By0U9">
|
<table class="By0U9">
|
||||||
<!-- correction suggested -->
|
<!-- correction suggested -->
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table class="GpQGbf">
|
<table class="GpQGbf">
|
||||||
{% for i in range((length // 4) + 1) %}
|
{% for i in range((length // 4) + 1) %}
|
||||||
<tr>
|
<tr>
|
||||||
{% for j in range([length - (i*4), 4]|min) %}
|
{% for j in range([length - (i*4), 4]|min) %}
|
||||||
<td align="center" class="e3goi">
|
<td align="center" class="e3goi">
|
||||||
<div class="svla5d">
|
<div class="svla5d">
|
||||||
<div>
|
<div>
|
||||||
<div class="lIMUZd">
|
<div class="lIMUZd">
|
||||||
<div>
|
<div>
|
||||||
<table class="TxbwNb">
|
<table class="TxbwNb">
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ results[(i*4)+j].webpage }}">
|
<a href="{{ results[(i*4)+j].web_page }}">
|
||||||
<div class="RAyV4b">
|
<div class="RAyV4b">
|
||||||
<img alt="" class="t0fcAb" src="{{ results[(i*4)+j].img_tbn }}"/>
|
<img
|
||||||
</div>
|
alt=""
|
||||||
</a>
|
class="t0fcAb"
|
||||||
</td>
|
src="{{ results[(i*4)+j].img_tbn }}"
|
||||||
</tr>
|
/>
|
||||||
<tr>
|
</div>
|
||||||
<td>
|
</a>
|
||||||
<a href="{{ results[(i*4)+j].webpage }}">
|
</td>
|
||||||
<div class="Tor4Ec">
|
</tr>
|
||||||
<span class="qXLe6d x3G5ab">
|
<tr>
|
||||||
<span class="fYyStc">
|
<td>
|
||||||
{{ results[(i*4)+j].domain }}
|
<a href="{{ results[(i*4)+j].web_page }}">
|
||||||
</span>
|
<div class="Tor4Ec">
|
||||||
</span>
|
<span class="qXLe6d x3G5ab">
|
||||||
</div>
|
<span class="fYyStc">
|
||||||
</a>
|
{{ results[(i*4)+j].domain }}
|
||||||
<a href="{{ results[(i*4)+j].img_url }}">
|
</span>
|
||||||
<div class="Tor4Ec">
|
</span>
|
||||||
<span class="qXLe6d F9iS2e">
|
</div>
|
||||||
<span class="fYyStc">
|
</a>
|
||||||
{{ view_label }}
|
<a href="{{ results[(i*4)+j].img_url }}">
|
||||||
</span>
|
<div class="Tor4Ec">
|
||||||
</span>
|
<span class="qXLe6d F9iS2e">
|
||||||
</div>
|
<span class="fYyStc"> {{ view_label }} </span>
|
||||||
</a>
|
</span>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
</a>
|
||||||
</table>
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<table class="uZgmoc">
|
<table class="uZgmoc">
|
||||||
<!-- next page object -->
|
<!-- next page object -->
|
||||||
</table>
|
</table>
|
||||||
<br/>
|
<br />
|
||||||
<div class="TuS8Ad">
|
</div>
|
||||||
<!-- information about user connection -->
|
|
||||||
<div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
|
@ -1,181 +1,260 @@
|
||||||
<html>
|
<html style="background: #000;">
|
||||||
<head>
|
<head>
|
||||||
<link rel="apple-touch-icon" sizes="57x57" href="static/img/favicon/apple-icon-57x57.png">
|
<link rel="apple-touch-icon" sizes="57x57" href="static/img/favicon/apple-icon-57x57.png">
|
||||||
<link rel="apple-touch-icon" sizes="60x60" href="static/img/favicon/apple-icon-60x60.png">
|
<link rel="apple-touch-icon" sizes="60x60" href="static/img/favicon/apple-icon-60x60.png">
|
||||||
<link rel="apple-touch-icon" sizes="72x72" href="static/img/favicon/apple-icon-72x72.png">
|
<link rel="apple-touch-icon" sizes="72x72" href="static/img/favicon/apple-icon-72x72.png">
|
||||||
<link rel="apple-touch-icon" sizes="76x76" href="static/img/favicon/apple-icon-76x76.png">
|
<link rel="apple-touch-icon" sizes="76x76" href="static/img/favicon/apple-icon-76x76.png">
|
||||||
<link rel="apple-touch-icon" sizes="114x114" href="static/img/favicon/apple-icon-114x114.png">
|
<link rel="apple-touch-icon" sizes="114x114" href="static/img/favicon/apple-icon-114x114.png">
|
||||||
<link rel="apple-touch-icon" sizes="120x120" href="static/img/favicon/apple-icon-120x120.png">
|
<link rel="apple-touch-icon" sizes="120x120" href="static/img/favicon/apple-icon-120x120.png">
|
||||||
<link rel="apple-touch-icon" sizes="144x144" href="static/img/favicon/apple-icon-144x144.png">
|
<link rel="apple-touch-icon" sizes="144x144" href="static/img/favicon/apple-icon-144x144.png">
|
||||||
<link rel="apple-touch-icon" sizes="152x152" href="static/img/favicon/apple-icon-152x152.png">
|
<link rel="apple-touch-icon" sizes="152x152" href="static/img/favicon/apple-icon-152x152.png">
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="static/img/favicon/apple-icon-180x180.png">
|
<link rel="apple-touch-icon" sizes="180x180" href="static/img/favicon/apple-icon-180x180.png">
|
||||||
<link rel="icon" type="image/png" sizes="192x192" href="static/img/favicon/android-icon-192x192.png">
|
<link rel="icon" type="image/png" sizes="192x192" href="static/img/favicon/android-icon-192x192.png">
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="static/img/favicon/favicon-32x32.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="static/img/favicon/favicon-32x32.png">
|
||||||
<link rel="icon" type="image/png" sizes="96x96" href="static/img/favicon/favicon-96x96.png">
|
<link rel="icon" type="image/png" sizes="96x96" href="static/img/favicon/favicon-96x96.png">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="static/img/favicon/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="static/img/favicon/favicon-16x16.png">
|
||||||
<link rel="manifest" href="static/img/favicon/manifest.json">
|
<link rel="manifest" href="static/img/favicon/manifest.json">
|
||||||
<meta name="referrer" content="no-referrer">
|
<meta name="referrer" content="no-referrer">
|
||||||
<meta name="msapplication-TileColor" content="#ffffff">
|
<meta name="msapplication-TileColor" content="#ffffff">
|
||||||
<meta name="msapplication-TileImage" content="static/img/favicon/ms-icon-144x144.png">
|
<meta name="msapplication-TileImage" content="static/img/favicon/ms-icon-144x144.png">
|
||||||
<script type="text/javascript" src="static/js/autocomplete.js"></script>
|
{% if autocomplete_enabled == '1' %}
|
||||||
<script type="text/javascript" src="static/js/controller.js"></script>
|
<script src="{{ cb_url('autocomplete.js') }}"></script>
|
||||||
<link rel="search" href="opensearch.xml" type="application/opensearchdescription+xml" title="Whoogle Search">
|
{% endif %}
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<script type="text/javascript" src="{{ cb_url('controller.js') }}"></script>
|
||||||
<link rel="stylesheet" href="static/css/variables.css">
|
<link rel="search" href="opensearch.xml" type="application/opensearchdescription+xml" title="Whoogle Search">
|
||||||
<link rel="stylesheet" href="static/css/main.css">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link rel="stylesheet" href="static/css/{{ 'dark' if config.dark else 'light' }}-theme.css"/>
|
<link rel="stylesheet" href="{{ cb_url('logo.css') }}">
|
||||||
<noscript>
|
{% if config.theme %}
|
||||||
<style>
|
{% if config.theme == 'system' %}
|
||||||
#main { display: inherit !important; }
|
<style>
|
||||||
.content { max-height: 720px; padding: 18px; border-radius: 10px; }
|
@import "{{ cb_url('light-theme.css') }}" screen;
|
||||||
.collapsible { display: none; }
|
@import "{{ cb_url('dark-theme.css') }}" screen and (prefers-color-scheme: dark);
|
||||||
</style>
|
</style>
|
||||||
</noscript>
|
{% else %}
|
||||||
<style>{{ config.style }}</style>
|
<link rel="stylesheet" href="{{ cb_url(config.theme + '-theme.css') }}"/>
|
||||||
<title>Whoogle Search</title>
|
{% endif %}
|
||||||
</head>
|
{% else %}
|
||||||
<body id="main" style="display: none; background-color: {{ '#000' if config.dark else '#fff' }}">
|
<link rel="stylesheet" href="{{ cb_url(('dark' if config.dark else 'light') + '-theme.css') }}"/>
|
||||||
<div class="search-container">
|
{% endif %}
|
||||||
<div class="logo-container">
|
<link rel="stylesheet" href="{{ cb_url('main.css') }}">
|
||||||
{{ logo|safe }}
|
<noscript>
|
||||||
</div>
|
<style>
|
||||||
<form id="search-form" action="search" method="{{ 'get' if config.get_only else 'post' }}">
|
#main {
|
||||||
<div class="search-fields">
|
display: inherit !important;
|
||||||
<div class="autocomplete">
|
}
|
||||||
<input
|
|
||||||
type="text"
|
.content {
|
||||||
name="q"
|
max-height: 400px;
|
||||||
id="search-bar"
|
padding: 18px;
|
||||||
class="home-search"
|
border-radius: 10px;
|
||||||
autofocus="autofocus"
|
overflow-y: scroll;
|
||||||
autocapitalize="none"
|
}
|
||||||
spellcheck="false"
|
|
||||||
autocorrect="off"
|
.collapsible {
|
||||||
autocomplete="off">
|
display: none;
|
||||||
</div>
|
}
|
||||||
<input type="submit" id="search-submit" value="{{ translation['search'] }}">
|
</style>
|
||||||
</div>
|
</noscript>
|
||||||
</form>
|
<style>{{ config.style }}</style>
|
||||||
{% if not config_disabled %}
|
<title>Whoogle Search</title>
|
||||||
<br/>
|
</head>
|
||||||
<button id="config-collapsible" class="collapsible">{{ translation['config'] }}</button>
|
<body id="main">
|
||||||
<div class="content">
|
<div class="search-container">
|
||||||
<div class="config-fields">
|
<div class="logo-container">
|
||||||
<form id="config-form" action="config" method="post">
|
{{ logo|safe }}
|
||||||
<div class="config-div config-div-ctry">
|
</div>
|
||||||
<label for="config-ctry">{{ translation['config-country'] }}: </label>
|
<form id="search-form" action="search" method="{{ 'get' if config.get_only else 'post' }}">
|
||||||
<select name="ctry" id="config-ctry">
|
<div class="search-fields">
|
||||||
{% for ctry in countries %}
|
<div class="autocomplete">
|
||||||
<option value="{{ ctry.value }}"
|
{% if config.preferences %}
|
||||||
{% if ctry.value in config.ctry %}
|
<input type="hidden" name="preferences" value="{{ config.preferences }}" />
|
||||||
selected
|
{% endif %}
|
||||||
{% endif %}>
|
<input
|
||||||
{{ ctry.name }}
|
type="text"
|
||||||
</option>
|
name="q"
|
||||||
{% endfor %}
|
id="search-bar"
|
||||||
</select>
|
class="home-search"
|
||||||
<div><span class="info-text"> — {{ translation['config-country-help'] }}</span></div>
|
autofocus="autofocus"
|
||||||
</div>
|
autocapitalize="none"
|
||||||
<div class="config-div config-div-lang">
|
spellcheck="false"
|
||||||
<label for="config-lang-interface">{{ translation['config-lang'] }}: </label>
|
autocorrect="off"
|
||||||
<select name="lang_interface" id="config-lang-interface">
|
autocomplete="off"
|
||||||
{% for lang in languages %}
|
dir="auto">
|
||||||
<option value="{{ lang.value }}"
|
</div>
|
||||||
{% if lang.value in config.lang_interface %}
|
<input type="submit" id="search-submit" value="{{ translation['search'] }}">
|
||||||
selected
|
</div>
|
||||||
{% endif %}>
|
</form>
|
||||||
{{ lang.name }}
|
{% if not config_disabled %}
|
||||||
</option>
|
<br/>
|
||||||
{% endfor %}
|
<button id="config-collapsible" class="collapsible">{{ translation['config'] }}</button>
|
||||||
</select>
|
<div class="content">
|
||||||
</div>
|
<div class="config-fields">
|
||||||
<div class="config-div config-div-search-lang">
|
<form id="config-form" action="config" method="post">
|
||||||
<label for="config-lang-search">{{ translation['config-lang-search'] }}: </label>
|
<div class="config-options">
|
||||||
<select name="lang_search" id="config-lang-search">
|
<div class="config-div config-div-country">
|
||||||
{% for lang in languages %}
|
<label for="config-country">{{ translation['config-country'] }}: </label>
|
||||||
<option value="{{ lang.value }}"
|
<select name="country" id="config-country">
|
||||||
{% if lang.value in config.lang_search %}
|
{% for country in countries %}
|
||||||
selected
|
<option value="{{ country.value }}"
|
||||||
{% endif %}>
|
{% if (
|
||||||
{{ lang.name }}
|
config.country != '' and config.country in country.value
|
||||||
</option>
|
) or (
|
||||||
{% endfor %}
|
config.country == '' and country.value == '')
|
||||||
</select>
|
%}
|
||||||
</div>
|
selected
|
||||||
<div class="config-div config-div-near">
|
{% endif %}>
|
||||||
<label for="config-near">{{ translation['config-near'] }}: </label>
|
{{ country.name }}
|
||||||
<input type="text" name="near" id="config-near" placeholder="City Name" value="{{ config.near }}">
|
</option>
|
||||||
</div>
|
{% endfor %}
|
||||||
<div class="config-div config-div-block">
|
</select>
|
||||||
<label for="config-block">{{ translation['config-block'] }}: </label>
|
</div>
|
||||||
<input type="text" name="block" id="config-block" placeholder="Comma-separated site list" value="{{ config.block }}">
|
<div class="config-div config-div-lang">
|
||||||
</div>
|
<label for="config-lang-interface">{{ translation['config-lang'] }}: </label>
|
||||||
<div class="config-div config-div-nojs">
|
<select name="lang_interface" id="config-lang-interface">
|
||||||
<label for="config-nojs">{{ translation['config-nojs'] }}: </label>
|
{% for lang in languages %}
|
||||||
<input type="checkbox" name="nojs" id="config-nojs" {{ 'checked' if config.nojs else '' }}>
|
<option value="{{ lang.value }}"
|
||||||
</div>
|
{% if lang.value in config.lang_interface %}
|
||||||
<div class="config-div config-div-dark">
|
selected
|
||||||
<label for="config-dark">{{ translation['config-dark'] }}: </label>
|
{% endif %}>
|
||||||
<input type="checkbox" name="dark" id="config-dark" {{ 'checked' if config.dark else '' }}>
|
{{ lang.name }}
|
||||||
</div>
|
</option>
|
||||||
<div class="config-div config-div-safe">
|
{% endfor %}
|
||||||
<label for="config-safe">{{ translation['config-safe'] }}: </label>
|
</select>
|
||||||
<input type="checkbox" name="safe" id="config-safe" {{ 'checked' if config.safe else '' }}>
|
</div>
|
||||||
</div>
|
<div class="config-div config-div-search-lang">
|
||||||
<div class="config-div config-div-alts">
|
<label for="config-lang-search">{{ translation['config-lang-search'] }}: </label>
|
||||||
<label class="tooltip" for="config-alts">{{ translation['config-alts'] }}: </label>
|
<select name="lang_search" id="config-lang-search">
|
||||||
<input type="checkbox" name="alts" id="config-alts" {{ 'checked' if config.alts else '' }}>
|
{% for lang in languages %}
|
||||||
<div><span class="info-text"> — {{ translation['config-alts-help'] }}</span></div>
|
<option value="{{ lang.value }}"
|
||||||
</div>
|
{% if lang.value in config.lang_search %}
|
||||||
<div class="config-div config-div-new-tab">
|
selected
|
||||||
<label for="config-new-tab">{{ translation['config-new-tab'] }}: </label>
|
{% endif %}>
|
||||||
<input type="checkbox" name="new_tab" id="config-new-tab" {{ 'checked' if config.new_tab else '' }}>
|
{{ lang.name }}
|
||||||
</div>
|
</option>
|
||||||
<div class="config-div config-div-view-image">
|
{% endfor %}
|
||||||
<label for="config-view-image">{{ translation['config-images'] }}: </label>
|
</select>
|
||||||
<input type="checkbox" name="view_image" id="config-view-image" {{ 'checked' if config.view_image else '' }}>
|
</div>
|
||||||
<div><span class="info-text"> — {{ translation['config-images-help'] }}</span></div>
|
<div class="config-div config-div-near">
|
||||||
</div>
|
<label for="config-near">{{ translation['config-near'] }}: </label>
|
||||||
<div class="config-div config-div-tor">
|
<input type="text" name="near" id="config-near"
|
||||||
<label for="config-tor">{{ translation['config-tor'] }}: {{ '' if tor_available else 'Unavailable' }}</label>
|
placeholder="{{ translation['config-near-help'] }}" value="{{ config.near }}">
|
||||||
<input type="checkbox" name="tor" id="config-tor" {{ '' if tor_available else 'hidden' }} {{ 'checked' if config.tor else '' }}>
|
</div>
|
||||||
</div>
|
<div class="config-div config-div-block">
|
||||||
<div class="config-div config-div-get-only">
|
<label for="config-block">{{ translation['config-block'] }}: </label>
|
||||||
<label for="config-get-only">{{ translation['config-get-only'] }}: </label>
|
<input type="text" name="block" id="config-block"
|
||||||
<input type="checkbox" name="get_only" id="config-get-only" {{ 'checked' if config.get_only else '' }}>
|
placeholder="{{ translation['config-block-help'] }}" value="{{ config.block }}">
|
||||||
</div>
|
</div>
|
||||||
<div class="config-div config-div-root-url">
|
<div class="config-div config-div-block">
|
||||||
<label for="config-url">{{ translation['config-url'] }}: </label>
|
<label for="config-block-title">{{ translation['config-block-title'] }}: </label>
|
||||||
<input type="text" name="url" id="config-url" value="{{ config.url }}">
|
<input type="text" name="block_title" id="config-block"
|
||||||
</div>
|
placeholder="{{ translation['config-block-title-help'] }}"
|
||||||
<div class="config-div config-div-custom-css">
|
value="{{ config.block_title }}">
|
||||||
<label for="config-style">{{ translation['config-css'] }}:</label>
|
</div>
|
||||||
<textarea
|
<div class="config-div config-div-block">
|
||||||
name="style"
|
<label for="config-block-url">{{ translation['config-block-url'] }}: </label>
|
||||||
id="config-style"
|
<input type="text" name="block_url" id="config-block"
|
||||||
autocapitalize="off"
|
placeholder="{{ translation['config-block-url-help'] }}" value="{{ config.block_url }}">
|
||||||
autocomplete="off"
|
</div>
|
||||||
spellcheck="false"
|
<div class="config-div config-div-anon-view">
|
||||||
autocorrect="off"
|
<label for="config-anon-view">{{ translation['config-anon-view'] }}: </label>
|
||||||
value="">
|
<input type="checkbox" name="anon_view" id="config-anon-view" {{ 'checked' if config.anon_view else '' }}>
|
||||||
{{ config.style.replace('\t', '') }}
|
</div>
|
||||||
</textarea>
|
<div class="config-div config-div-nojs">
|
||||||
</div>
|
<label for="config-nojs">{{ translation['config-nojs'] }}: </label>
|
||||||
<div class="config-div">
|
<input type="checkbox" name="nojs" id="config-nojs" {{ 'checked' if config.nojs else '' }}>
|
||||||
<input type="submit" id="config-load" value="{{ translation['load'] }}">
|
</div>
|
||||||
<input type="submit" id="config-submit" value="{{ translation['apply'] }}">
|
<div class="config-div config-div-theme">
|
||||||
<input type="submit" id="config-save" value="{{ translation['save-as'] }}">
|
<label for="config-theme">{{ translation['config-theme'] }}: </label>
|
||||||
</div>
|
<select name="theme" id="config-theme">
|
||||||
</form>
|
{% for theme in themes %}
|
||||||
</div>
|
<option value="{{ theme }}"
|
||||||
</div>
|
{% if theme in config.theme %}
|
||||||
{% endif %}
|
selected
|
||||||
</div>
|
{% endif %}>
|
||||||
<footer>
|
{{ translation[theme].capitalize() }}
|
||||||
<p style="color: {{ 'var(--whoogle-dark-text)' if config.dark else 'var(--whoogle-text)' }};">
|
</option>
|
||||||
Whoogle Search v{{ version_number }} ||
|
{% endfor %}
|
||||||
<a id="gh-link" href="https://github.com/benbusby/whoogle-search">{{ translation['github-link'] }}</a>
|
</select>
|
||||||
</p>
|
</div>
|
||||||
</footer>
|
<!-- DEPRECATED -->
|
||||||
</body>
|
<!--<div class="config-div config-div-dark">-->
|
||||||
|
<!--<label for="config-dark">{{ translation['config-dark'] }}: </label>-->
|
||||||
|
<!--<input type="checkbox" name="dark" id="config-dark" {{ 'checked' if config.dark else '' }}>-->
|
||||||
|
<!--</div>-->
|
||||||
|
<div class="config-div config-div-safe">
|
||||||
|
<label for="config-safe">{{ translation['config-safe'] }}: </label>
|
||||||
|
<input type="checkbox" name="safe" id="config-safe" {{ 'checked' if config.safe else '' }}>
|
||||||
|
</div>
|
||||||
|
<div class="config-div config-div-alts">
|
||||||
|
<label class="tooltip" for="config-alts">{{ translation['config-alts'] }}: </label>
|
||||||
|
<input type="checkbox" name="alts" id="config-alts" {{ 'checked' if config.alts else '' }}>
|
||||||
|
<div><span class="info-text"> — {{ translation['config-alts-help'] }}</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="config-div config-div-new-tab">
|
||||||
|
<label for="config-new-tab">{{ translation['config-new-tab'] }}: </label>
|
||||||
|
<input type="checkbox" name="new_tab"
|
||||||
|
id="config-new-tab" {{ 'checked' if config.new_tab else '' }}>
|
||||||
|
</div>
|
||||||
|
<div class="config-div config-div-view-image">
|
||||||
|
<label for="config-view-image">{{ translation['config-images'] }}: </label>
|
||||||
|
<input type="checkbox" name="view_image"
|
||||||
|
id="config-view-image" {{ 'checked' if config.view_image else '' }}>
|
||||||
|
<div><span class="info-text"> — {{ translation['config-images-help'] }}</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="config-div config-div-tor">
|
||||||
|
<label for="config-tor">{{ translation['config-tor'] }}: {{ '' if tor_available else 'Unavailable' }}</label>
|
||||||
|
<input type="checkbox" name="tor"
|
||||||
|
id="config-tor" {{ '' if tor_available else 'hidden' }} {{ 'checked' if config.tor else '' }}>
|
||||||
|
</div>
|
||||||
|
<div class="config-div config-div-get-only">
|
||||||
|
<label for="config-get-only">{{ translation['config-get-only'] }}: </label>
|
||||||
|
<input type="checkbox" name="get_only"
|
||||||
|
id="config-get-only" {{ 'checked' if config.get_only else '' }}>
|
||||||
|
</div>
|
||||||
|
<div class="config-div config-div-accept-language">
|
||||||
|
<label for="config-accept-language">Set Accept-Language: </label>
|
||||||
|
<input type="checkbox" name="accept_language"
|
||||||
|
id="config-accept-language" {{ 'checked' if config.accept_language else '' }}>
|
||||||
|
</div>
|
||||||
|
<div class="config-div config-div-root-url">
|
||||||
|
<label for="config-url">{{ translation['config-url'] }}: </label>
|
||||||
|
<input type="text" name="url" id="config-url" value="{{ config.url }}">
|
||||||
|
</div>
|
||||||
|
<div class="config-div config-div-custom-css">
|
||||||
|
<a id="css-link"
|
||||||
|
href="https://github.com/benbusby/whoogle-search/wiki/User-Contributed-CSS-Themes">
|
||||||
|
{{ translation['config-css'] }}:
|
||||||
|
</a>
|
||||||
|
<textarea
|
||||||
|
name="style"
|
||||||
|
id="config-style"
|
||||||
|
autocapitalize="off"
|
||||||
|
autocomplete="off"
|
||||||
|
spellcheck="false"
|
||||||
|
autocorrect="off"
|
||||||
|
value="">
|
||||||
|
{{ config.style.replace('\t', '') }}
|
||||||
|
</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="config-div config-div-pref-url">
|
||||||
|
<label for="config-pref-encryption">{{ translation['config-pref-encryption'] }}: </label>
|
||||||
|
<input type="checkbox" name="preferences_encrypted"
|
||||||
|
id="config-pref-encryption" {{ 'checked' if config.preferences_encrypted and config.preferences_key else '' }}>
|
||||||
|
<div><span class="info-text"> — {{ translation['config-pref-help'] }}</span></div>
|
||||||
|
<label for="config-pref-url">{{ translation['config-pref-url'] }}: </label>
|
||||||
|
<input type="text" name="pref-url" id="config-pref-url" value="{{ config.url }}?preferences={{ config.preferences }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="config-div config-buttons">
|
||||||
|
<input type="submit" id="config-load" value="{{ translation['load'] }}">
|
||||||
|
<input type="submit" id="config-submit" value="{{ translation['apply'] }}">
|
||||||
|
<input type="submit" id="config-save" value="{{ translation['save-as'] }}">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% include 'footer.html' %}
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,10 +1,4 @@
|
||||||
<link rel="stylesheet" href="static/css/logo.css">
|
|
||||||
<svg id="Layer_1" class="whoogle-svg" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1028 254">
|
<svg id="Layer_1" class="whoogle-svg" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1028 254">
|
||||||
<style>
|
|
||||||
path {
|
|
||||||
fill: {{ 'var(--whoogle-dark-logo)' if dark else 'var(--whoogle-logo)' }};
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<defs>
|
<defs>
|
||||||
<style>
|
<style>
|
||||||
</style>
|
</style>
|
||||||
|
@ -22,4 +16,3 @@
|
||||||
<path class="cls-1" d="M950.51,539.43c-.31,20.82-10.91,37.89-28,44.71-25.32,10.11-53.89-7-57.87-34.41-1.51-10.43-1.06-20.59,2.68-30.44,7.08-18.66,25.09-29.59,45-27.58,17.76,1.79,33.92,17.68,36.86,36.35C949.79,531.82,950.08,535.64,950.51,539.43Z" transform="translate(-446 -413)"></path>
|
<path class="cls-1" d="M950.51,539.43c-.31,20.82-10.91,37.89-28,44.71-25.32,10.11-53.89-7-57.87-34.41-1.51-10.43-1.06-20.59,2.68-30.44,7.08-18.66,25.09-29.59,45-27.58,17.76,1.79,33.92,17.68,36.86,36.35C949.79,531.82,950.08,535.64,950.51,539.43Z" transform="translate(-446 -413)"></path>
|
||||||
<path class="cls-1" d="M1099.71,539.39c-.39,22.14-11.74,39.51-30.16,45.6-25.8,8.54-53.64-10.27-55.87-37.67-.78-9.54-.55-18.93,3-28,7.25-18.72,24.95-29.59,45-27.62,17.2,1.68,33.14,16.78,36.57,34.84C1099,530.77,1099.23,535.1,1099.71,539.39Z" transform="translate(-446 -413)"></path>
|
<path class="cls-1" d="M1099.71,539.39c-.39,22.14-11.74,39.51-30.16,45.6-25.8,8.54-53.64-10.27-55.87-37.67-.78-9.54-.55-18.93,3-28,7.25-18.72,24.95-29.59,45-27.62,17.2,1.68,33.14,16.78,36.57,34.84C1099,530.77,1099.23,535.1,1099.71,539.39Z" transform="translate(-446 -413)"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
|
||||||
|
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.4 KiB |
File diff suppressed because one or more lines are too long
|
@ -1,4 +1,4 @@
|
||||||
<form id="search-form" action="{{ url }}/search" method="post">
|
<form id="search-form" action="search" method="post">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="q"
|
name="q"
|
||||||
|
@ -8,6 +8,7 @@
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
autocorrect="off"
|
autocorrect="off"
|
||||||
placeholder="Whoogle Search"
|
placeholder="Whoogle Search"
|
||||||
autocomplete="off">
|
autocomplete="off"
|
||||||
|
dir="auto">
|
||||||
<input type="submit" style="width: 9%" id="search-submit" value="Search">
|
<input type="submit" style="width: 9%" id="search-submit" value="Search">
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import json
|
import json
|
||||||
import requests
|
import requests
|
||||||
|
import urllib.parse as urlparse
|
||||||
|
|
||||||
DDG_BANGS = 'https://duckduckgo.com/bang.v255.js'
|
DDG_BANGS = 'https://duckduckgo.com/bang.v255.js'
|
||||||
|
|
||||||
|
@ -35,6 +36,7 @@ def gen_bangs_json(bangs_file: str) -> None:
|
||||||
}
|
}
|
||||||
|
|
||||||
json.dump(bangs_data, open(bangs_file, 'w'))
|
json.dump(bangs_data, open(bangs_file, 'w'))
|
||||||
|
print('* Finished creating ddg bangs json')
|
||||||
|
|
||||||
|
|
||||||
def resolve_bang(query: str, bangs_dict: dict) -> str:
|
def resolve_bang(query: str, bangs_dict: dict) -> str:
|
||||||
|
@ -51,11 +53,38 @@ def resolve_bang(query: str, bangs_dict: dict) -> str:
|
||||||
wasn't a match or didn't contain a bang operator
|
wasn't a match or didn't contain a bang operator
|
||||||
|
|
||||||
"""
|
"""
|
||||||
split_query = query.split(' ')
|
|
||||||
for operator in bangs_dict.keys():
|
|
||||||
if operator not in split_query:
|
|
||||||
continue
|
|
||||||
|
|
||||||
return bangs_dict[operator]['url'].format(
|
#if ! not in query simply return (speed up processing)
|
||||||
query.replace(operator, '').strip())
|
if '!' not in query:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
split_query = query.strip().split(' ')
|
||||||
|
|
||||||
|
# look for operator in query if one is found, list operator should be of
|
||||||
|
# length 1, operator should not be case-sensitive here to remove it later
|
||||||
|
operator = [
|
||||||
|
word
|
||||||
|
for word in split_query
|
||||||
|
if word.lower() in bangs_dict
|
||||||
|
]
|
||||||
|
if len(operator) == 1:
|
||||||
|
# get operator
|
||||||
|
operator = operator[0]
|
||||||
|
|
||||||
|
# removes operator from query
|
||||||
|
split_query.remove(operator)
|
||||||
|
|
||||||
|
# rebuild the query string
|
||||||
|
bang_query = ' '.join(split_query).strip()
|
||||||
|
|
||||||
|
# Check if operator is a key in bangs and get bang if exists
|
||||||
|
bang = bangs_dict.get(operator.lower(), None)
|
||||||
|
if bang:
|
||||||
|
bang_url = bang['url']
|
||||||
|
|
||||||
|
if bang_query:
|
||||||
|
return bang_url.replace('{}', bang_query, 1)
|
||||||
|
else:
|
||||||
|
parsed_url = urlparse.urlparse(bang_url)
|
||||||
|
return f'{parsed_url.scheme}://{parsed_url.netloc}'
|
||||||
return ''
|
return ''
|
||||||
|
|
72
app/utils/misc.py
Normal file
72
app/utils/misc.py
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
from bs4 import BeautifulSoup as bsoup
|
||||||
|
from flask import Request
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
from requests import exceptions, get
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
|
||||||
|
def gen_file_hash(path: str, static_file: str) -> str:
|
||||||
|
file_contents = open(os.path.join(path, static_file), 'rb').read()
|
||||||
|
file_hash = hashlib.md5(file_contents).hexdigest()[:8]
|
||||||
|
filename_split = os.path.splitext(static_file)
|
||||||
|
|
||||||
|
return filename_split[0] + '.' + file_hash + filename_split[-1]
|
||||||
|
|
||||||
|
|
||||||
|
def read_config_bool(var: str) -> bool:
|
||||||
|
val = os.getenv(var, '0')
|
||||||
|
# user can specify one of the following values as 'true' inputs (all
|
||||||
|
# variants with upper case letters will also work):
|
||||||
|
# ('true', 't', '1', 'yes', 'y')
|
||||||
|
val = val.lower() in ('true', 't', '1', 'yes', 'y')
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
def get_client_ip(r: Request) -> str:
|
||||||
|
if r.environ.get('HTTP_X_FORWARDED_FOR') is None:
|
||||||
|
return r.environ['REMOTE_ADDR']
|
||||||
|
else:
|
||||||
|
return r.environ['HTTP_X_FORWARDED_FOR']
|
||||||
|
|
||||||
|
|
||||||
|
def get_request_url(url: str) -> str:
|
||||||
|
if os.getenv('HTTPS_ONLY', False):
|
||||||
|
return url.replace('http://', 'https://', 1)
|
||||||
|
|
||||||
|
return url
|
||||||
|
|
||||||
|
|
||||||
|
def get_proxy_host_url(r: Request, default: str, root=False) -> str:
|
||||||
|
scheme = r.headers.get('X-Forwarded-Proto', 'https')
|
||||||
|
http_host = r.headers.get('X-Forwarded-Host')
|
||||||
|
if http_host:
|
||||||
|
return f'{scheme}://{http_host}{r.full_path if not root else "/"}'
|
||||||
|
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def check_for_update(version_url: str, current: str) -> int:
|
||||||
|
# Check for the latest version of Whoogle
|
||||||
|
try:
|
||||||
|
update = bsoup(get(version_url).text, 'html.parser')
|
||||||
|
latest = update.select_one('[class="Link--primary"]').string[1:]
|
||||||
|
current = int(''.join(filter(str.isdigit, current)))
|
||||||
|
latest = int(''.join(filter(str.isdigit, latest)))
|
||||||
|
has_update = '' if current >= latest else latest
|
||||||
|
except (exceptions.ConnectionError, AttributeError):
|
||||||
|
# Ignore failures, assume current version is up to date
|
||||||
|
has_update = ''
|
||||||
|
|
||||||
|
return has_update
|
||||||
|
|
||||||
|
|
||||||
|
def get_abs_url(url, page_url):
|
||||||
|
# Creates a valid absolute URL using a partial or relative URL
|
||||||
|
if url.startswith('//'):
|
||||||
|
return f'https:{url}'
|
||||||
|
elif url.startswith('/'):
|
||||||
|
return f'{urlparse(page_url).netloc}{url}'
|
||||||
|
elif url.startswith('./'):
|
||||||
|
return f'{page_url}{url[2:]}'
|
||||||
|
return url
|
|
@ -1,35 +1,90 @@
|
||||||
from bs4 import BeautifulSoup
|
from app.models.config import Config
|
||||||
|
from app.models.endpoint import Endpoint
|
||||||
|
from bs4 import BeautifulSoup, NavigableString
|
||||||
|
import copy
|
||||||
|
from flask import current_app
|
||||||
|
import html
|
||||||
import os
|
import os
|
||||||
import urllib.parse as urlparse
|
import urllib.parse as urlparse
|
||||||
from urllib.parse import parse_qs
|
from urllib.parse import parse_qs
|
||||||
|
import re
|
||||||
|
|
||||||
SKIP_ARGS = ['ref_src', 'utm']
|
SKIP_ARGS = ['ref_src', 'utm']
|
||||||
SKIP_PREFIX = ['//www.', '//mobile.', '//m.']
|
SKIP_PREFIX = ['//www.', '//mobile.', '//m.']
|
||||||
GOOG_STATIC = 'www.gstatic.com'
|
GOOG_STATIC = 'www.gstatic.com'
|
||||||
|
G_M_LOGO_URL = 'https://www.gstatic.com/m/images/icons/googleg.gif'
|
||||||
GOOG_IMG = '/images/branding/searchlogo/1x/googlelogo'
|
GOOG_IMG = '/images/branding/searchlogo/1x/googlelogo'
|
||||||
LOGO_URL = GOOG_IMG + '_desk'
|
LOGO_URL = GOOG_IMG + '_desk'
|
||||||
BLANK_B64 = ('data:image/png;base64,'
|
BLANK_B64 = ('data:image/png;base64,'
|
||||||
'iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAD0lEQVR42mNkw'
|
'iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAD0lEQVR42mNkw'
|
||||||
'AIYh7IgAAVVAAuInjI5AAAAAElFTkSuQmCC')
|
'AIYh7IgAAVVAAuInjI5AAAAAElFTkSuQmCC')
|
||||||
|
|
||||||
|
|
||||||
# Ad keywords
|
# Ad keywords
|
||||||
BLACKLIST = [
|
BLACKLIST = [
|
||||||
'ad', 'anuncio', 'annuncio', 'annonce', 'Anzeige', '广告', '廣告', 'Reklama',
|
'ad', 'ads', 'anuncio', 'annuncio', 'annonce', 'Anzeige', '广告', '廣告',
|
||||||
'Реклама', 'Anunț', '광고', 'annons', 'Annonse', 'Iklan', '広告', 'Augl.',
|
'Reklama', 'Реклама', 'Anunț', '광고', 'annons', 'Annonse', 'Iklan',
|
||||||
'Mainos', 'Advertentie', 'إعلان', 'Գովազդ', 'विज्ञापन', 'Reklam', 'آگهی',
|
'広告', 'Augl.', 'Mainos', 'Advertentie', 'إعلان', 'Գովազդ', 'विज्ञापन',
|
||||||
'Reklāma', 'Reklaam', 'Διαφήμιση', 'מודעה', 'Hirdetés', 'Anúncio'
|
'Reklam', 'آگهی', 'Reklāma', 'Reklaam', 'Διαφήμιση', 'מודעה', 'Hirdetés',
|
||||||
|
'Anúncio', 'Quảng cáo','โฆษณา'
|
||||||
]
|
]
|
||||||
|
|
||||||
SITE_ALTS = {
|
SITE_ALTS = {
|
||||||
'twitter.com': os.getenv('WHOOGLE_ALT_TW', 'nitter.net'),
|
'twitter.com': os.getenv('WHOOGLE_ALT_TW', 'farside.link/nitter'),
|
||||||
'youtube.com': os.getenv('WHOOGLE_ALT_YT', 'invidious.snopyta.org'),
|
'youtube.com': os.getenv('WHOOGLE_ALT_YT', 'farside.link/invidious'),
|
||||||
'instagram.com': os.getenv('WHOOGLE_ALT_IG', 'bibliogram.art/u'),
|
'instagram.com': os.getenv('WHOOGLE_ALT_IG', 'farside.link/bibliogram/u'),
|
||||||
'reddit.com': os.getenv('WHOOGLE_ALT_RD', 'libredd.it')
|
'reddit.com': os.getenv('WHOOGLE_ALT_RD', 'farside.link/libreddit'),
|
||||||
|
**dict.fromkeys([
|
||||||
|
'medium.com',
|
||||||
|
'levelup.gitconnected.com'
|
||||||
|
], os.getenv('WHOOGLE_ALT_MD', 'farside.link/scribe')),
|
||||||
|
'imgur.com': os.getenv('WHOOGLE_ALT_IMG', 'farside.link/rimgo'),
|
||||||
|
'wikipedia.org': os.getenv('WHOOGLE_ALT_WIKI', 'farside.link/wikiless'),
|
||||||
|
'imdb.com': os.getenv('WHOOGLE_ALT_IMDB', 'farside.link/libremdb'),
|
||||||
|
'quora.com': os.getenv('WHOOGLE_ALT_QUORA', 'farside.link/quetre')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def bold_search_terms(response: str, query: str) -> BeautifulSoup:
|
||||||
|
"""Wraps all search terms in bold tags (<b>). If any terms are wrapped
|
||||||
|
in quotes, only that exact phrase will be made bold.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
response: The initial response body for the query
|
||||||
|
query: The original search query
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BeautifulSoup: modified soup object with bold items
|
||||||
|
"""
|
||||||
|
response = BeautifulSoup(response, 'html.parser')
|
||||||
|
|
||||||
|
def replace_any_case(element: NavigableString, target_word: str) -> None:
|
||||||
|
# Replace all instances of the word, but maintaining the same case in
|
||||||
|
# the replacement
|
||||||
|
if len(element) == len(target_word):
|
||||||
|
return
|
||||||
|
|
||||||
|
if not re.match('.*[a-zA-Z0-9].*', target_word) or (
|
||||||
|
element.parent and element.parent.name == 'style'):
|
||||||
|
return
|
||||||
|
|
||||||
|
element.replace_with(BeautifulSoup(
|
||||||
|
re.sub(fr'\b((?![{{}}<>-]){target_word}(?![{{}}<>-]))\b',
|
||||||
|
r'<b>\1</b>',
|
||||||
|
html.escape(element),
|
||||||
|
flags=re.I), 'html.parser')
|
||||||
|
)
|
||||||
|
|
||||||
|
# Split all words out of query, grouping the ones wrapped in quotes
|
||||||
|
for word in re.split(r'\s+(?=[^"]*(?:"[^"]*"[^"]*)*$)', query):
|
||||||
|
word = re.sub(r'[^A-Za-z0-9 ]+', '', word)
|
||||||
|
target = response.find_all(
|
||||||
|
text=re.compile(r'' + re.escape(word), re.I))
|
||||||
|
for nav_str in target:
|
||||||
|
replace_any_case(nav_str, word)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
def has_ad_content(element: str) -> bool:
|
def has_ad_content(element: str) -> bool:
|
||||||
"""Inspects an HTML element for ad related content
|
"""Inspects an HTML element for ad related content
|
||||||
|
|
||||||
|
@ -40,7 +95,8 @@ def has_ad_content(element: str) -> bool:
|
||||||
bool: True/False for the element containing an ad
|
bool: True/False for the element containing an ad
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return (element.upper() in (value.upper() for value in BLACKLIST)
|
element_str = ''.join(filter(str.isalpha, element))
|
||||||
|
return (element_str.upper() in (value.upper() for value in BLACKLIST)
|
||||||
or 'ⓘ' in element)
|
or 'ⓘ' in element)
|
||||||
|
|
||||||
|
|
||||||
|
@ -72,16 +128,37 @@ def get_site_alt(link: str) -> str:
|
||||||
str: An updated (or ignored) result link
|
str: An updated (or ignored) result link
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
# Need to replace full hostname with alternative to encapsulate
|
||||||
|
# subdomains as well
|
||||||
|
parsed_link = urlparse.urlparse(link)
|
||||||
|
hostname = parsed_link.hostname
|
||||||
|
|
||||||
for site_key in SITE_ALTS.keys():
|
for site_key in SITE_ALTS.keys():
|
||||||
if site_key not in link:
|
if not hostname or site_key not in hostname or not SITE_ALTS[site_key]:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
link = link.replace(site_key, SITE_ALTS[site_key])
|
# Wikipedia -> Wikiless replacements require the subdomain (if it's
|
||||||
break
|
# a 2-char language code) to be passed as a URL param to Wikiless
|
||||||
|
# in order to preserve the language setting.
|
||||||
|
params = ''
|
||||||
|
if 'wikipedia' in hostname:
|
||||||
|
subdomain = hostname.split('.')[0]
|
||||||
|
if len(subdomain) == 2:
|
||||||
|
params = f'?lang={subdomain}'
|
||||||
|
|
||||||
for prefix in SKIP_PREFIX:
|
parsed_alt = urlparse.urlparse(SITE_ALTS[site_key])
|
||||||
link = link.replace(prefix, '//')
|
link = link.replace(hostname, SITE_ALTS[site_key]) + params
|
||||||
|
|
||||||
|
# If a scheme is specified in the alternative, this results in a
|
||||||
|
# replaced link that looks like "https://http://altservice.tld".
|
||||||
|
# In this case, we can remove the original scheme from the result
|
||||||
|
# and use the one specified for the alt.
|
||||||
|
if parsed_alt.scheme:
|
||||||
|
link = '//'.join(link.split('//')[1:])
|
||||||
|
|
||||||
|
for prefix in SKIP_PREFIX:
|
||||||
|
link = link.replace(prefix, '//')
|
||||||
|
break
|
||||||
|
|
||||||
return link
|
return link
|
||||||
|
|
||||||
|
@ -130,8 +207,224 @@ def append_nojs(result: BeautifulSoup) -> None:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
nojs_link = BeautifulSoup(features='html.parser').new_tag('a')
|
nojs_link = BeautifulSoup(features='html.parser').new_tag('a')
|
||||||
nojs_link['href'] = '/window?location=' + result['href']
|
nojs_link['href'] = f'{Endpoint.window}?nojs=1&location=' + result['href']
|
||||||
nojs_link['style'] = 'display:block;width:100%;'
|
nojs_link.string = ' NoJS Link'
|
||||||
nojs_link.string = 'NoJS Link: ' + nojs_link['href']
|
|
||||||
result.append(BeautifulSoup('<br><hr><br>', 'html.parser'))
|
|
||||||
result.append(nojs_link)
|
result.append(nojs_link)
|
||||||
|
|
||||||
|
|
||||||
|
def append_anon_view(result: BeautifulSoup, config: Config) -> None:
|
||||||
|
"""Appends an 'anonymous view' for a search result, where all site
|
||||||
|
contents are viewed through Whoogle as a proxy.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
result: The search result to append an anon view link to
|
||||||
|
nojs: Remove Javascript from Anonymous View
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
|
||||||
|
"""
|
||||||
|
av_link = BeautifulSoup(features='html.parser').new_tag('a')
|
||||||
|
nojs = 'nojs=1' if config.nojs else 'nojs=0'
|
||||||
|
location = f'location={result["href"]}'
|
||||||
|
av_link['href'] = f'{Endpoint.window}?{nojs}&{location}'
|
||||||
|
translation = current_app.config['TRANSLATIONS'][
|
||||||
|
config.get_localization_lang()
|
||||||
|
]
|
||||||
|
av_link.string = f'{translation["anon-view"]}'
|
||||||
|
av_link['class'] = 'anon-view'
|
||||||
|
result.append(av_link)
|
||||||
|
|
||||||
|
|
||||||
|
def add_ip_card(html_soup: BeautifulSoup, ip: str) -> BeautifulSoup:
|
||||||
|
"""Adds the client's IP address to the search results
|
||||||
|
if query contains keywords
|
||||||
|
|
||||||
|
Args:
|
||||||
|
html_soup: The parsed search result containing the keywords
|
||||||
|
ip: ip address of the client
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BeautifulSoup
|
||||||
|
|
||||||
|
"""
|
||||||
|
main_div = html_soup.select_one('#main')
|
||||||
|
if main_div:
|
||||||
|
# HTML IP card tag
|
||||||
|
ip_tag = html_soup.new_tag('div')
|
||||||
|
ip_tag['class'] = 'ZINbbc xpd O9g5cc uUPGi'
|
||||||
|
|
||||||
|
# For IP Address html tag
|
||||||
|
ip_address = html_soup.new_tag('div')
|
||||||
|
ip_address['class'] = 'kCrYT ip-address-div'
|
||||||
|
ip_address.string = ip
|
||||||
|
|
||||||
|
# Text below the IP address
|
||||||
|
ip_text = html_soup.new_tag('div')
|
||||||
|
ip_text.string = 'Your public IP address'
|
||||||
|
ip_text['class'] = 'kCrYT ip-text-div'
|
||||||
|
|
||||||
|
# Adding all the above html tags to the IP card
|
||||||
|
ip_tag.append(ip_address)
|
||||||
|
ip_tag.append(ip_text)
|
||||||
|
|
||||||
|
# Insert the element at the top of the result list
|
||||||
|
main_div.insert_before(ip_tag)
|
||||||
|
return html_soup
|
||||||
|
|
||||||
|
|
||||||
|
def check_currency(response: str) -> dict:
|
||||||
|
"""Check whether the results have currency conversion
|
||||||
|
|
||||||
|
Args:
|
||||||
|
response: Search query Result
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Consists of currency names and values
|
||||||
|
|
||||||
|
"""
|
||||||
|
soup = BeautifulSoup(response, 'html.parser')
|
||||||
|
currency_link = soup.find('a', {'href': 'https://g.co/gfd'})
|
||||||
|
if currency_link:
|
||||||
|
while 'class' not in currency_link.attrs or \
|
||||||
|
'ZINbbc' not in currency_link.attrs['class']:
|
||||||
|
if currency_link.parent:
|
||||||
|
currency_link = currency_link.parent
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
currency_link = currency_link.find_all(class_='BNeawe')
|
||||||
|
currency1 = currency_link[0].text
|
||||||
|
currency2 = currency_link[1].text
|
||||||
|
currency1 = currency1.rstrip('=').split(' ', 1)
|
||||||
|
currency2 = currency2.split(' ', 1)
|
||||||
|
|
||||||
|
# Handle differences in currency formatting
|
||||||
|
# i.e. "5.000" vs "5,000"
|
||||||
|
if currency2[0][-3] == ',':
|
||||||
|
currency1[0] = currency1[0].replace('.', '')
|
||||||
|
currency1[0] = currency1[0].replace(',', '.')
|
||||||
|
currency2[0] = currency2[0].replace('.', '')
|
||||||
|
currency2[0] = currency2[0].replace(',', '.')
|
||||||
|
else:
|
||||||
|
currency1[0] = currency1[0].replace(',', '')
|
||||||
|
currency2[0] = currency2[0].replace(',', '')
|
||||||
|
|
||||||
|
currency1_value = float(re.sub(r'[^\d\.]', '', currency1[0]))
|
||||||
|
currency1_label = currency1[1]
|
||||||
|
|
||||||
|
currency2_value = float(re.sub(r'[^\d\.]', '', currency2[0]))
|
||||||
|
currency2_label = currency2[1]
|
||||||
|
|
||||||
|
return {'currencyValue1': currency1_value,
|
||||||
|
'currencyLabel1': currency1_label,
|
||||||
|
'currencyValue2': currency2_value,
|
||||||
|
'currencyLabel2': currency2_label
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def add_currency_card(soup: BeautifulSoup,
|
||||||
|
conversion_details: dict) -> BeautifulSoup:
|
||||||
|
"""Adds the currency conversion boxes
|
||||||
|
to response of the search query
|
||||||
|
|
||||||
|
Args:
|
||||||
|
soup: Parsed search result
|
||||||
|
conversion_details: Dictionary of currency
|
||||||
|
related information
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BeautifulSoup
|
||||||
|
"""
|
||||||
|
# Element before which the code will be changed
|
||||||
|
# (This is the 'disclaimer' link)
|
||||||
|
element1 = soup.find('a', {'href': 'https://g.co/gfd'})
|
||||||
|
|
||||||
|
while 'class' not in element1.attrs or \
|
||||||
|
'nXE3Ob' not in element1.attrs['class']:
|
||||||
|
element1 = element1.parent
|
||||||
|
|
||||||
|
# Creating the conversion factor
|
||||||
|
conversion_factor = (conversion_details['currencyValue1'] /
|
||||||
|
conversion_details['currencyValue2'])
|
||||||
|
|
||||||
|
# Creating a new div for the input boxes
|
||||||
|
conversion_box = soup.new_tag('div')
|
||||||
|
conversion_box['class'] = 'conversion_box'
|
||||||
|
|
||||||
|
# Currency to be converted from
|
||||||
|
input_box1 = soup.new_tag('input')
|
||||||
|
input_box1['id'] = 'cb1'
|
||||||
|
input_box1['type'] = 'number'
|
||||||
|
input_box1['class'] = 'cb'
|
||||||
|
input_box1['value'] = conversion_details['currencyValue1']
|
||||||
|
input_box1['oninput'] = f'convert(1, 2, {1 / conversion_factor})'
|
||||||
|
|
||||||
|
label_box1 = soup.new_tag('label')
|
||||||
|
label_box1['for'] = 'cb1'
|
||||||
|
label_box1['class'] = 'cb_label'
|
||||||
|
label_box1.append(conversion_details['currencyLabel1'])
|
||||||
|
|
||||||
|
br = soup.new_tag('br')
|
||||||
|
|
||||||
|
# Currency to be converted to
|
||||||
|
input_box2 = soup.new_tag('input')
|
||||||
|
input_box2['id'] = 'cb2'
|
||||||
|
input_box2['type'] = 'number'
|
||||||
|
input_box2['class'] = 'cb'
|
||||||
|
input_box2['value'] = conversion_details['currencyValue2']
|
||||||
|
input_box2['oninput'] = f'convert(2, 1, {conversion_factor})'
|
||||||
|
|
||||||
|
label_box2 = soup.new_tag('label')
|
||||||
|
label_box2['for'] = 'cb2'
|
||||||
|
label_box2['class'] = 'cb_label'
|
||||||
|
label_box2.append(conversion_details['currencyLabel2'])
|
||||||
|
|
||||||
|
conversion_box.append(input_box1)
|
||||||
|
conversion_box.append(label_box1)
|
||||||
|
conversion_box.append(br)
|
||||||
|
conversion_box.append(input_box2)
|
||||||
|
conversion_box.append(label_box2)
|
||||||
|
|
||||||
|
element1.insert_before(conversion_box)
|
||||||
|
return soup
|
||||||
|
|
||||||
|
|
||||||
|
def get_tabs_content(tabs: dict,
|
||||||
|
full_query: str,
|
||||||
|
search_type: str,
|
||||||
|
preferences: str,
|
||||||
|
translation: dict) -> dict:
|
||||||
|
"""Takes the default tabs content and updates it according to the query.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tabs: The default content for the tabs
|
||||||
|
full_query: The original search query
|
||||||
|
search_type: The current search_type
|
||||||
|
translation: The translation to get the names of the tabs
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: contains the name, the href and if the tab is selected or not
|
||||||
|
"""
|
||||||
|
tabs = copy.deepcopy(tabs)
|
||||||
|
for tab_id, tab_content in tabs.items():
|
||||||
|
# update name to desired language
|
||||||
|
if tab_id in translation:
|
||||||
|
tab_content['name'] = translation[tab_id]
|
||||||
|
|
||||||
|
# update href with query
|
||||||
|
query = full_query.replace(f'&tbm={search_type}', '')
|
||||||
|
|
||||||
|
if tab_content['tbm'] is not None:
|
||||||
|
query = f"{query}&tbm={tab_content['tbm']}"
|
||||||
|
|
||||||
|
if preferences:
|
||||||
|
query = f"{query}&preferences={preferences}"
|
||||||
|
|
||||||
|
tab_content['href'] = tab_content['href'].format(query=query)
|
||||||
|
|
||||||
|
# update if selected tab (default all tab is selected)
|
||||||
|
if tab_content['tbm'] == search_type:
|
||||||
|
tabs['all']['selected'] = False
|
||||||
|
tab_content['selected'] = True
|
||||||
|
return tabs
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from app.filter import Filter
|
||||||
|
from app.request import gen_query
|
||||||
|
from app.utils.misc import get_proxy_host_url
|
||||||
|
from app.utils.results import get_first_link
|
||||||
from bs4 import BeautifulSoup as bsoup
|
from bs4 import BeautifulSoup as bsoup
|
||||||
from cryptography.fernet import Fernet, InvalidToken
|
from cryptography.fernet import Fernet, InvalidToken
|
||||||
from flask import g
|
from flask import g
|
||||||
|
|
||||||
from app.filter import Filter, get_first_link
|
|
||||||
from app.request import gen_query
|
|
||||||
|
|
||||||
TOR_BANNER = '<hr><h1 style="text-align: center">You are using Tor</h1><hr>'
|
TOR_BANNER = '<hr><h1 style="text-align: center">You are using Tor</h1><hr>'
|
||||||
CAPTCHA = 'div class="g-recaptcha"'
|
CAPTCHA = 'div class="g-recaptcha"'
|
||||||
|
|
||||||
|
@ -52,15 +54,16 @@ class Search:
|
||||||
Attributes:
|
Attributes:
|
||||||
request: the incoming flask request
|
request: the incoming flask request
|
||||||
config: the current user config settings
|
config: the current user config settings
|
||||||
session: the flask user session
|
session_key: the flask user fernet key
|
||||||
"""
|
"""
|
||||||
def __init__(self, request, config, session, cookies_disabled=False):
|
def __init__(self, request, config, session_key, cookies_disabled=False):
|
||||||
method = request.method
|
method = request.method
|
||||||
|
self.request = request
|
||||||
self.request_params = request.args if method == 'GET' else request.form
|
self.request_params = request.args if method == 'GET' else request.form
|
||||||
self.user_agent = request.headers.get('User-Agent')
|
self.user_agent = request.headers.get('User-Agent')
|
||||||
self.feeling_lucky = False
|
self.feeling_lucky = False
|
||||||
self.config = config
|
self.config = config
|
||||||
self.session = session
|
self.session_key = session_key
|
||||||
self.query = ''
|
self.query = ''
|
||||||
self.cookies_disabled = cookies_disabled
|
self.cookies_disabled = cookies_disabled
|
||||||
self.search_type = self.request_params.get(
|
self.search_type = self.request_params.get(
|
||||||
|
@ -95,7 +98,7 @@ class Search:
|
||||||
else:
|
else:
|
||||||
# Attempt to decrypt if this is an internal link
|
# Attempt to decrypt if this is an internal link
|
||||||
try:
|
try:
|
||||||
q = Fernet(self.session['key']).decrypt(q.encode()).decode()
|
q = Fernet(self.session_key).decrypt(q.encode()).decode()
|
||||||
except InvalidToken:
|
except InvalidToken:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -113,14 +116,21 @@ class Search:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
mobile = 'Android' in self.user_agent or 'iPhone' in self.user_agent
|
mobile = 'Android' in self.user_agent or 'iPhone' in self.user_agent
|
||||||
|
# reconstruct url if X-Forwarded-Host header present
|
||||||
|
root_url = get_proxy_host_url(
|
||||||
|
self.request,
|
||||||
|
self.request.url_root,
|
||||||
|
root=True)
|
||||||
|
|
||||||
content_filter = Filter(self.session['key'],
|
content_filter = Filter(self.session_key,
|
||||||
|
root_url=root_url,
|
||||||
mobile=mobile,
|
mobile=mobile,
|
||||||
config=self.config)
|
config=self.config,
|
||||||
|
query=self.query)
|
||||||
full_query = gen_query(self.query,
|
full_query = gen_query(self.query,
|
||||||
self.request_params,
|
self.request_params,
|
||||||
self.config,
|
self.config)
|
||||||
content_filter.near)
|
self.full_query = full_query
|
||||||
|
|
||||||
# force mobile search when view image is true and
|
# force mobile search when view image is true and
|
||||||
# the request is not already made by a mobile
|
# the request is not already made by a mobile
|
||||||
|
@ -132,17 +142,15 @@ class Search:
|
||||||
force_mobile=view_image)
|
force_mobile=view_image)
|
||||||
|
|
||||||
# Produce cleanable html soup from response
|
# Produce cleanable html soup from response
|
||||||
html_soup = bsoup(content_filter.reskin(get_body.text), 'html.parser')
|
html_soup = bsoup(get_body.text, 'html.parser')
|
||||||
|
|
||||||
# Replace current soup if view_image is active
|
# Replace current soup if view_image is active
|
||||||
if view_image:
|
if view_image:
|
||||||
html_soup = content_filter.view_image(html_soup)
|
html_soup = content_filter.view_image(html_soup)
|
||||||
|
|
||||||
# Indicate whether or not a Tor connection is active
|
# Indicate whether or not a Tor connection is active
|
||||||
tor_banner = bsoup('', 'html.parser')
|
|
||||||
if g.user_request.tor_valid:
|
if g.user_request.tor_valid:
|
||||||
tor_banner = bsoup(TOR_BANNER, 'html.parser')
|
html_soup.insert(0, bsoup(TOR_BANNER, 'html.parser'))
|
||||||
html_soup.insert(0, tor_banner)
|
|
||||||
|
|
||||||
if self.feeling_lucky:
|
if self.feeling_lucky:
|
||||||
return get_first_link(html_soup)
|
return get_first_link(html_soup)
|
||||||
|
@ -155,9 +163,21 @@ class Search:
|
||||||
self.request_params.to_dict(flat=True).items()
|
self.request_params.to_dict(flat=True).items()
|
||||||
if self.config.is_safe_key(k))
|
if self.config.is_safe_key(k))
|
||||||
for link in formatted_results.find_all('a', href=True):
|
for link in formatted_results.find_all('a', href=True):
|
||||||
|
link['rel'] = "nofollow noopener noreferrer"
|
||||||
if 'search?' not in link['href'] or link['href'].index(
|
if 'search?' not in link['href'] or link['href'].index(
|
||||||
'search?') > 1:
|
'search?') > 1:
|
||||||
continue
|
continue
|
||||||
link['href'] += param_str
|
link['href'] += param_str
|
||||||
|
|
||||||
return str(formatted_results)
|
return str(formatted_results)
|
||||||
|
|
||||||
|
def check_kw_ip(self) -> re.Match:
|
||||||
|
"""Checks for keywords related to 'my ip' in the query
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool
|
||||||
|
|
||||||
|
"""
|
||||||
|
return re.search("([^a-z0-9]|^)my *[^a-z0-9] *(ip|internet protocol)" +
|
||||||
|
"($|( *[^a-z0-9] *(((addres|address|adres|" +
|
||||||
|
"adress)|a)? *$)))", self.query.lower())
|
||||||
|
|
|
@ -4,7 +4,7 @@ from flask import current_app as app
|
||||||
REQUIRED_SESSION_VALUES = ['uuid', 'config', 'key']
|
REQUIRED_SESSION_VALUES = ['uuid', 'config', 'key']
|
||||||
|
|
||||||
|
|
||||||
def generate_user_key(cookies_disabled=False) -> bytes:
|
def generate_user_key() -> bytes:
|
||||||
"""Generates a key for encrypting searches and element URLs
|
"""Generates a key for encrypting searches and element URLs
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
@ -16,9 +16,6 @@ def generate_user_key(cookies_disabled=False) -> bytes:
|
||||||
str: A unique Fernet key
|
str: A unique Fernet key
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if cookies_disabled:
|
|
||||||
return app.default_key
|
|
||||||
|
|
||||||
# Generate/regenerate unique key per user
|
# Generate/regenerate unique key per user
|
||||||
return Fernet.generate_key()
|
return Fernet.generate_key()
|
||||||
|
|
||||||
|
|
23
charts/whoogle/.helmignore
Normal file
23
charts/whoogle/.helmignore
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# Patterns to ignore when building packages.
|
||||||
|
# This supports shell glob matching, relative path matching, and
|
||||||
|
# negation (prefixed with !). Only one pattern per line.
|
||||||
|
.DS_Store
|
||||||
|
# Common VCS dirs
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
.bzr/
|
||||||
|
.bzrignore
|
||||||
|
.hg/
|
||||||
|
.hgignore
|
||||||
|
.svn/
|
||||||
|
# Common backup files
|
||||||
|
*.swp
|
||||||
|
*.bak
|
||||||
|
*.tmp
|
||||||
|
*.orig
|
||||||
|
*~
|
||||||
|
# Various IDEs
|
||||||
|
.project
|
||||||
|
.idea/
|
||||||
|
*.tmproj
|
||||||
|
.vscode/
|
23
charts/whoogle/Chart.yaml
Normal file
23
charts/whoogle/Chart.yaml
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
apiVersion: v2
|
||||||
|
name: whoogle
|
||||||
|
description: A self hosted search engine on Kubernetes
|
||||||
|
type: application
|
||||||
|
version: 0.1.0
|
||||||
|
appVersion: 0.7.4
|
||||||
|
|
||||||
|
icon: https://github.com/benbusby/whoogle-search/raw/main/app/static/img/favicon/favicon-96x96.png
|
||||||
|
|
||||||
|
sources:
|
||||||
|
- https://github.com/benbusby/whoogle-search
|
||||||
|
- https://gitlab.com/benbusby/whoogle-search
|
||||||
|
- https://gogs.benbusby.com/benbusby/whoogle-search
|
||||||
|
|
||||||
|
keywords:
|
||||||
|
- whoogle
|
||||||
|
- degoogle
|
||||||
|
- search
|
||||||
|
- google
|
||||||
|
- search-engine
|
||||||
|
- privacy
|
||||||
|
- tor
|
||||||
|
- python
|
22
charts/whoogle/templates/NOTES.txt
Normal file
22
charts/whoogle/templates/NOTES.txt
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
1. Get the application URL by running these commands:
|
||||||
|
{{- if .Values.ingress.enabled }}
|
||||||
|
{{- range $host := .Values.ingress.hosts }}
|
||||||
|
{{- range .paths }}
|
||||||
|
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- else if contains "NodePort" .Values.service.type }}
|
||||||
|
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "whoogle.fullname" . }})
|
||||||
|
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||||
|
echo http://$NODE_IP:$NODE_PORT
|
||||||
|
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||||
|
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||||
|
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "whoogle.fullname" . }}'
|
||||||
|
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "whoogle.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
||||||
|
echo http://$SERVICE_IP:{{ .Values.service.port }}
|
||||||
|
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||||
|
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "whoogle.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||||
|
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
|
||||||
|
echo "Visit http://127.0.0.1:8080 to use your application"
|
||||||
|
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
|
||||||
|
{{- end }}
|
62
charts/whoogle/templates/_helpers.tpl
Normal file
62
charts/whoogle/templates/_helpers.tpl
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
{{/*
|
||||||
|
Expand the name of the chart.
|
||||||
|
*/}}
|
||||||
|
{{- define "whoogle.name" -}}
|
||||||
|
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create a default fully qualified app name.
|
||||||
|
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||||
|
If release name contains chart name it will be used as a full name.
|
||||||
|
*/}}
|
||||||
|
{{- define "whoogle.fullname" -}}
|
||||||
|
{{- if .Values.fullnameOverride }}
|
||||||
|
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- else }}
|
||||||
|
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||||
|
{{- if contains $name .Release.Name }}
|
||||||
|
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- else }}
|
||||||
|
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create chart name and version as used by the chart label.
|
||||||
|
*/}}
|
||||||
|
{{- define "whoogle.chart" -}}
|
||||||
|
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Common labels
|
||||||
|
*/}}
|
||||||
|
{{- define "whoogle.labels" -}}
|
||||||
|
helm.sh/chart: {{ include "whoogle.chart" . }}
|
||||||
|
{{ include "whoogle.selectorLabels" . }}
|
||||||
|
{{- if .Chart.AppVersion }}
|
||||||
|
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||||
|
{{- end }}
|
||||||
|
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Selector labels
|
||||||
|
*/}}
|
||||||
|
{{- define "whoogle.selectorLabels" -}}
|
||||||
|
app.kubernetes.io/name: {{ include "whoogle.name" . }}
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create the name of the service account to use
|
||||||
|
*/}}
|
||||||
|
{{- define "whoogle.serviceAccountName" -}}
|
||||||
|
{{- if .Values.serviceAccount.create }}
|
||||||
|
{{- default (include "whoogle.fullname" .) .Values.serviceAccount.name }}
|
||||||
|
{{- else }}
|
||||||
|
{{- default "default" .Values.serviceAccount.name }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
72
charts/whoogle/templates/deployment.yaml
Normal file
72
charts/whoogle/templates/deployment.yaml
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: {{ include "whoogle.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "whoogle.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
{{- if not .Values.autoscaling.enabled }}
|
||||||
|
replicas: {{ .Values.replicaCount }}
|
||||||
|
{{- end }}
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
{{- include "whoogle.selectorLabels" . | nindent 6 }}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
{{- with .Values.podAnnotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
labels:
|
||||||
|
{{- include "whoogle.selectorLabels" . | nindent 8 }}
|
||||||
|
spec:
|
||||||
|
{{- with .Values.image.pullSecrets }}
|
||||||
|
imagePullSecrets:
|
||||||
|
{{- range .}}
|
||||||
|
- name: {{ . }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
serviceAccountName: {{ include "whoogle.serviceAccountName" . }}
|
||||||
|
securityContext:
|
||||||
|
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||||
|
containers:
|
||||||
|
- name: whoogle
|
||||||
|
securityContext:
|
||||||
|
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||||
|
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||||
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||||
|
{{- with .Values.conf }}
|
||||||
|
env:
|
||||||
|
{{- range $k,$v := . }}
|
||||||
|
{{- if $v }}
|
||||||
|
- name: {{ $k }}
|
||||||
|
value: {{ tpl (toString $v) $ | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: {{ default 5000 .Values.conf.EXPOSE_PORT }}
|
||||||
|
protocol: TCP
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: http
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: http
|
||||||
|
resources:
|
||||||
|
{{- toYaml .Values.resources | nindent 12 }}
|
||||||
|
{{- with .Values.nodeSelector }}
|
||||||
|
nodeSelector:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.affinity }}
|
||||||
|
affinity:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.tolerations }}
|
||||||
|
tolerations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
28
charts/whoogle/templates/hpa.yaml
Normal file
28
charts/whoogle/templates/hpa.yaml
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
{{- if .Values.autoscaling.enabled }}
|
||||||
|
apiVersion: autoscaling/v2beta1
|
||||||
|
kind: HorizontalPodAutoscaler
|
||||||
|
metadata:
|
||||||
|
name: {{ include "whoogle.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "whoogle.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
scaleTargetRef:
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
name: {{ include "whoogle.fullname" . }}
|
||||||
|
minReplicas: {{ .Values.autoscaling.minReplicas }}
|
||||||
|
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
|
||||||
|
metrics:
|
||||||
|
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||||
|
- type: Resource
|
||||||
|
resource:
|
||||||
|
name: cpu
|
||||||
|
targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||||
|
- type: Resource
|
||||||
|
resource:
|
||||||
|
name: memory
|
||||||
|
targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
61
charts/whoogle/templates/ingress.yaml
Normal file
61
charts/whoogle/templates/ingress.yaml
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
{{- if .Values.ingress.enabled -}}
|
||||||
|
{{- $fullName := include "whoogle.fullname" . -}}
|
||||||
|
{{- $svcPort := .Values.service.port -}}
|
||||||
|
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
|
||||||
|
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
|
||||||
|
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||||
|
apiVersion: networking.k8s.io/v1beta1
|
||||||
|
{{- else -}}
|
||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
{{- end }}
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: {{ $fullName }}
|
||||||
|
labels:
|
||||||
|
{{- include "whoogle.labels" . | nindent 4 }}
|
||||||
|
{{- with .Values.ingress.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
spec:
|
||||||
|
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
|
||||||
|
ingressClassName: {{ .Values.ingress.className }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.ingress.tls }}
|
||||||
|
tls:
|
||||||
|
{{- range .Values.ingress.tls }}
|
||||||
|
- hosts:
|
||||||
|
{{- range .hosts }}
|
||||||
|
- {{ . | quote }}
|
||||||
|
{{- end }}
|
||||||
|
secretName: {{ .secretName }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
rules:
|
||||||
|
{{- range .Values.ingress.hosts }}
|
||||||
|
- host: {{ .host | quote }}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
{{- range .paths }}
|
||||||
|
- path: {{ .path }}
|
||||||
|
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
|
||||||
|
pathType: {{ .pathType }}
|
||||||
|
{{- end }}
|
||||||
|
backend:
|
||||||
|
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||||
|
service:
|
||||||
|
name: {{ $fullName }}
|
||||||
|
port:
|
||||||
|
number: {{ $svcPort }}
|
||||||
|
{{- else }}
|
||||||
|
serviceName: {{ $fullName }}
|
||||||
|
servicePort: {{ $svcPort }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
15
charts/whoogle/templates/service.yaml
Normal file
15
charts/whoogle/templates/service.yaml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ include "whoogle.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "whoogle.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
type: {{ .Values.service.type }}
|
||||||
|
ports:
|
||||||
|
- port: {{ .Values.service.port }}
|
||||||
|
targetPort: http
|
||||||
|
protocol: TCP
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
{{- include "whoogle.selectorLabels" . | nindent 4 }}
|
12
charts/whoogle/templates/serviceaccount.yaml
Normal file
12
charts/whoogle/templates/serviceaccount.yaml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{{- if .Values.serviceAccount.create -}}
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: {{ include "whoogle.serviceAccountName" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "whoogle.labels" . | nindent 4 }}
|
||||||
|
{{- with .Values.serviceAccount.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
15
charts/whoogle/templates/tests/test-connection.yaml
Normal file
15
charts/whoogle/templates/tests/test-connection.yaml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: "{{ include "whoogle.fullname" . }}-test-connection"
|
||||||
|
labels:
|
||||||
|
{{- include "whoogle.labels" . | nindent 4 }}
|
||||||
|
annotations:
|
||||||
|
"helm.sh/hook": test
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: wget
|
||||||
|
image: busybox
|
||||||
|
command: ['wget']
|
||||||
|
args: ['{{ include "whoogle.fullname" . }}:{{ .Values.service.port }}']
|
||||||
|
restartPolicy: Never
|
115
charts/whoogle/values.yaml
Normal file
115
charts/whoogle/values.yaml
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
# Default values for whoogle.
|
||||||
|
# This is a YAML-formatted file.
|
||||||
|
# Declare variables to be passed into your templates.
|
||||||
|
|
||||||
|
nameOverride: ""
|
||||||
|
fullnameOverride: ""
|
||||||
|
|
||||||
|
replicaCount: 1
|
||||||
|
image:
|
||||||
|
repository: benbusby/whoogle-search
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
# Overrides the image tag whose default is the chart appVersion.
|
||||||
|
tag: ""
|
||||||
|
pullSecrets: []
|
||||||
|
# - my-image-pull-secret
|
||||||
|
|
||||||
|
serviceAccount:
|
||||||
|
# Specifies whether a service account should be created
|
||||||
|
create: true
|
||||||
|
# Annotations to add to the service account
|
||||||
|
annotations: {}
|
||||||
|
# The name of the service account to use.
|
||||||
|
# If not set and create is true, a name is generated using the fullname template
|
||||||
|
name: ""
|
||||||
|
|
||||||
|
conf: {}
|
||||||
|
# WHOOGLE_URL_PREFIX: "" # The URL prefix to use for the whoogle instance (i.e. "/whoogle")
|
||||||
|
# WHOOGLE_DOTENV: "" # Load environment variables in whoogle.env
|
||||||
|
# WHOOGLE_USER: "" # The username for basic auth. WHOOGLE_PASS must also be set if used.
|
||||||
|
# WHOOGLE_PASS: "" # The password for basic auth. WHOOGLE_USER must also be set if used.
|
||||||
|
# WHOOGLE_PROXY_USER: "" # The username of the proxy server.
|
||||||
|
# WHOOGLE_PROXY_PASS: "" # The password of the proxy server.
|
||||||
|
# WHOOGLE_PROXY_TYPE: "" # The type of the proxy server. Can be "socks5", "socks4", or "http".
|
||||||
|
# WHOOGLE_PROXY_LOC: "" # The location of the proxy server (host or ip).
|
||||||
|
# EXPOSE_PORT: "" # The port where Whoogle will be exposed. (default 5000)
|
||||||
|
# HTTPS_ONLY: "" # Enforce HTTPS. (See https://github.com/benbusby/whoogle-search#https-enforcement)
|
||||||
|
# WHOOGLE_ALT_TW: "" # The twitter.com alternative to use when site alternatives are enabled in the config.
|
||||||
|
# WHOOGLE_ALT_YT: "" # The youtube.com alternative to use when site alternatives are enabled in the config.
|
||||||
|
# WHOOGLE_ALT_IG: "" # The instagram.com alternative to use when site alternatives are enabled in the config.
|
||||||
|
# WHOOGLE_ALT_RD: "" # The reddit.com alternative to use when site alternatives are enabled in the config.
|
||||||
|
# WHOOGLE_ALT_TL: "" # The Google Translate alternative to use. This is used for all "translate ____" searches.
|
||||||
|
# WHOOGLE_ALT_MD: "" # The medium.com alternative to use when site alternatives are enabled in the config.
|
||||||
|
# WHOOGLE_ALT_IMG: "" # The imgur.com alternative to use when site alternatives are enabled in the config.
|
||||||
|
# WHOOGLE_ALT_WIKI: "" # The wikipedia.com alternative to use when site alternatives are enabled in the config.
|
||||||
|
# WHOOGLE_ALT_IMDB: "" # The imdb.com alternative to use. Set to "" to continue using imdb.com when site alternatives are enabled.
|
||||||
|
# WHOOGLE_ALT_QUORA: "" # The quora.com alternative to use. Set to "" to continue using quora.com when site alternatives are enabled.
|
||||||
|
# WHOOGLE_AUTOCOMPLETE: "" # Controls visibility of autocomplete/search suggestions. Default on -- use '0' to disable
|
||||||
|
# WHOOGLE_MINIMAL: "" # Remove everything except basic result cards from all search queries.
|
||||||
|
|
||||||
|
# WHOOGLE_CONFIG_DISABLE: "" # Hide config from UI and disallow changes to config by client
|
||||||
|
# WHOOGLE_CONFIG_COUNTRY: "" # Filter results by hosting country
|
||||||
|
# WHOOGLE_CONFIG_LANGUAGE: "" # Set interface language
|
||||||
|
# WHOOGLE_CONFIG_SEARCH_LANGUAGE: "" # Set search result language
|
||||||
|
# WHOOGLE_CONFIG_BLOCK: "" # Block websites from search results (use comma-separated list)
|
||||||
|
# WHOOGLE_CONFIG_THEME: "" # Set theme mode (light, dark, or system)
|
||||||
|
# WHOOGLE_CONFIG_SAFE: "" # Enable safe searches
|
||||||
|
# WHOOGLE_CONFIG_ALTS: "" # Use social media site alternatives (nitter, invidious, etc)
|
||||||
|
# WHOOGLE_CONFIG_NEAR: "" # Restrict results to only those near a particular city
|
||||||
|
# WHOOGLE_CONFIG_TOR: "" # Use Tor routing (if available)
|
||||||
|
# WHOOGLE_CONFIG_NEW_TAB: "" # Always open results in new tab
|
||||||
|
# WHOOGLE_CONFIG_VIEW_IMAGE: "" # Enable View Image option
|
||||||
|
# WHOOGLE_CONFIG_GET_ONLY: "" # Search using GET requests only
|
||||||
|
# WHOOGLE_CONFIG_URL: "" # The root url of the instance (https://<your url>/)
|
||||||
|
# WHOOGLE_CONFIG_STYLE: "" # The custom CSS to use for styling (should be single line)
|
||||||
|
# WHOOGLE_CONFIG_PREFERENCES_ENCRYPTED: "" # Encrypt preferences token, requires key
|
||||||
|
# WHOOGLE_CONFIG_PREFERENCES_KEY: "" # Key to encrypt preferences in URL (REQUIRED to show url)
|
||||||
|
|
||||||
|
podAnnotations: {}
|
||||||
|
podSecurityContext: {}
|
||||||
|
# fsGroup: 2000
|
||||||
|
securityContext:
|
||||||
|
runAsUser: 0
|
||||||
|
# capabilities:
|
||||||
|
# drop:
|
||||||
|
# - ALL
|
||||||
|
# readOnlyRootFilesystem: true
|
||||||
|
|
||||||
|
service:
|
||||||
|
type: ClusterIP
|
||||||
|
port: 5000
|
||||||
|
|
||||||
|
ingress:
|
||||||
|
enabled: false
|
||||||
|
className: ""
|
||||||
|
annotations: {}
|
||||||
|
# kubernetes.io/ingress.class: nginx
|
||||||
|
# kubernetes.io/tls-acme: "true"
|
||||||
|
hosts:
|
||||||
|
- host: whoogle.example.com
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: ImplementationSpecific
|
||||||
|
tls: []
|
||||||
|
# - secretName: chart-example-tls
|
||||||
|
# hosts:
|
||||||
|
# - whoogle.example.com
|
||||||
|
|
||||||
|
resources: {}
|
||||||
|
# requests:
|
||||||
|
# cpu: 100m
|
||||||
|
# memory: 128Mi
|
||||||
|
# limits:
|
||||||
|
# cpu: 100m
|
||||||
|
# memory: 128Mi
|
||||||
|
|
||||||
|
autoscaling:
|
||||||
|
enabled: false
|
||||||
|
minReplicas: 1
|
||||||
|
maxReplicas: 100
|
||||||
|
targetCPUUtilizationPercentage: 80
|
||||||
|
# targetMemoryUtilizationPercentage: 80
|
||||||
|
|
||||||
|
nodeSelector: {}
|
||||||
|
tolerations: []
|
||||||
|
affinity: {}
|
80
docker-compose-traefik.yaml
Normal file
80
docker-compose-traefik.yaml
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
# can't use mem_limit in a 3.x docker-compose file in non swarm mode
|
||||||
|
# see https://github.com/docker/compose/issues/4513
|
||||||
|
version: "2.4"
|
||||||
|
|
||||||
|
services:
|
||||||
|
traefik:
|
||||||
|
image: "traefik:v2.7"
|
||||||
|
container_name: "traefik"
|
||||||
|
command:
|
||||||
|
#- "--log.level=DEBUG"
|
||||||
|
- "--api.insecure=true"
|
||||||
|
- "--providers.docker=true"
|
||||||
|
- "--providers.docker.exposedbydefault=false"
|
||||||
|
- "--entrypoints.websecure.address=:443"
|
||||||
|
- "--certificatesresolvers.myresolver.acme.tlschallenge=true"
|
||||||
|
#- "--certificatesresolvers.myresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"
|
||||||
|
- "--certificatesresolvers.myresolver.acme.email=change@domain.name"
|
||||||
|
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
|
||||||
|
ports:
|
||||||
|
- "443:443"
|
||||||
|
- "8080:8080"
|
||||||
|
volumes:
|
||||||
|
- "./letsencrypt:/letsencrypt"
|
||||||
|
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
||||||
|
|
||||||
|
whoogle-search:
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.whoami.rule=Host(`change.host.name`)"
|
||||||
|
- "traefik.http.routers.whoami.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.whoami.tls.certresolver=myresolver"
|
||||||
|
- "traefik.http.services.whoogle-search.loadbalancer.server.port=5000"
|
||||||
|
image: ${WHOOGLE_IMAGE:-benbusby/whoogle-search}
|
||||||
|
container_name: whoogle-search
|
||||||
|
restart: unless-stopped
|
||||||
|
pids_limit: 50
|
||||||
|
mem_limit: 256mb
|
||||||
|
memswap_limit: 256mb
|
||||||
|
# user debian-tor from tor package
|
||||||
|
user: whoogle
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
tmpfs:
|
||||||
|
- /config/:size=10M,uid=927,gid=927,mode=1700
|
||||||
|
- /var/lib/tor/:size=15M,uid=927,gid=927,mode=1700
|
||||||
|
- /run/tor/:size=1M,uid=927,gid=927,mode=1700
|
||||||
|
environment: # Uncomment to configure environment variables
|
||||||
|
# Basic auth configuration, uncomment to enable
|
||||||
|
#- WHOOGLE_USER=<auth username>
|
||||||
|
#- WHOOGLE_PASS=<auth password>
|
||||||
|
# Proxy configuration, uncomment to enable
|
||||||
|
#- WHOOGLE_PROXY_USER=<proxy username>
|
||||||
|
#- WHOOGLE_PROXY_PASS=<proxy password>
|
||||||
|
#- WHOOGLE_PROXY_TYPE=<proxy type (http|https|socks4|socks5)
|
||||||
|
#- WHOOGLE_PROXY_LOC=<proxy host/ip>
|
||||||
|
# Site alternative configurations, uncomment to enable
|
||||||
|
# Note: If not set, the feature will still be available
|
||||||
|
# with default values.
|
||||||
|
#- WHOOGLE_ALT_TW=farside.link/nitter
|
||||||
|
#- WHOOGLE_ALT_YT=farside.link/invidious
|
||||||
|
#- WHOOGLE_ALT_IG=farside.link/bibliogram/u
|
||||||
|
#- WHOOGLE_ALT_RD=farside.link/libreddit
|
||||||
|
#- WHOOGLE_ALT_MD=farside.link/scribe
|
||||||
|
#- WHOOGLE_ALT_TL=farside.link/lingva
|
||||||
|
#- WHOOGLE_ALT_IMG=farside.link/rimgo
|
||||||
|
#- WHOOGLE_ALT_WIKI=farside.link/wikiless
|
||||||
|
#- WHOOGLE_ALT_IMDB=farside.link/libremdb
|
||||||
|
#- WHOOGLE_ALT_QUORA=farside.link/quetre
|
||||||
|
# - WHOOGLE_CONFIG_DISABLE=1
|
||||||
|
# - WHOOGLE_CONFIG_SEARCH_LANGUAGE=lang_en
|
||||||
|
# - WHOOGLE_CONFIG_GET_ONLY=1
|
||||||
|
# - WHOOGLE_CONFIG_COUNTRY=FR
|
||||||
|
# - WHOOGLE_CONFIG_PREFERENCES_ENCRYPTED=1
|
||||||
|
# - WHOOGLE_CONFIG_PREFERENCES_KEY="NEEDS_TO_BE_MODIFIED"
|
||||||
|
#env_file: # Alternatively, load variables from whoogle.env
|
||||||
|
#- whoogle.env
|
||||||
|
ports:
|
||||||
|
- 8000:5000
|
|
@ -1,26 +1,25 @@
|
||||||
# cant use mem_limit in a 3.x docker-compose file in non swarm mode
|
# can't use mem_limit in a 3.x docker-compose file in non swarm mode
|
||||||
# see https://github.com/docker/compose/issues/4513
|
# see https://github.com/docker/compose/issues/4513
|
||||||
version: "2.4"
|
version: "2.4"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
whoogle-search:
|
whoogle-search:
|
||||||
image: benbusby/whoogle-search
|
image: ${WHOOGLE_IMAGE:-benbusby/whoogle-search}
|
||||||
container_name: whoogle-search
|
container_name: whoogle-search
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
pids_limit: 50
|
pids_limit: 50
|
||||||
mem_limit: 256mb
|
mem_limit: 256mb
|
||||||
memswap_limit: 256mb
|
memswap_limit: 256mb
|
||||||
# user debian-tor from tor package
|
# user debian-tor from tor package
|
||||||
user: '102'
|
user: whoogle
|
||||||
security_opt:
|
security_opt:
|
||||||
- no-new-privileges
|
- no-new-privileges
|
||||||
cap_drop:
|
cap_drop:
|
||||||
- ALL
|
- ALL
|
||||||
read_only: true
|
|
||||||
tmpfs:
|
tmpfs:
|
||||||
- /config/:size=10M,uid=102,gid=102,mode=1700
|
- /config/:size=10M,uid=927,gid=927,mode=1700
|
||||||
- /var/lib/tor/:size=10M,uid=102,gid=102,mode=1700
|
- /var/lib/tor/:size=15M,uid=927,gid=927,mode=1700
|
||||||
- /run/tor/:size=1M,uid=102,gid=102,mode=1700
|
- /run/tor/:size=1M,uid=927,gid=927,mode=1700
|
||||||
#environment: # Uncomment to configure environment variables
|
#environment: # Uncomment to configure environment variables
|
||||||
# Basic auth configuration, uncomment to enable
|
# Basic auth configuration, uncomment to enable
|
||||||
#- WHOOGLE_USER=<auth username>
|
#- WHOOGLE_USER=<auth username>
|
||||||
|
@ -32,11 +31,17 @@ services:
|
||||||
#- WHOOGLE_PROXY_LOC=<proxy host/ip>
|
#- WHOOGLE_PROXY_LOC=<proxy host/ip>
|
||||||
# Site alternative configurations, uncomment to enable
|
# Site alternative configurations, uncomment to enable
|
||||||
# Note: If not set, the feature will still be available
|
# Note: If not set, the feature will still be available
|
||||||
# with default values.
|
# with default values.
|
||||||
#- WHOOGLE_ALT_TW=nitter.net
|
#- WHOOGLE_ALT_TW=farside.link/nitter
|
||||||
#- WHOOGLE_ALT_YT=invidious.snopyta.org
|
#- WHOOGLE_ALT_YT=farside.link/invidious
|
||||||
#- WHOOGLE_ALT_IG=bibliogram.art/u
|
#- WHOOGLE_ALT_IG=farside.link/bibliogram/u
|
||||||
#- WHOOGLE_ALT_RD=libredd.it
|
#- WHOOGLE_ALT_RD=farside.link/libreddit
|
||||||
|
#- WHOOGLE_ALT_MD=farside.link/scribe
|
||||||
|
#- WHOOGLE_ALT_TL=farside.link/lingva
|
||||||
|
#- WHOOGLE_ALT_IMG=farside.link/rimgo
|
||||||
|
#- WHOOGLE_ALT_WIKI=farside.link/wikiless
|
||||||
|
#- WHOOGLE_ALT_IMDB=farside.link/libremdb
|
||||||
|
#- WHOOGLE_ALT_QUORA=farside.link/quetre
|
||||||
#env_file: # Alternatively, load variables from whoogle.env
|
#env_file: # Alternatively, load variables from whoogle.env
|
||||||
#- whoogle.env
|
#- whoogle.env
|
||||||
ports:
|
ports:
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 215 KiB |
BIN
docs/screenshot_desktop.png
Normal file
BIN
docs/screenshot_desktop.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 214 KiB |
Binary file not shown.
Before Width: | Height: | Size: 139 KiB |
BIN
docs/screenshot_mobile.png
Normal file
BIN
docs/screenshot_mobile.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 61 KiB |
0
letsencrypt/acme.json
Normal file
0
letsencrypt/acme.json
Normal file
13
misc/instances.txt
Normal file
13
misc/instances.txt
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
https://gowogle.voring.me
|
||||||
|
https://s.tokhmi.xyz
|
||||||
|
https://search.albony.xyz
|
||||||
|
https://search.garudalinux.org
|
||||||
|
https://search.dr460nf1r3.org
|
||||||
|
https://search.sethforprivacy.com
|
||||||
|
https://whoogle.fossho.st
|
||||||
|
https://www.whooglesearch.ml
|
||||||
|
https://whoogle.dcs0.hu
|
||||||
|
https://whoogle.esmailelbob.xyz
|
||||||
|
https://whoogle.lunar.icu
|
||||||
|
https://whoogle.privacydev.net
|
||||||
|
https://search.wef.lol
|
1
misc/tor/control.conf
Normal file
1
misc/tor/control.conf
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# Place password here. Keep this safe.
|
|
@ -1,7 +1,16 @@
|
||||||
#!/bin/bash
|
#!/bin/sh
|
||||||
|
|
||||||
|
if [ "$WHOOGLE_TOR_SERVICE" == "0" ]; then
|
||||||
|
echo "Skipping Tor startup..."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "$(whoami)" != "root" ]; then
|
if [ "$(whoami)" != "root" ]; then
|
||||||
tor -f /etc/tor/torrc
|
tor -f /etc/tor/torrc
|
||||||
else
|
else
|
||||||
service tor start
|
if (grep alpine /etc/os-release >/dev/null); then
|
||||||
|
rc-service tor start
|
||||||
|
else
|
||||||
|
service tor start
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
|
@ -6,3 +6,4 @@ CookieAuthFileGroupReadable 1
|
||||||
ExtORPortCookieAuthFileGroupReadable 1
|
ExtORPortCookieAuthFileGroupReadable 1
|
||||||
CacheDirectoryGroupReadable 1
|
CacheDirectoryGroupReadable 1
|
||||||
CookieAuthFile /var/lib/tor/control_auth_cookie
|
CookieAuthFile /var/lib/tor/control_auth_cookie
|
||||||
|
Log debug-notice file /dev/null
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
attrs==19.3.0
|
attrs==19.3.0
|
||||||
beautifulsoup4==4.8.2
|
beautifulsoup4==4.10.0
|
||||||
bs4==0.0.1
|
brotli==1.0.9
|
||||||
cachelib==0.1
|
cachelib==0.4.1
|
||||||
certifi==2020.4.5.1
|
certifi==2020.4.5.1
|
||||||
cffi==1.13.2
|
cffi==1.15.0
|
||||||
chardet==3.0.4
|
chardet==3.0.4
|
||||||
Click==7.0
|
click==8.0.3
|
||||||
cryptography==3.3.2
|
cryptography==3.3.2
|
||||||
|
cssutils==2.4.0
|
||||||
|
defusedxml==0.7.1
|
||||||
Flask==1.1.1
|
Flask==1.1.1
|
||||||
Flask-Session==0.3.2
|
|
||||||
idna==2.9
|
idna==2.9
|
||||||
itsdangerous==1.1.0
|
itsdangerous==1.1.0
|
||||||
Jinja2==2.11.3
|
Jinja2==2.11.3
|
||||||
|
@ -18,17 +19,17 @@ packaging==20.4
|
||||||
pluggy==0.13.1
|
pluggy==0.13.1
|
||||||
py==1.10.0
|
py==1.10.0
|
||||||
pycodestyle==2.6.0
|
pycodestyle==2.6.0
|
||||||
pycparser==2.19
|
pycparser==2.21
|
||||||
pyOpenSSL==19.1.0
|
pyOpenSSL==19.1.0
|
||||||
pyparsing==2.4.7
|
pyparsing==2.4.7
|
||||||
PySocks==1.7.1
|
PySocks==1.7.1
|
||||||
pytest==5.4.1
|
pytest==6.2.5
|
||||||
python-dateutil==2.8.1
|
python-dateutil==2.8.1
|
||||||
requests==2.25.1
|
requests==2.25.1
|
||||||
soupsieve==1.9.5
|
soupsieve==1.9.5
|
||||||
stem==1.8.0
|
stem==1.8.0
|
||||||
urllib3==1.26.5
|
urllib3==1.26.5
|
||||||
waitress==1.4.3
|
waitress==2.1.2
|
||||||
wcwidth==0.1.9
|
wcwidth==0.1.9
|
||||||
Werkzeug==0.16.0
|
Werkzeug==0.16.0
|
||||||
python-dotenv==0.16.0
|
python-dotenv==0.16.0
|
||||||
|
|
24
run
24
run
|
@ -1,26 +1,36 @@
|
||||||
#!/bin/bash
|
#!/bin/sh
|
||||||
# Usage:
|
# Usage:
|
||||||
# ./run # Runs the full web app
|
# ./run # Runs the full web app
|
||||||
# ./run test # Runs the testing suite
|
# ./run test # Runs the testing suite
|
||||||
|
|
||||||
set -euo pipefail
|
set -e
|
||||||
|
|
||||||
SCRIPT_DIR="$(builtin cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
SCRIPT_DIR="$(CDPATH= command cd -- "$(dirname -- "$0")" && pwd -P)"
|
||||||
|
|
||||||
# Set directory to serve static content from
|
# Set directory to serve static content from
|
||||||
SUBDIR="${1:-app}"
|
SUBDIR="${1:-app}"
|
||||||
export APP_ROOT="$SCRIPT_DIR/$SUBDIR"
|
export APP_ROOT="$SCRIPT_DIR/$SUBDIR"
|
||||||
export STATIC_FOLDER="$APP_ROOT/static"
|
export STATIC_FOLDER="$APP_ROOT/static"
|
||||||
|
|
||||||
|
# Clear out build directory
|
||||||
|
rm -f "$SCRIPT_DIR"/app/static/build/*.js
|
||||||
|
rm -f "$SCRIPT_DIR"/app/static/build/*.css
|
||||||
|
|
||||||
# Check for regular vs test run
|
# Check for regular vs test run
|
||||||
if [[ "$SUBDIR" == "test" ]]; then
|
if [ "$SUBDIR" = "test" ]; then
|
||||||
# Set up static files for testing
|
# Set up static files for testing
|
||||||
rm -rf "$STATIC_FOLDER"
|
rm -rf "$STATIC_FOLDER"
|
||||||
ln -s "$SCRIPT_DIR/app/static" "$STATIC_FOLDER"
|
ln -s "$SCRIPT_DIR/app/static" "$STATIC_FOLDER"
|
||||||
pytest -sv
|
pytest -sv
|
||||||
else
|
else
|
||||||
mkdir -p "$STATIC_FOLDER"
|
mkdir -p "$STATIC_FOLDER"
|
||||||
python3 -um app \
|
|
||||||
--host "${ADDRESS:-0.0.0.0}" \
|
if [ ! -z "$UNIX_SOCKET" ]; then
|
||||||
--port "${PORT:-"${EXPOSE_PORT:-5000}"}"
|
python3 -um app \
|
||||||
|
--unix-socket "$UNIX_SOCKET"
|
||||||
|
else
|
||||||
|
python3 -um app \
|
||||||
|
--host "${ADDRESS:-0.0.0.0}" \
|
||||||
|
--port "${PORT:-"${EXPOSE_PORT:-5000}"}"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
39
setup.cfg
Normal file
39
setup.cfg
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
[metadata]
|
||||||
|
name = whoogle-search
|
||||||
|
url = https://github.com/benbusby/whoogle-search
|
||||||
|
description = Self-hosted, ad-free, privacy-respecting metasearch engine
|
||||||
|
long_description = file: README.md
|
||||||
|
long_description_content_type = text/markdown
|
||||||
|
keywords = search, metasearch, flask, adblock, degoogle, privacy
|
||||||
|
author = Ben Busby
|
||||||
|
author_email = contact@benbusby.com
|
||||||
|
license = MIT
|
||||||
|
classifiers =
|
||||||
|
Programming Language :: Python :: 3
|
||||||
|
License :: OSI Approved :: MIT License
|
||||||
|
Operating System :: OS Independent
|
||||||
|
|
||||||
|
[options]
|
||||||
|
packages = find:
|
||||||
|
include_package_data = True
|
||||||
|
install_requires=
|
||||||
|
beautifulsoup4
|
||||||
|
cssutils
|
||||||
|
cryptography
|
||||||
|
defusedxml
|
||||||
|
Flask
|
||||||
|
Flask-Session
|
||||||
|
python-dotenv
|
||||||
|
requests
|
||||||
|
stem
|
||||||
|
waitress
|
||||||
|
|
||||||
|
[options.extras_require]
|
||||||
|
test =
|
||||||
|
pytest
|
||||||
|
python-dateutil
|
||||||
|
dev = pycodestyle
|
||||||
|
|
||||||
|
[options.entry_points]
|
||||||
|
console_scripts =
|
||||||
|
whoogle-search = app.routes:run_app
|
31
setup.py
31
setup.py
|
@ -1,29 +1,8 @@
|
||||||
|
import os
|
||||||
import setuptools
|
import setuptools
|
||||||
|
|
||||||
long_description = open('README.md', 'r').read()
|
optional_dev_tag = ''
|
||||||
|
if os.getenv('DEV_BUILD'):
|
||||||
|
optional_dev_tag = '.dev' + os.getenv('DEV_BUILD')
|
||||||
|
|
||||||
requirements = list(open('requirements.txt', 'r'))
|
setuptools.setup(version='0.7.4' + optional_dev_tag)
|
||||||
|
|
||||||
setuptools.setup(
|
|
||||||
author='Ben Busby',
|
|
||||||
author_email='benbusby@protonmail.com',
|
|
||||||
name='whoogle-search',
|
|
||||||
version='0.5.4',
|
|
||||||
include_package_data=True,
|
|
||||||
install_requires=requirements,
|
|
||||||
description='Self-hosted, ad-free, privacy-respecting metasearch engine',
|
|
||||||
long_description=long_description,
|
|
||||||
long_description_content_type='text/markdown',
|
|
||||||
url='https://github.com/benbusby/whoogle-search',
|
|
||||||
entry_points={
|
|
||||||
'console_scripts': [
|
|
||||||
'whoogle-search=app.routes:run_app',
|
|
||||||
]
|
|
||||||
},
|
|
||||||
packages=setuptools.find_packages(),
|
|
||||||
classifiers=[
|
|
||||||
'Programming Language :: Python :: 3',
|
|
||||||
'License :: OSI Approved :: MIT License',
|
|
||||||
'Operating System :: OS Independent',
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ demo_config = {
|
||||||
'nojs': str(random.getrandbits(1)),
|
'nojs': str(random.getrandbits(1)),
|
||||||
'lang_interface': random.choice(app.config['LANGUAGES'])['value'],
|
'lang_interface': random.choice(app.config['LANGUAGES'])['value'],
|
||||||
'lang_search': random.choice(app.config['LANGUAGES'])['value'],
|
'lang_search': random.choice(app.config['LANGUAGES'])['value'],
|
||||||
'ctry': random.choice(app.config['COUNTRIES'])['value']
|
'country': random.choice(app.config['COUNTRIES'])['value']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
|
from app.models.endpoint import Endpoint
|
||||||
|
|
||||||
|
|
||||||
def test_autocomplete_get(client):
|
def test_autocomplete_get(client):
|
||||||
rv = client.get('/autocomplete?q=green+eggs+and')
|
rv = client.get(f'/{Endpoint.autocomplete}?q=green+eggs+and')
|
||||||
assert rv._status_code == 200
|
assert rv._status_code == 200
|
||||||
assert len(rv.data) >= 1
|
assert len(rv.data) >= 1
|
||||||
assert b'green eggs and ham' in rv.data
|
assert b'green eggs and ham' in rv.data
|
||||||
|
|
||||||
|
|
||||||
def test_autocomplete_post(client):
|
def test_autocomplete_post(client):
|
||||||
rv = client.post('/autocomplete', data=dict(q='the+cat+in+the'))
|
rv = client.post(f'/{Endpoint.autocomplete}',
|
||||||
|
data=dict(q='the+cat+in+the'))
|
||||||
assert rv._status_code == 200
|
assert rv._status_code == 200
|
||||||
assert len(rv.data) >= 1
|
assert len(rv.data) >= 1
|
||||||
assert b'the cat in the hat' in rv.data
|
assert b'the cat in the hat' in rv.data
|
||||||
|
|
|
@ -1,9 +1,24 @@
|
||||||
from cryptography.fernet import Fernet
|
from cryptography.fernet import Fernet
|
||||||
|
|
||||||
from app import app
|
from app import app
|
||||||
|
from app.models.endpoint import Endpoint
|
||||||
from app.utils.session import generate_user_key, valid_user_session
|
from app.utils.session import generate_user_key, valid_user_session
|
||||||
|
|
||||||
|
|
||||||
|
JAPAN_PREFS = 'uG-gGIJwHdqxl6DrS3mnu_511HlQcRpxYlG03Xs-' \
|
||||||
|
+ '_znXNiJWI9nLOkRLkiiFwIpeUYMTGfUF5-t9fP5DGmzDLEt04DCx703j3nPf' \
|
||||||
|
+ '29v_RWkU7gXw_44m2oAFIaKGmYlu4Z0bKyu9k5WXfL9Dy6YKKnpcR5CiaFsG' \
|
||||||
|
+ 'rccNRkAPYm-eYGAFUV8M59f8StsGd_M-gHKGS9fLok7EhwBWjHxBJ2Kv8hsT' \
|
||||||
|
+ '87zftP2gMJOevTdNnezw2Y5WOx-ZotgeheCW1BYCFcRqatlov21PHp22NGVG' \
|
||||||
|
+ '8ZuBNAFW0bE99WSdyT7dUIvzeWCLJpbdSsq-3FUUZkxbRdFYlGd8vY1UgVAp' \
|
||||||
|
+ 'OSie2uAmpgLFXygO-VfNBBZ68Q7gAap2QtzHCiKD5cFYwH3LPgVJ-DoZvJ6k' \
|
||||||
|
+ 'alt34TaYiJphgiqFKV4SCeVmLWTkr0SF3xakSR78yYJU_d41D2ng-TojA9XZ' \
|
||||||
|
+ 'uR2ZqjSvPKOWvjimu89YhFOgJxG1Po8Henj5h9OL9VXXvdvlJwBSAKw1E3FV' \
|
||||||
|
+ '7UHWiglMxPblfxqou1cYckMYkFeIMCD2SBtju68mBiQh2k328XRPTsQ_ocby' \
|
||||||
|
+ 'cgVKnleGperqbD6crRk3Z9xE5sVCjujn9JNVI-7mqOITMZ0kntq9uJ3R5n25' \
|
||||||
|
+ 'Vec0TJ0P19nEtvjY0nJIrIjtnBg=='
|
||||||
|
|
||||||
|
|
||||||
def test_generate_user_keys():
|
def test_generate_user_keys():
|
||||||
key = generate_user_key()
|
key = generate_user_key()
|
||||||
assert Fernet(key)
|
assert Fernet(key)
|
||||||
|
@ -37,14 +52,27 @@ def test_query_decryption(client):
|
||||||
rv = client.get('/')
|
rv = client.get('/')
|
||||||
cookie = rv.headers['Set-Cookie']
|
cookie = rv.headers['Set-Cookie']
|
||||||
|
|
||||||
rv = client.get('/search?q=test+1', headers={'Cookie': cookie})
|
rv = client.get(f'/{Endpoint.search}?q=test+1', headers={'Cookie': cookie})
|
||||||
assert rv._status_code == 200
|
assert rv._status_code == 200
|
||||||
|
|
||||||
with client.session_transaction() as session:
|
with client.session_transaction() as session:
|
||||||
assert valid_user_session(session)
|
assert valid_user_session(session)
|
||||||
|
|
||||||
rv = client.get('/search?q=test+2', headers={'Cookie': cookie})
|
rv = client.get(f'/{Endpoint.search}?q=test+2', headers={'Cookie': cookie})
|
||||||
assert rv._status_code == 200
|
assert rv._status_code == 200
|
||||||
|
|
||||||
with client.session_transaction() as session:
|
with client.session_transaction() as session:
|
||||||
assert valid_user_session(session)
|
assert valid_user_session(session)
|
||||||
|
|
||||||
|
|
||||||
|
def test_prefs_url(client):
|
||||||
|
base_url = f'/{Endpoint.search}?q=wikipedia'
|
||||||
|
rv = client.get(base_url)
|
||||||
|
assert rv._status_code == 200
|
||||||
|
assert b'wikipedia.org' in rv.data
|
||||||
|
assert b'ja.wikipedia.org' not in rv.data
|
||||||
|
|
||||||
|
rv = client.get(f'{base_url}&preferences={JAPAN_PREFS}')
|
||||||
|
assert rv._status_code == 200
|
||||||
|
assert b'ja.wikipedia.org' in rv.data
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from app.filter import Filter
|
from app.filter import Filter
|
||||||
|
from app.models.config import Config
|
||||||
|
from app.models.endpoint import Endpoint
|
||||||
from app.utils.session import generate_user_key
|
from app.utils.session import generate_user_key
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from dateutil.parser import *
|
from dateutil.parser import ParserError, parse
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from test.conftest import demo_config
|
from test.conftest import demo_config
|
||||||
|
@ -10,7 +12,7 @@ from test.conftest import demo_config
|
||||||
|
|
||||||
def get_search_results(data):
|
def get_search_results(data):
|
||||||
secret_key = generate_user_key()
|
secret_key = generate_user_key()
|
||||||
soup = Filter(user_key=secret_key).clean(
|
soup = Filter(user_key=secret_key, config=Config(**demo_config)).clean(
|
||||||
BeautifulSoup(data, 'html.parser'))
|
BeautifulSoup(data, 'html.parser'))
|
||||||
|
|
||||||
main_divs = soup.find('div', {'id': 'main'})
|
main_divs = soup.find('div', {'id': 'main'})
|
||||||
|
@ -30,27 +32,39 @@ def get_search_results(data):
|
||||||
|
|
||||||
|
|
||||||
def test_get_results(client):
|
def test_get_results(client):
|
||||||
rv = client.get('/search?q=test')
|
rv = client.get(f'/{Endpoint.search}?q=test')
|
||||||
assert rv._status_code == 200
|
assert rv._status_code == 200
|
||||||
|
|
||||||
# Depending on the search, there can be more
|
# Depending on the search, there can be more
|
||||||
# than 10 result divs
|
# than 10 result divs
|
||||||
assert len(get_search_results(rv.data)) >= 10
|
results = get_search_results(rv.data)
|
||||||
assert len(get_search_results(rv.data)) <= 15
|
assert len(results) >= 10
|
||||||
|
assert len(results) <= 15
|
||||||
|
|
||||||
|
|
||||||
def test_post_results(client):
|
def test_post_results(client):
|
||||||
rv = client.post('/search', data=dict(q='test'))
|
rv = client.post(f'/{Endpoint.search}', data=dict(q='test'))
|
||||||
assert rv._status_code == 200
|
assert rv._status_code == 200
|
||||||
|
|
||||||
# Depending on the search, there can be more
|
# Depending on the search, there can be more
|
||||||
# than 10 result divs
|
# than 10 result divs
|
||||||
assert len(get_search_results(rv.data)) >= 10
|
results = get_search_results(rv.data)
|
||||||
assert len(get_search_results(rv.data)) <= 15
|
assert len(results) >= 10
|
||||||
|
assert len(results) <= 15
|
||||||
|
|
||||||
|
|
||||||
|
def test_translate_search(client):
|
||||||
|
rv = client.post(f'/{Endpoint.search}', data=dict(q='translate hola'))
|
||||||
|
assert rv._status_code == 200
|
||||||
|
|
||||||
|
# Pretty weak test, but better than nothing
|
||||||
|
str_data = str(rv.data)
|
||||||
|
assert 'iframe' in str_data
|
||||||
|
assert '/auto/en/ hola' in str_data
|
||||||
|
|
||||||
|
|
||||||
def test_block_results(client):
|
def test_block_results(client):
|
||||||
rv = client.post('/search', data=dict(q='pinterest'))
|
rv = client.post(f'/{Endpoint.search}', data=dict(q='pinterest'))
|
||||||
assert rv._status_code == 200
|
assert rv._status_code == 200
|
||||||
|
|
||||||
has_pinterest = False
|
has_pinterest = False
|
||||||
|
@ -62,28 +76,27 @@ def test_block_results(client):
|
||||||
assert has_pinterest
|
assert has_pinterest
|
||||||
|
|
||||||
demo_config['block'] = 'pinterest.com'
|
demo_config['block'] = 'pinterest.com'
|
||||||
rv = client.post('/config', data=demo_config)
|
rv = client.post(f'/{Endpoint.config}', data=demo_config)
|
||||||
assert rv._status_code == 302
|
assert rv._status_code == 302
|
||||||
|
|
||||||
rv = client.post('/search', data=dict(q='pinterest'))
|
rv = client.post(f'/{Endpoint.search}', data=dict(q='pinterest'))
|
||||||
assert rv._status_code == 200
|
assert rv._status_code == 200
|
||||||
|
|
||||||
for link in BeautifulSoup(rv.data, 'html.parser').find_all('a', href=True):
|
for link in BeautifulSoup(rv.data, 'html.parser').find_all('a', href=True):
|
||||||
assert 'pinterest.com' not in urlparse(link['href']).netloc
|
result_site = urlparse(link['href']).netloc
|
||||||
|
if not result_site:
|
||||||
|
continue
|
||||||
|
assert result_site not in 'pinterest.com'
|
||||||
|
|
||||||
|
|
||||||
# TODO: Unit test the site alt method instead -- the results returned
|
def test_view_my_ip(client):
|
||||||
# are too unreliable for this test in particular.
|
rv = client.post(f'/{Endpoint.search}', data=dict(q='my ip address'))
|
||||||
# def test_site_alts(client):
|
assert rv._status_code == 200
|
||||||
# rv = client.post('/search', data=dict(q='twitter official account'))
|
|
||||||
# assert b'twitter.com/Twitter' in rv.data
|
|
||||||
|
|
||||||
# client.post('/config', data=dict(alts=True))
|
# Pretty weak test, but better than nothing
|
||||||
# assert json.loads(client.get('/config').data)['alts']
|
str_data = str(rv.data)
|
||||||
|
assert 'Your public IP address' in str_data
|
||||||
# rv = client.post('/search', data=dict(q='twitter official account'))
|
assert '127.0.0.1' in str_data
|
||||||
# assert b'twitter.com/Twitter' not in rv.data
|
|
||||||
# assert b'nitter.net/Twitter' in rv.data
|
|
||||||
|
|
||||||
|
|
||||||
def test_recent_results(client):
|
def test_recent_results(client):
|
||||||
|
@ -94,7 +107,7 @@ def test_recent_results(client):
|
||||||
}
|
}
|
||||||
|
|
||||||
for time, num_days in times.items():
|
for time, num_days in times.items():
|
||||||
rv = client.post('/search', data=dict(q='test :' + time))
|
rv = client.post(f'/{Endpoint.search}', data=dict(q='test :' + time))
|
||||||
result_divs = get_search_results(rv.data)
|
result_divs = get_search_results(rv.data)
|
||||||
|
|
||||||
current_date = datetime.now()
|
current_date = datetime.now()
|
||||||
|
@ -109,3 +122,23 @@ def test_recent_results(client):
|
||||||
assert (current_date - date).days <= (num_days + 5)
|
assert (current_date - date).days <= (num_days + 5)
|
||||||
except ParserError:
|
except ParserError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_leading_slash_search(client):
|
||||||
|
# Ensure searches with a leading slash are interpreted
|
||||||
|
# correctly as queries and not endpoints
|
||||||
|
q = '/test'
|
||||||
|
rv = client.get(f'/{Endpoint.search}?q={q}')
|
||||||
|
assert rv._status_code == 200
|
||||||
|
|
||||||
|
soup = Filter(
|
||||||
|
user_key=generate_user_key(),
|
||||||
|
config=Config(**demo_config),
|
||||||
|
query=q
|
||||||
|
).clean(BeautifulSoup(rv.data, 'html.parser'))
|
||||||
|
|
||||||
|
for link in soup.find_all('a', href=True):
|
||||||
|
if 'start=' not in link['href']:
|
||||||
|
continue
|
||||||
|
|
||||||
|
assert link['href'].startswith(f'{Endpoint.search}')
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from app import app
|
from app import app
|
||||||
|
from app.models.endpoint import Endpoint
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
@ -11,62 +12,66 @@ def test_main(client):
|
||||||
|
|
||||||
|
|
||||||
def test_search(client):
|
def test_search(client):
|
||||||
rv = client.get('/search?q=test')
|
rv = client.get(f'/{Endpoint.search}?q=test')
|
||||||
assert rv._status_code == 200
|
assert rv._status_code == 200
|
||||||
|
|
||||||
|
|
||||||
def test_feeling_lucky(client):
|
def test_feeling_lucky(client):
|
||||||
rv = client.get('/search?q=!%20test')
|
rv = client.get(f'/{Endpoint.search}?q=!%20test')
|
||||||
assert rv._status_code == 303
|
assert rv._status_code == 303
|
||||||
|
|
||||||
|
|
||||||
def test_ddg_bang(client):
|
def test_ddg_bang(client):
|
||||||
# Bang at beginning of query
|
# Bang at beginning of query
|
||||||
rv = client.get('/search?q=!gh%20whoogle')
|
rv = client.get(f'/{Endpoint.search}?q=!gh%20whoogle')
|
||||||
assert rv._status_code == 302
|
assert rv._status_code == 302
|
||||||
assert rv.headers.get('Location').startswith('https://github.com')
|
assert rv.headers.get('Location').startswith('https://github.com')
|
||||||
|
|
||||||
# Move bang to end of query
|
# Move bang to end of query
|
||||||
rv = client.get('/search?q=github%20!w')
|
rv = client.get(f'/{Endpoint.search}?q=github%20!w')
|
||||||
assert rv._status_code == 302
|
assert rv._status_code == 302
|
||||||
assert rv.headers.get('Location').startswith('https://en.wikipedia.org')
|
assert rv.headers.get('Location').startswith('https://en.wikipedia.org')
|
||||||
|
|
||||||
# Move bang to middle of query
|
# Move bang to middle of query
|
||||||
rv = client.get('/search?q=big%20!r%20chungus')
|
rv = client.get(f'/{Endpoint.search}?q=big%20!r%20chungus')
|
||||||
assert rv._status_code == 302
|
assert rv._status_code == 302
|
||||||
assert rv.headers.get('Location').startswith('https://www.reddit.com')
|
assert rv.headers.get('Location').startswith('https://www.reddit.com')
|
||||||
|
|
||||||
|
# Ensure bang is case insensitive
|
||||||
|
rv = client.get(f'/{Endpoint.search}?q=!GH%20whoogle')
|
||||||
|
assert rv._status_code == 302
|
||||||
|
assert rv.headers.get('Location').startswith('https://github.com')
|
||||||
|
|
||||||
|
# Ensure bang without a query still redirects to the result
|
||||||
|
rv = client.get(f'/{Endpoint.search}?q=!gh')
|
||||||
|
assert rv._status_code == 302
|
||||||
|
assert rv.headers.get('Location').startswith('https://github.com')
|
||||||
|
|
||||||
|
|
||||||
def test_config(client):
|
def test_config(client):
|
||||||
rv = client.post('/config', data=demo_config)
|
rv = client.post(f'/{Endpoint.config}', data=demo_config)
|
||||||
assert rv._status_code == 302
|
assert rv._status_code == 302
|
||||||
|
|
||||||
rv = client.get('/config')
|
rv = client.get(f'/{Endpoint.config}')
|
||||||
assert rv._status_code == 200
|
assert rv._status_code == 200
|
||||||
|
|
||||||
config = json.loads(rv.data)
|
config = json.loads(rv.data)
|
||||||
for key in demo_config.keys():
|
for key in demo_config.keys():
|
||||||
assert config[key] == demo_config[key]
|
assert config[key] == demo_config[key]
|
||||||
|
|
||||||
# Test setting config via search
|
|
||||||
custom_config = '&dark=1&lang_interface=lang_en'
|
|
||||||
rv = client.get('/search?q=test' + custom_config)
|
|
||||||
assert rv._status_code == 200
|
|
||||||
assert custom_config.replace('&', '&') in str(rv.data)
|
|
||||||
|
|
||||||
# Test disabling changing config from client
|
# Test disabling changing config from client
|
||||||
app.config['CONFIG_DISABLE'] = 1
|
app.config['CONFIG_DISABLE'] = 1
|
||||||
dark_mod = not demo_config['dark']
|
dark_mod = not demo_config['dark']
|
||||||
demo_config['dark'] = dark_mod
|
demo_config['dark'] = dark_mod
|
||||||
rv = client.post('/config', data=demo_config)
|
rv = client.post(f'/{Endpoint.config}', data=demo_config)
|
||||||
assert rv._status_code == 403
|
assert rv._status_code == 403
|
||||||
|
|
||||||
rv = client.get('/config')
|
rv = client.get(f'/{Endpoint.config}')
|
||||||
config = json.loads(rv.data)
|
config = json.loads(rv.data)
|
||||||
assert config['dark'] != dark_mod
|
assert config['dark'] != dark_mod
|
||||||
|
|
||||||
|
|
||||||
def test_opensearch(client):
|
def test_opensearch(client):
|
||||||
rv = client.get('/opensearch.xml')
|
rv = client.get(f'/{Endpoint.opensearch}')
|
||||||
assert rv._status_code == 200
|
assert rv._status_code == 200
|
||||||
assert '<ShortName>Whoogle</ShortName>' in str(rv.data)
|
assert '<ShortName>Whoogle</ShortName>' in str(rv.data)
|
||||||
|
|
|
@ -1,23 +1,39 @@
|
||||||
|
# ----------------------------------
|
||||||
|
# Rename to "whoogle.env" before use
|
||||||
|
# ----------------------------------
|
||||||
# You can set Whoogle environment variables here, but must
|
# You can set Whoogle environment variables here, but must
|
||||||
# modify your deployment to enable these values:
|
# modify your deployment to enable these values:
|
||||||
# - Local: Set WHOOGLE_DOTENV=1
|
# - Local: Set WHOOGLE_DOTENV=1
|
||||||
# - docker-compose: Uncomment the env_file option
|
# - docker-compose: Uncomment the env_file option
|
||||||
# - docker: Add "--env-file ./whoogle.env" to your build command
|
# - docker: Add "--env-file ./whoogle.env" to your build command
|
||||||
|
|
||||||
#WHOOGLE_ALT_TW=nitter.net
|
#WHOOGLE_ALT_TW=farside.link/nitter
|
||||||
#WHOOGLE_ALT_YT=invidious.snopyta.org
|
#WHOOGLE_ALT_YT=farside.link/invidious
|
||||||
#WHOOGLE_ALT_IG=bibliogram.art/u
|
#WHOOGLE_ALT_IG=farside.link/bibliogram/u
|
||||||
#WHOOGLE_ALT_RD=libredd.it
|
#WHOOGLE_ALT_RD=farside.link/libreddit
|
||||||
|
#WHOOGLE_ALT_MD=farside.link/scribe
|
||||||
|
#WHOOGLE_ALT_TL=farside.link/lingva
|
||||||
|
#WHOOGLE_ALT_IMG=farside.link/rimgo
|
||||||
|
#WHOOGLE_ALT_WIKI=farside.link/wikiless
|
||||||
|
#WHOOGLE_ALT_IMDB=farside.link/libremdb
|
||||||
|
#WHOOGLE_ALT_QUORA=farside.link/quetre
|
||||||
#WHOOGLE_USER=""
|
#WHOOGLE_USER=""
|
||||||
#WHOOGLE_PASS=""
|
#WHOOGLE_PASS=""
|
||||||
#WHOOGLE_PROXY_USER=""
|
#WHOOGLE_PROXY_USER=""
|
||||||
#WHOOGLE_PROXY_PASS=""
|
#WHOOGLE_PROXY_PASS=""
|
||||||
#WHOOGLE_PROXY_TYPE=""
|
#WHOOGLE_PROXY_TYPE=""
|
||||||
#WHOOGLE_PROXY_LOC=""
|
#WHOOGLE_PROXY_LOC=""
|
||||||
|
#WHOOGLE_CSP=1
|
||||||
#HTTPS_ONLY=1
|
#HTTPS_ONLY=1
|
||||||
|
|
||||||
|
# The URL prefix to use for the whoogle instance (i.e. "/whoogle")
|
||||||
|
#WHOOGLE_URL_PREFIX=""
|
||||||
|
|
||||||
|
# Restrict results to only those near a particular city
|
||||||
|
#WHOOGLE_CONFIG_NEAR=denver
|
||||||
|
|
||||||
# See app/static/settings/countries.json for values
|
# See app/static/settings/countries.json for values
|
||||||
#WHOOGLE_CONFIG_COUNTRY=countryUK
|
#WHOOGLE_CONFIG_COUNTRY=US
|
||||||
|
|
||||||
# See app/static/settings/languages.json for values
|
# See app/static/settings/languages.json for values
|
||||||
#WHOOGLE_CONFIG_LANGUAGE=lang_en
|
#WHOOGLE_CONFIG_LANGUAGE=lang_en
|
||||||
|
@ -31,8 +47,8 @@
|
||||||
# Block websites from search results (comma-separated list)
|
# Block websites from search results (comma-separated list)
|
||||||
#WHOOGLE_CONFIG_BLOCK=pinterest.com,whitehouse.gov
|
#WHOOGLE_CONFIG_BLOCK=pinterest.com,whitehouse.gov
|
||||||
|
|
||||||
# Dark mode
|
# Theme (light, dark, or system)
|
||||||
#WHOOGLE_CONFIG_DARK=1
|
#WHOOGLE_CONFIG_THEME=system
|
||||||
|
|
||||||
# Safe search mode
|
# Safe search mode
|
||||||
#WHOOGLE_CONFIG_SAFE=1
|
#WHOOGLE_CONFIG_SAFE=1
|
||||||
|
@ -47,13 +63,31 @@
|
||||||
#WHOOGLE_CONFIG_NEW_TAB=1
|
#WHOOGLE_CONFIG_NEW_TAB=1
|
||||||
|
|
||||||
# Enable View Image option
|
# Enable View Image option
|
||||||
#WHOOGLE_CONFIG_VIEW_IMAGE=1
|
#WHOOGLE_CONFIG_VIEW_IMAGE=1
|
||||||
|
|
||||||
# Search using GET requests only (exposes query in logs)
|
# Search using GET requests only (exposes query in logs)
|
||||||
#WHOOGLE_CONFIG_GET_ONLY=1
|
#WHOOGLE_CONFIG_GET_ONLY=1
|
||||||
|
|
||||||
|
# Remove everything except basic result cards from all search queries
|
||||||
|
#WHOOGLE_MINIMAL=0
|
||||||
|
|
||||||
|
# Set the number of results per page
|
||||||
|
#WHOOGLE_RESULTS_PER_PAGE=10
|
||||||
|
|
||||||
|
# Controls visibility of autocomplete/search suggestions
|
||||||
|
#WHOOGLE_AUTOCOMPLETE=1
|
||||||
|
|
||||||
|
# The port where Whoogle will be exposed
|
||||||
|
#EXPOSE_PORT=5000
|
||||||
|
|
||||||
# Set instance URL
|
# Set instance URL
|
||||||
#WHOOGLE_CONFIG_URL=https://<whoogle url>/
|
#WHOOGLE_CONFIG_URL=https://<whoogle url>/
|
||||||
|
|
||||||
# Set custom CSS styling/theming
|
# Set custom CSS styling/theming
|
||||||
#WHOOGLE_CONFIG_STYLE=":root { /* LIGHT THEME COLORS */ --whoogle-background: #d8dee9; --whoogle-accent: #2e3440; --whoogle-text: #3B4252; --whoogle-contrast-text: #eceff4; --whoogle-secondary-text: #70757a; --whoogle-result-bg: #fff; --whoogle-result-title: #4c566a; --whoogle-result-url: #81a1c1; --whoogle-result-visited: #a3be8c; /* DARK THEME COLORS */ --whoogle-dark-background: #222; --whoogle-dark-accent: #685e79; --whoogle-dark-text: #fff; --whoogle-dark-contrast-text: #000; --whoogle-dark-secondary-text: #bbb; --whoogle-dark-result-bg: #000; --whoogle-dark-result-title: #1967d2; --whoogle-dark-result-url: #4b11a8; --whoogle-dark-result-visited: #bbbbff; }"
|
#WHOOGLE_CONFIG_STYLE=":root { /* LIGHT THEME COLORS */ --whoogle-background: #d8dee9; --whoogle-accent: #2e3440; --whoogle-text: #3B4252; --whoogle-contrast-text: #eceff4; --whoogle-secondary-text: #70757a; --whoogle-result-bg: #fff; --whoogle-result-title: #4c566a; --whoogle-result-url: #81a1c1; --whoogle-result-visited: #a3be8c; /* DARK THEME COLORS */ --whoogle-dark-background: #222; --whoogle-dark-accent: #685e79; --whoogle-dark-text: #fff; --whoogle-dark-contrast-text: #000; --whoogle-dark-secondary-text: #bbb; --whoogle-dark-result-bg: #000; --whoogle-dark-result-title: #1967d2; --whoogle-dark-result-url: #4b11a8; --whoogle-dark-result-visited: #bbbbff; }"
|
||||||
|
|
||||||
|
# Enable preferences encryption (requires key)
|
||||||
|
#WHOOGLE_CONFIG_PREFERENCES_ENCRYPTED=1
|
||||||
|
|
||||||
|
# Set Key to encode config in url
|
||||||
|
#WHOOGLE_CONFIG_PREFERENCES_KEY="NEEDS_TO_BE_MODIFIED"
|
Loading…
Reference in New Issue
Block a user