Compare commits
238 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e70bbabd63 | ||
![]() |
4fad68adf2 | ||
![]() |
89d07abb6c | ||
![]() |
a8205c3043 | ||
![]() |
1b9f7990b6 | ||
![]() |
c54f4a79a6 | ||
![]() |
223f00c3c0 | ||
![]() |
466cb63d0e | ||
![]() |
c056322037 | ||
![]() |
5eb609d0b2 | ||
![]() |
a016a1bcf4 | ||
![]() |
6924f5ce0d | ||
![]() |
6abe5511f4 | ||
![]() |
9d7ab1e2f8 | ||
![]() |
441ce72527 | ||
![]() |
58d54c6384 | ||
![]() |
add9313a99 | ||
![]() |
436233f718 | ||
![]() |
58b7512707 | ||
![]() |
cc7907fb84 | ||
![]() |
ee5a536861 | ||
![]() |
5b8ac0c52e | ||
![]() |
97394f2b02 | ||
![]() |
91112f1b7b | ||
![]() |
81420e035a | ||
![]() |
a509169110 | ||
![]() |
79b6e9a157 | ||
![]() |
5cf5ad992a | ||
![]() |
95f872ccde | ||
![]() |
06c2310e3a | ||
![]() |
9bfdd88a5e | ||
![]() |
37ff61dfac | ||
![]() |
80e41e6b44 | ||
![]() |
f18bf07ac3 | ||
![]() |
fd20135af0 | ||
![]() |
7a1ebfe975 | ||
![]() |
efbbd6b9d6 | ||
![]() |
4d1d3f4984 | ||
![]() |
c389c26220 | ||
![]() |
ef54f00212 | ||
![]() |
af60509a8d | ||
![]() |
2395bb7a6a | ||
![]() |
c216c033ef | ||
![]() |
aaf90b52bb | ||
![]() |
7313edff46 | ||
![]() |
cdbe550737 | ||
![]() |
70dc750c7a | ||
![]() |
b5ae07613b | ||
![]() |
166b28040a | ||
![]() |
57398a9b3b | ||
![]() |
b97f3dd4c0 | ||
![]() |
9f68c843d6 | ||
![]() |
2a0b9a47b2 | ||
![]() |
1644a4a04e | ||
![]() |
6a10efbd9e | ||
![]() |
9cc1004fb8 | ||
![]() |
cdf0b50284 | ||
![]() |
2950aa869b | ||
![]() |
7bda165ca3 | ||
![]() |
81b7fd1876 | ||
![]() |
a7e937f7c6 | ||
![]() |
c2873190c9 | ||
![]() |
f7513bab69 | ||
![]() |
330ae964f3 | ||
![]() |
67b6110087 | ||
![]() |
4292ec7f63 | ||
![]() |
3ef191b5d8 | ||
![]() |
c36396e9cb | ||
![]() |
0d013c788f | ||
![]() |
ec05f8d52f | ||
![]() |
e2070045bf | ||
![]() |
b093d39ed1 | ||
![]() |
e9c4ed6399 | ||
![]() |
8b99a87020 | ||
![]() |
3a2e0b262e | ||
![]() |
8830615abc | ||
![]() |
693ca3a9a8 | ||
![]() |
a35b1dabbc | ||
![]() |
a623210244 | ||
![]() |
abbe29d9d3 | ||
![]() |
19be033ba2 | ||
![]() |
92e8ede24e | ||
![]() |
4a0089686e | ||
![]() |
a40b98341b | ||
![]() |
18fc14dc5b | ||
![]() |
8a3c9ea397 | ||
![]() |
ee25d3a23d | ||
![]() |
c705bc7391 | ||
![]() |
2ac9e37696 | ||
![]() |
6f9d11a6ed | ||
![]() |
63a2ea56ed | ||
![]() |
4962659acb | ||
![]() |
8a2a6f3265 | ||
![]() |
1b4e4e144e | ||
![]() |
29992985bc | ||
![]() |
179fc5e020 | ||
![]() |
421a8ac054 | ||
![]() |
0003da4bb9 | ||
![]() |
2b8d100cfb | ||
![]() |
f0a1223f40 | ||
![]() |
958f74e1f5 | ||
![]() |
08732fac9e | ||
![]() |
f65529f328 | ||
![]() |
f213a2a64a | ||
![]() |
2cb4b9e3ca | ||
![]() |
17dd1f1df1 | ||
![]() |
4b7ab3b283 | ||
![]() |
349b87ec18 | ||
![]() |
1eb8e04ed1 | ||
![]() |
74a5e24901 | ||
![]() |
bf6c2505b1 | ||
![]() |
9d9022ed99 | ||
![]() |
25e7134bef | ||
![]() |
5ae9160d38 | ||
![]() |
076948dd0e | ||
![]() |
b39ba0533a | ||
![]() |
229c9388cf | ||
![]() |
f970b62f12 | ||
![]() |
31feb7228f | ||
![]() |
b1e468ff01 | ||
![]() |
8c426ab180 | ||
![]() |
f7c4381ba6 | ||
![]() |
baa8bd0eb4 | ||
![]() |
1759c119a8 | ||
![]() |
74f7975e62 | ||
![]() |
0c65eb9616 | ||
![]() |
3f827bbf19 | ||
![]() |
fb8a2ea325 | ||
![]() |
6b56dab4c1 | ||
![]() |
7ca69e752d | ||
![]() |
da53db2a81 | ||
![]() |
c4c32a4bcc | ||
![]() |
991fe6d910 | ||
![]() |
fab65d720d | ||
![]() |
df760ffbae | ||
![]() |
09db4ff730 | ||
![]() |
16794df68d | ||
![]() |
94b208dd3f | ||
![]() |
12ce174b9a | ||
![]() |
e318594d9b | ||
![]() |
026714bb84 | ||
![]() |
e5a5aad997 | ||
![]() |
ccf9f06f2f | ||
![]() |
6955ec6161 | ||
![]() |
fdc63b862e | ||
![]() |
aa54491ae0 | ||
![]() |
cec10e81d3 | ||
![]() |
2827a4ef47 | ||
![]() |
a760476d1b | ||
![]() |
4f77f3680d | ||
![]() |
253ea62f8f | ||
![]() |
c24caceb03 | ||
![]() |
4f85076a2b | ||
![]() |
76c78d8584 | ||
![]() |
3dda8b25ef | ||
![]() |
08aa1ab8f1 | ||
![]() |
7041b43db9 | ||
![]() |
424e6dd341 | ||
![]() |
c9c197bb5f | ||
![]() |
7a852aa876 | ||
![]() |
8fbbdf2cec | ||
![]() |
3dc6d14377 | ||
![]() |
cd7fce2822 | ||
![]() |
fd85f1573a | ||
![]() |
0310f0f542 | ||
![]() |
1226b8db9c | ||
![]() |
cde05ea55d | ||
![]() |
3bd785b9b7 | ||
![]() |
33742ce247 | ||
![]() |
08b16f5a0c | ||
![]() |
d099b46336 | ||
![]() |
09a90ec46a | ||
![]() |
6bd48e40a7 | ||
![]() |
2d23e0e952 | ||
![]() |
1a66b195d4 | ||
![]() |
a7fe1fd0df | ||
![]() |
abbf037115 | ||
![]() |
06fd29f663 | ||
![]() |
7494a14bc2 | ||
![]() |
6696f2b12b | ||
![]() |
77884d05f2 | ||
![]() |
3e39e0e041 | ||
![]() |
2a37619028 | ||
![]() |
75682de892 | ||
![]() |
e99db8db26 | ||
![]() |
6ca51ecdcb | ||
![]() |
a3fa999b0d | ||
![]() |
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 |
65 changed files with 3058 additions and 798 deletions
4
.github/workflows/buildx.yml
vendored
4
.github/workflows/buildx.yml
vendored
|
@ -42,10 +42,10 @@ jobs:
|
|||
docker buildx ls
|
||||
docker buildx build --push \
|
||||
--tag benbusby/whoogle-search:latest \
|
||||
--platform linux/amd64,linux/arm/v7,linux/arm64 .
|
||||
--platform linux/amd64,linux/arm64 .
|
||||
docker buildx build --push \
|
||||
--tag ghcr.io/benbusby/whoogle-search:latest \
|
||||
--platform linux/amd64,linux/arm/v7,linux/arm64 .
|
||||
--platform linux/amd64,linux/arm64 .
|
||||
- name: build and push tag
|
||||
if: startsWith(github.ref, 'refs/tags')
|
||||
run: |
|
||||
|
|
2
.github/workflows/docker_main.yml
vendored
2
.github/workflows/docker_main.yml
vendored
|
@ -23,6 +23,6 @@ jobs:
|
|||
- name: build and test (docker-compose)
|
||||
run: |
|
||||
docker rm -f whoogle-search-nocompose
|
||||
WHOOGLE_IMAGE="whoogle-search:test" docker-compose up --detach
|
||||
WHOOGLE_IMAGE="whoogle-search:test" docker compose up --detach
|
||||
sleep 15
|
||||
docker exec whoogle-search curl -f http://localhost:5000/healthz || exit 1
|
||||
|
|
4
.github/workflows/docker_tests.yml
vendored
4
.github/workflows/docker_tests.yml
vendored
|
@ -18,9 +18,9 @@ jobs:
|
|||
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)
|
||||
- name: build and test (docker compose)
|
||||
run: |
|
||||
docker rm -f whoogle-search-nocompose
|
||||
WHOOGLE_IMAGE="whoogle-search:test" docker-compose up --detach
|
||||
WHOOGLE_IMAGE="whoogle-search:test" docker compose up --detach
|
||||
sleep 15
|
||||
docker exec whoogle-search curl -f http://localhost:5000/healthz || exit 1
|
||||
|
|
21
.github/workflows/pep8.yml
vendored
21
.github/workflows/pep8.yml
vendored
|
@ -1,21 +0,0 @@
|
|||
name: pep8
|
||||
|
||||
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: |
|
||||
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/*
|
103
.github/workflows/pypi.yml
vendored
103
.github/workflows/pypi.yml
vendored
|
@ -10,59 +10,58 @@ jobs:
|
|||
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/
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v5
|
||||
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 }}
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v5
|
||||
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 }}
|
||||
|
|
18
.github/workflows/tests.yml
vendored
18
.github/workflows/tests.yml
vendored
|
@ -6,12 +6,12 @@ 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
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Install dependencies
|
||||
run: pip install --upgrade pip && pip install -r requirements.txt
|
||||
- name: Run tests
|
||||
run: ./run test
|
||||
|
|
9
.gitignore
vendored
9
.gitignore
vendored
|
@ -1,15 +1,18 @@
|
|||
venv/
|
||||
.venv/
|
||||
.idea/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pem
|
||||
*.conf
|
||||
*.key
|
||||
config.json
|
||||
test/static
|
||||
flask_session/
|
||||
app/static/config
|
||||
app/static/custom_config
|
||||
app/static/bangs
|
||||
app/static/bangs/*
|
||||
!app/static/bangs/00-whoogle.json
|
||||
|
||||
# pip stuff
|
||||
/build/
|
||||
|
@ -18,3 +21,7 @@ dist/
|
|||
|
||||
# env
|
||||
whoogle.env
|
||||
|
||||
# vim
|
||||
*~
|
||||
*.swp
|
||||
|
|
4
.replit
4
.replit
|
@ -1,3 +1 @@
|
|||
language = "bash"
|
||||
run = "killall -q python3 > /dev/null 2>&1; pip install -r requirements.txt && ./run"
|
||||
onBoot = "killall -q python3 > /dev/null 2>&1; pip install -r requirements.txt && ./run"
|
||||
entrypoint = "misc/replit.py"
|
||||
|
|
26
Dockerfile
26
Dockerfile
|
@ -1,4 +1,4 @@
|
|||
FROM python:3.11.0a5-alpine as builder
|
||||
FROM python:3.12.6-alpine3.20 AS builder
|
||||
|
||||
RUN apk --update add \
|
||||
build-base \
|
||||
|
@ -12,13 +12,20 @@ COPY requirements.txt .
|
|||
RUN pip install --upgrade pip
|
||||
RUN pip install --prefix /install --no-warn-script-location --no-cache-dir -r requirements.txt
|
||||
|
||||
FROM python:3.11.0a5-alpine
|
||||
FROM python:3.12.6-alpine3.20
|
||||
|
||||
RUN apk add --update --no-cache tor curl openrc
|
||||
RUN apk add --update --no-cache tor curl openrc libstdc++
|
||||
# git go //for obfs4proxy
|
||||
# libcurl4-openssl-dev
|
||||
|
||||
RUN apk -U upgrade
|
||||
|
||||
# uncomment to build obfs4proxy
|
||||
# RUN git clone https://gitlab.com/yawning/obfs4.git
|
||||
# WORKDIR /obfs4
|
||||
# RUN go build -o obfs4proxy/obfs4proxy ./obfs4proxy
|
||||
# RUN cp ./obfs4proxy/obfs4proxy /usr/bin/obfs4proxy
|
||||
|
||||
ARG DOCKER_USER=whoogle
|
||||
ARG DOCKER_USERID=927
|
||||
ARG config_dir=/config
|
||||
|
@ -38,12 +45,14 @@ ARG use_https=''
|
|||
ARG whoogle_port=5000
|
||||
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 so_alt='farside.link/anonymousoverflow'
|
||||
|
||||
ENV CONFIG_VOLUME=$config_dir \
|
||||
WHOOGLE_URL_PREFIX=$url_prefix \
|
||||
|
@ -58,12 +67,14 @@ ENV CONFIG_VOLUME=$config_dir \
|
|||
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_WIKI=$wikipedia_alt \
|
||||
WHOOGLE_ALT_IMDB=$imdb_alt \
|
||||
WHOOGLE_ALT_QUORA=$quora_alt \
|
||||
WHOOGLE_ALT_SO=$so_alt
|
||||
|
||||
WORKDIR /whoogle
|
||||
|
||||
|
@ -71,8 +82,7 @@ COPY --from=builder /install /usr/local
|
|||
COPY misc/tor/torrc /etc/tor/torrc
|
||||
COPY misc/tor/start-tor.sh misc/tor/start-tor.sh
|
||||
COPY app/ app/
|
||||
COPY run .
|
||||
#COPY whoogle.env .
|
||||
COPY run whoogle.env* ./
|
||||
|
||||
# Create user/group to run as
|
||||
RUN adduser -D -g $DOCKER_USERID -u $DOCKER_USERID $DOCKER_USER
|
||||
|
|
|
@ -2,4 +2,5 @@ graft app/static
|
|||
graft app/templates
|
||||
graft app/misc
|
||||
include requirements.txt
|
||||
recursive-include test
|
||||
global-exclude *.pyc
|
||||
|
|
380
README.md
380
README.md
|
@ -4,39 +4,47 @@
|
|||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://github.com/benbusby/whoogle-search/actions/workflows/tests.yml)
|
||||
[](https://github.com/benbusby/whoogle-search/actions/workflows/buildx.yml)
|
||||
[](https://github.com/benbusby/whoogle-search/actions?query=workflow%3Apep8)
|
||||
[](https://codebeat.co/projects/github-com-benbusby-shoogle-master)
|
||||
[](https://hub.docker.com/r/benbusby/whoogle-search)
|
||||
|
||||
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.
|
||||
<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.
|
||||
|
||||
Contents
|
||||
1. [Features](#features)
|
||||
2. [Dependencies](#dependencies)
|
||||
3. [Install/Deploy](#install)
|
||||
1. [Heroku Quick Deploy](#a-heroku-quick-deploy)
|
||||
2. [Repl.it](#b-replit)
|
||||
3. [Fly.io](#c-flyio)
|
||||
4. [pipx](#d-pipx)
|
||||
5. [pip](#e-pip)
|
||||
6. [Manual](#f-manual)
|
||||
7. [Docker](#g-manual-docker)
|
||||
8. [Arch/AUR](#arch-linux--arch-based-distributions)
|
||||
9. [Helm/Kubernetes](#helm-chart-for-kubernetes)
|
||||
3. [Install/Deploy Options](#install)
|
||||
1. [Heroku Quick Deploy](#heroku-quick-deploy)
|
||||
1. [Render.com](#render)
|
||||
1. [Repl.it](#replit)
|
||||
1. [Fly.io](#flyio)
|
||||
1. [Koyeb](#koyeb)
|
||||
1. [pipx](#pipx)
|
||||
1. [pip](#pip)
|
||||
1. [Manual](#manual)
|
||||
1. [Docker](#manual-docker)
|
||||
1. [Arch/AUR](#arch-linux--arch-based-distributions)
|
||||
1. [Helm/Kubernetes](#helm-chart-for-kubernetes)
|
||||
4. [Environment Variables and Configuration](#environment-variables)
|
||||
5. [Usage](#usage)
|
||||
6. [Extra Steps](#extra-steps)
|
||||
1. [Set Primary Search Engine](#set-whoogle-as-your-primary-search-engine)
|
||||
2. [Prevent Downtime (Heroku Only)](#prevent-downtime-heroku-only)
|
||||
3. [Manual HTTPS Enforcement](#https-enforcement)
|
||||
4. [Using with Firefox Containers](#using-with-firefox-containers)
|
||||
2. [Custom Redirecting](#custom-redirecting)
|
||||
2. [Custom Bangs](#custom-bangs)
|
||||
3. [Prevent Downtime (Heroku Only)](#prevent-downtime-heroku-only)
|
||||
4. [Manual HTTPS Enforcement](#https-enforcement)
|
||||
5. [Using with Firefox Containers](#using-with-firefox-containers)
|
||||
6. [Reverse Proxying](#reverse-proxying)
|
||||
1. [Nginx](#nginx)
|
||||
7. [Contributing](#contributing)
|
||||
8. [FAQ](#faq)
|
||||
9. [Public Instances](#public-instances)
|
||||
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
|
||||
- No ads or sponsored content
|
||||
|
@ -54,6 +62,7 @@ Contents
|
|||
- Randomly generated User Agent
|
||||
- Easy to install/deploy
|
||||
- DDG-style bang (i.e. `!<tag> <query>`) searches
|
||||
- User-defined [custom bangs](#custom-bangs)
|
||||
- Optional location-based searching (i.e. results near \<city\>)
|
||||
- Optional NoJS mode to view search results in a separate window with JavaScript blocked
|
||||
|
||||
|
@ -63,33 +72,35 @@ Contents
|
|||
|
||||
<sup>***If deployed to a remote server, or configured to send requests through a VPN, Tor, proxy, etc.</sup>
|
||||
|
||||
## Dependencies
|
||||
If using Heroku Quick Deploy, **you can skip this section**.
|
||||
|
||||
- Docker ([Windows](https://docs.docker.com/docker-for-windows/install/), [macOS](https://docs.docker.com/docker-for-mac/install/), [Ubuntu](https://docs.docker.com/engine/install/ubuntu/), [other Linux distros](https://docs.docker.com/engine/install/binaries/))
|
||||
- Only needed if you intend on deploying the app as a Docker image
|
||||
- [Python3](https://www.python.org/downloads/)
|
||||
- `libcurl4-openssl-dev` and `libssl-dev`
|
||||
- macOS: `brew install openssl curl-openssl`
|
||||
- Ubuntu: `sudo apt-get install -y libcurl4-openssl-dev libssl-dev`
|
||||
- Arch: `pacman -S curl openssl`
|
||||
|
||||
## Install
|
||||
There are a few different ways to begin using the app, depending on your preferences:
|
||||
|
||||
### A) [Heroku Quick Deploy](https://heroku.com/about)
|
||||
___
|
||||
|
||||
### [Heroku Quick Deploy](https://heroku.com/about)
|
||||
[](https://heroku.com/deploy?template=https://github.com/benbusby/whoogle-search/tree/main)
|
||||
|
||||
Provides:
|
||||
- Free deployment of app
|
||||
- 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)\)
|
||||
- Easy Deployment of App
|
||||
- A HTTPS url (https://\<your app name\>.herokuapp.com)
|
||||
|
||||
Notes:
|
||||
- Requires a (free) Heroku account
|
||||
- Requires a **PAID** 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)
|
||||
___
|
||||
|
||||
### [Render](https://render.com)
|
||||
|
||||
Create an account on [render.com](https://render.com) and import the Whoogle repo with the following settings:
|
||||
|
||||
- Runtime: `Python 3`
|
||||
- Build Command: `pip install -r requirements.txt`
|
||||
- Run Command: `./run`
|
||||
|
||||
___
|
||||
|
||||
### [Repl.it](https://repl.it)
|
||||
[](https://repl.it/github/benbusby/whoogle-search)
|
||||
|
||||
*Note: Requires a (free) Replit account*
|
||||
|
@ -98,44 +109,54 @@ Provides:
|
|||
- Free deployment of app
|
||||
- Free HTTPS url (https://\<app name\>.\<username\>\.repl\.co)
|
||||
- Supports custom domains
|
||||
- Downtime after periods of inactivity \([solution 1](https://repl.it/talk/ask/use-this-pingmat1replco-just-enter/28821/101298), [solution 2](https://repl.it/talk/learn/How-to-use-and-setup-UptimeRobot/9003)\)
|
||||
- Downtime after periods of inactivity ([solution](https://repl.it/talk/learn/How-to-use-and-setup-UptimeRobot/9003)\)
|
||||
|
||||
### 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.
|
||||
### [Fly.io](https://fly.io)
|
||||
|
||||
#### Install the CLI:
|
||||
You will need a [Fly.io](https://fly.io) account to deploy Whoogle.
|
||||
|
||||
#### Install the CLI: https://fly.io/docs/hands-on/installing/
|
||||
|
||||
#### Deploy the app
|
||||
|
||||
```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
|
||||
```
|
||||
The first deploy won't succeed because the default `internal_port` is wrong.
|
||||
To fix this, open the generated `fly.toml` file, set `services.internal_port` to `5000` and run `flyctl launch` again.
|
||||
|
||||
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`.
|
||||
Notes:
|
||||
- Requires a [**PAID**](https://fly.io/docs/about/pricing/#free-allowances) Fly.io Account.
|
||||
|
||||
### D) [pipx](https://github.com/pipxproject/pipx#install-pipx)
|
||||
___
|
||||
|
||||
### [Koyeb](https://www.koyeb.com)
|
||||
|
||||
Use one of the following guides to install Whoogle on Koyeb:
|
||||
|
||||
1. Using GitHub: https://www.koyeb.com/docs/quickstart/deploy-with-git
|
||||
2. Using Docker: https://www.koyeb.com/docs/quickstart/deploy-a-docker-application
|
||||
|
||||
___
|
||||
|
||||
### [pipx](https://github.com/pipxproject/pipx#install-pipx)
|
||||
Persistent install:
|
||||
|
||||
`pipx install git+https://github.com/benbusby/whoogle-search.git`
|
||||
`pipx install https://github.com/benbusby/whoogle-search/archive/refs/heads/main.zip`
|
||||
|
||||
Sandboxed temporary instance:
|
||||
|
||||
`pipx run --spec git+https://github.com/benbusby/whoogle-search.git whoogle-search`
|
||||
|
||||
### E) pip
|
||||
___
|
||||
|
||||
### pip
|
||||
`pip install whoogle-search`
|
||||
|
||||
```bash
|
||||
|
@ -162,10 +183,21 @@ optional arguments:
|
|||
```
|
||||
See the [available environment variables](#environment-variables) for additional configuration.
|
||||
|
||||
### F) Manual
|
||||
___
|
||||
|
||||
### Manual
|
||||
|
||||
*Note: `Content-Security-Policy` headers can be sent by Whoogle if you set `WHOOGLE_CSP`.*
|
||||
|
||||
#### Dependencies
|
||||
- [Python3](https://www.python.org/downloads/)
|
||||
- `libcurl4-openssl-dev` and `libssl-dev`
|
||||
- macOS: `brew install openssl curl-openssl`
|
||||
- Ubuntu: `sudo apt-get install -y libcurl4-openssl-dev libssl-dev`
|
||||
- Arch: `pacman -S curl openssl`
|
||||
|
||||
#### Install
|
||||
|
||||
Clone the repo and run the following commands to start the app in a local-only environment:
|
||||
|
||||
```bash
|
||||
|
@ -199,14 +231,18 @@ Description=Whoogle
|
|||
# with default values.
|
||||
#Environment=WHOOGLE_ALT_TW=farside.link/nitter
|
||||
#Environment=WHOOGLE_ALT_YT=farside.link/invidious
|
||||
#Environment=WHOOGLE_ALT_IG=farside.link/bibliogram/u
|
||||
#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
|
||||
#Environment=WHOOGLE_ALT_SO=farside.link/anonymousoverflow
|
||||
# Load values from dotenv only
|
||||
#Environment=WHOOGLE_DOTENV=1
|
||||
# specify dotenv location if not in default location
|
||||
#Environment=WHOOGLE_DOTENV_PATH=<path/to>/whoogle.env
|
||||
Type=simple
|
||||
User=<username>
|
||||
# If installed as a package, add:
|
||||
|
@ -217,6 +253,7 @@ ExecStart=<python_install_dir>/python3 <whoogle_install_dir>/whoogle-search --ho
|
|||
ExecStart=<whoogle_repo_dir>/run
|
||||
# For example:
|
||||
# ExecStart=/var/www/whoogle-search/run
|
||||
WorkingDirectory=<whoogle_repo_dir>
|
||||
ExecReload=/bin/kill -HUP $MAINPID
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
|
@ -232,7 +269,54 @@ sudo systemctl enable whoogle
|
|||
sudo systemctl start whoogle
|
||||
```
|
||||
|
||||
### G) Manual (Docker)
|
||||
#### 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`
|
||||
|
||||
___
|
||||
|
||||
### Manual (Docker)
|
||||
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`
|
||||
- Running `docker ps` should return something besides an error. If you encounter an error saying the daemon isn't running, try `sudo systemctl start docker` (Linux) or ensure the docker tool is running (Windows/macOS).
|
||||
|
@ -293,16 +377,22 @@ heroku open
|
|||
This series of commands can take a while, but once you run it once, you shouldn't have to run it again. The final command, `heroku open` will launch a tab in your web browser, where you can test out Whoogle and even [set it as your primary search engine](https://github.com/benbusby/whoogle#set-whoogle-as-your-primary-search-engine).
|
||||
You may also edit environment variables from your app’s Settings tab in the Heroku Dashboard.
|
||||
|
||||
#### 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).
|
||||
|
||||
#### Helm chart for Kubernetes
|
||||
___
|
||||
|
||||
### 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
|
||||
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.
|
||||
|
||||
|
@ -323,47 +413,65 @@ There are a few optional environment variables available for customizing a Whoog
|
|||
| -------------------- | ----------------------------------------------------------------------------------------- |
|
||||
| WHOOGLE_URL_PREFIX | The URL prefix to use for the whoogle instance (i.e. "/whoogle") |
|
||||
| WHOOGLE_DOTENV | Load environment variables in `whoogle.env` |
|
||||
| WHOOGLE_DOTENV_PATH | The path to `whoogle.env` if not in default location |
|
||||
| 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). |
|
||||
| WHOOGLE_USER_AGENT | The desktop user agent to use. Defaults to a randomly generated one. |
|
||||
| WHOOGLE_USER_AGENT_MOBILE | The mobile user agent to use. Defaults to a randomly generated one. |
|
||||
| WHOOGLE_USE_CLIENT_USER_AGENT | Enable to use your own user agent for all requests. Defaults to false. |
|
||||
| WHOOGLE_REDIRECTS | Specify sites that should be redirected elsewhere. See [custom redirecting](#custom-redirecting). |
|
||||
| EXPOSE_PORT | The port where Whoogle will be exposed. |
|
||||
| HTTPS_ONLY | Enforce HTTPS. (See [here](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_AUTOCOMPLETE | Controls visibility of autocomplete/search suggestions. Default on -- use '0' to disable |
|
||||
| WHOOGLE_ALT_TW | The twitter.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
|
||||
| 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. 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.org 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_ALT_SO | The stackoverflow.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_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.|
|
||||
| WHOOGLE_SHOW_FAVICONS | Show/hide favicons next to search result URLs. Default on. |
|
||||
| WHOOGLE_UPDATE_CHECK | Enable/disable the automatic daily check for new versions of Whoogle. Default on. |
|
||||
| WHOOGLE_FALLBACK_ENGINE_URL | Set a fallback Search Engine URL when there is internal server error or instance is rate-limited. Search query is appended to the end of the URL (eg. https://duckduckgo.com/?k1=-1&q=). |
|
||||
|
||||
### 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.
|
||||
|
||||
| Variable | Description |
|
||||
| ------------------------------ | --------------------------------------------------------------- |
|
||||
| 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) |
|
||||
| Variable | Description |
|
||||
| ------------------------------------ | --------------------------------------------------------------- |
|
||||
| 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_BLOCK_TITLE | Block search result with a REGEX filter on title |
|
||||
| WHOOGLE_CONFIG_BLOCK_URL | Block search result with a REGEX filter on URL |
|
||||
| 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 preferences key |
|
||||
| WHOOGLE_CONFIG_PREFERENCES_KEY | Key to encrypt preferences in URL (REQUIRED to show url) |
|
||||
| WHOOGLE_CONFIG_ANON_VIEW | Include the "anonymous view" option for each search result |
|
||||
|
||||
## Usage
|
||||
Same as most search engines, with the exception of filtering by time range.
|
||||
|
@ -371,6 +479,7 @@ Same as most search engines, with the exception of filtering by time range.
|
|||
To filter by a range of time, append ":past <time>" to the end of your search, where <time> can be `hour`, `day`, `month`, or `year`. Example: `coronavirus updates :past hour`
|
||||
|
||||
## Extra Steps
|
||||
|
||||
### Set Whoogle as your primary search engine
|
||||
*Note: If you're using a reverse proxy to run Whoogle Search, make sure the "Root URL" config option on the home page is set to your URL before going through these steps.*
|
||||
|
||||
|
@ -404,7 +513,7 @@ Browser settings:
|
|||
- Search string to use: `https://\<your whoogle url\>/search?q=%s`
|
||||
- [Alfred](https://www.alfredapp.com/) (Mac OS X)
|
||||
1. Go to `Alfred Preferences` > `Features` > `Web Search` and click `Add Custom Search`. Then configure these settings
|
||||
- Search URL: `https://\<your whoogle url\>/search?q={query}
|
||||
- Search URL: `https://\<your whoogle url\>/search?q={query}`
|
||||
- Title: `Whoogle for '{query}'` (or whatever you want)
|
||||
- Keyword: `whoogle`
|
||||
|
||||
|
@ -415,6 +524,40 @@ Browser settings:
|
|||
- Manual
|
||||
- Under search engines > manage search engines > add, manually enter your Whoogle instance details with a `<whoogle url>/search?q=%s` formatted search URL.
|
||||
|
||||
### Custom Redirecting
|
||||
You can set custom site redirects using the `WHOOGLE_REDIRECTS` environment
|
||||
variable. A lot of sites, such as Twitter, Reddit, etc, have built-in redirects
|
||||
to [Farside links](https://sr.ht/~benbusby/farside), but you may want to define
|
||||
your own.
|
||||
|
||||
To do this, you can use the following syntax:
|
||||
|
||||
```
|
||||
WHOOGLE_REDIRECTS="<parent_domain>:<new_domain>"
|
||||
```
|
||||
|
||||
For example, if you want to redirect from "badsite.com" to "goodsite.com":
|
||||
|
||||
```
|
||||
WHOOGLE_REDIRECTS="badsite.com:goodsite.com"
|
||||
```
|
||||
|
||||
This can be used for multiple sites as well, with comma separation:
|
||||
|
||||
```
|
||||
WHOOGLE_REDIRECTS="badA.com:goodA.com,badB.com:goodB.com"
|
||||
```
|
||||
|
||||
NOTE: Do not include "http(s)://" when defining your redirect.
|
||||
|
||||
### Custom Bangs
|
||||
You can create your own custom bangs. By default, bangs are stored in
|
||||
`app/static/bangs`. See [`00-whoogle.json`](https://github.com/benbusby/whoogle-search/blob/main/app/static/bangs/00-whoogle.json)
|
||||
for an example. These are parsed in alphabetical order with later files
|
||||
overriding bangs set in earlier files, with the exception that DDG bangs
|
||||
(downloaded to `app/static/bangs/bangs.json`) are always parsed first. Thus,
|
||||
any custom bangs will always override the DDG ones.
|
||||
|
||||
### Prevent Downtime (Heroku only)
|
||||
Part of the deal with Heroku's free tier is that you're allocated 550 hours/month (meaning it can't stay active 24/7), and the app is temporarily shut down after 30 minutes of inactivity. Once it becomes inactive, any Whoogle searches will still work, but it'll take an extra 10-15 seconds for the app to come back online before displaying the result, which can be frustrating if you're in a hurry.
|
||||
|
||||
|
@ -444,6 +587,32 @@ Unfortunately, Firefox Containers do not currently pass through `POST` requests
|
|||
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_set_header X-Forwarded-Host $http_host;
|
||||
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
|
||||
|
||||
Under the hood, Whoogle is a basic Flask app with the following structure:
|
||||
|
@ -464,9 +633,9 @@ Under the hood, Whoogle is a basic Flask app with the following structure:
|
|||
- `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)
|
||||
- `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>`
|
||||
- CSS/Javascript files, should be self-explanatory
|
||||
- CSS/JavaScript files, should be self-explanatory
|
||||
- `static/settings`
|
||||
- Key-value JSON files for establishing valid configuration values
|
||||
|
||||
|
@ -505,7 +674,7 @@ I'm a huge fan of Searx though and encourage anyone to use that instead if they
|
|||
|
||||
**Why does the image results page look different?**
|
||||
|
||||
A lot of the app currently piggybacks on Google's existing support for fetching results pages with Javascript disabled. To their credit, they've done an excellent job with styling pages, but it seems that the image results page - particularly on mobile - is a little rough. Moving forward, with enough interest, I'd like to transition to fetching the results and parsing them into a unique Whoogle-fied interface that I can style myself.
|
||||
A lot of the app currently piggybacks on Google's existing support for fetching results pages with JavaScript disabled. To their credit, they've done an excellent job with styling pages, but it seems that the image results page - particularly on mobile - is a little rough. Moving forward, with enough interest, I'd like to transition to fetching the results and parsing them into a unique Whoogle-fied interface that I can style myself.
|
||||
|
||||
## Public Instances
|
||||
|
||||
|
@ -516,16 +685,24 @@ A lot of the app currently piggybacks on Google's existing support for fetching
|
|||
| [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://whooglesearch.net](https://whooglesearch.net) | 🇩🇪 DE | Spanish | |
|
||||
| [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.privacydev.net](https://whoogle.privacydev.net) | 🇫🇷 FR | English | |
|
||||
| [https://wg.vern.cc](https://wg.vern.cc) | 🇺🇸 US | English | |
|
||||
| [https://whoogle.hxvy0.gq](https://whoogle.hxvy0.gq) | 🇨🇦 CA | Turkish Only | ✅ |
|
||||
| [https://whoogle.hostux.net](https://whoogle.hostux.net) | 🇫🇷 FR | 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://wgl.frail.duckdns.org](https://wgl.frail.duckdns.org) | 🇧🇷 BR | Multi-choice | |
|
||||
| [https://whoogle.no-logs.com](https://whoogle.no-logs.com/) | 🇸🇪 SE | Multi-choice | |
|
||||
| [https://whoogle.ftw.lol](https://whoogle.ftw.lol) | 🇩🇪 DE | Multi-choice | |
|
||||
| [https://whoogle-search--replitcomreside.repl.co](https://whoogle-search--replitcomreside.repl.co) | 🇺🇸 US | English | |
|
||||
| [https://search.notrustverify.ch](https://search.notrustverify.ch) | 🇨🇭 CH | Multi-choice | |
|
||||
| [https://whoogle.datura.network](https://whoogle.datura.network) | 🇩🇪 DE | Multi-choice | |
|
||||
| [https://whoogle.yepserver.xyz](https://whoogle.yepserver.xyz) | 🇺🇦 UA | Multi-choice | |
|
||||
| [https://search.nezumi.party](https://search.nezumi.party) | 🇮🇹 IT | Multi-choice | |
|
||||
| [https://search.snine.nl](https://search.snine.nl) | 🇳🇱 NL | Mult-choice | ✅ |
|
||||
|
||||
|
||||
* 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.
|
||||
|
@ -536,6 +713,15 @@ A lot of the app currently piggybacks on Google's existing support for fetching
|
|||
|-|-|-|
|
||||
| [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 |
|
||||
| [http://whoogle.g4c3eya4clenolymqbpgwz3q3tawoxw56yhzk4vugqrl6dtu3ejvhjid.onion](http://whoogle.g4c3eya4clenolymqbpgwz3q3tawoxw56yhzk4vugqrl6dtu3ejvhjid.onion/) | 🇫🇷 FR | English |
|
||||
| [http://whoogle.daturab6drmkhyeia4ch5gvfc2f3wgo6bhjrv3pz6n7kxmvoznlkq4yd.onion](http://whoogle.daturab6drmkhyeia4ch5gvfc2f3wgo6bhjrv3pz6n7kxmvoznlkq4yd.onion/) | 🇩🇪 DE | Multi-choice | |
|
||||
|
||||
#### I2P Instances
|
||||
|
||||
| Website | Country | Language |
|
||||
|-|-|-|
|
||||
| [http://verneks7rfjptpz5fpii7n7nrxilsidi2qxepeuuf66c3tsf4nhq.b32.i2p](http://verneks7rfjptpz5fpii7n7nrxilsidi2qxepeuuf66c3tsf4nhq.b32.i2p) | 🇺🇸 US | English |
|
||||
|
||||
## Screenshots
|
||||
#### Desktop
|
||||
|
|
51
app.json
51
app.json
|
@ -60,11 +60,6 @@
|
|||
"value": "farside.link/invidious",
|
||||
"required": false
|
||||
},
|
||||
"WHOOGLE_ALT_IG": {
|
||||
"description": "The site to use as a replacement for instagram.com when site alternatives are enabled in the config.",
|
||||
"value": "farside.link/bibliogram/u",
|
||||
"required": false
|
||||
},
|
||||
"WHOOGLE_ALT_RD": {
|
||||
"description": "The site to use as a replacement for reddit.com when site alternatives are enabled in the config.",
|
||||
"value": "farside.link/libreddit",
|
||||
|
@ -81,15 +76,30 @@
|
|||
"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
|
||||
},
|
||||
"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
|
||||
},
|
||||
"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_ALT_SO": {
|
||||
"description": "The site to use as a replacement for stackoverflow.com when site alternatives are enabled in the config.",
|
||||
"value": "farside.link/anonymousoverflow",
|
||||
"required": false
|
||||
},
|
||||
"WHOOGLE_MINIMAL": {
|
||||
"description": "Remove everything except basic result cards from all search queries (set to 1 or leave blank)",
|
||||
"value": "",
|
||||
|
@ -100,6 +110,11 @@
|
|||
"value": "",
|
||||
"required": false
|
||||
},
|
||||
"WHOOGLE_CONFIG_TIME_PERIOD" : {
|
||||
"description": "[CONFIG] The time period to use for restricting search results",
|
||||
"value": "",
|
||||
"required": false
|
||||
},
|
||||
"WHOOGLE_CONFIG_LANGUAGE": {
|
||||
"description": "[CONFIG] The language to use for the interface (use values from https://raw.githubusercontent.com/benbusby/whoogle-search/develop/app/static/settings/languages.json)",
|
||||
"value": "",
|
||||
|
@ -164,6 +179,16 @@
|
|||
"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": "",
|
||||
"required": false
|
||||
},
|
||||
"WHOOGLE_CONFIG_PREFERENCES_KEY": {
|
||||
"description": "[CONFIG] Key to encrypt preferences",
|
||||
"value": "NEEDS_TO_BE_MODIFIED",
|
||||
"required": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
105
app/__init__.py
105
app/__init__.py
|
@ -1,49 +1,46 @@
|
|||
from app.filter import clean_query
|
||||
from app.request import send_tor_signal
|
||||
from app.utils.session import generate_user_key
|
||||
from app.utils.bangs import gen_bangs_json
|
||||
from app.utils.session import generate_key
|
||||
from app.utils.bangs import gen_bangs_json, load_all_bangs
|
||||
from app.utils.misc import gen_file_hash, read_config_bool
|
||||
from base64 import b64encode
|
||||
from bs4 import MarkupResemblesLocatorWarning
|
||||
from datetime import datetime, timedelta
|
||||
from dotenv import load_dotenv
|
||||
from flask import Flask
|
||||
from flask_session import Session
|
||||
import json
|
||||
import logging.config
|
||||
import os
|
||||
from stem import Signal
|
||||
import threading
|
||||
from dotenv import load_dotenv
|
||||
import warnings
|
||||
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
|
||||
from app.utils.misc import read_config_bool
|
||||
from app.version import __version__
|
||||
|
||||
app = Flask(__name__, static_folder=os.path.dirname(
|
||||
os.path.abspath(__file__)) + '/static')
|
||||
|
||||
app.wsgi_app = ProxyFix(app.wsgi_app)
|
||||
|
||||
# look for WHOOGLE_ENV, else look in parent directory
|
||||
dot_env_path = os.getenv(
|
||||
"WHOOGLE_DOTENV_PATH",
|
||||
os.path.join(os.path.dirname(os.path.abspath(__file__)), "../whoogle.env"))
|
||||
|
||||
# Load .env file if enabled
|
||||
if os.getenv('WHOOGLE_DOTENV', ''):
|
||||
dotenv_path = '../whoogle.env'
|
||||
load_dotenv(os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
dotenv_path))
|
||||
if os.path.exists(dot_env_path):
|
||||
load_dotenv(dot_env_path)
|
||||
|
||||
# Session values
|
||||
# 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.
|
||||
#
|
||||
# This could be re-evaluated if Whoogle ever switches to client side
|
||||
# configuration instead.
|
||||
app.default_key = generate_user_key()
|
||||
app.config['SECRET_KEY'] = os.urandom(32)
|
||||
app.config['SESSION_TYPE'] = 'filesystem'
|
||||
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
|
||||
app.enc_key = generate_key()
|
||||
|
||||
if os.getenv('HTTPS_ONLY'):
|
||||
if read_config_bool('HTTPS_ONLY'):
|
||||
app.config['SESSION_COOKIE_NAME'] = '__Secure-session'
|
||||
app.config['SESSION_COOKIE_SECURE'] = True
|
||||
|
||||
app.config['VERSION_NUMBER'] = '0.7.4'
|
||||
app.config['VERSION_NUMBER'] = __version__
|
||||
app.config['APP_ROOT'] = os.getenv(
|
||||
'APP_ROOT',
|
||||
os.path.dirname(os.path.abspath(__file__)))
|
||||
|
@ -59,6 +56,9 @@ app.config['LANGUAGES'] = json.load(open(
|
|||
app.config['COUNTRIES'] = json.load(open(
|
||||
os.path.join(app.config['STATIC_FOLDER'], 'settings/countries.json'),
|
||||
encoding='utf-8'))
|
||||
app.config['TIME_PERIODS'] = json.load(open(
|
||||
os.path.join(app.config['STATIC_FOLDER'], 'settings/time_periods.json'),
|
||||
encoding='utf-8'))
|
||||
app.config['TRANSLATIONS'] = json.load(open(
|
||||
os.path.join(app.config['STATIC_FOLDER'], 'settings/translations.json'),
|
||||
encoding='utf-8'))
|
||||
|
@ -78,6 +78,7 @@ app.config['CONFIG_DISABLE'] = read_config_bool('WHOOGLE_CONFIG_DISABLE')
|
|||
app.config['SESSION_FILE_DIR'] = os.path.join(
|
||||
app.config['CONFIG_PATH'],
|
||||
'session')
|
||||
app.config['MAX_SESSION_SIZE'] = 4000 # Sessions won't exceed 4KB
|
||||
app.config['BANG_PATH'] = os.getenv(
|
||||
'CONFIG_VOLUME',
|
||||
os.path.join(app.config['STATIC_FOLDER'], 'bangs'))
|
||||
|
@ -85,6 +86,39 @@ app.config['BANG_FILE'] = os.path.join(
|
|||
app.config['BANG_PATH'],
|
||||
'bangs.json')
|
||||
|
||||
# Ensure all necessary directories exist
|
||||
if not os.path.exists(app.config['CONFIG_PATH']):
|
||||
os.makedirs(app.config['CONFIG_PATH'])
|
||||
|
||||
if not os.path.exists(app.config['SESSION_FILE_DIR']):
|
||||
os.makedirs(app.config['SESSION_FILE_DIR'])
|
||||
|
||||
if not os.path.exists(app.config['BANG_PATH']):
|
||||
os.makedirs(app.config['BANG_PATH'])
|
||||
|
||||
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):
|
||||
try:
|
||||
app.config['SECRET_KEY'] = open(app_key_path, 'r').read()
|
||||
except PermissionError:
|
||||
app.config['SECRET_KEY'] = str(b64encode(os.urandom(32)))
|
||||
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'
|
||||
|
@ -108,16 +142,10 @@ app.config['CSP'] = 'default-src \'none\';' \
|
|||
'media-src \'self\';' \
|
||||
'connect-src \'self\';'
|
||||
|
||||
if not os.path.exists(app.config['CONFIG_PATH']):
|
||||
os.makedirs(app.config['CONFIG_PATH'])
|
||||
|
||||
if not os.path.exists(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']):
|
||||
os.makedirs(app.config['BANG_PATH'])
|
||||
# Generate DDG bang filter
|
||||
generating_bangs = False
|
||||
if not os.path.exists(app.config['BANG_FILE']):
|
||||
generating_bangs = True
|
||||
json.dump({}, open(app.config['BANG_FILE'], 'w'))
|
||||
bangs_thread = threading.Thread(
|
||||
target=gen_bangs_json,
|
||||
|
@ -125,9 +153,6 @@ if not os.path.exists(app.config['BANG_FILE']):
|
|||
bangs_thread.start()
|
||||
|
||||
# Build new mapping of static files for cache busting
|
||||
if not os.path.exists(app.config['BUILD_FOLDER']):
|
||||
os.makedirs(app.config['BUILD_FOLDER'])
|
||||
|
||||
cache_busting_dirs = ['css', 'js']
|
||||
for cb_dir in cache_busting_dirs:
|
||||
full_cb_dir = os.path.join(app.config['STATIC_FOLDER'], cb_dir)
|
||||
|
@ -152,15 +177,21 @@ for cb_dir in cache_busting_dirs:
|
|||
# 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])
|
||||
|
||||
Session(app)
|
||||
cb_url=lambda f: app.config['CACHE_BUSTING_MAP'][f.lower()])
|
||||
|
||||
# Attempt to acquire tor identity, to determine if Tor config is available
|
||||
send_tor_signal(Signal.HEARTBEAT)
|
||||
|
||||
# Suppress spurious warnings from BeautifulSoup
|
||||
warnings.simplefilter('ignore', MarkupResemblesLocatorWarning)
|
||||
|
||||
from app import routes # noqa
|
||||
|
||||
# The gen_bangs_json function takes care of loading bangs, so skip it here if
|
||||
# it's already being loaded
|
||||
if not generating_bangs:
|
||||
load_all_bangs(app.config['BANG_FILE'])
|
||||
|
||||
# Disable logging from imported modules
|
||||
logging.config.dictConfig({
|
||||
'version': 1,
|
||||
|
|
312
app/filter.py
312
app/filter.py
|
@ -3,11 +3,23 @@ from bs4 import BeautifulSoup
|
|||
from bs4.element import ResultSet, Tag
|
||||
from cryptography.fernet import Fernet
|
||||
from flask import render_template
|
||||
import html
|
||||
import urllib.parse as urlparse
|
||||
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 *
|
||||
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 = [
|
||||
|
@ -17,9 +29,12 @@ unsupported_g_pages = [
|
|||
'google.com/preferences',
|
||||
'google.com/intl',
|
||||
'advanced_search',
|
||||
'tbm=shop'
|
||||
'tbm=shop',
|
||||
'ageverification.google.co.kr'
|
||||
]
|
||||
|
||||
unsupported_g_divs = ['google.com/preferences?hl=', 'ageverification.google.co.kr']
|
||||
|
||||
|
||||
def extract_q(q_str: str, href: str) -> str:
|
||||
"""Extracts the 'q' element from a result link. This is typically
|
||||
|
@ -33,7 +48,29 @@ def extract_q(q_str: str, href: str) -> str:
|
|||
Returns:
|
||||
str: The 'q' element of the link, or an empty string
|
||||
"""
|
||||
return parse_qs(q_str)['q'][0] if ('&q=' in href or '?q=' in href) else ''
|
||||
return parse_qs(q_str, keep_blank_values=True)['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:
|
||||
|
@ -86,6 +123,7 @@ class Filter:
|
|||
page_url='',
|
||||
query='',
|
||||
mobile=False) -> None:
|
||||
self.soup = None
|
||||
self.config = config
|
||||
self.mobile = mobile
|
||||
self.user_key = user_key
|
||||
|
@ -116,46 +154,141 @@ class Filter:
|
|||
return Fernet(self.user_key).encrypt(path.encode()).decode()
|
||||
|
||||
def clean(self, soup) -> BeautifulSoup:
|
||||
self.main_divs = soup.find('div', {'id': 'main'})
|
||||
self.soup = soup
|
||||
self.main_divs = self.soup.find('div', {'id': 'main'})
|
||||
self.remove_ads()
|
||||
self.remove_block_titles()
|
||||
self.remove_block_url()
|
||||
self.collapse_sections()
|
||||
self.update_css(soup)
|
||||
self.update_styling(soup)
|
||||
self.remove_block_tabs(soup)
|
||||
self.update_css()
|
||||
self.update_styling()
|
||||
self.remove_block_tabs()
|
||||
|
||||
for img in [_ for _ in soup.find_all('img') if 'src' in _.attrs]:
|
||||
# self.main_divs is only populated for the main page of search results
|
||||
# (i.e. not images/news/etc).
|
||||
if self.main_divs:
|
||||
for div in self.main_divs:
|
||||
self.sanitize_div(div)
|
||||
|
||||
for img in [_ for _ in self.soup.find_all('img') if 'src' in _.attrs]:
|
||||
self.update_element_src(img, 'image/png')
|
||||
|
||||
for audio in [_ for _ in soup.find_all('audio') if 'src' in _.attrs]:
|
||||
for audio in [_ for _ in self.soup.find_all('audio') if 'src' in _.attrs]:
|
||||
self.update_element_src(audio, 'audio/mpeg')
|
||||
audio['controls'] = ''
|
||||
|
||||
for link in soup.find_all('a', href=True):
|
||||
for link in self.soup.find_all('a', href=True):
|
||||
self.update_link(link)
|
||||
self.add_favicon(link)
|
||||
|
||||
input_form = soup.find('form')
|
||||
if self.config.alts:
|
||||
self.site_alt_swap()
|
||||
|
||||
input_form = self.soup.find('form')
|
||||
if input_form is not None:
|
||||
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
|
||||
for script in soup('script'):
|
||||
for script in self.soup('script'):
|
||||
script.decompose()
|
||||
|
||||
# Update default footer and header
|
||||
footer = soup.find('footer')
|
||||
footer = self.soup.find('footer')
|
||||
if footer:
|
||||
# Remove divs that have multiple links beyond just page navigation
|
||||
[_.decompose() for _ in footer.find_all('div', recursive=False)
|
||||
if len(_.find_all('a', href=True)) > 3]
|
||||
for link in footer.find_all('a', href=True):
|
||||
link['href'] = f'{link["href"]}&preferences={self.config.preferences}'
|
||||
|
||||
header = soup.find('header')
|
||||
header = self.soup.find('header')
|
||||
if header:
|
||||
header.decompose()
|
||||
self.remove_site_blocks(soup)
|
||||
return soup
|
||||
self.remove_site_blocks(self.soup)
|
||||
return self.soup
|
||||
|
||||
def sanitize_div(self, div) -> None:
|
||||
"""Removes escaped script and iframe tags from results
|
||||
|
||||
Returns:
|
||||
None (The soup object is modified directly)
|
||||
"""
|
||||
if not div:
|
||||
return
|
||||
|
||||
for d in div.find_all('div', recursive=True):
|
||||
d_text = d.find(text=True, recursive=False)
|
||||
|
||||
# Ensure we're working with tags that contain text content
|
||||
if not d_text or not d.string:
|
||||
continue
|
||||
|
||||
d.string = html.unescape(d_text)
|
||||
div_soup = BeautifulSoup(d.string, 'html.parser')
|
||||
|
||||
# Remove all valid script or iframe tags in the div
|
||||
for script in div_soup.find_all('script'):
|
||||
script.decompose()
|
||||
|
||||
for iframe in div_soup.find_all('iframe'):
|
||||
iframe.decompose()
|
||||
|
||||
d.string = str(div_soup)
|
||||
|
||||
def add_favicon(self, link) -> None:
|
||||
"""Adds icons for each returned result, using the result site's favicon
|
||||
|
||||
Returns:
|
||||
None (The soup object is modified directly)
|
||||
"""
|
||||
# Skip empty, parentless, or internal links
|
||||
show_favicons = read_config_bool('WHOOGLE_SHOW_FAVICONS', True)
|
||||
is_valid_link = link and link.parent and link['href'].startswith('http')
|
||||
if not show_favicons or not is_valid_link:
|
||||
return
|
||||
|
||||
parent = link.parent
|
||||
is_result_div = False
|
||||
|
||||
# Check each parent to make sure that the div doesn't already have a
|
||||
# favicon attached, and that the div is a result div
|
||||
while parent:
|
||||
p_cls = parent.attrs.get('class') or []
|
||||
if 'has-favicon' in p_cls or GClasses.scroller_class in p_cls:
|
||||
return
|
||||
elif GClasses.result_class_a not in p_cls:
|
||||
parent = parent.parent
|
||||
else:
|
||||
is_result_div = True
|
||||
break
|
||||
|
||||
if not is_result_div:
|
||||
return
|
||||
|
||||
# Construct the html for inserting the icon into the parent div
|
||||
parsed = urlparse.urlparse(link['href'])
|
||||
favicon = self.encrypt_path(
|
||||
f'{parsed.scheme}://{parsed.netloc}/favicon.ico',
|
||||
is_element=True)
|
||||
src = f'{self.root_url}/{Endpoint.element}?url={favicon}' + \
|
||||
'&type=image/x-icon'
|
||||
html = f'<img class="site-favicon" src="{src}">'
|
||||
|
||||
favicon = BeautifulSoup(html, 'html.parser')
|
||||
link.parent.insert(0, favicon)
|
||||
|
||||
# Update all parents to indicate that a favicon has been attached
|
||||
parent = link.parent
|
||||
while parent:
|
||||
p_cls = parent.get('class') or []
|
||||
p_cls.append('has-favicon')
|
||||
parent['class'] = p_cls
|
||||
parent = parent.parent
|
||||
|
||||
if GClasses.result_class_a in p_cls:
|
||||
break
|
||||
|
||||
def remove_site_blocks(self, soup) -> None:
|
||||
if not self.config.block or not soup.body:
|
||||
|
@ -185,7 +318,7 @@ class Filter:
|
|||
def remove_block_titles(self) -> None:
|
||||
if not self.main_divs or not self.config.block_title:
|
||||
return
|
||||
block_title = re.compile(self.block_title)
|
||||
block_title = re.compile(self.config.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]
|
||||
|
@ -194,13 +327,13 @@ class Filter:
|
|||
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)
|
||||
block_url = re.compile(self.config.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:
|
||||
def remove_block_tabs(self) -> None:
|
||||
if self.main_divs:
|
||||
for div in self.main_divs.find_all(
|
||||
'div',
|
||||
|
@ -209,7 +342,7 @@ class Filter:
|
|||
_ = div.decompose()
|
||||
else:
|
||||
# when in images tab
|
||||
for div in soup.find_all(
|
||||
for div in self.soup.find_all(
|
||||
'div',
|
||||
attrs={'class': f'{GClasses.images_tbm_tab}'}
|
||||
):
|
||||
|
@ -336,7 +469,7 @@ class Filter:
|
|||
) + '&type=' + urlparse.quote(mime)
|
||||
)
|
||||
|
||||
def update_css(self, soup) -> None:
|
||||
def update_css(self) -> None:
|
||||
"""Updates URLs used in inline styles to be proxied by Whoogle
|
||||
using the /element endpoint.
|
||||
|
||||
|
@ -345,7 +478,7 @@ class Filter:
|
|||
|
||||
"""
|
||||
# Filter all <style> tags
|
||||
for style in soup.find_all('style'):
|
||||
for style in self.soup.find_all('style'):
|
||||
style.string = clean_css(style.string, self.page_url)
|
||||
|
||||
# TODO: Convert remote stylesheets to style tags and proxy all
|
||||
|
@ -353,20 +486,20 @@ class Filter:
|
|||
# for link in soup.find_all('link', attrs={'rel': 'stylesheet'}):
|
||||
# print(link)
|
||||
|
||||
def update_styling(self, soup) -> None:
|
||||
def update_styling(self) -> None:
|
||||
# Update CSS classes for result divs
|
||||
soup = GClasses.replace_css_classes(soup)
|
||||
soup = GClasses.replace_css_classes(self.soup)
|
||||
|
||||
# Remove unnecessary button(s)
|
||||
for button in soup.find_all('button'):
|
||||
for button in self.soup.find_all('button'):
|
||||
button.decompose()
|
||||
|
||||
# Remove svg logos
|
||||
for svg in soup.find_all('svg'):
|
||||
for svg in self.soup.find_all('svg'):
|
||||
svg.decompose()
|
||||
|
||||
# Update logo
|
||||
logo = soup.find('a', {'class': 'l'})
|
||||
logo = self.soup.find('a', {'class': 'l'})
|
||||
if logo and self.mobile:
|
||||
logo['style'] = ('display:flex; justify-content:center; '
|
||||
'align-items:center; color:#685e79; '
|
||||
|
@ -374,14 +507,15 @@ class Filter:
|
|||
|
||||
# Fix search bar length on mobile
|
||||
try:
|
||||
search_bar = soup.find('header').find('form').find('div')
|
||||
search_bar = self.soup.find('header').find('form').find('div')
|
||||
search_bar['style'] = 'width: 100%;'
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
# Fix body max width on images tab
|
||||
style = soup.find('style')
|
||||
div = soup.find('div', attrs={'class': f'{GClasses.images_tbm_tab}'})
|
||||
style = self.soup.find('style')
|
||||
div = self.soup.find('div', attrs={
|
||||
'class': f'{GClasses.images_tbm_tab}'})
|
||||
if style and div and not self.mobile:
|
||||
css = style.string
|
||||
css_html_tag = (
|
||||
|
@ -410,26 +544,51 @@ class Filter:
|
|||
None (the tag is updated directly)
|
||||
|
||||
"""
|
||||
link_netloc = urlparse.urlparse(link['href']).netloc
|
||||
parsed_link = urlparse.urlparse(link['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)
|
||||
# Temporarily removing all links to that tab for now.
|
||||
|
||||
# Replaces the /url google unsupported link to the direct url
|
||||
link['href'] = link_netloc
|
||||
parent = link.parent
|
||||
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
|
||||
|
||||
if any(divlink in link_netloc for divlink in unsupported_g_divs):
|
||||
# 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
|
||||
|
||||
if link.decomposed:
|
||||
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)
|
||||
q = extract_q(result_link.query, href)
|
||||
|
||||
if q.startswith('/') and q not in self.query:
|
||||
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
|
||||
# be forwarded to Google
|
||||
link['href'] = 'https://google.com' + q
|
||||
|
@ -462,12 +621,10 @@ class Filter:
|
|||
self._av.add(netloc)
|
||||
append_anon_view(link, self.config)
|
||||
|
||||
if self.config.new_tab:
|
||||
link['target'] = '_blank'
|
||||
else:
|
||||
if href.startswith(MAPS_URL):
|
||||
# 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
|
||||
|
@ -482,25 +639,60 @@ class Filter:
|
|||
else:
|
||||
link['href'] = href
|
||||
|
||||
# Replace link location if "alts" config is enabled
|
||||
if self.config.alts:
|
||||
# Search and replace all link descriptions
|
||||
# with alternative location
|
||||
link['href'] = get_site_alt(link['href'])
|
||||
link_desc = link.find_all(
|
||||
text=re.compile('|'.join(SITE_ALTS.keys())))
|
||||
if len(link_desc) == 0:
|
||||
return
|
||||
if self.config.new_tab and (
|
||||
link["href"].startswith("http")
|
||||
or link["href"].startswith("imgres?")
|
||||
):
|
||||
link["target"] = "_blank"
|
||||
|
||||
# Replace link description
|
||||
link_desc = link_desc[0]
|
||||
for site, alt in SITE_ALTS.items():
|
||||
def site_alt_swap(self) -> None:
|
||||
"""Replaces link locations and page elements if "alts" config
|
||||
is enabled
|
||||
"""
|
||||
for site, alt in SITE_ALTS.items():
|
||||
if site != "medium.com" and alt != "":
|
||||
# Ignore medium.com replacements since these are handled
|
||||
# specifically in the link description replacement, and medium
|
||||
# results are never given their own "card" result where this
|
||||
# replacement would make sense.
|
||||
# Also ignore if the alt is empty, since this is used to indicate
|
||||
# that the alt is not enabled.
|
||||
for div in self.soup.find_all('div', text=re.compile(site)):
|
||||
# Use the number of words in the div string to determine if the
|
||||
# string is a result description (shouldn't replace domains used
|
||||
# in desc text).
|
||||
if len(div.string.split(' ')) == 1:
|
||||
div.string = div.string.replace(site, alt)
|
||||
|
||||
for link in self.soup.find_all('a', href=True):
|
||||
# Search and replace all link descriptions
|
||||
# with alternative location
|
||||
link['href'] = get_site_alt(link['href'])
|
||||
link_desc = link.find_all(
|
||||
text=re.compile('|'.join(SITE_ALTS.keys())))
|
||||
if len(link_desc) == 0:
|
||||
continue
|
||||
|
||||
# Replace link description
|
||||
link_desc = link_desc[0]
|
||||
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_str = str(link_desc)
|
||||
|
||||
# Medium links should be handled differently, since 'medium.com'
|
||||
# is a common substring of domain names, but shouldn't be
|
||||
# replaced (i.e. 'philomedium.com' should stay as it is).
|
||||
if 'medium.com' in link_str:
|
||||
if link_str.startswith('medium.com') or '.medium.com' in link_str:
|
||||
link_str = SITE_ALTS['medium.com'] + link_str[
|
||||
link_str.find('medium.com') + len('medium.com'):]
|
||||
new_desc.string = link_str
|
||||
else:
|
||||
new_desc.string = link_str.replace(site, alt)
|
||||
|
||||
link_desc.replace_with(new_desc)
|
||||
break
|
||||
|
||||
def view_image(self, soup) -> BeautifulSoup:
|
||||
"""Replaces the soup with a new one that handles mobile results and
|
||||
|
@ -515,13 +707,15 @@ class Filter:
|
|||
|
||||
# get some tags that are unchanged between mobile and pc versions
|
||||
cor_suggested = soup.find_all('table', attrs={'class': "By0U9"})
|
||||
next_pages = soup.find_all('table', attrs={'class': "uZgmoc"})[0]
|
||||
next_pages = soup.find('table', attrs={'class': "uZgmoc"})
|
||||
|
||||
results = []
|
||||
# find results div
|
||||
results_div = soup.find_all('div', attrs={'class': "nQvrDb"})[0]
|
||||
# find all the results
|
||||
results_all = results_div.find_all('div', attrs={'class': "lIMUZd"})
|
||||
results_div = soup.find('div', attrs={'class': "nQvrDb"})
|
||||
# find all the results (if any)
|
||||
results_all = []
|
||||
if results_div:
|
||||
results_all = results_div.find_all('div', attrs={'class': "lIMUZd"})
|
||||
|
||||
for item in results_all:
|
||||
urls = item.find('a')['href'].split('&imgrefurl=')
|
||||
|
|
|
@ -1,7 +1,38 @@
|
|||
from inspect import Attribute
|
||||
from typing import Optional
|
||||
from app.utils.misc import read_config_bool
|
||||
from flask import current_app
|
||||
import os
|
||||
import re
|
||||
from base64 import urlsafe_b64encode, urlsafe_b64decode
|
||||
from cryptography.fernet import Fernet
|
||||
import hashlib
|
||||
import brotli
|
||||
import logging
|
||||
import json
|
||||
|
||||
import cssutils
|
||||
from cssutils.css.cssstylesheet import CSSStyleSheet
|
||||
from cssutils.css.cssstylerule import CSSStyleRule
|
||||
|
||||
# removes warnings from cssutils
|
||||
cssutils.log.setLevel(logging.CRITICAL)
|
||||
|
||||
|
||||
def get_rule_for_selector(stylesheet: CSSStyleSheet,
|
||||
selector: str) -> Optional[CSSStyleRule]:
|
||||
"""Search for a rule that matches a given selector in a stylesheet.
|
||||
|
||||
Args:
|
||||
stylesheet (CSSStyleSheet) -- the stylesheet to search
|
||||
selector (str) -- the selector to search for
|
||||
|
||||
Returns:
|
||||
Optional[CSSStyleRule] -- the rule that matches the selector or None
|
||||
"""
|
||||
for rule in stylesheet.cssRules:
|
||||
if hasattr(rule, "selectorText") and selector == rule.selectorText:
|
||||
return rule
|
||||
return None
|
||||
|
||||
|
||||
class Config:
|
||||
|
@ -10,14 +41,13 @@ class Config:
|
|||
self.url = os.getenv('WHOOGLE_CONFIG_URL', '')
|
||||
self.lang_search = os.getenv('WHOOGLE_CONFIG_SEARCH_LANGUAGE', '')
|
||||
self.lang_interface = os.getenv('WHOOGLE_CONFIG_LANGUAGE', '')
|
||||
self.style = os.getenv(
|
||||
'WHOOGLE_CONFIG_STYLE',
|
||||
open(os.path.join(app_config['STATIC_FOLDER'],
|
||||
'css/variables.css')).read())
|
||||
self.style_modified = os.getenv(
|
||||
'WHOOGLE_CONFIG_STYLE', '')
|
||||
self.block = os.getenv('WHOOGLE_CONFIG_BLOCK', '')
|
||||
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.tbs = os.getenv('WHOOGLE_CONFIG_TIME_PERIOD', '')
|
||||
self.theme = os.getenv('WHOOGLE_CONFIG_THEME', 'system')
|
||||
self.safe = read_config_bool('WHOOGLE_CONFIG_SAFE')
|
||||
self.dark = read_config_bool('WHOOGLE_CONFIG_DARK') # deprecated
|
||||
|
@ -29,6 +59,9 @@ class Config:
|
|||
self.view_image = read_config_bool('WHOOGLE_CONFIG_VIEW_IMAGE')
|
||||
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 = [
|
||||
|
@ -42,7 +75,9 @@ class Config:
|
|||
'block',
|
||||
'safe',
|
||||
'nojs',
|
||||
'anon_view'
|
||||
'anon_view',
|
||||
'preferences_encrypted',
|
||||
'tbs'
|
||||
]
|
||||
|
||||
# Skip setting custom config if there isn't one
|
||||
|
@ -71,6 +106,51 @@ class Config:
|
|||
if not name.startswith("__")
|
||||
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 style(self) -> str:
|
||||
"""Returns the default style updated with specified modifications.
|
||||
|
||||
Returns:
|
||||
str -- the new style
|
||||
"""
|
||||
style_sheet = cssutils.parseString(
|
||||
open(os.path.join(current_app.config['STATIC_FOLDER'],
|
||||
'css/variables.css')).read()
|
||||
)
|
||||
|
||||
modified_sheet = cssutils.parseString(self.style_modified)
|
||||
for rule in modified_sheet:
|
||||
rule_default = get_rule_for_selector(style_sheet,
|
||||
rule.selectorText)
|
||||
# if modified rule is in default stylesheet, update it
|
||||
if rule_default is not None:
|
||||
# TODO: update this in a smarter way to handle :root better
|
||||
# for now if we change a varialbe in :root all other default
|
||||
# variables need to be also present
|
||||
rule_default.style = rule.style
|
||||
# else add the new rule to the default stylesheet
|
||||
else:
|
||||
style_sheet.add(rule)
|
||||
return str(style_sheet.cssText, 'utf-8')
|
||||
|
||||
@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:
|
||||
"""Establishes a group of config options that are safe to set
|
||||
in the url.
|
||||
|
@ -109,6 +189,13 @@ class Config:
|
|||
Returns:
|
||||
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():
|
||||
if not self.is_safe_key(param_key):
|
||||
continue
|
||||
|
@ -116,22 +203,65 @@ class Config:
|
|||
|
||||
if param_val == 'off':
|
||||
param_val = False
|
||||
elif param_val.isdigit():
|
||||
param_val = int(param_val)
|
||||
elif isinstance(param_val, str):
|
||||
if param_val.isdigit():
|
||||
param_val = int(param_val)
|
||||
|
||||
self[param_key] = param_val
|
||||
return self
|
||||
|
||||
def to_params(self) -> str:
|
||||
def to_params(self, keys: list = []) -> str:
|
||||
"""Generates a set of safe params for using in Whoogle URLs
|
||||
|
||||
Args:
|
||||
keys (list) -- optional list of keys of URL parameters
|
||||
|
||||
Returns:
|
||||
str -- a set of URL parameters
|
||||
"""
|
||||
if not len(keys):
|
||||
keys = self.safe_keys
|
||||
|
||||
param_str = ''
|
||||
for safe_key in self.safe_keys:
|
||||
for safe_key in 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:
|
||||
preferences_json = json.dumps(self.get_attrs()).encode()
|
||||
compressed_preferences = brotli.compress(preferences_json)
|
||||
|
||||
if self.preferences_encrypted and self.preferences_key:
|
||||
key = self._get_fernet_key(self.preferences_key)
|
||||
encrypted_preferences = Fernet(key).encrypt(compressed_preferences)
|
||||
compressed_preferences = brotli.compress(encrypted_preferences)
|
||||
|
||||
return urlsafe_b64encode(compressed_preferences).decode()
|
||||
|
||||
def _decode_preferences(self, preferences: str) -> dict:
|
||||
mode = preferences[0]
|
||||
preferences = preferences[1:]
|
||||
|
||||
try:
|
||||
decoded_data = brotli.decompress(urlsafe_b64decode(preferences.encode() + b'=='))
|
||||
|
||||
if mode == 'e' and self.preferences_key:
|
||||
# preferences are encrypted
|
||||
key = self._get_fernet_key(self.preferences_key)
|
||||
decrypted_data = Fernet(key).decrypt(decoded_data)
|
||||
decoded_data = brotli.decompress(decrypted_data)
|
||||
|
||||
config = json.loads(decoded_data)
|
||||
except Exception:
|
||||
config = {}
|
||||
|
||||
return config
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ class Endpoint(Enum):
|
|||
autocomplete = 'autocomplete'
|
||||
home = 'home'
|
||||
healthz = 'healthz'
|
||||
session = 'session'
|
||||
config = 'config'
|
||||
opensearch = 'opensearch.xml'
|
||||
search = 'search'
|
||||
|
|
|
@ -14,6 +14,8 @@ class GClasses:
|
|||
footer = 'TuS8Ad'
|
||||
result_class_a = 'ZINbbc'
|
||||
result_class_b = 'luh4td'
|
||||
scroller_class = 'idg8be'
|
||||
line_tag = 'BsXmcf'
|
||||
|
||||
result_classes = {
|
||||
result_class_a: ['Gx5Zad'],
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from app.models.config import Config
|
||||
from app.utils.misc import read_config_bool
|
||||
from datetime import datetime
|
||||
from defusedxml import ElementTree as ET
|
||||
import random
|
||||
|
@ -7,7 +8,9 @@ from requests import Response, ConnectionError
|
|||
import urllib.parse as urlparse
|
||||
import os
|
||||
from stem import Signal, SocketError
|
||||
from stem.connection import AuthenticationFailure
|
||||
from stem.control import Controller
|
||||
from stem.connection import authenticate_cookie, authenticate_password
|
||||
|
||||
MAPS_URL = 'https://maps.google.com/maps'
|
||||
AUTOCOMPLETE_URL = ('https://suggestqueries.google.com/'
|
||||
|
@ -37,19 +40,47 @@ class TorError(Exception):
|
|||
|
||||
|
||||
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:
|
||||
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)
|
||||
os.environ['TOR_AVAILABLE'] = '1'
|
||||
return True
|
||||
except (SocketError, ConnectionRefusedError, ConnectionError):
|
||||
except (SocketError, AuthenticationFailure,
|
||||
ConnectionRefusedError, ConnectionError):
|
||||
# TODO: Handle Tor authentication (password and cookie)
|
||||
os.environ['TOR_AVAILABLE'] = '0'
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def gen_user_agent(is_mobile) -> str:
|
||||
user_agent = os.environ.get('WHOOGLE_USER_AGENT', '')
|
||||
user_agent_mobile = os.environ.get('WHOOGLE_USER_AGENT_MOBILE', '')
|
||||
if user_agent and not is_mobile:
|
||||
return user_agent
|
||||
|
||||
if user_agent_mobile and is_mobile:
|
||||
return user_agent_mobile
|
||||
|
||||
firefox = random.choice(['Choir', 'Squier', 'Higher', 'Wire']) + 'fox'
|
||||
linux = random.choice(['Win', 'Sin', 'Gin', 'Fin', 'Kin']) + 'ux'
|
||||
|
||||
|
@ -68,8 +99,8 @@ def gen_query(query, args, config) -> str:
|
|||
if ':past' in query and 'tbs' not in args:
|
||||
time_range = str.strip(query.split(':past', 1)[-1])
|
||||
param_dict['tbs'] = '&tbs=' + ('qdr:' + str.lower(time_range[0]))
|
||||
elif 'tbs' in args:
|
||||
result_tbs = args.get('tbs')
|
||||
elif 'tbs' in args or 'tbs' in config:
|
||||
result_tbs = args.get('tbs') if 'tbs' in args else config['tbs']
|
||||
param_dict['tbs'] = '&tbs=' + result_tbs
|
||||
|
||||
# Occasionally the 'tbs' param provided by google also contains a
|
||||
|
@ -165,6 +196,8 @@ class Request:
|
|||
config.lang_search if config.lang_search else ''
|
||||
)
|
||||
|
||||
self.country = config.country if config.country else ''
|
||||
|
||||
# For setting Accept-language Header
|
||||
self.lang_interface = ''
|
||||
if config.accept_language:
|
||||
|
@ -184,19 +217,13 @@ class Request:
|
|||
proxy_pass = os.environ.get('WHOOGLE_PROXY_PASS', '')
|
||||
auth_str = ''
|
||||
if proxy_user:
|
||||
auth_str = proxy_user + ':' + proxy_pass
|
||||
self.proxies = {
|
||||
'https': proxy_type + '://' +
|
||||
((auth_str + '@') if auth_str else '') + proxy_path,
|
||||
}
|
||||
auth_str = f'{proxy_user}:{proxy_pass}@'
|
||||
|
||||
# Need to ensure both HTTP and HTTPS are in the proxy dict,
|
||||
# regardless of underlying protocol
|
||||
if proxy_type == 'https':
|
||||
self.proxies['http'] = self.proxies['https'].replace(
|
||||
'https', 'http')
|
||||
else:
|
||||
self.proxies['http'] = self.proxies['https']
|
||||
proxy_str = f'{proxy_type}://{auth_str}{proxy_path}'
|
||||
self.proxies = {
|
||||
'https': proxy_str,
|
||||
'http': proxy_str
|
||||
}
|
||||
else:
|
||||
self.proxies = {
|
||||
'http': 'socks5://127.0.0.1:9050',
|
||||
|
@ -221,7 +248,11 @@ class Request:
|
|||
"""
|
||||
ac_query = dict(q=query)
|
||||
if self.language:
|
||||
ac_query['hl'] = self.language
|
||||
ac_query['lr'] = self.language
|
||||
if self.country:
|
||||
ac_query['gl'] = self.country
|
||||
if self.lang_interface:
|
||||
ac_query['hl'] = self.lang_interface
|
||||
|
||||
response = self.send(base_url=AUTOCOMPLETE_URL,
|
||||
query=urlparse.urlencode(ac_query)).text
|
||||
|
@ -238,7 +269,7 @@ class Request:
|
|||
return []
|
||||
|
||||
def send(self, base_url='', query='', attempt=0,
|
||||
force_mobile=False) -> Response:
|
||||
force_mobile=False, user_agent='') -> Response:
|
||||
"""Sends an outbound request to a URL. Optionally sends the request
|
||||
using Tor, if enabled by the user.
|
||||
|
||||
|
@ -254,10 +285,14 @@ class Request:
|
|||
Response: The Response object returned by the requests call
|
||||
|
||||
"""
|
||||
if force_mobile and not self.mobile:
|
||||
modified_user_agent = self.modified_user_agent_mobile
|
||||
use_client_user_agent = int(os.environ.get('WHOOGLE_USE_CLIENT_USER_AGENT', '0'))
|
||||
if user_agent and use_client_user_agent == 1:
|
||||
modified_user_agent = user_agent
|
||||
else:
|
||||
modified_user_agent = self.modified_user_agent
|
||||
if force_mobile and not self.mobile:
|
||||
modified_user_agent = self.modified_user_agent_mobile
|
||||
else:
|
||||
modified_user_agent = self.modified_user_agent
|
||||
|
||||
headers = {
|
||||
'User-Agent': modified_user_agent
|
||||
|
@ -272,9 +307,8 @@ class Request:
|
|||
# view is suppressed correctly
|
||||
now = datetime.now()
|
||||
cookies = {
|
||||
'CONSENT': 'YES+cb.{:d}{:02d}{:02d}-17-p0.de+F+678'.format(
|
||||
now.year, now.month, now.day
|
||||
)
|
||||
'CONSENT': 'PENDING+987',
|
||||
'SOCS': 'CAESHAgBEhIaAB',
|
||||
}
|
||||
|
||||
# Validate Tor conn and request new identity if the last one failed
|
||||
|
|
293
app/routes.py
293
app/routes.py
|
@ -1,12 +1,15 @@
|
|||
import argparse
|
||||
import base64
|
||||
import io
|
||||
import os
|
||||
import json
|
||||
import os
|
||||
import pickle
|
||||
import re
|
||||
import urllib.parse as urlparse
|
||||
import uuid
|
||||
import validators
|
||||
import sys
|
||||
import traceback
|
||||
from datetime import datetime, timedelta
|
||||
from functools import wraps
|
||||
|
||||
|
@ -15,32 +18,47 @@ from app import app
|
|||
from app.models.config import Config
|
||||
from app.models.endpoint import Endpoint
|
||||
from app.request import Request, TorError
|
||||
from app.utils.bangs import resolve_bang
|
||||
from app.utils.bangs import suggest_bang, resolve_bang
|
||||
from app.utils.misc import empty_gif, placeholder_img, get_proxy_host_url, \
|
||||
fetch_favicon
|
||||
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,\
|
||||
check_for_update, encrypt_string
|
||||
from app.utils.widgets import *
|
||||
from app.utils.results import 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 valid_user_session
|
||||
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, get
|
||||
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
|
||||
bang_json = json.load(open(app.config['BANG_FILE'])) or {}
|
||||
from werkzeug.datastructures import MultiDict
|
||||
|
||||
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):
|
||||
@wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
# do not ask password if cookies already present
|
||||
if (
|
||||
valid_user_session(session)
|
||||
and 'cookies_disabled' not in request.args
|
||||
and session['auth']
|
||||
):
|
||||
return f(*args, **kwargs)
|
||||
|
||||
auth = request.authorization
|
||||
|
||||
# Skip if username/password not set
|
||||
|
@ -50,6 +68,7 @@ def auth_required(f):
|
|||
auth
|
||||
and whoogle_user == auth.username
|
||||
and whoogle_pass == auth.password):
|
||||
session['auth'] = True
|
||||
return f(*args, **kwargs)
|
||||
else:
|
||||
return make_response('Not logged in', 401, {
|
||||
|
@ -61,27 +80,39 @@ def auth_required(f):
|
|||
def session_required(f):
|
||||
@wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
if (valid_user_session(session) and
|
||||
'cookies_disabled' not in request.args):
|
||||
g.session_key = session['key']
|
||||
else:
|
||||
if not valid_user_session(session):
|
||||
session.pop('_permanent', None)
|
||||
g.session_key = app.default_key
|
||||
|
||||
# Note: This sets all requests to use the encryption key determined per
|
||||
# instance on app init. This can be updated in the future to use a key
|
||||
# that is unique for their session (session['key']) but this should use
|
||||
# a config setting to enable the session based key. Otherwise there can
|
||||
# be problems with searches performed by users with cookies blocked if
|
||||
# a session based key is always used.
|
||||
g.session_key = app.enc_key
|
||||
|
||||
# Clear out old sessions
|
||||
invalid_sessions = []
|
||||
for user_session in os.listdir(app.config['SESSION_FILE_DIR']):
|
||||
session_path = os.path.join(
|
||||
file_path = os.path.join(
|
||||
app.config['SESSION_FILE_DIR'],
|
||||
user_session)
|
||||
|
||||
try:
|
||||
with open(session_path, 'rb') as session_file:
|
||||
# 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(session_path)
|
||||
except (EOFError, FileNotFoundError, pickle.UnpicklingError):
|
||||
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:
|
||||
|
@ -98,11 +129,12 @@ def session_required(f):
|
|||
|
||||
@app.before_request
|
||||
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']:
|
||||
needs_update_check = now - timedelta(hours=24) > app.config['LAST_UPDATE_CHECK']
|
||||
if read_config_bool('WHOOGLE_UPDATE_CHECK', True) and needs_update_check:
|
||||
app.config['LAST_UPDATE_CHECK'] = now
|
||||
app.config['HAS_UPDATE'] = check_for_update(
|
||||
app.config['RELEASES_URL'],
|
||||
|
@ -112,40 +144,21 @@ def before_request_func():
|
|||
request.args if request.method == 'GET' else request.form
|
||||
)
|
||||
|
||||
# Skip pre-request actions if verifying session
|
||||
if '/session' in request.path and not valid_user_session(session):
|
||||
return
|
||||
|
||||
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
|
||||
if (not valid_user_session(session) and
|
||||
'cookies_disabled' not in request.args):
|
||||
if not valid_user_session(session):
|
||||
session['config'] = default_config
|
||||
session['uuid'] = str(uuid.uuid4())
|
||||
session['key'] = generate_user_key()
|
||||
session['key'] = app.enc_key
|
||||
session['auth'] = False
|
||||
|
||||
# Skip checking for session on any searches that don't
|
||||
# require a valid session
|
||||
if (not Endpoint.autocomplete.in_path(request.path) and
|
||||
not Endpoint.healthz.in_path(request.path) and
|
||||
not Endpoint.opensearch.in_path(request.path)):
|
||||
return redirect(url_for(
|
||||
'session_check',
|
||||
session_id=session['uuid'],
|
||||
follow=get_request_url(request.url)), code=307)
|
||||
else:
|
||||
g.user_config = Config(**session['config'])
|
||||
elif 'cookies_disabled' not in request.args:
|
||||
# Set session as permanent
|
||||
session.permanent = True
|
||||
app.permanent_session_lifetime = timedelta(days=365)
|
||||
g.user_config = Config(**session['config'])
|
||||
else:
|
||||
# User has cookies disabled, fall back to immutable default config
|
||||
session.pop('_permanent', None)
|
||||
g.user_config = Config(**default_config)
|
||||
# Establish config values per user session
|
||||
g.user_config = Config(**session['config'])
|
||||
|
||||
# Update user config if specified in search args
|
||||
g.user_config = g.user_config.from_params(g.request_params)
|
||||
|
||||
if not g.user_config.url:
|
||||
g.user_config.url = get_request_url(request.url_root)
|
||||
|
@ -157,20 +170,12 @@ def before_request_func():
|
|||
|
||||
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
|
||||
def after_request_func(resp):
|
||||
resp.headers['X-Content-Type-Options'] = 'nosniff'
|
||||
resp.headers['X-Frame-Options'] = 'DENY'
|
||||
resp.headers['Cache-Control'] = 'max-age=86400'
|
||||
|
||||
if os.getenv('WHOOGLE_CSP', False):
|
||||
resp.headers['Content-Security-Policy'] = app.config['CSP']
|
||||
|
@ -192,19 +197,6 @@ def healthz():
|
|||
return ''
|
||||
|
||||
|
||||
@app.route(f'/{Endpoint.session}/<session_id>', methods=['GET', 'PUT', 'POST'])
|
||||
def session_check(session_id):
|
||||
if 'uuid' in session and session['uuid'] == session_id:
|
||||
session['valid'] = True
|
||||
return redirect(request.args.get('follow'), code=307)
|
||||
else:
|
||||
follow_url = request.args.get('follow')
|
||||
req = PreparedRequest()
|
||||
req.prepare_url(follow_url, {'cookies_disabled': 1})
|
||||
session.pop('_permanent', None)
|
||||
return redirect(req.url, code=307)
|
||||
|
||||
|
||||
@app.route('/', methods=['GET'])
|
||||
@app.route(f'/{Endpoint.home}', methods=['GET'])
|
||||
@auth_required
|
||||
|
@ -219,6 +211,7 @@ def index():
|
|||
has_update=app.config['HAS_UPDATE'],
|
||||
languages=app.config['LANGUAGES'],
|
||||
countries=app.config['COUNTRIES'],
|
||||
time_periods=app.config['TIME_PERIODS'],
|
||||
themes=app.config['THEMES'],
|
||||
autocomplete_enabled=autocomplete_enabled,
|
||||
translation=app.config['TRANSLATIONS'][
|
||||
|
@ -229,8 +222,7 @@ def index():
|
|||
dark=g.user_config.dark),
|
||||
config_disabled=(
|
||||
app.config['CONFIG_DISABLE'] or
|
||||
not valid_user_session(session) or
|
||||
'cookies_disabled' in request.args),
|
||||
not valid_user_session(session)),
|
||||
config=g.user_config,
|
||||
tor_available=int(os.environ.get('TOR_AVAILABLE')),
|
||||
version_number=app.config['VERSION_NUMBER'])
|
||||
|
@ -252,7 +244,9 @@ def opensearch():
|
|||
return render_template(
|
||||
'opensearch.xml',
|
||||
main_url=opensearch_url,
|
||||
request_type='' if get_only else 'method="post"'
|
||||
request_type='' if get_only else 'method="post"',
|
||||
search_type=request.args.get('tbm'),
|
||||
search_name=get_search_name(request.args.get('tbm'))
|
||||
), 200, {'Content-Type': 'application/xml'}
|
||||
|
||||
|
||||
|
@ -277,8 +271,7 @@ def autocomplete():
|
|||
|
||||
# Search bangs if the query begins with "!", but not "! " (feeling lucky)
|
||||
if q.startswith('!') and len(q) > 1 and not q.startswith('! '):
|
||||
return jsonify([q, [bang_json[_]['suggestion'] for _ in bang_json if
|
||||
_.startswith(q)]])
|
||||
return jsonify([q, suggest_bang(q)])
|
||||
|
||||
if not q and not request.data:
|
||||
return jsonify({'?': []})
|
||||
|
@ -295,18 +288,21 @@ def autocomplete():
|
|||
g.user_request.autocomplete(q) if not g.user_config.tor else []
|
||||
])
|
||||
|
||||
|
||||
@app.route(f'/{Endpoint.search}', methods=['GET', 'POST'])
|
||||
@session_required
|
||||
@auth_required
|
||||
def search():
|
||||
# Update user config if specified in search args
|
||||
g.user_config = g.user_config.from_params(g.request_params)
|
||||
if request.method == 'POST':
|
||||
# Redirect as a GET request with an encrypted query
|
||||
post_data = MultiDict(request.form)
|
||||
post_data['q'] = encrypt_string(g.session_key, post_data['q'])
|
||||
get_req_str = urlparse.urlencode(post_data)
|
||||
return redirect(url_for('.search') + '?' + get_req_str)
|
||||
|
||||
search_util = Search(request, g.user_config, g.session_key)
|
||||
query = search_util.new_search_query()
|
||||
|
||||
bang = resolve_bang(query, bang_json)
|
||||
bang = resolve_bang(query)
|
||||
if bang:
|
||||
return redirect(bang)
|
||||
|
||||
|
@ -333,8 +329,20 @@ def search():
|
|||
translation = app.config['TRANSLATIONS'][localization_lang]
|
||||
translate_to = localization_lang.replace('lang_', '')
|
||||
|
||||
# removing st-card to only use whoogle time selector
|
||||
soup = bsoup(response, "html.parser");
|
||||
for x in soup.find_all(attrs={"id": "st-card"}):
|
||||
x.replace_with("")
|
||||
|
||||
response = str(soup)
|
||||
|
||||
# Return 503 if temporarily blocked by captcha
|
||||
if has_captcha(str(response)):
|
||||
app.logger.error('503 (CAPTCHA)')
|
||||
fallback_engine = os.environ.get('WHOOGLE_FALLBACK_ENGINE_URL', '')
|
||||
if (fallback_engine):
|
||||
return redirect(fallback_engine + query)
|
||||
|
||||
return render_template(
|
||||
'error.html',
|
||||
blocked=True,
|
||||
|
@ -343,31 +351,43 @@ def search():
|
|||
farside='https://farside.link',
|
||||
config=g.user_config,
|
||||
query=urlparse.unquote(query),
|
||||
params=g.user_config.to_params()), 503
|
||||
params=g.user_config.to_params(keys=['preferences'])), 503
|
||||
|
||||
response = bold_search_terms(response, query)
|
||||
|
||||
# Feature to display IP address
|
||||
if search_util.check_kw_ip():
|
||||
# check for widgets and add if requested
|
||||
if search_util.widget != '':
|
||||
html_soup = bsoup(str(response), 'html.parser')
|
||||
response = add_ip_card(html_soup, get_client_ip(request))
|
||||
if search_util.widget == 'ip':
|
||||
response = add_ip_card(html_soup, get_client_ip(request))
|
||||
elif search_util.widget == 'calculator' and not 'nojs' in request.args:
|
||||
response = add_calculator_card(html_soup)
|
||||
|
||||
# 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
|
||||
# Since this is determined by more than just the
|
||||
# query is it not defined as a standard widget
|
||||
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"
|
||||
cleanresponse = str(response).replace("andlt;","<").replace("andgt;",">")
|
||||
|
||||
return render_template(
|
||||
'display.html',
|
||||
has_update=app.config['HAS_UPDATE'],
|
||||
query=urlparse.unquote(query),
|
||||
search_type=search_util.search_type,
|
||||
search_name=get_search_name(search_util.search_type),
|
||||
config=g.user_config,
|
||||
autocomplete_enabled=autocomplete_enabled,
|
||||
lingva_url=app.config['TRANSLATE_URL'],
|
||||
|
@ -381,16 +401,21 @@ def search():
|
|||
is_translation=any(
|
||||
_ in query.lower() for _ in [translation['translate'], 'translate']
|
||||
) and not search_util.search_type, # Standard search queries only
|
||||
response=response,
|
||||
response=cleanresponse,
|
||||
version_number=app.config['VERSION_NUMBER'],
|
||||
search_header=render_template(
|
||||
'header.html',
|
||||
home_url=home_url,
|
||||
config=g.user_config,
|
||||
translation=translation,
|
||||
languages=app.config['LANGUAGES'],
|
||||
countries=app.config['COUNTRIES'],
|
||||
time_periods=app.config['TIME_PERIODS'],
|
||||
logo=render_template('logo.html', dark=g.user_config.dark),
|
||||
query=urlparse.unquote(query),
|
||||
search_type=search_util.search_type,
|
||||
mobile=g.user_request.mobile,
|
||||
tabs=tabs))
|
||||
tabs=tabs)).replace(" ", "")
|
||||
|
||||
|
||||
@app.route(f'/{Endpoint.config}', methods=['GET', 'POST', 'PUT'])
|
||||
|
@ -400,13 +425,18 @@ def config():
|
|||
config_disabled = (
|
||||
app.config['CONFIG_DISABLE'] or
|
||||
not valid_user_session(session))
|
||||
|
||||
name = ''
|
||||
if 'name' in request.args:
|
||||
name = os.path.normpath(request.args.get('name'))
|
||||
if not re.match(r'^[A-Za-z0-9_.+-]+$', name):
|
||||
return make_response('Invalid config name', 400)
|
||||
|
||||
if request.method == 'GET':
|
||||
return json.dumps(g.user_config.__dict__)
|
||||
elif request.method == 'PUT' and not config_disabled:
|
||||
if 'name' in request.args:
|
||||
config_pkl = os.path.join(
|
||||
app.config['CONFIG_PATH'],
|
||||
request.args.get('name'))
|
||||
if name:
|
||||
config_pkl = os.path.join(app.config['CONFIG_PATH'], name)
|
||||
session['config'] = (pickle.load(open(config_pkl, 'rb'))
|
||||
if os.path.exists(config_pkl)
|
||||
else session['config'])
|
||||
|
@ -424,7 +454,7 @@ def config():
|
|||
config_data,
|
||||
open(os.path.join(
|
||||
app.config['CONFIG_PATH'],
|
||||
request.args.get('name')), 'wb'))
|
||||
name), 'wb'))
|
||||
|
||||
session['config'] = config_data
|
||||
return redirect(config_data['url'])
|
||||
|
@ -455,8 +485,23 @@ def element():
|
|||
|
||||
src_type = request.args.get('type')
|
||||
|
||||
# Ensure requested element is from a valid domain
|
||||
domain = urlparse.urlparse(src_url).netloc
|
||||
if not validators.domain(domain):
|
||||
return send_file(io.BytesIO(empty_gif), mimetype='image/gif')
|
||||
|
||||
try:
|
||||
file_data = g.user_request.send(base_url=src_url).content
|
||||
response = g.user_request.send(base_url=src_url)
|
||||
|
||||
# Display an empty gif if the requested element couldn't be retrieved
|
||||
if response.status_code != 200 or len(response.content) == 0:
|
||||
if 'favicon' in src_url:
|
||||
favicon = fetch_favicon(src_url)
|
||||
return send_file(io.BytesIO(favicon), mimetype='image/png')
|
||||
else:
|
||||
return send_file(io.BytesIO(empty_gif), mimetype='image/gif')
|
||||
|
||||
file_data = response.content
|
||||
tmp_mem = io.BytesIO()
|
||||
tmp_mem.write(file_data)
|
||||
tmp_mem.seek(0)
|
||||
|
@ -465,8 +510,6 @@ def element():
|
|||
except exceptions.RequestException:
|
||||
pass
|
||||
|
||||
empty_gif = base64.b64decode(
|
||||
'R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==')
|
||||
return send_file(io.BytesIO(empty_gif), mimetype='image/gif')
|
||||
|
||||
|
||||
|
@ -484,6 +527,13 @@ def window():
|
|||
root_url=request.url_root,
|
||||
config=g.user_config)
|
||||
target = urlparse.urlparse(target_url)
|
||||
|
||||
# Ensure requested URL has a valid domain
|
||||
if not validators.domain(target.netloc):
|
||||
return render_template(
|
||||
'error.html',
|
||||
error_message='Invalid location'), 400
|
||||
|
||||
host_url = f'{target.scheme}://{target.netloc}'
|
||||
|
||||
get_body = g.user_request.send(base_url=target_url).text
|
||||
|
@ -537,6 +587,58 @@ def window():
|
|||
)
|
||||
|
||||
|
||||
@app.route('/robots.txt')
|
||||
def robots():
|
||||
response = make_response(
|
||||
'''User-Agent: *
|
||||
Disallow: /''', 200)
|
||||
response.mimetype = 'text/plain'
|
||||
return response
|
||||
|
||||
|
||||
@app.route('/favicon.ico')
|
||||
def favicon():
|
||||
return app.send_static_file('img/favicon.ico')
|
||||
|
||||
|
||||
@app.errorhandler(404)
|
||||
def page_not_found(e):
|
||||
return render_template('error.html', error_message=str(e)), 404
|
||||
|
||||
|
||||
@app.errorhandler(Exception)
|
||||
def internal_error(e):
|
||||
query = ''
|
||||
if request.method == 'POST':
|
||||
query = request.form.get('q')
|
||||
else:
|
||||
query = request.args.get('q')
|
||||
|
||||
# Attempt to parse the query
|
||||
try:
|
||||
search_util = Search(request, g.user_config, g.session_key)
|
||||
query = search_util.new_search_query()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
print(traceback.format_exc(), file=sys.stderr)
|
||||
|
||||
fallback_engine = os.environ.get('WHOOGLE_FALLBACK_ENGINE_URL', '')
|
||||
if (fallback_engine):
|
||||
return redirect(fallback_engine + query)
|
||||
|
||||
localization_lang = g.user_config.get_localization_lang()
|
||||
translation = app.config['TRANSLATIONS'][localization_lang]
|
||||
return render_template(
|
||||
'error.html',
|
||||
error_message='Internal server error (500)',
|
||||
translation=translation,
|
||||
farside='https://farside.link',
|
||||
config=g.user_config,
|
||||
query=urlparse.unquote(query),
|
||||
params=g.user_config.to_params(keys=['preferences'])), 500
|
||||
|
||||
|
||||
def run_app() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Whoogle Search console runner')
|
||||
|
@ -555,6 +657,11 @@ def run_app() -> None:
|
|||
default='',
|
||||
metavar='</path/to/unix.sock>',
|
||||
help='Listen for app on unix socket instead of host:port')
|
||||
parser.add_argument(
|
||||
'--unix-socket-perms',
|
||||
default='600',
|
||||
metavar='<octal permissions>',
|
||||
help='Octal permissions to use for the Unix domain socket (default 600)')
|
||||
parser.add_argument(
|
||||
'--debug',
|
||||
default=False,
|
||||
|
@ -606,7 +713,7 @@ def run_app() -> None:
|
|||
if 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)
|
||||
waitress.serve(app, unix_socket=args.unix_socket, unix_socket_perms=args.unix_socket_perms)
|
||||
else:
|
||||
waitress.serve(
|
||||
app,
|
||||
|
|
14
app/static/bangs/00-whoogle.json
Normal file
14
app/static/bangs/00-whoogle.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"!i": {
|
||||
"url": "search?q={}&tbm=isch",
|
||||
"suggestion": "!i (Whoogle Images)"
|
||||
},
|
||||
"!v": {
|
||||
"url": "search?q={}&tbm=vid",
|
||||
"suggestion": "!v (Whoogle Videos)"
|
||||
},
|
||||
"!n": {
|
||||
"url": "search?q={}&tbm=nws",
|
||||
"suggestion": "!n (Whoogle News)"
|
||||
}
|
||||
}
|
|
@ -70,6 +70,10 @@ select {
|
|||
border-radius: 8px !important;
|
||||
}
|
||||
|
||||
.BsXmcf {
|
||||
background-color: unset !important;
|
||||
}
|
||||
|
||||
.KP7LCb {
|
||||
box-shadow: 0 0 0 0 !important;
|
||||
}
|
||||
|
@ -143,7 +147,7 @@ select {
|
|||
color: var(--whoogle-dark-contrast-text) !important;
|
||||
}
|
||||
|
||||
.content {
|
||||
.content, .result-config {
|
||||
background-color: var(--whoogle-dark-element-bg) !important;
|
||||
color: var(--whoogle-contrast-text) !important;
|
||||
}
|
||||
|
|
|
@ -13,6 +13,12 @@ header {
|
|||
border-radius: 2px 0 0 0;
|
||||
}
|
||||
|
||||
.result-config {
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.mobile-logo {
|
||||
font: 22px/36px Futura, Arial, sans-serif;
|
||||
padding-left: 5px;
|
||||
|
@ -121,8 +127,8 @@ a {
|
|||
.header-tab-div {
|
||||
border-radius: 0 0 8px 8px;
|
||||
box-shadow: 0 2px 3px rgba(32, 33, 36, 0.18);
|
||||
margin-bottom: 20px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.header-tab-div-2 {
|
||||
|
@ -202,6 +208,24 @@ a.header-tab-a:visited {
|
|||
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;
|
||||
|
@ -215,9 +239,12 @@ a.header-tab-a:visited {
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
#result-country {
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
@media (max-width: 801px) {
|
||||
.header-tab-div {
|
||||
margin-bottom: 10px !important
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -45,6 +45,10 @@ select {
|
|||
box-shadow: 0 1px 6px rgba(32,33,36,0.28) !important;
|
||||
}
|
||||
|
||||
.BsXmcf {
|
||||
background-color: unset !important;
|
||||
}
|
||||
|
||||
.BVG0Nb {
|
||||
background-color: var(--whoogle-result-bg) !important;
|
||||
}
|
||||
|
@ -129,7 +133,7 @@ input {
|
|||
color: var(--whoogle-contrast-text) !important;
|
||||
}
|
||||
|
||||
.content {
|
||||
.content, .result-config {
|
||||
background-color: var(--whoogle-element-bg) !important;
|
||||
color: var(--whoogle-contrast-text) !important;
|
||||
}
|
||||
|
|
|
@ -58,6 +58,26 @@ details summary span {
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.site-favicon {
|
||||
float: left;
|
||||
width: 25px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.has-favicon .sCuL3 {
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
#flex_text_audio_icon_chunk {
|
||||
display: none;
|
||||
}
|
||||
|
||||
audio {
|
||||
display: block;
|
||||
margin-right: auto;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
@media (min-width: 801px) {
|
||||
body {
|
||||
min-width: 736px !important;
|
||||
|
|
|
@ -21,16 +21,6 @@ const handleUserInput = () => {
|
|||
xhrRequest.send('q=' + searchInput.value);
|
||||
};
|
||||
|
||||
const closeAllLists = el => {
|
||||
// Close all autocomplete suggestions
|
||||
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]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const removeActive = suggestion => {
|
||||
// Remove "autocomplete-active" class from previously active suggestion
|
||||
for (let i = 0; i < suggestion.length; i++) {
|
||||
|
@ -71,7 +61,7 @@ const addActive = (suggestion) => {
|
|||
|
||||
const autocompleteInput = (e) => {
|
||||
// Handle navigation between autocomplete suggestions
|
||||
let suggestion = document.getElementById(this.id + "-autocomplete-list");
|
||||
let suggestion = document.getElementById("autocomplete-list");
|
||||
if (suggestion) suggestion = suggestion.getElementsByTagName("div");
|
||||
if (e.keyCode === 40) { // down
|
||||
e.preventDefault();
|
||||
|
@ -92,29 +82,28 @@ const autocompleteInput = (e) => {
|
|||
};
|
||||
|
||||
const updateAutocompleteList = () => {
|
||||
let autocompleteList, autocompleteItem, i;
|
||||
let autocompleteItem, i;
|
||||
let val = originalSearch;
|
||||
closeAllLists();
|
||||
|
||||
let autocompleteList = document.getElementById("autocomplete-list");
|
||||
autocompleteList.innerHTML = "";
|
||||
|
||||
if (!val || !autocompleteResults) {
|
||||
return false;
|
||||
}
|
||||
|
||||
currentFocus = -1;
|
||||
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.setAttribute("class", "autocomplete-item");
|
||||
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();
|
||||
autocompleteList.innerHTML = "";
|
||||
document.getElementById("search-form").submit();
|
||||
});
|
||||
autocompleteList.appendChild(autocompleteItem);
|
||||
|
@ -123,10 +112,16 @@ const updateAutocompleteList = () => {
|
|||
};
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
let autocompleteList = document.createElement("div");
|
||||
autocompleteList.setAttribute("id", "autocomplete-list");
|
||||
autocompleteList.setAttribute("class", "autocomplete-items");
|
||||
|
||||
searchInput = document.getElementById("search-bar");
|
||||
searchInput.parentNode.appendChild(autocompleteList);
|
||||
|
||||
searchInput.addEventListener("keydown", (event) => autocompleteInput(event));
|
||||
|
||||
document.addEventListener("click", function (e) {
|
||||
closeAllLists(e.target);
|
||||
autocompleteList.innerHTML = "";
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,8 +1,61 @@
|
|||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const advSearchToggle = document.getElementById("adv-search-toggle");
|
||||
const advSearchDiv = document.getElementById("adv-search-div");
|
||||
const searchBar = document.getElementById("search-bar");
|
||||
const countrySelect = document.getElementById("result-country");
|
||||
const timePeriodSelect = document.getElementById("result-time-period");
|
||||
const arrowKeys = [37, 38, 39, 40];
|
||||
let searchValue = searchBar.value;
|
||||
|
||||
countrySelect.onchange = () => {
|
||||
let str = window.location.href;
|
||||
n = str.lastIndexOf("/search");
|
||||
if (n > 0) {
|
||||
str = str.substring(0, n) + `/search?q=${searchBar.value}`;
|
||||
str = tackOnParams(str);
|
||||
window.location.href = str;
|
||||
}
|
||||
}
|
||||
|
||||
timePeriodSelect.onchange = () => {
|
||||
let str = window.location.href;
|
||||
n = str.lastIndexOf("/search");
|
||||
if (n > 0) {
|
||||
str = str.substring(0, n) + `/search?q=${searchBar.value}`;
|
||||
str = tackOnParams(str);
|
||||
window.location.href = str;
|
||||
}
|
||||
}
|
||||
|
||||
function tackOnParams(str) {
|
||||
if (timePeriodSelect.value != "") {
|
||||
str = str + `&tbs=${timePeriodSelect.value}`;
|
||||
}
|
||||
if (countrySelect.value != "") {
|
||||
str = str + `&country=${countrySelect.value}`;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
const toggleAdvancedSearch = on => {
|
||||
if (on) {
|
||||
advSearchDiv.style.maxHeight = "70px";
|
||||
} 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();
|
||||
|
|
|
@ -1,44 +1,62 @@
|
|||
(function () {
|
||||
let searchBar, results;
|
||||
const keymap = {
|
||||
ArrowUp: goUp,
|
||||
ArrowDown: goDown,
|
||||
k: goUp,
|
||||
j: goDown,
|
||||
'/': focusSearch,
|
||||
};
|
||||
let activeIdx = -1;
|
||||
let searchBar, results;
|
||||
let shift = false;
|
||||
const keymap = {
|
||||
ArrowUp: goUp,
|
||||
ArrowDown: goDown,
|
||||
ShiftTab: goUp,
|
||||
Tab: goDown,
|
||||
k: goUp,
|
||||
j: goDown,
|
||||
'/': focusSearch,
|
||||
};
|
||||
let activeIdx = -1;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
searchBar = document.querySelector('#search-bar');
|
||||
results = document.querySelectorAll('#main>div>div>div>a');
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
searchBar = document.querySelector('#search-bar');
|
||||
results = document.querySelectorAll('#main>div>div>div>a');
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.target.tagName === 'INPUT') return true;
|
||||
if (typeof keymap[e.key] === 'function') {
|
||||
e.preventDefault();
|
||||
keymap[e.key]();
|
||||
}
|
||||
});
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Shift') {
|
||||
shift = true;
|
||||
}
|
||||
|
||||
function goUp () {
|
||||
if (activeIdx > 0) focusResult(activeIdx - 1);
|
||||
else focusSearch();
|
||||
}
|
||||
if (e.target.tagName === 'INPUT') return true;
|
||||
if (typeof keymap[e.key] === 'function') {
|
||||
e.preventDefault();
|
||||
|
||||
function goDown () {
|
||||
if (activeIdx < results.length - 1) focusResult(activeIdx + 1);
|
||||
}
|
||||
keymap[`${shift && e.key == 'Tab' ? 'Shift' : ''}${e.key}`]();
|
||||
}
|
||||
});
|
||||
|
||||
function focusResult (idx) {
|
||||
activeIdx = idx;
|
||||
results[activeIdx].scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
|
||||
results[activeIdx].focus();
|
||||
}
|
||||
document.addEventListener('keyup', (e) => {
|
||||
if (e.key === 'Shift') {
|
||||
shift = false;
|
||||
}
|
||||
});
|
||||
|
||||
function focusSearch () {
|
||||
activeIdx = -1;
|
||||
searchBar.focus();
|
||||
}
|
||||
function goUp () {
|
||||
if (activeIdx > 0) focusResult(activeIdx - 1);
|
||||
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 () {
|
||||
if (window.usingCalculator) {
|
||||
// if this function exists, it means the calculator widget has been displayed
|
||||
if (usingCalculator()) return;
|
||||
}
|
||||
activeIdx = -1;
|
||||
searchBar.focus();
|
||||
}
|
||||
}());
|
||||
|
|
|
@ -16,7 +16,7 @@ const checkForTracking = () => {
|
|||
]
|
||||
},
|
||||
"usps": {
|
||||
"link": `https://tools.usps.com/go/TrackConfirmAction?tLabels=${query}`,
|
||||
"link": `https://tools.usps.com/go/TrackConfirmAction_input?origTrackNum=${query}`,
|
||||
"expr": [
|
||||
/(\b\d{30}\b)|(\b91\d+\b)|(\b\d{20}\b)/,
|
||||
/^E\D{1}\d{9}\D{2}$|^9\d{15,21}$/,
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
},
|
||||
"maps": {
|
||||
"tbm": null,
|
||||
"href": "https://maps.google.com/maps?q={query}",
|
||||
"href": "https://maps.google.com/maps?q={map_query}",
|
||||
"name": "Maps",
|
||||
"selected": false
|
||||
},
|
||||
|
@ -28,11 +28,5 @@
|
|||
"href": "search?q={query}",
|
||||
"name": "News",
|
||||
"selected": false
|
||||
},
|
||||
"books": {
|
||||
"tbm": "bks",
|
||||
"href": "search?q={query}",
|
||||
"name": "Books",
|
||||
"selected": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
{"name": "Afrikaans (Afrikaans)", "value": "lang_af"},
|
||||
{"name": "Arabic (عربى)", "value": "lang_ar"},
|
||||
{"name": "Armenian (հայերեն)", "value": "lang_hy"},
|
||||
{"name": "Azerbaijani (Azərbaycanca)", "value": "lang_az"},
|
||||
{"name": "Belarusian (Беларуская)", "value": "lang_be"},
|
||||
{"name": "Bulgarian (български)", "value": "lang_bg"},
|
||||
{"name": "Catalan (Català)", "value": "lang_ca"},
|
||||
|
@ -28,6 +29,7 @@
|
|||
{"name": "Italian (Italiano)", "value": "lang_it"},
|
||||
{"name": "Japanese (日本語)", "value": "lang_ja"},
|
||||
{"name": "Korean (한국어)", "value": "lang_ko"},
|
||||
{"name": "Kurdish (Kurdî)", "value": "lang_ku"},
|
||||
{"name": "Latvian (Latvietis)", "value": "lang_lv"},
|
||||
{"name": "Lithuanian (Lietuvis)", "value": "lang_lt"},
|
||||
{"name": "Norwegian (Norwegian)", "value": "lang_no"},
|
||||
|
@ -45,8 +47,9 @@
|
|||
{"name": "Swedish (Svenska)", "value": "lang_sv"},
|
||||
{"name": "Thai (ไทย)", "value": "lang_th"},
|
||||
{"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": "Welsh (Cymraeg)", "value": "lang_cy"},
|
||||
{"name": "Xhosa (isiXhosa)", "value": "lang_xh"},
|
||||
{"name": "Zulu (isiZulu)", "value": "lang_zu"}
|
||||
]
|
||||
|
|
8
app/static/settings/time_periods.json
Normal file
8
app/static/settings/time_periods.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
[
|
||||
{"name": "Any time", "value": ""},
|
||||
{"name": "Past hour", "value": "qdr:h"},
|
||||
{"name": "Past 24 hours", "value": "qdr:d"},
|
||||
{"name": "Past week", "value": "qdr:w"},
|
||||
{"name": "Past month", "value": "qdr:m"},
|
||||
{"name": "Past year", "value": "qdr:y"}
|
||||
]
|
|
@ -1,8 +1,9 @@
|
|||
{
|
||||
"lang_en": {
|
||||
"": "--",
|
||||
"search": "Search",
|
||||
"config": "Configuration",
|
||||
"config-country": "Set Country",
|
||||
"config-country": "Country",
|
||||
"config-lang": "Interface Language",
|
||||
"config-lang-search": "Search Language",
|
||||
"config-near": "Near",
|
||||
|
@ -19,14 +20,18 @@
|
|||
"config-dark": "Dark Mode",
|
||||
"config-safe": "Safe Search",
|
||||
"config-alts": "Replace Social Media Links",
|
||||
"config-alts-help": "Replaces Twitter/YouTube/Instagram/etc links with privacy respecting alternatives.",
|
||||
"config-alts-help": "Replaces Twitter/YouTube/etc links with privacy respecting alternatives.",
|
||||
"config-new-tab": "Open Links in New Tab",
|
||||
"config-images": "Full Size Image Search",
|
||||
"config-images-help": "(Experimental) Adds the 'View Image' option to desktop image searches. This will cause image result thumbnails to be lower resolution.",
|
||||
"config-tor": "Use Tor",
|
||||
"config-get-only": "GET Requests Only",
|
||||
"config-url": "Root URL",
|
||||
"config-pref-url": "Preferences URL",
|
||||
"config-pref-encryption": "Encrypt Preferences",
|
||||
"config-pref-help": "Requires WHOOGLE_CONFIG_PREFERENCES_KEY, otherwise this will be ignored.",
|
||||
"config-css": "Custom CSS",
|
||||
"config-time-period": "Time Period",
|
||||
"load": "Load",
|
||||
"apply": "Apply",
|
||||
"save-as": "Save As...",
|
||||
|
@ -43,7 +48,12 @@
|
|||
"videos": "Videos",
|
||||
"news": "News",
|
||||
"books": "Books",
|
||||
"anon-view": "Anonymous View"
|
||||
"anon-view": "Anonymous View",
|
||||
"qdr:h": "Past hour",
|
||||
"qdr:d": "Past 24 hours",
|
||||
"qdr:w": "Past week",
|
||||
"qdr:m": "Past month",
|
||||
"qdr:y": "Past year"
|
||||
},
|
||||
"lang_nl": {
|
||||
"search": "Zoeken",
|
||||
|
@ -65,13 +75,16 @@
|
|||
"config-dark": "Donkere Modus",
|
||||
"config-safe": "Veilig zoeken",
|
||||
"config-alts": "Social Media Links Vervangen",
|
||||
"config-alts-help": "Vervang Twitter/YouTube/Instagram/etc links met privacy gerespecteerde alternatieve.",
|
||||
"config-alts-help": "Vervang Twitter/YouTube/etc links met privacy gerespecteerde alternatieve.",
|
||||
"config-new-tab": "Open Links in New Tab",
|
||||
"config-images": "Volledige Grote Afbeelding Zoeken",
|
||||
"config-images-help": "(Expirimenteel) Voegt de optie 'View Image' toe aan desktop afbeeldingen zoeken. Dit zorgt ervoor dat de voorbeeld foto's kleiner zijn.",
|
||||
"config-tor": "Gebruik Tor",
|
||||
"config-get-only": "Alleen GET Requests",
|
||||
"config-url": "Root URL",
|
||||
"config-pref-url": "Voorkeurs URL",
|
||||
"config-pref-encryption": "Versleutel voorkeuren",
|
||||
"config-pref-help": "Vereist WHOOGLE_CONFIG_PREFERENCES_KEY, anders wordt dit genegeerd.",
|
||||
"config-css": "Eigen CSS",
|
||||
"load": "Laden",
|
||||
"apply": "Opslaan",
|
||||
|
@ -89,7 +102,14 @@
|
|||
"videos": "Videos",
|
||||
"news": "Nieuws",
|
||||
"books": "Boeken",
|
||||
"anon-view": "Anonieme Weergave"
|
||||
"anon-view": "Anonieme Weergave",
|
||||
"": "--",
|
||||
"qdr:h": "Afgelopen uur",
|
||||
"qdr:d": "Afgelopen 24 uur",
|
||||
"qdr:w": "Vorige week",
|
||||
"qdr:m": "Afgelopen maand",
|
||||
"qdr:y": "Afgelopen jaar",
|
||||
"config-time-period": "Tijdsperiode"
|
||||
},
|
||||
"lang_de": {
|
||||
"search": "Suchen",
|
||||
|
@ -111,13 +131,16 @@
|
|||
"config-dark": "Dark Mode",
|
||||
"config-safe": "Sicheres Suchen",
|
||||
"config-alts": "Social-Media-Links ersetzen",
|
||||
"config-alts-help": "Ersetzt Twitter/YouTube/Instagram/etc Links mit Alternativen, welche die Privatsphäre respektieren.",
|
||||
"config-alts-help": "Ersetzt Twitter/YouTube/etc Links mit Alternativen, welche die Privatsphäre respektieren.",
|
||||
"config-new-tab": "Links in neuen Tabs öffnen",
|
||||
"config-images": "Bilder-Suche in Vollbild",
|
||||
"config-images-help": "(Experimentell) Fügt 'View Image'-Einstellung zu Dekstop Bilder-Suchen hinzu. Dadurch werden Thumbnails in niedrigerer Auflösung angezeigt.",
|
||||
"config-tor": "Tor benutzen",
|
||||
"config-get-only": "Auschließlich GET-Anfragen",
|
||||
"config-url": "Root URL",
|
||||
"config-pref-url": "Einstellungs URL",
|
||||
"config-pref-encryption": "Einstellungen verschlüsseln",
|
||||
"config-pref-help": "Erfordert WHOOGLE_CONFIG_PREFERENCES_KEY, sonst wird dies ignoriert.",
|
||||
"config-css": "Custom CSS",
|
||||
"load": "Laden",
|
||||
"apply": "Übernehmen",
|
||||
|
@ -135,7 +158,14 @@
|
|||
"videos": "Videos",
|
||||
"news": "Nachrichten",
|
||||
"books": "Bücher",
|
||||
"anon-view": "Anonyme Ansicht"
|
||||
"anon-view": "Anonyme Ansicht",
|
||||
"": "--",
|
||||
"qdr:h": "Letzte Stunde",
|
||||
"qdr:d": "Vergangene 24 Stunden",
|
||||
"qdr:w": "Letzte Woche",
|
||||
"qdr:m": "Letzten Monat",
|
||||
"qdr:y": "Vergangenes Jahr",
|
||||
"config-time-period": "Zeitraum"
|
||||
},
|
||||
"lang_es": {
|
||||
"search": "Buscar",
|
||||
|
@ -157,13 +187,16 @@
|
|||
"config-dark": "Modo Oscuro",
|
||||
"config-safe": "Búsqueda Segura",
|
||||
"config-alts": "Reemplazar Enlaces de Redes Sociales",
|
||||
"config-alts-help": "Reemplaza los enlaces de Twitter/YouTube/Instagram/etc con alternativas que respetan la privacidad.",
|
||||
"config-alts-help": "Reemplaza los enlaces de Twitter/YouTube/etc con alternativas que respetan la privacidad.",
|
||||
"config-new-tab": "Abrir enlaces en una pestaña nueva",
|
||||
"config-images": "Búsqueda de imágenes a tamaño completo",
|
||||
"config-images-help": "(Experimental) Agrega la opción 'Ver imagen' a las búsquedas de imágenes de escritorio. Esto hará que las miniaturas de los resultados de la imagen aparezcan con una resolución más baja.",
|
||||
"config-tor": "Usa Tor",
|
||||
"config-get-only": "GET solo solicitudes",
|
||||
"config-url": "URL raíz",
|
||||
"config-pref-url": "URL de preferencias",
|
||||
"config-pref-encryption": "Cifrar preferencias",
|
||||
"config-pref-help": "Requiere WHOOGLE_CONFIG_PREFERENCES_KEY; de lo contrario, se ignorará.",
|
||||
"config-css": "CSS personalizado",
|
||||
"load": "Cargar",
|
||||
"apply": "Aplicar",
|
||||
|
@ -181,7 +214,70 @@
|
|||
"videos": "Vídeos",
|
||||
"news": "Noticias",
|
||||
"books": "Libros",
|
||||
"anon-view": "Vista Anónima"
|
||||
"anon-view": "Vista Anónima",
|
||||
"": "--",
|
||||
"qdr:h": "Hora pasada",
|
||||
"qdr:d": "últimas 24 horas",
|
||||
"qdr:w": "Semana pasada",
|
||||
"qdr:m": "El mes pasado",
|
||||
"qdr:y": "Año pasado",
|
||||
"config-time-period": "Periodo de tiempo"
|
||||
},
|
||||
"lang_id": {
|
||||
"": "--",
|
||||
"search": "Telusuri",
|
||||
"config": "Konfigurasi",
|
||||
"config-country": "Negara",
|
||||
"config-lang": "Bahasa Antarmuka",
|
||||
"config-lang-search": "Bahasa Penelusuran",
|
||||
"config-near": "Dekat",
|
||||
"config-near-help": "Nama Kota",
|
||||
"config-block": "Blokir",
|
||||
"config-block-help": "Daftar situs yang dipisahkan dengan koma",
|
||||
"config-block-title": "Blokir berdasarkan Judul",
|
||||
"config-block-title-help": "Gunakan regex",
|
||||
"config-block-url": "Blokir berdasarkan URL",
|
||||
"config-block-url-help": "Gunakan regex",
|
||||
"config-theme": "Tema",
|
||||
"config-nojs": "Hapus Javascript dalam Tampilan Anonim",
|
||||
"config-anon-view": "Tampilkan Tautan Tampilan Anonim",
|
||||
"config-dark": "Mode Gelap",
|
||||
"config-safe": "Pencarian Aman",
|
||||
"config-alts": "Ganti Tautan Media Sosial",
|
||||
"config-alts-help": "Mengganti tautan Twitter/YouTube/dll dengan alternatif yang lebih menjaga privasi.",
|
||||
"config-new-tab": "Buka Tautan dalam Tab Baru",
|
||||
"config-images": "Pencarian Gambar Ukuran Penuh",
|
||||
"config-images-help": "(Eksperimental) Menambahkan opsi 'Lihat Gambar' ke pencarian gambar desktop. Ini akan menyebabkan resolusi thumbnail hasil gambar menjadi lebih rendah.",
|
||||
"config-tor": "Gunakan Tor",
|
||||
"config-get-only": "Hanya Gunakan GET",
|
||||
"config-url": "URL Dasar",
|
||||
"config-pref-url": "URL Preferensi",
|
||||
"config-pref-encryption": "Enkripsi Preferensi",
|
||||
"config-pref-help": "Memerlukan WHOOGLE_CONFIG_PREFERENCES_KEY, jika tidak akan diabaikan.",
|
||||
"config-css": "CSS Kustom",
|
||||
"config-time-period": "Periode Waktu",
|
||||
"load": "Muat",
|
||||
"apply": "Terapkan",
|
||||
"save-as": "Simpan Sebagai...",
|
||||
"github-link": "Lihat di GitHub",
|
||||
"translate": "terjemahkan",
|
||||
"light": "terang",
|
||||
"dark": "gelap",
|
||||
"system": "sistem",
|
||||
"ratelimit": "Instansi telah ratelimited",
|
||||
"continue-search": "Lanjutkan penelusuran Anda dengan Farside",
|
||||
"all": "Semua",
|
||||
"images": "Gambar",
|
||||
"maps": "Peta",
|
||||
"videos": "Video",
|
||||
"news": "Berita",
|
||||
"books": "Buku",
|
||||
"anon-view": "Tampilan Anonim",
|
||||
"qdr:h": "1 jam yang lalu",
|
||||
"qdr:d": "24 jam yang lalu",
|
||||
"qdr:w": "1 minggu yang lalu",
|
||||
"qdr:m": "1 bulan yang lalu",
|
||||
"qdr:y": "1 tahun yang lalu"
|
||||
},
|
||||
"lang_it": {
|
||||
"search": "Cerca",
|
||||
|
@ -203,13 +299,16 @@
|
|||
"config-dark": "Modalità Notte",
|
||||
"config-safe": "Ricerca Sicura",
|
||||
"config-alts": "Sostituisci link dei social",
|
||||
"config-alts-help": "Sostituisci link di Twitter/YouTube/Instagram/etc con alternative che rispettano la privacy.",
|
||||
"config-alts-help": "Sostituisci link di Twitter/YouTube/etc con alternative che rispettano la privacy.",
|
||||
"config-new-tab": "Apri i link in una nuova scheda",
|
||||
"config-images": "Ricerca Immagini",
|
||||
"config-images-help": "(Sperimentale) Aggiunge la modalità 'Ricerca Immagini'. Questo ridurrà drasticamente la qualità delle miniature durante la ricerca.",
|
||||
"config-tor": "Usa Tor",
|
||||
"config-get-only": "Utilizza solo richieste GET",
|
||||
"config-url": "Root URL",
|
||||
"config-pref-url": "URL delle preferenze",
|
||||
"config-pref-encryption": "Crittografa le preferenze",
|
||||
"config-pref-help": "Richiede WHOOGLE_CONFIG_PREFERENCES_KEY, altrimenti verrà ignorato.",
|
||||
"config-css": "CSS Personalizzato",
|
||||
"load": "Carica",
|
||||
"apply": "Applica",
|
||||
|
@ -227,7 +326,14 @@
|
|||
"videos": "Video",
|
||||
"news": "Notizie",
|
||||
"books": "Libri",
|
||||
"anon-view": "Vista Anonima"
|
||||
"anon-view": "Vista Anonima",
|
||||
"": "--",
|
||||
"qdr:h": "Ultima ora",
|
||||
"qdr:d": "Ultime 24 ore",
|
||||
"qdr:w": "Settimana scorsa",
|
||||
"qdr:m": "Mese scorso",
|
||||
"qdr:y": "L'anno scorso",
|
||||
"config-time-period": "Periodo di tempo"
|
||||
},
|
||||
"lang_pt": {
|
||||
"search": "Pesquisar",
|
||||
|
@ -249,13 +355,16 @@
|
|||
"config-dark": "Modo Escuro",
|
||||
"config-safe": "Pesquisa Segura",
|
||||
"config-alts": "Substituir Links de Redes Sociais",
|
||||
"config-alts-help": "Substitui os links do Twitter/YouTube/Instagram/etc. por alternativas que respeitam sua privacidade.",
|
||||
"config-alts-help": "Substitui os links do Twitter/YouTube/etc. por alternativas que respeitam sua privacidade.",
|
||||
"config-new-tab": "Abrir Links em Nova Aba",
|
||||
"config-images": "Pesquisa de Imagem em Tamanho Real",
|
||||
"config-images-help": "(Experimental) Adiciona a opção 'Mostrar Imagem' às pesquisas de imagens no modo 'para computador'. Isso fará com que as miniaturas do resultado da imagem sejam de menor resolução.",
|
||||
"config-tor": "Usar Tor",
|
||||
"config-get-only": "Apenas Pedidos GET",
|
||||
"config-url": "URL Fonte",
|
||||
"config-pref-url": "URL de preferências",
|
||||
"config-pref-encryption": "Criptografar preferências",
|
||||
"config-pref-help": "Requer WHOOGLE_CONFIG_PREFERENCES_KEY, caso contrário, será ignorado.",
|
||||
"config-css": "CSS Personalizado",
|
||||
"load": "Carregar",
|
||||
"apply": "Aplicar",
|
||||
|
@ -273,7 +382,14 @@
|
|||
"videos": "Vídeos",
|
||||
"news": "Notícias",
|
||||
"books": "Livros",
|
||||
"anon-view": "Visualização Anônima"
|
||||
"anon-view": "Visualização Anônima",
|
||||
"": "--",
|
||||
"qdr:h": "Hora passada",
|
||||
"qdr:d": "últimas 24 horas",
|
||||
"qdr:w": "Semana passada",
|
||||
"qdr:m": "Mês passado",
|
||||
"qdr:y": "Ano passado",
|
||||
"config-time-period": "Período de tempo"
|
||||
},
|
||||
"lang_ru": {
|
||||
"search": "Поиск",
|
||||
|
@ -295,13 +411,16 @@
|
|||
"config-dark": "Тёмный режим",
|
||||
"config-safe": "Безопасный поиск",
|
||||
"config-alts": "Заменить ссылки на социальные сети",
|
||||
"config-alts-help": "Замена ссылкок Twitter, YouTube, Instagram и т.д. на альтернативы, уважающие конфиденциальность.",
|
||||
"config-alts-help": "Замена ссылкок Twitter, YouTube, и т.д. на альтернативы, уважающие конфиденциальность.",
|
||||
"config-new-tab": "Открывать ссылки в новой вкладке",
|
||||
"config-images": "Поиск полноразмерных изображений",
|
||||
"config-images-help": "(Эксперимент) Добавляет опцию 'Просмотр изображения' к поиску изображений в ПК-режиме. Это приведет к тому, что миниатюры изображений будут иметь более низкое разрешение.",
|
||||
"config-tor": "Использовать Tor",
|
||||
"config-get-only": "Только GET-запросы",
|
||||
"config-url": "Корневой URL-адрес",
|
||||
"config-pref-url": "URL-адрес настроек",
|
||||
"config-pref-encryption": "Зашифровать настройки",
|
||||
"config-pref-help": "Требуется WHOOGLE_CONFIG_PREFERENCES_KEY, иначе это будет проигнорировано.",
|
||||
"config-css": "Пользовательский CSS",
|
||||
"load": "Загрузить",
|
||||
"apply": "Применить",
|
||||
|
@ -319,7 +438,14 @@
|
|||
"videos": "Видео",
|
||||
"news": "Новости",
|
||||
"books": "Книги",
|
||||
"anon-view": "Анонимный просмотр"
|
||||
"anon-view": "Анонимный просмотр",
|
||||
"": "--",
|
||||
"qdr:h": "Прошедший час",
|
||||
"qdr:d": "Последние 24 часа",
|
||||
"qdr:w": "На прошлой неделе",
|
||||
"qdr:m": "Прошлый месяц",
|
||||
"qdr:y": "Прошлый год",
|
||||
"config-time-period": "Временной период"
|
||||
},
|
||||
"lang_zh-CN": {
|
||||
"search": "搜索",
|
||||
|
@ -341,13 +467,16 @@
|
|||
"config-dark": "深色模式",
|
||||
"config-safe": "安全搜索",
|
||||
"config-alts": "替换社交媒体链接",
|
||||
"config-alts-help": "使用尊重隐私的第三方网站替换 Twitter/YouTube/Instagram 等链接。",
|
||||
"config-alts-help": "使用尊重隐私的第三方网站替换 Twitter/YouTube 等链接。",
|
||||
"config-new-tab": "在新标签页打开链接",
|
||||
"config-images": "完整尺寸图片搜索",
|
||||
"config-images-help": "(实验性)为桌面版图片搜索添加“查看图片”选项。这会降低图片结果缩略图的分辨率。",
|
||||
"config-tor": "使用 Tor",
|
||||
"config-get-only": "仅限 GET 请求",
|
||||
"config-url": "站点根 URL",
|
||||
"config-pref-url": "首选项网址",
|
||||
"config-pref-encryption": "加密首选项",
|
||||
"config-pref-help": "需要 WHOOGLE_CONFIG_PREFERENCES_KEY,否则将被忽略。",
|
||||
"config-css": "自定义 CSS",
|
||||
"load": "载入",
|
||||
"apply": "应用",
|
||||
|
@ -360,12 +489,19 @@
|
|||
"ratelimit": "实例已被限速",
|
||||
"continue-search": "继续搜索 Farside",
|
||||
"all": "全部",
|
||||
"images": "圖片",
|
||||
"maps": "地圖",
|
||||
"videos": "影片",
|
||||
"news": "新聞",
|
||||
"books": "書籍",
|
||||
"anon-view": "匿名视图"
|
||||
"images": "图片",
|
||||
"maps": "地图",
|
||||
"videos": "视频",
|
||||
"news": "新闻",
|
||||
"books": "书籍",
|
||||
"anon-view": "匿名视图",
|
||||
"": "--",
|
||||
"qdr:h": "过去一小时",
|
||||
"qdr:d": "过去 24 小时",
|
||||
"qdr:w": "上周",
|
||||
"qdr:m": "过去一个月",
|
||||
"qdr:y": "过去一年",
|
||||
"config-time-period": "时间段"
|
||||
},
|
||||
"lang_si": {
|
||||
"search": "සොයන්න",
|
||||
|
@ -394,6 +530,9 @@
|
|||
"config-tor": "ටෝර් භාවිතා කරන්න",
|
||||
"config-get-only": "ඉල්ලීම් පමණක් ලබා ගන්න",
|
||||
"config-url": "ඒ.ස.නි.(URL) මූලය",
|
||||
"config-pref-url": "මනාප URL",
|
||||
"config-pref-encryption": "මනාප සංකේතනය කරන්න",
|
||||
"config-pref-help": "WHOOGLE_CONFIG_PREFERENCES_KEY අවශ්ය වේ, එසේ නොමැතිනම් මෙය නොසලකා හරිනු ඇත.",
|
||||
"config-css": "අභිරුචි සීඑස්එස්",
|
||||
"load": "පූරනය කරන්න",
|
||||
"apply": "යොදන්න",
|
||||
|
@ -411,7 +550,14 @@
|
|||
"videos": "වීඩියෝ",
|
||||
"news": "අනුරූප",
|
||||
"books": "පොත්",
|
||||
"anon-view": "නිර්නාමික දසුන"
|
||||
"anon-view": "නිර්නාමික දසුන",
|
||||
"": "--",
|
||||
"qdr:h": "පසුගිය පැය",
|
||||
"qdr:d": "පසුගිය පැය 24",
|
||||
"qdr:w": "පසුගිය සතිය",
|
||||
"qdr:m": "පසුගිය මාසය",
|
||||
"qdr:y": "පසුගිය වසර",
|
||||
"config-time-period": "කාල සීමාව"
|
||||
},
|
||||
"lang_fr": {
|
||||
"search": "Chercher",
|
||||
|
@ -433,13 +579,16 @@
|
|||
"config-dark": "Mode Sombre",
|
||||
"config-safe": "Recherche sécurisée",
|
||||
"config-alts": "Remplacer les liens des réseaux sociaux",
|
||||
"config-alts-help": "Remplacer les liens Twitter/YouTube/Instagram/etc avec leurs alternatives respectueuses de la vie privée.",
|
||||
"config-alts-help": "Remplacer les liens Twitter/YouTube/etc avec leurs alternatives respectueuses de la vie privée.",
|
||||
"config-new-tab": "Ouvrir les Liens dans un Nouveau Onglet",
|
||||
"config-images": "Recherche d'image en plein écran",
|
||||
"config-images-help": "(Expérimental) Ajouter l'option 'Voir Image' aux recherches d'images sur ordinateur. Les vignettes des résultats d'image seront de plus faible résolution.",
|
||||
"config-tor": "Utiliser Tor",
|
||||
"config-get-only": "Requêtes GET seulement",
|
||||
"config-url": "URL de la racine",
|
||||
"config-pref-url": "URL des préférences",
|
||||
"config-pref-encryption": "Chiffrer les préférences",
|
||||
"config-pref-help": "Nécessite WHOOGLE_CONFIG_PREFERENCES_KEY, sinon cela sera ignoré.",
|
||||
"config-css": "CSS Personalisé",
|
||||
"load": "Charger",
|
||||
"apply": "Appliquer",
|
||||
|
@ -457,7 +606,14 @@
|
|||
"videos": "Vidéos",
|
||||
"news": "Actualités",
|
||||
"books": "Livres",
|
||||
"anon-view": "Vue anonyme"
|
||||
"anon-view": "Vue anonyme",
|
||||
"": "--",
|
||||
"qdr:h": "Heure passée",
|
||||
"qdr:d": "Dernières 24 heures",
|
||||
"qdr:w": "La semaine dernière",
|
||||
"qdr:m": "Mois passé",
|
||||
"qdr:y": "L'année passée",
|
||||
"config-time-period": "Période de temps"
|
||||
},
|
||||
"lang_fa": {
|
||||
"search": "جستجو",
|
||||
|
@ -486,6 +642,9 @@
|
|||
"config-tor": "استفاده از تور",
|
||||
"config-get-only": "فقط درخواستهای GET",
|
||||
"config-url": "آدرس ریشهی سایت",
|
||||
"config-pref-url": "URL تنظیمات برگزیده",
|
||||
"config-pref-encryption": "رمزگذاری تنظیمات برگزیده",
|
||||
"config-pref-help": "به WHOOGLE_CONFIG_PREFERENCES_KEY نیاز دارد، در غیر این صورت نادیده گرفته خواهد شد.",
|
||||
"config-css": "CSS دلخواه",
|
||||
"load": "بارگذاری",
|
||||
"apply": "تایید",
|
||||
|
@ -503,7 +662,14 @@
|
|||
"videos": "ویدئوها",
|
||||
"news": "اخبار",
|
||||
"books": "کتابها",
|
||||
"anon-view": "نمای ناشناس"
|
||||
"anon-view": "نمای ناشناس",
|
||||
"": "--",
|
||||
"qdr:h": "ساعت گذشته",
|
||||
"qdr:d": "24 ساعت گذشته",
|
||||
"qdr:w": "هفته گذشته",
|
||||
"qdr:m": "ماه گذشته",
|
||||
"qdr:y": "سال گذشته",
|
||||
"config-time-period": "بازه زمانی"
|
||||
},
|
||||
"lang_cs": {
|
||||
"search": "Hledat",
|
||||
|
@ -525,13 +691,16 @@
|
|||
"config-dark": "Tmavý motiv",
|
||||
"config-safe": "Bezpečné vyhledávání",
|
||||
"config-alts": "Nahradit odkazy na sociální média",
|
||||
"config-alts-help": "Nahradí odkazy na Twitter, YouTube, Instagram atd. alternativami respektujícími soukromí.",
|
||||
"config-alts-help": "Nahradí odkazy na Twitter, YouTube, atd. alternativami respektujícími soukromí.",
|
||||
"config-new-tab": "Otevírat odkazy na novém listu",
|
||||
"config-images": "Vyhledávání obrázků v plné velikosti",
|
||||
"config-images-help": "(Experimentální) Přidá volbu ‚Zobrazit obrázek‘ do vyhledávání obrázků na ploše. Způsobí to, že náhledy výsledků vyhledávání obrázků budou mít nižší rozlišení.",
|
||||
"config-tor": "Používat Tor",
|
||||
"config-get-only": "Pouze požadavky GET",
|
||||
"config-url": "Kořenová adresa URL",
|
||||
"config-pref-url": "Adresa URL předvoleb",
|
||||
"config-pref-encryption": "Předvolby šifrování",
|
||||
"config-pref-help": "Vyžaduje WHOOGLE_CONFIG_PREFERENCES_KEY, jinak bude ignorována.",
|
||||
"config-css": "Vlastní CSS",
|
||||
"load": "Načíst",
|
||||
"apply": "Použít",
|
||||
|
@ -549,9 +718,17 @@
|
|||
"videos": "Videa",
|
||||
"news": "Zprávy",
|
||||
"books": "Knihy",
|
||||
"anon-view": "Anonymní pohled"
|
||||
"anon-view": "Anonymní pohled",
|
||||
"": "--",
|
||||
"qdr:h": "Poslední hodina",
|
||||
"qdr:d": "Posledních 24 hodin",
|
||||
"qdr:w": "Minulý týden",
|
||||
"qdr:m": "Minulý měsíc",
|
||||
"qdr:y": "Minulý rok",
|
||||
"config-time-period": "Časový úsek"
|
||||
},
|
||||
"lang_zh-TW": {
|
||||
"": "--",
|
||||
"search": "搜尋",
|
||||
"config": "設定",
|
||||
"config-country": "設定國家",
|
||||
|
@ -571,31 +748,40 @@
|
|||
"config-dark": "深色模式",
|
||||
"config-safe": "安全搜尋",
|
||||
"config-alts": "將社群網站連結替換",
|
||||
"config-alts-help": "將 Twitter/YouTube/Instagram 等網站之連結替換為尊重隱私的第三方網站。",
|
||||
"config-alts-help": "將 Twitter/YouTube 等網站之連結替換為尊重隱私的第三方網站。",
|
||||
"config-new-tab": "以新分頁開啟連結",
|
||||
"config-images": "完整尺寸圖片搜尋",
|
||||
"config-images-help": "(實驗性)在桌面版圖片搜尋中增加「檢視圖片」選項。這會使搜尋結果圖片解析度降低",
|
||||
"config-images-help": "(實驗性)在桌面版圖片搜尋中增加「檢視圖片」選項。這會使搜尋結果圖片解析度降低。",
|
||||
"config-tor": "使用 Tor",
|
||||
"config-get-only": "僅限於 GET 要求",
|
||||
"config-url": "首頁網址",
|
||||
"config-pref-url": "設定網址",
|
||||
"config-pref-encryption": "加密設定",
|
||||
"config-pref-help": "需要一併設定 WHOOGLE_CONFIG_PREFERENCES_KEY,否則將會被忽略。",
|
||||
"config-css": "自定 CSS",
|
||||
"config-time-period": "時間範圍",
|
||||
"load": "載入",
|
||||
"apply": "套用",
|
||||
"save-as": "另存為...",
|
||||
"github-link": "在 GitHub 上查看",
|
||||
"github-link": "在 GitHub 上檢視",
|
||||
"translate": "翻譯",
|
||||
"light": "明亮的",
|
||||
"dark": "黑暗的",
|
||||
"system": "依系統",
|
||||
"ratelimit": "實例已被限速",
|
||||
"continue-search": "繼續搜索 Farside",
|
||||
"system": "依照系統設定",
|
||||
"ratelimit": "該實例已被限速",
|
||||
"continue-search": "繼續搜尋 Farside",
|
||||
"all": "全部",
|
||||
"images": "圖片",
|
||||
"maps": "地圖",
|
||||
"videos": "影片",
|
||||
"news": "新聞",
|
||||
"books": "書籍",
|
||||
"anon-view": "匿名檢視"
|
||||
"anon-view": "匿名檢視",
|
||||
"qdr:h": "過去 1 小時",
|
||||
"qdr:d": "過去 24 小時",
|
||||
"qdr:w": "過去 1 週",
|
||||
"qdr:m": "過去 1 個月",
|
||||
"qdr:y": "過去 1 年"
|
||||
},
|
||||
"lang_bg": {
|
||||
"search": "Търсене",
|
||||
|
@ -617,13 +803,16 @@
|
|||
"config-dark": "Тъмен режим",
|
||||
"config-safe": "Безопасно търсене",
|
||||
"config-alts": "Заменете връзките към социалните медии",
|
||||
"config-alts-help": "Заменя връзките на Twitter/YouTube/Instagram и т.н. с защитени алтернативни поверителни връзки.",
|
||||
"config-alts-help": "Заменя връзките на Twitter/YouTube и т.н. с защитени алтернативни поверителни връзки.",
|
||||
"config-new-tab": "Отваряне на връзките в нов раздел",
|
||||
"config-images": "Търсене на изображения в пълен размер",
|
||||
"config-images-help": "(Експериментално) Добавя опцията „Преглед на изображение“ към резултатите от търсене на изображения през работния плот на компютъра. Това ще доведе до по-ниска разделителна способност на миниатюрите, в резултатите от търсене на изображения.",
|
||||
"config-tor": "Използвайте Tor",
|
||||
"config-get-only": "Само GET заявки",
|
||||
"config-url": "Основен URL адрес",
|
||||
"config-pref-url": "URL адрес на предпочитанията",
|
||||
"config-pref-encryption": "Шифроване на предпочитанията",
|
||||
"config-pref-help": "Изисква WHOOGLE_CONFIG_PREFERENCES_KEY, в противен случай това ще бъде игнорирано.",
|
||||
"config-css": "Персонализиран CSS",
|
||||
"load": "Зареди",
|
||||
"apply": "Приложи",
|
||||
|
@ -641,7 +830,14 @@
|
|||
"videos": "Новини",
|
||||
"news": "Карти",
|
||||
"books": "Книги",
|
||||
"anon-view": "Анонимен изглед"
|
||||
"anon-view": "Анонимен изглед",
|
||||
"": "--",
|
||||
"qdr:h": "Последния час",
|
||||
"qdr:d": "Последните 24 часа",
|
||||
"qdr:w": "Миналата седмица",
|
||||
"qdr:m": "Миналия месец",
|
||||
"qdr:y": "Изминалата година",
|
||||
"config-time-period": "Времеви период"
|
||||
},
|
||||
"lang_hi": {
|
||||
"search": "खोज",
|
||||
|
@ -670,6 +866,9 @@
|
|||
"config-tor": "TOR का प्रयोग करें",
|
||||
"config-get-only": "केवल GET अनुरोध",
|
||||
"config-url": "रूट यूआरएल",
|
||||
"config-pref-url": "वरीयताएँ URL",
|
||||
"config-pref-encryption": "एन्क्रिप्ट प्राथमिकताएं",
|
||||
"config-pref-help": "WHOOGLE_CONFIG_PREFERENCES_KEY की आवश्यकता है, अन्यथा इसे अनदेखा कर दिया जाएगा।",
|
||||
"config-css": "कस्टम सीएसएस",
|
||||
"load": "भार",
|
||||
"apply": "लागू करना",
|
||||
|
@ -687,7 +886,14 @@
|
|||
"videos": "मैप",
|
||||
"news": "समाचार",
|
||||
"books": "किताबें",
|
||||
"anon-view": "अनाम दृश्य"
|
||||
"anon-view": "अनाम दृश्य",
|
||||
"": "--",
|
||||
"qdr:h": "पिछले घंटे",
|
||||
"qdr:d": "पिछले 24 घंटे",
|
||||
"qdr:w": "पिछले सप्ताह",
|
||||
"qdr:m": "पिछले महीने",
|
||||
"qdr:y": "पिछला वर्ष",
|
||||
"config-time-period": "समय सीमा"
|
||||
},
|
||||
"lang_ja": {
|
||||
"search": "検索",
|
||||
|
@ -709,13 +915,16 @@
|
|||
"config-dark": "ダークモード",
|
||||
"config-safe": "セーフサーチ",
|
||||
"config-alts": "ソーシャルメディアのリンクを置き換え",
|
||||
"config-alts-help": "Twitter/YouTube/Instagramなどのリンクを、プライバシーを尊重した代替サイトに置き換えます。",
|
||||
"config-alts-help": "Twitter/YouTubeなどのリンクを、プライバシーを尊重した代替サイトに置き換えます。",
|
||||
"config-new-tab": "新しいタブでリンクを開く",
|
||||
"config-images": "フルサイズの画像を検索",
|
||||
"config-images-help": "(実験的) デスクトップの画像検索に「画像を表示」オプションを追加します。これにより、画像検索結果のサムネイルの解像度が低くなります。",
|
||||
"config-tor": "Torを使用",
|
||||
"config-get-only": "GETリクエストのみ",
|
||||
"config-url": "ルートURL",
|
||||
"config-pref-url": "設定 URL",
|
||||
"config-pref-encryption": "設定を暗号化する",
|
||||
"config-pref-help": "WHOOGLE_CONFIG_PREFERENCES_KEY が必要です。それ以外の場合、これは無視されます。",
|
||||
"config-css": "カスタムCSS",
|
||||
"load": "読み込み",
|
||||
"apply": "反映",
|
||||
|
@ -733,7 +942,14 @@
|
|||
"videos": "動画",
|
||||
"news": "ニュース",
|
||||
"books": "書籍",
|
||||
"anon-view": "匿名ビュー"
|
||||
"anon-view": "匿名ビュー",
|
||||
"": "--",
|
||||
"qdr:h": "過去 1 時間",
|
||||
"qdr:d": "過去 24 時間",
|
||||
"qdr:w": "この1週間",
|
||||
"qdr:m": "先月",
|
||||
"qdr:y": "過年度",
|
||||
"config-time-period": "期間"
|
||||
},
|
||||
"lang_ko": {
|
||||
"search": "검색",
|
||||
|
@ -755,13 +971,16 @@
|
|||
"config-dark": "다크 모드",
|
||||
"config-safe": "세이프서치",
|
||||
"config-alts": "소설 미디어 주소 수정",
|
||||
"config-alts-help": "Twitter/YouTube/Instagram 등의 링크를 프라이버시를 존중하는 링크로 대체합니다",
|
||||
"config-alts-help": "Twitter/YouTube 등의 링크를 프라이버시를 존중하는 링크로 대체합니다",
|
||||
"config-new-tab": "새 탭에서 열기",
|
||||
"config-images": "최대 크기 이미지 검색",
|
||||
"config-images-help": "(실험적) 데스크톱 이미지 검색에 '이미지 보기' 옵션을 추가합니다. 이미지 결과 미리보기 썸네일이 낮은 해상도로 표시됩니다.",
|
||||
"config-tor": "Tor 사용",
|
||||
"config-get-only": "GET 요청만",
|
||||
"config-url": "루트 URL",
|
||||
"config-pref-url": "환경설정 URL",
|
||||
"config-pref-encryption": "암호화 환경 설정",
|
||||
"config-pref-help": "WHOOGLE_CONFIG_PREFERENCES_KEY이 필요합니다. 그렇지 않으면 무시됩니다.",
|
||||
"config-css": "커스텀 CSS",
|
||||
"load": "불러오기",
|
||||
"apply": "적용",
|
||||
|
@ -779,6 +998,293 @@
|
|||
"videos": "동영상",
|
||||
"news": "뉴스",
|
||||
"books": "도서",
|
||||
"anon-view": "익명 보기"
|
||||
"anon-view": "익명 보기",
|
||||
"": "--",
|
||||
"qdr:h": "지난 시간",
|
||||
"qdr:d": "지난 24시간",
|
||||
"qdr:w": "지난 주",
|
||||
"qdr:m": "지난달",
|
||||
"qdr:y": "지난 해",
|
||||
"config-time-period": "기간"
|
||||
},
|
||||
"lang_ku": {
|
||||
"search": "Bigere",
|
||||
"config": "Sazkarî",
|
||||
"config-country": "Welat",
|
||||
"config-lang": "Zimanê Navrûyê",
|
||||
"config-lang-search": "Zimanê Lêgerînê",
|
||||
"config-near": "Nêzîk",
|
||||
"config-near-help": "Navê Bajêr",
|
||||
"config-block": "Astengkirin",
|
||||
"config-block-help": "Rêzoka malperê ya ji hev veqetandî bi riya bêhnok",
|
||||
"config-block-title": "Bi ya Sernavê Asteng bike",
|
||||
"config-block-title-help": "regex bi kar bîne",
|
||||
"config-block-url": "Bi ya Girêdanê asteng bike",
|
||||
"config-block-url-help": "regex bi kar bîne",
|
||||
"config-theme": "Rûkar",
|
||||
"config-nojs": "Javascript Rake di Nîşandanên Nenenas de",
|
||||
"config-anon-view": "Girêdanên Nenas Nîşan bide",
|
||||
"config-dark": "Awaya Tarî",
|
||||
"config-safe": "Lêgerîna Parastî",
|
||||
"config-alts": "Girêdanên Tora Civakî Biguherîne",
|
||||
"config-alts-help": "Girêdanên Twitter/YouTube/hwd biguherîne bi alternatîvên ku ji taybetiyê re rêzê digrin.",
|
||||
"config-new-tab": "Girêdanan di Rûgereke Nû de Veke",
|
||||
"config-images": "Lêgerîna Wêne bi Mezinahiya Tevahî",
|
||||
"config-images-help": "(Ezmûnî) Vebijêrka 'Wêneyê Nîşan bide' tevlî lêgerînên wêneyê yê sermaseyê bike. Ev ê bibe sedem ku çareseriya encamê wêneyên nîşanê kêmtir bibe.",
|
||||
"config-tor": "Tor bi kar bîne",
|
||||
"config-get-only": "Daxwazan bi Dest Bixe",
|
||||
"config-url": "Rêgeha girêdanê",
|
||||
"config-pref-url": "Vebijêrkên girêdanê",
|
||||
"config-pref-encryption": "Vebijêrkan şîfre bike",
|
||||
"config-pref-help": "Pêdivî bi WHOOGLE_CONFIG_PREFERENCES_KEY dike, wekî din ev ê were paşguhkirin.",
|
||||
"config-css": "CSS kesane bike",
|
||||
"load": "Bar bike",
|
||||
"apply": "Bisepîne",
|
||||
"save-as": "Biparêze wekî...",
|
||||
"github-link": "Li ser GitHub Nîşan bide",
|
||||
"translate": "werger",
|
||||
"light": "ronî",
|
||||
"dark": "tarî",
|
||||
"system": "pergal",
|
||||
"ratelimit": "Mînak bi rêjeya sînorkirî ye",
|
||||
"continue-search": "Lêgerîna xwe bi Farside re bidomîne",
|
||||
"all": "Hemû",
|
||||
"images": "Wêne",
|
||||
"maps": "Nexşe",
|
||||
"videos": "Vîdyo",
|
||||
"news": "Nûçe",
|
||||
"books": "Pirtûk",
|
||||
"anon-view": "Dîtina Nenas",
|
||||
"": "--",
|
||||
"qdr:h": "Demjimêra borî",
|
||||
"qdr:d": "24 Demjimêrên borî",
|
||||
"qdr:w": "Hefteya borî",
|
||||
"qdr:m": "Meha borî",
|
||||
"qdr:y": "Sala borî",
|
||||
"config-time-period": "Pêşsazkariyên demê"
|
||||
},
|
||||
"lang_th": {
|
||||
"search": "ค้นหา",
|
||||
"config": "กำหนดค่า",
|
||||
"config-country": "ประเทศ",
|
||||
"config-lang": "ภาษาหน้าอินเตอร์เฟซ",
|
||||
"config-lang-search": "ค้นหาในภาษา",
|
||||
"config-near": "รอบๆ",
|
||||
"config-near-help": "ชื่อเมือง",
|
||||
"config-block": "บล็อค",
|
||||
"config-block-help": "รายการเว็บไซต์คั่นด้วยเครื่องหมายจุลภาค(,)",
|
||||
"config-block-title": "บล็อกตามหัวชื่อเว็บไซต์",
|
||||
"config-block-title-help": "ใช้ regex",
|
||||
"config-block-url": "บล็อกตาม URL",
|
||||
"config-block-url-help": "ใช้ regex",
|
||||
"config-theme": "ธีม",
|
||||
"config-nojs": "ลบ Javascript ในมุมมองที่ไม่ระบุตัวตน",
|
||||
"config-anon-view": "แสดงลิงค์ในมุมมองไม่ระบุตัวตน",
|
||||
"config-dark": "โหมดมืด",
|
||||
"config-safe": "ค้นหาแบบปลอดภัย",
|
||||
"config-alts": "แทนที่ลิงก์โซเชียลมีเดีย",
|
||||
"config-alts-help": "แทนที่ลิงก์ Twitter/YouTube/อื่นๆ ตามความเป็นส่วนตัวด้วยทางเลือกอื่น",
|
||||
"config-new-tab": "เปิดลิงก์ในแท็บใหม่",
|
||||
"config-images": "ค้นหารูปภาพขนาดเต็ม",
|
||||
"config-images-help": "(ตัวอย่าง) เพิ่มตัวเลือก 'ดูภาพ' ในการค้นหารูปภาพบนเดสก์ท็อป ซึ่งจะทำให้ภาพขนาดย่อมีความละเอียดต่ำ",
|
||||
"config-tor": "ใช้ Tor",
|
||||
"config-get-only": "รับคำขอเท่านั้น",
|
||||
"config-url": "URL หลัก",
|
||||
"config-pref-url": "URL การตั้งค่า",
|
||||
"config-pref-encryption": "เข้ารหัสการตั้งค่า",
|
||||
"config-pref-help": "จำเป็นต้องมี WHOOGLE_CONFIG_PREFERENCES_KEY ไม่เช่นนั้นจะถูกละเว้นไป",
|
||||
"config-css": "กำหนด CSS เอง",
|
||||
"load": "โหลด",
|
||||
"apply": "ยอมรับ",
|
||||
"save-as": "บันทึกเป็น...",
|
||||
"github-link": "ดูบน GitHub",
|
||||
"translate": "แปลภาษา",
|
||||
"light": "สว่าง",
|
||||
"dark": "มืด",
|
||||
"system": "ระบบ",
|
||||
"ratelimit": "คำขอร้องจะถูกจำกัดจำนวน",
|
||||
"continue-search": "ค้นหาต่อไปด้วย Farside",
|
||||
"all": "ทั้งหมด",
|
||||
"images": "รูปภาพ",
|
||||
"maps": "แผนที่",
|
||||
"videos": "วิดีโอ",
|
||||
"news": "ข่าว",
|
||||
"books": "หนังสือ",
|
||||
"anon-view": "มุมมองที่ไม่ระบุตัวตน",
|
||||
"": "--",
|
||||
"qdr:h": "ชั่วโมงที่ผ่านมา",
|
||||
"qdr:d": "24 ชั่วโมงที่ผ่านมา",
|
||||
"qdr:w": "สัปดาห์ที่ผ่านมา",
|
||||
"qdr:m": "เดือนที่ผ่านมา",
|
||||
"qdr:y": "ปีที่ผ่านมา",
|
||||
"config-time-period": "ระยะเวลา"
|
||||
},
|
||||
"lang_cy": {
|
||||
"search": "Chwiliwch",
|
||||
"config": "Cyfluniad",
|
||||
"config-country": "Gwlad",
|
||||
"config-lang": "Iaith Rhyngwyneb",
|
||||
"config-lang-search": "Iaith Chwiliad",
|
||||
"config-near": "Ger",
|
||||
"config-near-help": "Enw'r Dinas",
|
||||
"config-block": "Blociwch",
|
||||
"config-block-help": "Rhestr Gwahanu Comma o Wefannau",
|
||||
"config-block-title": "Blocio yn ôl teitl",
|
||||
"config-block-title-help": "Defnyddio regex",
|
||||
"config-block-url": "Blocio yn ôl URL",
|
||||
"config-block-url-help": "Defnyddio regex",
|
||||
"config-theme": "Thema",
|
||||
"config-nojs": "Dileu Javascript mewn Golwg Anhysbys",
|
||||
"config-anon-view": "Dangos Cysylltau Golwg Anhysbys",
|
||||
"config-dark": "Modd Tywyll",
|
||||
"config-safe": "Chwilio'n Ddiogel",
|
||||
"config-alts": "Disodli Cysylltau Cyfryngau Cymdeithasol",
|
||||
"config-alts-help": "Yn Amnewid Cysylltau Twitter/YouTube/etc gyda Gwefanau Preifatrwydd.",
|
||||
"config-new-tab": "Agor Cysylltau mewn Tab Newydd",
|
||||
"config-images": "Chwiliad Delwedd Maint Llawn",
|
||||
"config-images-help": "(Arbrofol) Yn dangos y 'Gweld Delwedd' opsiwn i chwiliadau delweddau bwrdd gwaith. Mae hyn yn achosi delwedd o ansawdd is.",
|
||||
"config-tor": "Defnyddiwch Tor",
|
||||
"config-get-only": "Ceisiadau GET yn unig",
|
||||
"config-url": "URL am Gwraidd",
|
||||
"config-pref-url": "URL am Dewisiadau",
|
||||
"config-pref-encryption": "Cyfluniad Amgryptio",
|
||||
"config-pref-help": "Yn angen WHOOGLE_CONFIG_PREFERENCES_KEY, neu bydd hyn yn cael ei anwybyddu.",
|
||||
"config-css": "CSS Arferol",
|
||||
"load": "Llwythwch",
|
||||
"apply": "Cymhwyswch",
|
||||
"save-as": "Cadw Fel...",
|
||||
"github-link": "Gweld ar GitHub",
|
||||
"translate": "cyfieithu",
|
||||
"light": "golau",
|
||||
"dark": "tywyll",
|
||||
"system": "system",
|
||||
"ratelimit": "Whoogle wedi bod yn gyfyngedig",
|
||||
"continue-search": "Parhau eich chwiliad gyda Farside",
|
||||
"all": "Holl",
|
||||
"images": "Delweddau",
|
||||
"maps": "Mapiau",
|
||||
"videos": "Fideos",
|
||||
"news": "Newyddion",
|
||||
"books": "Llyfrau",
|
||||
"anon-view": "Golwg Anhysbys",
|
||||
"": "--",
|
||||
"qdr:h": "Yr awr ddiwethaf",
|
||||
"qdr:d": "24 awr diwethaf",
|
||||
"qdr:w": "Yr wythnos ddiwethaf",
|
||||
"qdr:m": "Mis diwethaf",
|
||||
"qdr:y": "Y flwyddyn ddiwethaf",
|
||||
"config-time-period": "Cyfnod Amser"
|
||||
},
|
||||
"lang_az": {
|
||||
"": "--",
|
||||
"search": "Axtar",
|
||||
"config": "Konfiqurasiya",
|
||||
"config-country": "Ölkə",
|
||||
"config-lang": "İnterfeys dili",
|
||||
"config-lang-search": "Axtarış dili",
|
||||
"config-near": "Yaxın",
|
||||
"config-near-help": "Şəhər Adı",
|
||||
"config-block": "Blok",
|
||||
"config-block-help": "Vergüllə ayrılmış sayt siyahısı",
|
||||
"config-block-title": "Başlığa görə bloklayın",
|
||||
"config-block-title-help": "Regex istifadə edin",
|
||||
"config-block-url": "URL ilə bloklayın",
|
||||
"config-block-url-help": "Regex istifadə edin",
|
||||
"config-theme": "Mövzu",
|
||||
"config-nojs": "Anonim Görünüşdə Javascript-i silin",
|
||||
"config-anon-view": "Anonim Baxış Linklərini göstərin",
|
||||
"config-dark": "Qaranlıq rejim",
|
||||
"config-safe": "Təhlükəsiz axtarış",
|
||||
"config-alts": "Sosial Media Linklərini dəyişdirin",
|
||||
"config-alts-help": "Twitter/YouTube/s. linkləri alternativlərə uyğun məxfiliklə əvəz edir.",
|
||||
"config-new-tab": "Linkləri Yeni Tabda açın",
|
||||
"config-images": "Tam ölçülü Şəkil Axtarışı",
|
||||
"config-images-help": "(Eksperimental) Masaüstü şəkil axtarışlarına 'Şəkilə Bax' seçimini əlavə edir. Bu, şəkil nəticəsi miniatürlərinin daha aşağı ayırdetmə keyfiyyətinə səbəb olacaq.",
|
||||
"config-tor": "Tor-dan istifadə edin",
|
||||
"config-get-only": "Yalnız GET Sorğuları",
|
||||
"config-url": "Kök URL",
|
||||
"config-pref-url": "URL Tərcihləri",
|
||||
"config-pref-encryption": "Encrypt Tərcihləri",
|
||||
"config-pref-help": "WHOOGLE_CONFIG_PREFERENCES_KEY tələb edir, əks halda bu nəzərə alınmayacaq.",
|
||||
"config-css": "Fərdi CSS",
|
||||
"config-time-period": "Müddət",
|
||||
"load": "Yüklə",
|
||||
"apply": "Tətbiq edin",
|
||||
"save-as": "Fərqli Saxla...",
|
||||
"github-link": "GitHub-da baxın",
|
||||
"translate": "tərcümə",
|
||||
"light": "işıqlı",
|
||||
"dark": "qaranlıq",
|
||||
"system": "sistem",
|
||||
"ratelimit": "Nümunə dərəcəsi məhdudlaşdırılıb",
|
||||
"continue-search": "Axtarışınızı Farside ilə davam etdirin",
|
||||
"all": "Hamısı",
|
||||
"images": "Şəkillər",
|
||||
"maps": "Xəritələr",
|
||||
"videos": "Videolar",
|
||||
"news": "Xəbərlər",
|
||||
"books": "Kitablar",
|
||||
"anon-view": "Anonim Baxış",
|
||||
"qdr:h": "Keçən saat",
|
||||
"qdr:d": "Keçən 24 saat",
|
||||
"qdr:w": "Keçən həftə",
|
||||
"qdr:m": "Keçən ay",
|
||||
"qdr:y": "Keçən il"
|
||||
},
|
||||
"lang_el": {
|
||||
"": "--",
|
||||
"search": "Αναζήτηση",
|
||||
"config": "Ρυθμήσεις",
|
||||
"config-country": "Χώρα",
|
||||
"config-lang": "Γλώσσα Περιβάλλοντος",
|
||||
"config-lang-search": "Γλώσσα Αναζήτησης",
|
||||
"config-near": "Κοντά",
|
||||
"config-near-help": "Όνομα Πόλης",
|
||||
"config-block": "Block",
|
||||
"config-block-help": "Comma-separated site list",
|
||||
"config-block-title": "Block by Title",
|
||||
"config-block-title-help": "Use regex",
|
||||
"config-block-url": "Block by URL",
|
||||
"config-block-url-help": "Use regex",
|
||||
"config-theme": "Θέμα",
|
||||
"config-nojs": "Αφαίρεση Javascript σε ανώνυμη προβολή",
|
||||
"config-anon-view": "Show Anonymous View Links",
|
||||
"config-dark": "Dark Mode",
|
||||
"config-safe": "Ασφαλής Αναζήτηση",
|
||||
"config-alts": "Replace Social Media Links",
|
||||
"config-alts-help": "Replaces Twitter/YouTube/etc links with privacy respecting alternatives.",
|
||||
"config-new-tab": "Άνοιγμα συνδέσμου σε νέα καρτέλα",
|
||||
"config-images": "Full Size Image Search",
|
||||
"config-images-help": "(Experimental) Adds the 'View Image' option to desktop image searches. This will cause image result thumbnails to be lower resolution.",
|
||||
"config-tor": "Χρήση Tor",
|
||||
"config-get-only": "GET Requests Only",
|
||||
"config-url": "Root URL",
|
||||
"config-pref-url": "Preferences URL",
|
||||
"config-pref-encryption": "Encrypt Preferences",
|
||||
"config-pref-help": "Requires WHOOGLE_CONFIG_PREFERENCES_KEY, otherwise this will be ignored.",
|
||||
"config-css": "Custom CSS",
|
||||
"config-time-period": "Time Period",
|
||||
"load": "Load",
|
||||
"apply": "Apply",
|
||||
"save-as": "Save As...",
|
||||
"github-link": "View on GitHub",
|
||||
"translate": "translate",
|
||||
"light": "light",
|
||||
"dark": "dark",
|
||||
"system": "system",
|
||||
"ratelimit": "Instance has been ratelimited",
|
||||
"continue-search": "Continue your search with Farside",
|
||||
"all": "All",
|
||||
"images": "Images",
|
||||
"maps": "Maps",
|
||||
"videos": "Videos",
|
||||
"news": "News",
|
||||
"books": "Books",
|
||||
"anon-view": "Ανώνυμη Προβολή",
|
||||
"qdr:h": "Τελευταία ώρα",
|
||||
"qdr:d": "Τελευταίες 24 ώρες",
|
||||
"qdr:w": "Τελευταία Βδομάδα",
|
||||
"qdr:m": "Τελευταίος Μήνας",
|
||||
"qdr:y": "Τελευταίος Χρόνος"
|
||||
}
|
||||
}
|
||||
|
|
260
app/static/widgets/calculator.html
Normal file
260
app/static/widgets/calculator.html
Normal file
|
@ -0,0 +1,260 @@
|
|||
<!--
|
||||
Calculator widget.
|
||||
This file should contain all required
|
||||
CSS, HTML, and JS for it.
|
||||
-->
|
||||
|
||||
<style>
|
||||
#calc-text {
|
||||
background: var(--whoogle-dark-page-bg);
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
text-align: right;
|
||||
font-family: monospace;
|
||||
font-size: 16px;
|
||||
color: var(--whoogle-dark-text);
|
||||
}
|
||||
#prev-equation {
|
||||
text-align: right;
|
||||
}
|
||||
.error-border {
|
||||
border: 1px solid red;
|
||||
}
|
||||
|
||||
#calc-btns {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
grid-template-rows: repeat(5, 1fr);
|
||||
gap: 5px;
|
||||
}
|
||||
#calc-btns button {
|
||||
background: #313141;
|
||||
color: var(--whoogle-dark-text);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#calc-btns button:hover {
|
||||
background: #414151;
|
||||
}
|
||||
#calc-btns .common {
|
||||
background: #51516a;
|
||||
}
|
||||
#calc-btns .common:hover {
|
||||
background: #61617a;
|
||||
}
|
||||
#calc-btn-0 { grid-row: 5; grid-column: 3; }
|
||||
#calc-btn-1 { grid-row: 4; grid-column: 3; }
|
||||
#calc-btn-2 { grid-row: 4; grid-column: 4; }
|
||||
#calc-btn-3 { grid-row: 4; grid-column: 5; }
|
||||
#calc-btn-4 { grid-row: 3; grid-column: 3; }
|
||||
#calc-btn-5 { grid-row: 3; grid-column: 4; }
|
||||
#calc-btn-6 { grid-row: 3; grid-column: 5; }
|
||||
#calc-btn-7 { grid-row: 2; grid-column: 3; }
|
||||
#calc-btn-8 { grid-row: 2; grid-column: 4; }
|
||||
#calc-btn-9 { grid-row: 2; grid-column: 5; }
|
||||
#calc-btn-EQ { grid-row: 5; grid-column: 5; }
|
||||
#calc-btn-PT { grid-row: 5; grid-column: 4; }
|
||||
#calc-btn-BCK { grid-row: 5; grid-column: 6; }
|
||||
#calc-btn-ADD { grid-row: 4; grid-column: 6; }
|
||||
#calc-btn-SUB { grid-row: 3; grid-column: 6; }
|
||||
#calc-btn-MLT { grid-row: 2; grid-column: 6; }
|
||||
#calc-btn-DIV { grid-row: 1; grid-column: 6; }
|
||||
#calc-btn-CLR { grid-row: 1; grid-column: 5; }
|
||||
#calc-btn-PRC{ grid-row: 1; grid-column: 4; }
|
||||
#calc-btn-RP { grid-row: 1; grid-column: 3; }
|
||||
#calc-btn-LP { grid-row: 1; grid-column: 2; }
|
||||
#calc-btn-ABS { grid-row: 1; grid-column: 1; }
|
||||
#calc-btn-SIN { grid-row: 2; grid-column: 2; }
|
||||
#calc-btn-COS { grid-row: 3; grid-column: 2; }
|
||||
#calc-btn-TAN { grid-row: 4; grid-column: 2; }
|
||||
#calc-btn-SQR { grid-row: 5; grid-column: 2; }
|
||||
#calc-btn-EXP { grid-row: 2; grid-column: 1; }
|
||||
#calc-btn-E { grid-row: 3; grid-column: 1; }
|
||||
#calc-btn-PI { grid-row: 4; grid-column: 1; }
|
||||
#calc-btn-LOG { grid-row: 5; grid-column: 1; }
|
||||
</style>
|
||||
<p id="prev-equation"></p>
|
||||
<div id="calculator-widget">
|
||||
<p id="calc-text">0</p>
|
||||
<div id="calc-btns">
|
||||
<button id="calc-btn-0" class="common">0</button>
|
||||
<button id="calc-btn-1" class="common">1</button>
|
||||
<button id="calc-btn-2" class="common">2</button>
|
||||
<button id="calc-btn-3" class="common">3</button>
|
||||
<button id="calc-btn-4" class="common">4</button>
|
||||
<button id="calc-btn-5" class="common">5</button>
|
||||
<button id="calc-btn-6" class="common">6</button>
|
||||
<button id="calc-btn-7" class="common">7</button>
|
||||
<button id="calc-btn-8" class="common">8</button>
|
||||
<button id="calc-btn-9" class="common">9</button>
|
||||
<button id="calc-btn-EQ" class="common">=</button>
|
||||
<button id="calc-btn-PT" class="common">.</button>
|
||||
<button id="calc-btn-BCK">⬅</button>
|
||||
<button id="calc-btn-ADD">+</button>
|
||||
<button id="calc-btn-SUB">-</button>
|
||||
<button id="calc-btn-MLT">x</button>
|
||||
<button id="calc-btn-DIV">/</button>
|
||||
<button id="calc-btn-CLR">C</button>
|
||||
<button id="calc-btn-PRC">%</button>
|
||||
<button id="calc-btn-RP">)</button>
|
||||
<button id="calc-btn-LP">(</button>
|
||||
<button id="calc-btn-ABS">|x|</button>
|
||||
<button id="calc-btn-SIN">sin</button>
|
||||
<button id="calc-btn-COS">cos</button>
|
||||
<button id="calc-btn-TAN">tan</button>
|
||||
<button id="calc-btn-SQR">√</button>
|
||||
<button id="calc-btn-EXP">^</button>
|
||||
<button id="calc-btn-E">ℇ</button>
|
||||
<button id="calc-btn-PI">π</button>
|
||||
<button id="calc-btn-LOG">log</button>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// JS does not have this by default.
|
||||
// from https://www.freecodecamp.org/news/how-to-factorialize-a-number-in-javascript-9263c89a4b38/
|
||||
function factorial(num) {
|
||||
if (num < 0)
|
||||
return -1;
|
||||
else if (num === 0)
|
||||
return 1;
|
||||
else {
|
||||
return (num * factorial(num - 1));
|
||||
}
|
||||
}
|
||||
// returns true if the user is currently focused on the calculator widget
|
||||
function usingCalculator() {
|
||||
let activeElement = document.activeElement;
|
||||
while (true) {
|
||||
if (!activeElement) return false;
|
||||
if (activeElement.id === "calculator-wrapper") return true;
|
||||
activeElement = activeElement.parentElement;
|
||||
}
|
||||
}
|
||||
const $ = q => document.querySelectorAll(q);
|
||||
// key bindings for commonly used buttons
|
||||
const keybindings = {
|
||||
"0": "0",
|
||||
"1": "1",
|
||||
"2": "2",
|
||||
"3": "3",
|
||||
"4": "4",
|
||||
"5": "5",
|
||||
"6": "6",
|
||||
"7": "7",
|
||||
"8": "8",
|
||||
"9": "9",
|
||||
"Enter": "EQ",
|
||||
".": "PT",
|
||||
"+": "ADD",
|
||||
"-": "SUB",
|
||||
"*": "MLT",
|
||||
"/": "DIV",
|
||||
"%": "PRC",
|
||||
"c": "CLR",
|
||||
"(": "LP",
|
||||
")": "RP",
|
||||
"Backspace": "BCK",
|
||||
}
|
||||
window.addEventListener("keydown", event => {
|
||||
if (!usingCalculator()) return;
|
||||
if (event.key === "Enter" && document.activeElement.id !== "search-bar")
|
||||
event.preventDefault();
|
||||
if (keybindings[event.key])
|
||||
document.getElementById("calc-btn-" + keybindings[event.key]).click();
|
||||
})
|
||||
// calculates the string
|
||||
const calc = () => {
|
||||
var mathtext = document.getElementById("calc-text");
|
||||
var statement = mathtext.innerHTML
|
||||
// remove empty ()
|
||||
.replace("()", "")
|
||||
// special constants
|
||||
.replace("π", "(Math.PI)")
|
||||
.replace("ℇ", "(Math.E)")
|
||||
// turns 3(1+2) into 3*(1+2) (for example)
|
||||
.replace(/(?<=[0-9\)])(?<=[^+\-x*\/%^])\(/, "x(")
|
||||
// same except reversed
|
||||
.replace(/\)(?=[0-9\(])(?=[^+\-x*\/%^])/, ")x")
|
||||
// replace human friendly x with JS *
|
||||
.replace("x", "*")
|
||||
// trig & misc functions
|
||||
.replace("sin", "Math.sin")
|
||||
.replace("cos", "Math.cos")
|
||||
.replace("tan", "Math.tan")
|
||||
.replace("√", "Math.sqrt")
|
||||
.replace("^", "**")
|
||||
.replace("abs", "Math.abs")
|
||||
.replace("log", "Math.log")
|
||||
;
|
||||
// add any missing )s to the end
|
||||
while(true) if (
|
||||
(statement.match(/\(/g) || []).length >
|
||||
(statement.match(/\)/g) || []).length
|
||||
) statement += ")"; else break;
|
||||
// evaluate the expression.
|
||||
console.log("calculating [" + statement + "]");
|
||||
try {
|
||||
var result = eval(statement);
|
||||
document.getElementById("prev-equation").innerHTML = mathtext.innerHTML + " = ";
|
||||
mathtext.innerHTML = result;
|
||||
mathtext.classList.remove("error-border");
|
||||
} catch (e) {
|
||||
mathtext.classList.add("error-border");
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
const updateCalc = (e) => {
|
||||
// character(s) recieved from button
|
||||
var c = event.target.innerHTML;
|
||||
var mathtext = document.getElementById("calc-text");
|
||||
if (mathtext.innerHTML === "0") mathtext.innerHTML = "";
|
||||
// special cases
|
||||
switch (c) {
|
||||
case "C":
|
||||
// Clear
|
||||
mathtext.innerHTML = "0";
|
||||
break;
|
||||
case "⬅":
|
||||
// Delete
|
||||
mathtext.innerHTML = mathtext.innerHTML.slice(0, -1);
|
||||
if (mathtext.innerHTML.length === 0) {
|
||||
mathtext.innerHTML = "0";
|
||||
}
|
||||
break;
|
||||
case "=":
|
||||
calc()
|
||||
break;
|
||||
case "sin":
|
||||
case "cos":
|
||||
case "tan":
|
||||
case "log":
|
||||
case "√":
|
||||
mathtext.innerHTML += `${c}(`;
|
||||
break;
|
||||
case "|x|":
|
||||
mathtext.innerHTML += "abs("
|
||||
break;
|
||||
case "+":
|
||||
case "-":
|
||||
case "x":
|
||||
case "/":
|
||||
case "%":
|
||||
case "^":
|
||||
if (mathtext.innerHTML.length === 0) mathtext.innerHTML = "0";
|
||||
// prevent typing 2 operators in a row
|
||||
if (mathtext.innerHTML.match(/[+\-x\/%^] $/))
|
||||
mathtext.innerHTML = mathtext.innerHTML.slice(0, -3);
|
||||
mathtext.innerHTML += ` ${c} `;
|
||||
break;
|
||||
default:
|
||||
mathtext.innerHTML += c;
|
||||
}
|
||||
}
|
||||
for (let i of $("#calc-btns button")) {
|
||||
i.addEventListener('click', event => {
|
||||
updateCalc(event);
|
||||
})
|
||||
}
|
||||
</script>
|
|
@ -2,7 +2,11 @@
|
|||
<head>
|
||||
<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="search" href="opensearch.xml" type="application/opensearchdescription+xml" title="Whoogle Search">
|
||||
{% if not search_type %}
|
||||
<link rel="search" href="opensearch.xml" type="application/opensearchdescription+xml" title="Whoogle Search">
|
||||
{% else %}
|
||||
<link rel="search" href="opensearch.xml?tbm={{ search_type }}" type="application/opensearchdescription+xml" title="Whoogle Search ({{ search_name }})">
|
||||
{% endif %}
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="referrer" content="no-referrer">
|
||||
<link rel="stylesheet" href="{{ cb_url('logo.css') }}">
|
||||
|
|
|
@ -19,22 +19,88 @@
|
|||
{{ 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>
|
||||
{% if query and translation %}
|
||||
<p>
|
||||
<h4><a class="link" href="https://farside.link">{{ translation['continue-search'] }}</a></h4>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://github.com/benbusby/whoogle-search">Whoogle</a>
|
||||
<ul>
|
||||
<li>
|
||||
<a class="link-color" href="{{farside}}/whoogle/search?q={{query}}{{params}}">
|
||||
{{farside}}/whoogle/search?q={{query}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/searxng/searxng">SearXNG</a>
|
||||
<ul>
|
||||
<li>
|
||||
<a class="link-color" href="{{farside}}/searxng/search?q={{query}}">
|
||||
{{farside}}/searxng/search?q={{query}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h4>Other options:</h4>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://kagi.com">Kagi</a>
|
||||
<ul>
|
||||
<li>Requires account</li>
|
||||
<li>
|
||||
<a class="link-color" href="https://kagi.com/search?q={{query}}">
|
||||
kagi.com/search?q={{query}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://duckduckgo.com">DuckDuckGo</a>
|
||||
<ul>
|
||||
<li>
|
||||
<a class="link-color" href="https://duckduckgo.com/search?q={{query}}">
|
||||
duckduckgo.com/search?q={{query}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://search.brave.com">Brave Search</a>
|
||||
<ul>
|
||||
<li>
|
||||
<a class="link-color" href="https://search.brave.com/search?q={{query}}">
|
||||
search.brave.com/search?q={{query}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://ecosia.com">Ecosia</a>
|
||||
<ul>
|
||||
<li>
|
||||
<a class="link-color" href="https://ecosia.com/search?q={{query}}">
|
||||
ecosia.com/search?q={{query}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://google.com">Google</a>
|
||||
<ul>
|
||||
<li>
|
||||
<a class="link-color" href="https://google.com/search?q={{query}}">
|
||||
google.com/search?q={{query}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<hr>
|
||||
</p>
|
||||
{% endif %}
|
||||
</p>
|
||||
<a class="link" href="home">Return Home</a>
|
||||
</div>
|
||||
|
|
|
@ -4,13 +4,16 @@
|
|||
<form class="search-form header"
|
||||
id="search-form"
|
||||
method="{{ 'GET' if config.get_only else 'POST' }}">
|
||||
<a class="logo-link mobile-logo" href="home">
|
||||
<a class="logo-link mobile-logo" href="{{ home_url }}">
|
||||
<div id="mobile-header-logo">
|
||||
{{ logo|safe }}
|
||||
</div>
|
||||
</a>
|
||||
<div class="H0PQec mobile-input-div">
|
||||
<div class="autocomplete-mobile esbc autocomplete">
|
||||
{% if config.preferences %}
|
||||
<input type="hidden" name="preferences" value="{{ config.preferences }}" />
|
||||
{% endif %}
|
||||
<input
|
||||
id="search-bar"
|
||||
class="mobile-search-bar"
|
||||
|
@ -25,6 +28,7 @@
|
|||
dir="auto">
|
||||
<input id="search-reset" type="reset" value="x">
|
||||
<input name="tbm" value="{{ search_type }}" style="display: none">
|
||||
<input name="country" value="{{ config.country }}" style="display: none;">
|
||||
<input type="submit" style="display: none;">
|
||||
<div class="sc"></div>
|
||||
</div>
|
||||
|
@ -43,6 +47,8 @@
|
|||
<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>
|
||||
|
@ -54,7 +60,7 @@
|
|||
{% else %}
|
||||
<header>
|
||||
<div class="logo-div">
|
||||
<a class="logo-link" href="home">
|
||||
<a class="logo-link" href="{{ home_url }}">
|
||||
<div class="desktop-header-logo">
|
||||
{{ logo|safe }}
|
||||
</div>
|
||||
|
@ -67,6 +73,9 @@
|
|||
method="{{ 'GET' if config.get_only else 'POST' }}">
|
||||
<div class="autocomplete header-autocomplete">
|
||||
<div style="width: 100%; display: flex">
|
||||
{% if config.preferences %}
|
||||
<input type="hidden" name="preferences" value="{{ config.preferences }}" />
|
||||
{% endif %}
|
||||
<input
|
||||
id="search-bar"
|
||||
autocapitalize="none"
|
||||
|
@ -79,6 +88,8 @@
|
|||
value="{{ clean_query(query) }}"
|
||||
dir="auto">
|
||||
<input name="tbm" value="{{ search_type }}" style="display: none">
|
||||
<input name="country" value="{{ config.country }}" style="display: none;">
|
||||
<input name="tbs" value="{{ config.tbs }}" style="display: none;">
|
||||
<input type="submit" style="display: none;">
|
||||
<div class="sc"></div>
|
||||
</div>
|
||||
|
@ -98,6 +109,8 @@
|
|||
<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>
|
||||
|
@ -106,5 +119,40 @@
|
|||
<div class="" id="s">
|
||||
</div>
|
||||
{% 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>
|
||||
<br />
|
||||
<label for="config-time-period">{{ translation['config-time-period'] }}: </label>
|
||||
<select name="tbs" id="result-time-period">
|
||||
{% for time_period in time_periods %}
|
||||
<option value="{{ time_period.value }}"
|
||||
{% if (
|
||||
config.tbs != '' and config.tbs in time_period.value
|
||||
) or (
|
||||
config.tbs == '' and time_period.value == '')
|
||||
%}
|
||||
selected
|
||||
{% endif %}>
|
||||
{{ translation[time_period.value] }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript" src="{{ cb_url('header.js') }}"></script>
|
||||
|
|
|
@ -66,6 +66,9 @@
|
|||
<form id="search-form" action="search" method="{{ 'get' if config.get_only else 'post' }}">
|
||||
<div class="search-fields">
|
||||
<div class="autocomplete">
|
||||
{% if config.preferences %}
|
||||
<input type="hidden" name="preferences" value="{{ config.preferences }}" />
|
||||
{% endif %}
|
||||
<input
|
||||
type="text"
|
||||
name="q"
|
||||
|
@ -93,7 +96,11 @@
|
|||
<select name="country" id="config-country">
|
||||
{% for country in countries %}
|
||||
<option value="{{ country.value }}"
|
||||
{% if country.value in config.country %}
|
||||
{% if (
|
||||
config.country != '' and config.country in country.value
|
||||
) or (
|
||||
config.country == '' and country.value == '')
|
||||
%}
|
||||
selected
|
||||
{% endif %}>
|
||||
{{ country.name }}
|
||||
|
@ -101,6 +108,23 @@
|
|||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="config-div">
|
||||
<label for="config-time-period">{{ translation['config-time-period'] }}</label>
|
||||
<select name="tbs" id="config-time-period">
|
||||
{% for time_period in time_periods %}
|
||||
<option value="{{ time_period.value }}"
|
||||
{% if (
|
||||
config.tbs != '' and config.tbs in time_period.value
|
||||
) or (
|
||||
config.tbs == '' and time_period.value == '')
|
||||
%}
|
||||
selected
|
||||
{% endif %}>
|
||||
{{ translation[time_period.value] }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="config-div config-div-lang">
|
||||
<label for="config-lang-interface">{{ translation['config-lang'] }}: </label>
|
||||
<select name="lang_interface" id="config-lang-interface">
|
||||
|
@ -219,15 +243,21 @@
|
|||
{{ translation['config-css'] }}:
|
||||
</a>
|
||||
<textarea
|
||||
name="style"
|
||||
name="style_modified"
|
||||
id="config-style"
|
||||
autocapitalize="off"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
autocorrect="off"
|
||||
value="">
|
||||
{{ config.style.replace('\t', '') }}
|
||||
</textarea>
|
||||
value="">{{ config.style_modified.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">
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,8 +1,56 @@
|
|||
import json
|
||||
import requests
|
||||
import urllib.parse as urlparse
|
||||
import os
|
||||
import glob
|
||||
|
||||
DDG_BANGS = 'https://duckduckgo.com/bang.v255.js'
|
||||
bangs_dict = {}
|
||||
DDG_BANGS = 'https://duckduckgo.com/bang.js'
|
||||
|
||||
|
||||
def load_all_bangs(ddg_bangs_file: str, ddg_bangs: dict = {}):
|
||||
"""Loads all the bang files in alphabetical order
|
||||
|
||||
Args:
|
||||
ddg_bangs_file: The str path to the new DDG bangs json file
|
||||
ddg_bangs: The dict of ddg bangs. If this is empty, it will load the
|
||||
bangs from the file
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
global bangs_dict
|
||||
ddg_bangs_file = os.path.normpath(ddg_bangs_file)
|
||||
|
||||
if (bangs_dict and not ddg_bangs) or os.path.getsize(ddg_bangs_file) <= 4:
|
||||
return
|
||||
|
||||
bangs = {}
|
||||
bangs_dir = os.path.dirname(ddg_bangs_file)
|
||||
bang_files = glob.glob(os.path.join(bangs_dir, '*.json'))
|
||||
|
||||
# Normalize the paths
|
||||
bang_files = [os.path.normpath(f) for f in bang_files]
|
||||
|
||||
# Move the ddg bangs file to the beginning
|
||||
bang_files = sorted([f for f in bang_files if f != ddg_bangs_file])
|
||||
|
||||
if ddg_bangs:
|
||||
bangs |= ddg_bangs
|
||||
else:
|
||||
bang_files.insert(0, ddg_bangs_file)
|
||||
|
||||
for i, bang_file in enumerate(bang_files):
|
||||
try:
|
||||
bangs |= json.load(open(bang_file))
|
||||
except json.decoder.JSONDecodeError:
|
||||
# Ignore decoding error only for the ddg bangs file, since this can
|
||||
# occur if file is still being written
|
||||
if i != 0:
|
||||
raise
|
||||
|
||||
bangs_dict = dict(sorted(bangs.items()))
|
||||
|
||||
|
||||
def gen_bangs_json(bangs_file: str) -> None:
|
||||
|
@ -37,40 +85,67 @@ def gen_bangs_json(bangs_file: str) -> None:
|
|||
|
||||
json.dump(bangs_data, open(bangs_file, 'w'))
|
||||
print('* Finished creating ddg bangs json')
|
||||
load_all_bangs(bangs_file, bangs_data)
|
||||
|
||||
|
||||
def resolve_bang(query: str, bangs_dict: dict) -> str:
|
||||
def suggest_bang(query: str) -> list[str]:
|
||||
"""Suggests bangs for a user's query
|
||||
|
||||
Args:
|
||||
query: The search query
|
||||
|
||||
Returns:
|
||||
list[str]: A list of bang suggestions
|
||||
|
||||
"""
|
||||
global bangs_dict
|
||||
return [bangs_dict[_]['suggestion'] for _ in bangs_dict if _.startswith(query)]
|
||||
|
||||
|
||||
def resolve_bang(query: str) -> str:
|
||||
"""Transform's a user's query to a bang search, if an operator is found
|
||||
|
||||
Args:
|
||||
query: The search query
|
||||
bangs_dict: The dict of available bang operators, with corresponding
|
||||
format string search URLs
|
||||
(i.e. "!w": "https://en.wikipedia.org...?search={}")
|
||||
|
||||
Returns:
|
||||
str: A formatted redirect for a bang search, or an empty str if there
|
||||
wasn't a match or didn't contain a bang operator
|
||||
|
||||
"""
|
||||
# Ensure bang search is case insensitive
|
||||
query = query.lower()
|
||||
split_query = query.split(' ')
|
||||
for operator in bangs_dict.keys():
|
||||
if operator not in split_query \
|
||||
and operator[1:] + operator[0] not in split_query:
|
||||
continue
|
||||
global bangs_dict
|
||||
|
||||
bang_query = query.replace(
|
||||
operator if operator in split_query else operator[1:] +
|
||||
operator[0], ''
|
||||
).strip()
|
||||
#if ! not in query simply return (speed up processing)
|
||||
if '!' not in query:
|
||||
return ''
|
||||
|
||||
bang_url = bangs_dict[operator]['url']
|
||||
split_query = query.strip().split(' ')
|
||||
|
||||
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}'
|
||||
# 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 ''
|
||||
|
|
|
@ -1,9 +1,50 @@
|
|||
from bs4 import BeautifulSoup as bsoup
|
||||
from flask import Request
|
||||
import base64
|
||||
import hashlib
|
||||
import contextlib
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
|
||||
from requests import exceptions, get
|
||||
from urllib.parse import urlparse
|
||||
from bs4 import BeautifulSoup as bsoup
|
||||
from cryptography.fernet import Fernet
|
||||
from flask import Request
|
||||
|
||||
ddg_favicon_site = 'http://icons.duckduckgo.com/ip2'
|
||||
|
||||
empty_gif = base64.b64decode(
|
||||
'R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==')
|
||||
|
||||
placeholder_img = base64.b64decode(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAYAAADE6YVjAAABF0lEQVRIS8XWPw9EMBQA8Eok' \
|
||||
'JBKrMFqMBt//GzAYLTZ/VomExPDu6uLiaPteqVynBn0/75W2Vp7nEIYhe6p1XcespmmAd7Is' \
|
||||
'M+4URcGiKPogvMMvmIS2eN9MOMKbKWgf54SYgI4vKkTuQKJKSJErkKzUSkQHUs0lilAg7GMh' \
|
||||
'ISoIA/hYMiKCKIA2soeowCWEMkfHtUmrXLcyGYYBfN9HF8djiaglWzNZlgVs21YisoAUaEXG' \
|
||||
'cQTP86QIFgi7vyLzPIPjOEIEC7ANQv/4aZrAdd0TUtc1i+MYnSsMWjPp+x6CIPgJVlUVS5KE' \
|
||||
'DKig/+wnVzM4pnzaGeHd+ENlWbI0TbVLJBtw2uMfP63wc9d2kDCWxi5Q27bsBerSJ9afJbeL' \
|
||||
'AAAAAElFTkSuQmCC'
|
||||
)
|
||||
|
||||
|
||||
def fetch_favicon(url: str) -> bytes:
|
||||
"""Fetches a favicon using DuckDuckGo's favicon retriever
|
||||
|
||||
Args:
|
||||
url: The url to fetch the favicon from
|
||||
Returns:
|
||||
bytes - the favicon bytes, or a placeholder image if one
|
||||
was not returned
|
||||
"""
|
||||
response = get(f'{ddg_favicon_site}/{urlparse(url).netloc}.ico')
|
||||
|
||||
if response.status_code == 200 and len(response.content) > 0:
|
||||
tmp_mem = io.BytesIO()
|
||||
tmp_mem.write(response.content)
|
||||
tmp_mem.seek(0)
|
||||
|
||||
return tmp_mem.read()
|
||||
return placeholder_img
|
||||
|
||||
|
||||
def gen_file_hash(path: str, static_file: str) -> str:
|
||||
|
@ -11,21 +52,22 @@ def gen_file_hash(path: str, static_file: str) -> str:
|
|||
file_hash = hashlib.md5(file_contents).hexdigest()[:8]
|
||||
filename_split = os.path.splitext(static_file)
|
||||
|
||||
return filename_split[0] + '.' + file_hash + filename_split[-1]
|
||||
return f'{filename_split[0]}.{file_hash}{filename_split[-1]}'
|
||||
|
||||
|
||||
def read_config_bool(var: str) -> bool:
|
||||
val = os.getenv(var, '0')
|
||||
if val.isdigit():
|
||||
return bool(int(val))
|
||||
return False
|
||||
def read_config_bool(var: str, default: bool=False) -> bool:
|
||||
val = os.getenv(var, '1' if default else '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')
|
||||
return val.lower() in ('true', 't', '1', 'yes', 'y')
|
||||
|
||||
|
||||
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']
|
||||
|
||||
return r.environ['HTTP_X_FORWARDED_FOR']
|
||||
|
||||
|
||||
def get_request_url(url: str) -> str:
|
||||
|
@ -35,27 +77,62 @@ def get_request_url(url: str) -> str:
|
|||
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')
|
||||
|
||||
full_path = r.full_path if not root else ''
|
||||
if full_path.startswith('/'):
|
||||
full_path = f'/{full_path}'
|
||||
|
||||
if http_host:
|
||||
prefix = os.environ.get('WHOOGLE_URL_PREFIX', '')
|
||||
if prefix:
|
||||
prefix = f'/{re.sub("[^0-9a-zA-Z]+", "", prefix)}'
|
||||
return f'{scheme}://{http_host}{prefix}{full_path}'
|
||||
|
||||
return default
|
||||
|
||||
|
||||
def check_for_update(version_url: str, current: str) -> int:
|
||||
# Check for the latest version of Whoogle
|
||||
try:
|
||||
has_update = ''
|
||||
with contextlib.suppress(exceptions.ConnectionError, AttributeError):
|
||||
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:]}'
|
||||
urls = {
|
||||
"//": f"https:{url}",
|
||||
"/": f"{urlparse(page_url).netloc}{url}",
|
||||
"./": f"{page_url}{url[2:]}"
|
||||
}
|
||||
for start in urls:
|
||||
if url.startswith(start):
|
||||
return urls[start]
|
||||
|
||||
return url
|
||||
|
||||
|
||||
def list_to_dict(lst: list) -> dict:
|
||||
if len(lst) < 2:
|
||||
return {}
|
||||
return {lst[i].replace(' ', ''): lst[i+1].replace(' ', '')
|
||||
for i in range(0, len(lst), 2)}
|
||||
|
||||
|
||||
def encrypt_string(key: bytes, string: str) -> str:
|
||||
cipher_suite = Fernet(key)
|
||||
return cipher_suite.encrypt(string.encode()).decode()
|
||||
|
||||
|
||||
def decrypt_string(key: bytes, string: str) -> str:
|
||||
cipher_suite = Fernet(g.session_key)
|
||||
return cipher_suite.decrypt(string.encode()).decode()
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from app.models.config import Config
|
||||
from app.models.endpoint import Endpoint
|
||||
from app.utils.misc import list_to_dict
|
||||
from bs4 import BeautifulSoup, NavigableString
|
||||
import copy
|
||||
from flask import current_app
|
||||
|
@ -8,6 +9,7 @@ import os
|
|||
import urllib.parse as urlparse
|
||||
from urllib.parse import parse_qs
|
||||
import re
|
||||
import warnings
|
||||
|
||||
SKIP_ARGS = ['ref_src', 'utm']
|
||||
SKIP_PREFIX = ['//www.', '//mobile.', '//m.']
|
||||
|
@ -25,22 +27,49 @@ BLACKLIST = [
|
|||
'Reklama', 'Реклама', 'Anunț', '광고', 'annons', 'Annonse', 'Iklan',
|
||||
'広告', 'Augl.', 'Mainos', 'Advertentie', 'إعلان', 'Գովազդ', 'विज्ञापन',
|
||||
'Reklam', 'آگهی', 'Reklāma', 'Reklaam', 'Διαφήμιση', 'מודעה', 'Hirdetés',
|
||||
'Anúncio'
|
||||
'Anúncio', 'Quảng cáo', 'โฆษณา', 'sponsored', 'patrocinado', 'gesponsert',
|
||||
'Sponzorováno', '스폰서', 'Gesponsord'
|
||||
]
|
||||
|
||||
SITE_ALTS = {
|
||||
'twitter.com': os.getenv('WHOOGLE_ALT_TW', 'farside.link/nitter'),
|
||||
'youtube.com': os.getenv('WHOOGLE_ALT_YT', 'farside.link/invidious'),
|
||||
'instagram.com': os.getenv('WHOOGLE_ALT_IG', 'farside.link/bibliogram/u'),
|
||||
'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')
|
||||
'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'),
|
||||
'stackoverflow.com': os.getenv('WHOOGLE_ALT_SO', 'farside.link/anonymousoverflow')
|
||||
}
|
||||
|
||||
# Include custom site redirects from WHOOGLE_REDIRECTS
|
||||
SITE_ALTS.update(list_to_dict(re.split(',|:', os.getenv('WHOOGLE_REDIRECTS', ''))))
|
||||
|
||||
|
||||
def contains_cjko(s: str) -> bool:
|
||||
"""This function check whether or not a string contains Chinese, Japanese,
|
||||
or Korean characters. It employs regex and uses the u escape sequence to
|
||||
match any character in a set of Unicode ranges.
|
||||
|
||||
Args:
|
||||
s (str): string to be checked
|
||||
|
||||
Returns:
|
||||
bool: True if the input s contains the characters and False otherwise
|
||||
"""
|
||||
unicode_ranges = ('\u4e00-\u9fff' # Chinese characters
|
||||
'\u3040-\u309f' # Japanese hiragana
|
||||
'\u30a0-\u30ff' # Japanese katakana
|
||||
'\u4e00-\u9faf' # Japanese kanji
|
||||
'\uac00-\ud7af' # Korean hangul syllables
|
||||
'\u1100-\u11ff' # Korean hangul jamo
|
||||
)
|
||||
return bool(re.search(fr'[{unicode_ranges}]', s))
|
||||
|
||||
|
||||
def bold_search_terms(response: str, query: str) -> BeautifulSoup:
|
||||
"""Wraps all search terms in bold tags (<b>). If any terms are wrapped
|
||||
|
@ -61,20 +90,29 @@ def bold_search_terms(response: str, query: str) -> BeautifulSoup:
|
|||
if len(element) == len(target_word):
|
||||
return
|
||||
|
||||
if not re.match('.*[a-zA-Z0-9].*', target_word) or (
|
||||
# Ensure target word is escaped for regex
|
||||
target_word = re.escape(target_word)
|
||||
|
||||
# Check if the word contains Chinese, Japanese, or Korean characters
|
||||
if contains_cjko(target_word):
|
||||
reg_pattern = fr'((?![{{}}<>-]){target_word}(?![{{}}<>-]))'
|
||||
else:
|
||||
reg_pattern = fr'\b((?![{{}}<>-]){target_word}(?![{{}}<>-]))\b'
|
||||
|
||||
if re.match(r'.*[@_!#$%^&*()<>?/\|}{~:].*', target_word) or (
|
||||
element.parent and element.parent.name == 'style'):
|
||||
return
|
||||
|
||||
element.replace_with(BeautifulSoup(
|
||||
re.sub(fr'\b((?![{{}}<>-]){target_word}(?![{{}}<>-]))\b',
|
||||
re.sub(reg_pattern,
|
||||
r'<b>\1</b>',
|
||||
html.escape(element),
|
||||
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)
|
||||
word = re.sub(r'[@_!#$%^&*()<>?/\|}{~:]+', '', word)
|
||||
target = response.find_all(
|
||||
text=re.compile(r'' + re.escape(word), re.I))
|
||||
for nav_str in target:
|
||||
|
@ -108,19 +146,34 @@ def get_first_link(soup: BeautifulSoup) -> str:
|
|||
str: A str link to the first result
|
||||
|
||||
"""
|
||||
first_link = ''
|
||||
orig_details = []
|
||||
|
||||
# Temporarily remove details so we don't grab those links
|
||||
for details in soup.find_all('details'):
|
||||
temp_details = soup.new_tag('removed_details')
|
||||
orig_details.append(details.replace_with(temp_details))
|
||||
|
||||
# Replace hrefs with only the intended destination (no "utm" type tags)
|
||||
for a in soup.find_all('a', href=True):
|
||||
# Return the first search result URL
|
||||
if 'url?q=' in a['href']:
|
||||
return filter_link_args(a['href'])
|
||||
return ''
|
||||
if a['href'].startswith('http://') or a['href'].startswith('https://'):
|
||||
first_link = a['href']
|
||||
break
|
||||
|
||||
# Add the details back
|
||||
for orig_detail, details in zip(orig_details, soup.find_all('removed_details')):
|
||||
details.replace_with(orig_detail)
|
||||
|
||||
return first_link
|
||||
|
||||
|
||||
def get_site_alt(link: str) -> str:
|
||||
def get_site_alt(link: str, site_alts: dict = SITE_ALTS) -> str:
|
||||
"""Returns an alternative to a particular site, if one is configured
|
||||
|
||||
Args:
|
||||
link: A string result URL to check against the SITE_ALTS map
|
||||
link: A string result URL to check against the site_alts map
|
||||
site_alts: A map of site alternatives to replace with. defaults to SITE_ALTS
|
||||
|
||||
Returns:
|
||||
str: An updated (or ignored) result link
|
||||
|
@ -128,15 +181,52 @@ def get_site_alt(link: str) -> str:
|
|||
"""
|
||||
# Need to replace full hostname with alternative to encapsulate
|
||||
# subdomains as well
|
||||
hostname = urlparse.urlparse(link).hostname
|
||||
parsed_link = urlparse.urlparse(link)
|
||||
|
||||
for site_key in SITE_ALTS.keys():
|
||||
if not hostname or site_key not in hostname or not SITE_ALTS[site_key]:
|
||||
# Extract subdomain separately from the domain+tld. The subdomain
|
||||
# is used for wikiless translations.
|
||||
split_host = parsed_link.netloc.split('.')
|
||||
subdomain = split_host[0] if len(split_host) > 2 else ''
|
||||
hostname = '.'.join(split_host[-2:])
|
||||
|
||||
# The full scheme + hostname is used when comparing against the list of
|
||||
# available alternative services, due to how Medium links are constructed.
|
||||
# (i.e. for medium.com: "https://something.medium.com" should match,
|
||||
# "https://medium.com/..." should match, but "philomedium.com" should not)
|
||||
hostcomp = f'{parsed_link.scheme}://{hostname}'
|
||||
|
||||
for site_key in site_alts.keys():
|
||||
site_alt = f'{parsed_link.scheme}://{site_key}'
|
||||
if not hostname or site_alt not in hostcomp or not site_alts[site_key]:
|
||||
continue
|
||||
|
||||
link = link.replace(hostname, SITE_ALTS[site_key])
|
||||
# Wikipedia -> Wikiless replacements require the subdomain (if it's
|
||||
# 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 and len(subdomain) == 2:
|
||||
hostname = f'{subdomain}.{hostname}'
|
||||
params = f'?lang={subdomain}'
|
||||
elif 'medium' in hostname and len(subdomain) > 0:
|
||||
hostname = f'{subdomain}.{hostname}'
|
||||
|
||||
parsed_alt = urlparse.urlparse(site_alts[site_key])
|
||||
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, '//')
|
||||
if parsed_alt.scheme:
|
||||
# If a scheme is specified, remove everything before the
|
||||
# first occurence of it
|
||||
link = f'{parsed_alt.scheme}{link.split(parsed_alt.scheme, 1)[-1]}'
|
||||
else:
|
||||
# Otherwise, replace the first occurrence of the prefix
|
||||
link = link.replace(prefix, '//', 1)
|
||||
break
|
||||
|
||||
return link
|
||||
|
@ -214,44 +304,6 @@ def append_anon_view(result: BeautifulSoup, config: Config) -> None:
|
|||
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
|
||||
|
||||
|
@ -267,7 +319,10 @@ def check_currency(response: str) -> dict:
|
|||
if currency_link:
|
||||
while 'class' not in currency_link.attrs or \
|
||||
'ZINbbc' not in currency_link.attrs['class']:
|
||||
currency_link = currency_link.parent
|
||||
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
|
||||
|
@ -369,6 +424,7 @@ def add_currency_card(soup: BeautifulSoup,
|
|||
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.
|
||||
|
||||
|
@ -381,6 +437,10 @@ def get_tabs_content(tabs: dict,
|
|||
Returns:
|
||||
dict: contains the name, the href and if the tab is selected or not
|
||||
"""
|
||||
map_query = full_query
|
||||
if '-site:' in full_query:
|
||||
block_idx = full_query.index('-site:')
|
||||
map_query = map_query[:block_idx]
|
||||
tabs = copy.deepcopy(tabs)
|
||||
for tab_id, tab_content in tabs.items():
|
||||
# update name to desired language
|
||||
|
@ -393,7 +453,12 @@ def get_tabs_content(tabs: dict,
|
|||
if tab_content['tbm'] is not None:
|
||||
query = f"{query}&tbm={tab_content['tbm']}"
|
||||
|
||||
tab_content['href'] = tab_content['href'].format(query=query)
|
||||
if preferences:
|
||||
query = f"{query}&preferences={preferences}"
|
||||
|
||||
tab_content['href'] = tab_content['href'].format(
|
||||
query=query,
|
||||
map_query=map_query)
|
||||
|
||||
# update if selected tab (default all tab is selected)
|
||||
if tab_content['tbm'] == search_type:
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import os
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from app.filter import Filter, get_first_link
|
||||
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 cryptography.fernet import Fernet, InvalidToken
|
||||
from flask import g
|
||||
|
@ -63,6 +64,7 @@ class Search:
|
|||
self.config = config
|
||||
self.session_key = session_key
|
||||
self.query = ''
|
||||
self.widget = ''
|
||||
self.cookies_disabled = cookies_disabled
|
||||
self.search_type = self.request_params.get(
|
||||
'tbm') if 'tbm' in self.request_params else ''
|
||||
|
@ -100,9 +102,22 @@ class Search:
|
|||
except InvalidToken:
|
||||
pass
|
||||
|
||||
# Strip leading '! ' for "feeling lucky" queries
|
||||
self.feeling_lucky = q.startswith('! ')
|
||||
self.query = q[2:] if self.feeling_lucky else q
|
||||
# Strip '!' for "feeling lucky" queries
|
||||
if match := re.search("(^|\s)!($|\s)", q):
|
||||
self.feeling_lucky = True
|
||||
start, end = match.span()
|
||||
self.query = " ".join([seg for seg in [q[:start], q[end:]] if seg])
|
||||
else:
|
||||
self.feeling_lucky = False
|
||||
self.query = q
|
||||
|
||||
# Check for possible widgets
|
||||
self.widget = "ip" if re.search("([^a-z0-9]|^)my *[^a-z0-9] *(ip|internet protocol)" +
|
||||
"($|( *[^a-z0-9] *(((addres|address|adres|" +
|
||||
"adress)|a)? *$)))", self.query.lower()) else self.widget
|
||||
self.widget = 'calculator' if re.search(
|
||||
r"\bcalculator\b|\bcalc\b|\bcalclator\b|\bmath\b",
|
||||
self.query.lower()) else self.widget
|
||||
return self.query
|
||||
|
||||
def generate_response(self) -> str:
|
||||
|
@ -114,9 +129,14 @@ class Search:
|
|||
|
||||
"""
|
||||
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,
|
||||
root_url=self.request.url_root,
|
||||
root_url=root_url,
|
||||
mobile=mobile,
|
||||
config=self.config,
|
||||
query=self.query)
|
||||
|
@ -132,10 +152,12 @@ class Search:
|
|||
and not g.user_request.mobile)
|
||||
|
||||
get_body = g.user_request.send(query=full_query,
|
||||
force_mobile=view_image)
|
||||
force_mobile=view_image,
|
||||
user_agent=self.user_agent)
|
||||
|
||||
# Produce cleanable html soup from response
|
||||
html_soup = bsoup(get_body.text, 'html.parser')
|
||||
get_body_safed = get_body.text.replace("<","andlt;").replace(">","andgt;")
|
||||
html_soup = bsoup(get_body_safed, 'html.parser')
|
||||
|
||||
# Replace current soup if view_image is active
|
||||
if view_image:
|
||||
|
@ -145,32 +167,25 @@ class Search:
|
|||
if g.user_request.tor_valid:
|
||||
html_soup.insert(0, bsoup(TOR_BANNER, 'html.parser'))
|
||||
|
||||
formatted_results = content_filter.clean(html_soup)
|
||||
if self.feeling_lucky:
|
||||
return get_first_link(html_soup)
|
||||
else:
|
||||
formatted_results = content_filter.clean(html_soup)
|
||||
if lucky_link := get_first_link(formatted_results):
|
||||
return lucky_link
|
||||
|
||||
# Append user config to all search links, if available
|
||||
param_str = ''.join('&{}={}'.format(k, v)
|
||||
for k, v in
|
||||
self.request_params.to_dict(flat=True).items()
|
||||
if self.config.is_safe_key(k))
|
||||
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(
|
||||
'search?') > 1:
|
||||
continue
|
||||
link['href'] += param_str
|
||||
# Fall through to regular search if unable to find link
|
||||
self.feeling_lucky = False
|
||||
|
||||
return str(formatted_results)
|
||||
# Append user config to all search links, if available
|
||||
param_str = ''.join('&{}={}'.format(k, v)
|
||||
for k, v in
|
||||
self.request_params.to_dict(flat=True).items()
|
||||
if self.config.is_safe_key(k))
|
||||
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(
|
||||
'search?') > 1:
|
||||
continue
|
||||
link['href'] += param_str
|
||||
|
||||
def check_kw_ip(self) -> re.Match:
|
||||
"""Checks for keywords related to 'my ip' in the query
|
||||
return str(formatted_results)
|
||||
|
||||
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())
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
from cryptography.fernet import Fernet
|
||||
from flask import current_app as app
|
||||
|
||||
REQUIRED_SESSION_VALUES = ['uuid', 'config', 'key']
|
||||
REQUIRED_SESSION_VALUES = ['uuid', 'config', 'key', 'auth']
|
||||
|
||||
|
||||
def generate_user_key() -> bytes:
|
||||
def generate_key() -> bytes:
|
||||
"""Generates a key for encrypting searches and element URLs
|
||||
|
||||
Args:
|
||||
|
|
71
app/utils/widgets.py
Normal file
71
app/utils/widgets.py
Normal file
|
@ -0,0 +1,71 @@
|
|||
from pathlib import Path
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
|
||||
# root
|
||||
BASE_DIR = Path(__file__).parent.parent.parent
|
||||
|
||||
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 add_calculator_card(html_soup: BeautifulSoup) -> BeautifulSoup:
|
||||
"""Adds the a calculator widget to the search results
|
||||
if query contains keywords
|
||||
|
||||
Args:
|
||||
html_soup: The parsed search result containing the keywords
|
||||
|
||||
Returns:
|
||||
BeautifulSoup
|
||||
"""
|
||||
main_div = html_soup.select_one('#main')
|
||||
if main_div:
|
||||
# absolute path
|
||||
widget_file = open(BASE_DIR / 'app/static/widgets/calculator.html', encoding="utf8")
|
||||
widget_tag = html_soup.new_tag('div')
|
||||
widget_tag['class'] = 'ZINbbc xpd O9g5cc uUPGi'
|
||||
widget_tag['id'] = 'calculator-wrapper'
|
||||
calculator_text = html_soup.new_tag('div')
|
||||
calculator_text['class'] = 'kCrYT ip-address-div'
|
||||
calculator_text.string = 'Calculator'
|
||||
calculator_widget = html_soup.new_tag('div')
|
||||
calculator_widget.append(BeautifulSoup(widget_file, 'html.parser'))
|
||||
calculator_widget['class'] = 'kCrYT ip-text-div'
|
||||
widget_tag.append(calculator_text)
|
||||
widget_tag.append(calculator_widget)
|
||||
main_div.insert_before(widget_tag)
|
||||
widget_file.close()
|
||||
return html_soup
|
|
@ -1,8 +1,7 @@
|
|||
import os
|
||||
import setuptools
|
||||
|
||||
optional_dev_tag = ''
|
||||
if os.getenv('DEV_BUILD'):
|
||||
optional_dev_tag = '.dev' + os.getenv('DEV_BUILD')
|
||||
|
||||
setuptools.setup(version='0.7.4' + optional_dev_tag)
|
||||
__version__ = '0.9.1' + optional_dev_tag
|
|
@ -3,7 +3,7 @@ name: whoogle
|
|||
description: A self hosted search engine on Kubernetes
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: 0.7.4
|
||||
appVersion: 0.9.1
|
||||
|
||||
icon: https://github.com/benbusby/whoogle-search/raw/main/app/static/img/favicon/favicon-96x96.png
|
||||
|
||||
|
|
|
@ -52,10 +52,20 @@ spec:
|
|||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
{{- if and .Values.conf.WHOOGLE_USER .Values.conf.WHOOGLE_PASS }}
|
||||
httpHeaders:
|
||||
- name: Authorization
|
||||
value: Basic {{ b64enc (printf "%s:%s" .Values.conf.WHOOGLE_USER .Values.conf.WHOOGLE_PASS) }}
|
||||
{{- end }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
{{- if and .Values.conf.WHOOGLE_USER .Values.conf.WHOOGLE_PASS }}
|
||||
httpHeaders:
|
||||
- name: Authorization
|
||||
value: Basic {{ b64enc (printf "%s:%s" .Values.conf.WHOOGLE_USER .Values.conf.WHOOGLE_PASS) }}
|
||||
{{- end }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
|
|
|
@ -36,30 +36,34 @@ conf: {}
|
|||
# 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_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_ALT_SO: "" # The stackoverflow.com alternative to use. Set to "" to continue using stackoverflow.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_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: {}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# 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
|
||||
version: "2.4"
|
||||
|
||||
|
@ -66,10 +66,15 @@ services:
|
|||
#- 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_ALT_SO=farside.link/anonymousoverflow
|
||||
# - 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:
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# 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
|
||||
version: "2.4"
|
||||
|
||||
|
@ -40,6 +40,9 @@ services:
|
|||
#- 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_ALT_SO=farside.link/anonymousoverflow
|
||||
#env_file: # Alternatively, load variables from whoogle.env
|
||||
#- whoogle.env
|
||||
ports:
|
||||
|
|
|
@ -1,13 +1,24 @@
|
|||
https://gowogle.voring.me
|
||||
https://s.tokhmi.xyz
|
||||
https://search.albony.xyz
|
||||
https://search.garudalinux.org
|
||||
https://search.dr460nf1r3.org
|
||||
https://search.nezumi.party
|
||||
https://s.tokhmi.xyz
|
||||
https://search.sethforprivacy.com
|
||||
https://whoogle.fossho.st
|
||||
https://whooglesearch.net
|
||||
https://www.whooglesearch.ml
|
||||
https://whoogle.dcs0.hu
|
||||
https://whoogle.esmailelbob.xyz
|
||||
https://whoogle.lunar.icu
|
||||
https://gowogle.voring.me
|
||||
https://whoogle.privacydev.net
|
||||
https://whoogle.hostux.net
|
||||
https://wg.vern.cc
|
||||
https://whoogle.hxvy0.gq
|
||||
https://whoogle.ungovernable.men
|
||||
https://whoogle2.ungovernable.men
|
||||
https://whoogle3.ungovernable.men
|
||||
https://wgl.frail.duckdns.org
|
||||
https://whoogle.no-logs.com
|
||||
https://whoogle.ftw.lol
|
||||
https://whoogle-search--replitcomreside.repl.co
|
||||
https://search.notrustverify.ch
|
||||
https://whoogle.datura.network
|
||||
https://whoogle.yepserver.xyz
|
||||
https://search.snine.nl
|
||||
|
|
5
misc/replit.py
Normal file
5
misc/replit.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
import subprocess
|
||||
|
||||
# A plague upon Replit and all who have built it
|
||||
replit_cmd = "killall -q python3 > /dev/null 2>&1; pip install -r requirements.txt && ./run"
|
||||
subprocess.run(replit_cmd, shell=True)
|
1
misc/tor/control.conf
Normal file
1
misc/tor/control.conf
Normal file
|
@ -0,0 +1 @@
|
|||
# Place password here. Keep this safe.
|
|
@ -1,5 +1,27 @@
|
|||
#!/bin/sh
|
||||
|
||||
FF_STRING="FascistFirewall 1"
|
||||
|
||||
if [ "$WHOOGLE_TOR_SERVICE" == "0" ]; then
|
||||
echo "Skipping Tor startup..."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$WHOOGLE_TOR_FF" == "1" ]; then
|
||||
if (grep -q "$FF_STRING" /etc/tor/torrc); then
|
||||
echo "FascistFirewall feature already enabled."
|
||||
else
|
||||
echo "$FF_STRING" >> /etc/tor/torrc
|
||||
|
||||
if [ "$?" -eq 0 ]; then
|
||||
echo "FascistFirewall added to /etc/tor/torrc"
|
||||
else
|
||||
echo "ERROR: Unable to modify /etc/tor/torrc with $FF_STRING."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$(whoami)" != "root" ]; then
|
||||
tor -f /etc/tor/torrc
|
||||
else
|
||||
|
|
|
@ -6,3 +6,7 @@ CookieAuthFileGroupReadable 1
|
|||
ExtORPortCookieAuthFileGroupReadable 1
|
||||
CacheDirectoryGroupReadable 1
|
||||
CookieAuthFile /var/lib/tor/control_auth_cookie
|
||||
Log debug-notice file /dev/null
|
||||
# UseBridges 1
|
||||
# ClientTransportPlugin obfs4 exec /usr/bin/obfs4proxy
|
||||
# Bridge obfs4 ip and so on
|
||||
|
|
67
misc/update-translations.py
Normal file
67
misc/update-translations.py
Normal file
|
@ -0,0 +1,67 @@
|
|||
import json
|
||||
import pathlib
|
||||
import requests
|
||||
|
||||
lingva = 'https://lingva.ml/api/v1/en'
|
||||
|
||||
|
||||
def format_lang(lang: str) -> str:
|
||||
# Chinese (traditional and simplified) require
|
||||
# a different format for lingva translations
|
||||
if 'zh-' in lang:
|
||||
if lang == 'zh-TW':
|
||||
return 'zh_HANT'
|
||||
return 'zh'
|
||||
|
||||
# Strip lang prefix to leave only the actual
|
||||
# language code (i.e. 'en', 'fr', etc)
|
||||
return lang.replace('lang_', '')
|
||||
|
||||
|
||||
def translate(v: str, lang: str) -> str:
|
||||
# Strip lang prefix to leave only the actual
|
||||
#language code (i.e. "es", "fr", etc)
|
||||
lang = format_lang(lang)
|
||||
|
||||
lingva_req = f'{lingva}/{lang}/{v}'
|
||||
|
||||
response = requests.get(lingva_req).json()
|
||||
|
||||
if 'translation' in response:
|
||||
return response['translation']
|
||||
return ''
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
file_path = pathlib.Path(__file__).parent.resolve()
|
||||
tl_path = 'app/static/settings/translations.json'
|
||||
|
||||
with open(f'{file_path}/../{tl_path}', 'r+', encoding='utf-8') as tl_file:
|
||||
tl_data = json.load(tl_file)
|
||||
|
||||
# If there are any english translations that don't
|
||||
# exist for other languages, extract them and translate
|
||||
# them now
|
||||
en_tl = tl_data['lang_en']
|
||||
for k, v in en_tl.items():
|
||||
for lang in tl_data:
|
||||
if lang == 'lang_en' or k in tl_data[lang]:
|
||||
continue
|
||||
|
||||
translation = ''
|
||||
if len(k) == 0:
|
||||
# Special case for placeholder text that gets used
|
||||
# for translations without any key present
|
||||
translation = v
|
||||
else:
|
||||
# Translate the string using lingva
|
||||
translation = translate(v, lang)
|
||||
|
||||
if len(translation) == 0:
|
||||
print(f'! Unable to translate {lang}[{k}]')
|
||||
continue
|
||||
print(f'{lang}[{k}] = {translation}')
|
||||
tl_data[lang][k] = translation
|
||||
|
||||
# Write out updated translations json
|
||||
print(json.dumps(tl_data, indent=4, ensure_ascii=False))
|
3
pyproject.toml
Normal file
3
pyproject.toml
Normal file
|
@ -0,0 +1,3 @@
|
|||
[build-system]
|
||||
requires = ["setuptools", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
|
@ -1,35 +1,37 @@
|
|||
attrs==19.3.0
|
||||
beautifulsoup4==4.10.0
|
||||
cachelib==0.4.1
|
||||
certifi==2020.4.5.1
|
||||
cffi==1.15.0
|
||||
chardet==3.0.4
|
||||
click==8.0.3
|
||||
cryptography==3.3.2
|
||||
cssutils==2.4.0
|
||||
attrs==22.2.0
|
||||
beautifulsoup4==4.11.2
|
||||
brotli==1.0.9
|
||||
cachelib==0.10.2
|
||||
certifi==2024.7.4
|
||||
cffi==1.17.1
|
||||
chardet==5.1.0
|
||||
click==8.1.3
|
||||
cryptography==3.3.2; platform_machine == 'armv7l'
|
||||
cryptography==43.0.1; platform_machine != 'armv7l'
|
||||
cssutils==2.6.0
|
||||
defusedxml==0.7.1
|
||||
Flask==1.1.1
|
||||
Flask-Session==0.4.0
|
||||
idna==2.9
|
||||
itsdangerous==1.1.0
|
||||
Jinja2==2.11.3
|
||||
MarkupSafe==1.1.1
|
||||
more-itertools==8.3.0
|
||||
packaging==20.4
|
||||
pluggy==0.13.1
|
||||
py==1.10.0
|
||||
pycodestyle==2.6.0
|
||||
pycparser==2.21
|
||||
pyOpenSSL==19.1.0
|
||||
pyparsing==2.4.7
|
||||
Flask==2.3.2
|
||||
idna==3.7
|
||||
itsdangerous==2.1.2
|
||||
Jinja2==3.1.4
|
||||
MarkupSafe==2.1.2
|
||||
more-itertools==9.0.0
|
||||
packaging==23.0
|
||||
pluggy==1.0.0
|
||||
pycodestyle==2.10.0
|
||||
pycparser==2.22
|
||||
pyOpenSSL==19.1.0; platform_machine == 'armv7l'
|
||||
pyOpenSSL==24.2.1; platform_machine != 'armv7l'
|
||||
pyparsing==3.0.9
|
||||
PySocks==1.7.1
|
||||
pytest==6.2.5
|
||||
python-dateutil==2.8.1
|
||||
requests==2.25.1
|
||||
soupsieve==1.9.5
|
||||
stem==1.8.0
|
||||
urllib3==1.26.5
|
||||
waitress==2.1.2
|
||||
wcwidth==0.1.9
|
||||
Werkzeug==0.16.0
|
||||
python-dotenv==0.16.0
|
||||
pytest==7.2.1
|
||||
python-dateutil==2.8.2
|
||||
requests==2.32.2
|
||||
soupsieve==2.4
|
||||
stem==1.8.1
|
||||
urllib3==1.26.19
|
||||
validators==0.22.0
|
||||
waitress==3.0.1
|
||||
wcwidth==0.2.6
|
||||
Werkzeug==3.0.6
|
||||
python-dotenv==0.21.1
|
||||
|
|
1
run
1
run
|
@ -29,6 +29,7 @@ else
|
|||
python3 -um app \
|
||||
--unix-socket "$UNIX_SOCKET"
|
||||
else
|
||||
echo "Running on http://${ADDRESS:-0.0.0.0}:${PORT:-"${EXPOSE_PORT:-5000}"}"
|
||||
python3 -um app \
|
||||
--host "${ADDRESS:-0.0.0.0}" \
|
||||
--port "${PORT:-"${EXPOSE_PORT:-5000}"}"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
[metadata]
|
||||
name = whoogle-search
|
||||
version = attr: app.version.__version__
|
||||
url = https://github.com/benbusby/whoogle-search
|
||||
description = Self-hosted, ad-free, privacy-respecting metasearch engine
|
||||
long_description = file: README.md
|
||||
|
@ -18,14 +19,15 @@ packages = find:
|
|||
include_package_data = True
|
||||
install_requires=
|
||||
beautifulsoup4
|
||||
brotli
|
||||
cssutils
|
||||
cryptography
|
||||
defusedxml
|
||||
Flask
|
||||
Flask-Session
|
||||
python-dotenv
|
||||
requests
|
||||
stem
|
||||
validators
|
||||
waitress
|
||||
|
||||
[options.extras_require]
|
||||
|
@ -34,6 +36,10 @@ test =
|
|||
python-dateutil
|
||||
dev = pycodestyle
|
||||
|
||||
[options.packages.find]
|
||||
exclude =
|
||||
test*
|
||||
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
whoogle-search = app.routes:run_app
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from app import app
|
||||
from app.utils.session import generate_user_key
|
||||
from app.utils.session import generate_key
|
||||
import pytest
|
||||
import random
|
||||
|
||||
|
@ -18,6 +18,7 @@ def client():
|
|||
with app.test_client() as client:
|
||||
with client.session_transaction() as session:
|
||||
session['uuid'] = 'test'
|
||||
session['key'] = generate_user_key()
|
||||
session['key'] = app.enc_key
|
||||
session['config'] = {}
|
||||
session['auth'] = False
|
||||
yield client
|
||||
|
|
|
@ -2,13 +2,19 @@ from cryptography.fernet import Fernet
|
|||
|
||||
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_key, valid_user_session
|
||||
|
||||
JAPAN_PREFS = 'uG7IBICwK7FgMJNpUawp2tKDb1Omuv_euy-cJHVZ' \
|
||||
+ 'BSydthgwxRFIHxiVA8qUGavKaDXyiM5uNuPIjKbEAW-zB_vzNXWVaafFhW7k2' \
|
||||
+ 'fO2_mS5e5eK41XXWwiViTz2VVmGWje0UgQwwVPe1A7aH0s10FgARsd2xl5nlg' \
|
||||
+ 'RLHT2krPUw-iLQ5uHZSnYXFuF4caYemWcj4vqB2ocHkt-aqn04jgnnlWWME_K' \
|
||||
+ '9ySWdWmPyS66HtLt1tCwc_-xGZklvbHw=='
|
||||
|
||||
|
||||
def test_generate_user_keys():
|
||||
key = generate_user_key()
|
||||
key = generate_key()
|
||||
assert Fernet(key)
|
||||
assert generate_user_key() != key
|
||||
assert generate_key() != key
|
||||
|
||||
|
||||
def test_valid_session(client):
|
||||
|
@ -49,3 +55,16 @@ def test_query_decryption(client):
|
|||
|
||||
with client.session_transaction() as 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
|
||||
|
||||
|
|
|
@ -2,16 +2,17 @@ from bs4 import BeautifulSoup
|
|||
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 import results
|
||||
from app.utils.session import generate_key
|
||||
from datetime import datetime
|
||||
from dateutil.parser import *
|
||||
from dateutil.parser import ParserError, parse
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from test.conftest import demo_config
|
||||
|
||||
|
||||
def get_search_results(data):
|
||||
secret_key = generate_user_key()
|
||||
secret_key = generate_key()
|
||||
soup = Filter(user_key=secret_key, config=Config(**demo_config)).clean(
|
||||
BeautifulSoup(data, 'html.parser'))
|
||||
|
||||
|
@ -44,17 +45,11 @@ def test_get_results(client):
|
|||
|
||||
def test_post_results(client):
|
||||
rv = client.post(f'/{Endpoint.search}', data=dict(q='test'))
|
||||
assert rv._status_code == 200
|
||||
|
||||
# Depending on the search, there can be more
|
||||
# than 10 result divs
|
||||
results = get_search_results(rv.data)
|
||||
assert len(results) >= 10
|
||||
assert len(results) <= 15
|
||||
assert rv._status_code == 302
|
||||
|
||||
|
||||
def test_translate_search(client):
|
||||
rv = client.post(f'/{Endpoint.search}', data=dict(q='translate hola'))
|
||||
rv = client.get(f'/{Endpoint.search}?q=translate hola')
|
||||
assert rv._status_code == 200
|
||||
|
||||
# Pretty weak test, but better than nothing
|
||||
|
@ -64,7 +59,7 @@ def test_translate_search(client):
|
|||
|
||||
|
||||
def test_block_results(client):
|
||||
rv = client.post(f'/{Endpoint.search}', data=dict(q='pinterest'))
|
||||
rv = client.get(f'/{Endpoint.search}?q=pinterest')
|
||||
assert rv._status_code == 200
|
||||
|
||||
has_pinterest = False
|
||||
|
@ -79,7 +74,7 @@ def test_block_results(client):
|
|||
rv = client.post(f'/{Endpoint.config}', data=demo_config)
|
||||
assert rv._status_code == 302
|
||||
|
||||
rv = client.post(f'/{Endpoint.search}', data=dict(q='pinterest'))
|
||||
rv = client.get(f'/{Endpoint.search}?q=pinterest')
|
||||
assert rv._status_code == 200
|
||||
|
||||
for link in BeautifulSoup(rv.data, 'html.parser').find_all('a', href=True):
|
||||
|
@ -90,7 +85,7 @@ def test_block_results(client):
|
|||
|
||||
|
||||
def test_view_my_ip(client):
|
||||
rv = client.post(f'/{Endpoint.search}', data=dict(q='my ip address'))
|
||||
rv = client.get(f'/{Endpoint.search}?q=my ip address')
|
||||
assert rv._status_code == 200
|
||||
|
||||
# Pretty weak test, but better than nothing
|
||||
|
@ -101,13 +96,13 @@ def test_view_my_ip(client):
|
|||
|
||||
def test_recent_results(client):
|
||||
times = {
|
||||
'past year': 365,
|
||||
'past month': 31,
|
||||
'past week': 7
|
||||
'tbs=qdr:y': 365,
|
||||
'tbs=qdr:m': 31,
|
||||
'tbs=qdr:w': 7
|
||||
}
|
||||
|
||||
for time, num_days in times.items():
|
||||
rv = client.post(f'/{Endpoint.search}', data=dict(q='test :' + time))
|
||||
rv = client.get(f'/{Endpoint.search}?q=test&' + time)
|
||||
result_divs = get_search_results(rv.data)
|
||||
|
||||
current_date = datetime.now()
|
||||
|
@ -132,7 +127,7 @@ def test_leading_slash_search(client):
|
|||
assert rv._status_code == 200
|
||||
|
||||
soup = Filter(
|
||||
user_key=generate_user_key(),
|
||||
user_key=generate_key(),
|
||||
config=Config(**demo_config),
|
||||
query=q
|
||||
).clean(BeautifulSoup(rv.data, 'html.parser'))
|
||||
|
@ -142,3 +137,22 @@ def test_leading_slash_search(client):
|
|||
continue
|
||||
|
||||
assert link['href'].startswith(f'{Endpoint.search}')
|
||||
|
||||
|
||||
def test_site_alt_prefix_skip():
|
||||
# Ensure prefixes are skipped correctly for site alts
|
||||
|
||||
# default silte_alts (farside.link)
|
||||
assert results.get_site_alt(link = 'https://www.reddit.com') == 'https://farside.link/libreddit'
|
||||
assert results.get_site_alt(link = 'https://www.twitter.com') == 'https://farside.link/nitter'
|
||||
assert results.get_site_alt(link = 'https://www.youtube.com') == 'https://farside.link/invidious'
|
||||
|
||||
test_site_alts = {
|
||||
'reddit.com': 'reddit.endswithmobile.domain',
|
||||
'twitter.com': 'https://twitter.endswithm.domain',
|
||||
'youtube.com': 'http://yt.endswithwww.domain',
|
||||
}
|
||||
# Domains with part of SKIP_PREFIX in them
|
||||
assert results.get_site_alt(link = 'https://www.reddit.com', site_alts = test_site_alts) == 'https://reddit.endswithmobile.domain'
|
||||
assert results.get_site_alt(link = 'https://www.twitter.com', site_alts = test_site_alts) == 'https://twitter.endswithm.domain'
|
||||
assert results.get_site_alt(link = 'https://www.youtube.com', site_alts = test_site_alts) == 'http://yt.endswithwww.domain'
|
||||
|
|
|
@ -17,8 +17,15 @@ def test_search(client):
|
|||
|
||||
|
||||
def test_feeling_lucky(client):
|
||||
rv = client.get(f'/{Endpoint.search}?q=!%20test')
|
||||
# Bang at beginning of query
|
||||
rv = client.get(f'/{Endpoint.search}?q=!%20wikipedia')
|
||||
assert rv._status_code == 303
|
||||
assert rv.headers.get('Location').startswith('https://www.wikipedia.org')
|
||||
|
||||
# Move bang to end of query
|
||||
rv = client.get(f'/{Endpoint.search}?q=github%20!')
|
||||
assert rv._status_code == 303
|
||||
assert rv.headers.get('Location').startswith('https://github.com')
|
||||
|
||||
|
||||
def test_ddg_bang(client):
|
||||
|
@ -37,11 +44,6 @@ def test_ddg_bang(client):
|
|||
assert rv._status_code == 302
|
||||
assert rv.headers.get('Location').startswith('https://www.reddit.com')
|
||||
|
||||
# Move '!' to end of the bang
|
||||
rv = client.get(f'/{Endpoint.search}?q=gitlab%20w!')
|
||||
assert rv._status_code == 302
|
||||
assert rv.headers.get('Location').startswith('https://en.wikipedia.org')
|
||||
|
||||
# Ensure bang is case insensitive
|
||||
rv = client.get(f'/{Endpoint.search}?q=!GH%20whoogle')
|
||||
assert rv._status_code == 302
|
||||
|
@ -53,6 +55,13 @@ def test_ddg_bang(client):
|
|||
assert rv.headers.get('Location').startswith('https://github.com')
|
||||
|
||||
|
||||
def test_custom_bang(client):
|
||||
# Bang at beginning of query
|
||||
rv = client.get(f'/{Endpoint.search}?q=!i%20whoogle')
|
||||
assert rv._status_code == 302
|
||||
assert rv.headers.get('Location').startswith('search?q=')
|
||||
|
||||
|
||||
def test_config(client):
|
||||
rv = client.post(f'/{Endpoint.config}', data=demo_config)
|
||||
assert rv._status_code == 302
|
||||
|
|
|
@ -15,6 +15,9 @@
|
|||
#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_ALT_SO=farside.link/anonymousoverflow
|
||||
#WHOOGLE_USER=""
|
||||
#WHOOGLE_PASS=""
|
||||
#WHOOGLE_PROXY_USER=""
|
||||
|
@ -83,3 +86,9 @@
|
|||
|
||||
# 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; }"
|
||||
|
||||
# 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…
Add table
Reference in a new issue