Merge branch 'main' into dev-header-tabs

This commit is contained in:
jacr13 2022-01-24 13:49:12 +01:00
commit 1b6d4fe243
57 changed files with 1748 additions and 737 deletions

View File

@ -2,7 +2,7 @@ name: buildx
on: on:
workflow_run: workflow_run:
workflows: ["tests"] workflows: ["docker_tests"]
branches: [main] branches: [main]
types: types:
- completed - completed

21
.github/workflows/docker_tests.yml vendored Normal file
View File

@ -0,0 +1,21 @@
name: docker_tests
on:
workflow_run:
workflows: ["tests"]
branches: [main]
types:
- completed
jobs:
on-success:
runs-on: ubuntu-latest
steps:
- name: checkout code
uses: actions/checkout@v2
- name: build and test
run: |
docker build --tag whoogle-search:test .
docker run --publish 5000:5000 --detach --name whoogle-search whoogle-search:test
sleep 15
docker exec whoogle-search curl -f http://localhost:5000/healthz || exit 1

View File

@ -1,3 +1,3 @@
language = "bash" language = "bash"
run = "pip install -r requirements.txt && ./run" run = "killall -q python3 > /dev/null 2>&1; pip install -r requirements.txt && ./run"
onBoot = "pip install -r requirements.txt && ./run" onBoot = "killall -q python3 > /dev/null 2>&1; pip install -r requirements.txt && ./run"

View File

@ -1,64 +1,64 @@
FROM python:3.8-slim as builder FROM python:3.8-alpine as builder
RUN apt-get update && apt-get install -y \ RUN apk --update add \
build-essential \ build-base \
libxml2-dev \ libxml2-dev \
libxslt-dev \ libxslt-dev \
libssl-dev \ openssl-dev \
libffi-dev libffi-dev
COPY requirements.txt . COPY requirements.txt .
RUN pip install --upgrade pip
RUN pip install --prefix /install --no-warn-script-location --no-cache-dir -r requirements.txt RUN pip install --prefix /install --no-warn-script-location --no-cache-dir -r requirements.txt
FROM python:3.8-slim FROM python:3.8-alpine
RUN apt-get update && apt-get install -y \ RUN apk add --update --no-cache tor curl bash openrc
libcurl4-openssl-dev \ # libcurl4-openssl-dev
tor \
curl \
&& rm -rf /var/lib/apt/lists/*
ARG DOCKER_USER=whoogle
ARG DOCKER_USERID=927
ARG config_dir=/config ARG config_dir=/config
RUN mkdir -p $config_dir RUN mkdir -p -m 777 $config_dir
VOLUME $config_dir VOLUME $config_dir
ENV CONFIG_VOLUME=$config_dir
ARG username='' ARG username=''
ENV WHOOGLE_USER=$username
ARG password='' ARG password=''
ENV WHOOGLE_PASS=$password
ARG proxyuser='' ARG proxyuser=''
ENV WHOOGLE_PROXY_USER=$proxyuser
ARG proxypass='' ARG proxypass=''
ENV WHOOGLE_PROXY_PASS=$proxypass
ARG proxytype='' ARG proxytype=''
ENV WHOOGLE_PROXY_TYPE=$proxytype
ARG proxyloc='' ARG proxyloc=''
ENV WHOOGLE_PROXY_LOC=$proxyloc
ARG whoogle_dotenv='' ARG whoogle_dotenv=''
ENV WHOOGLE_DOTENV=$whoogle_dotenv
ARG use_https='' ARG use_https=''
ENV HTTPS_ONLY=$use_https
ARG whoogle_port=5000 ARG whoogle_port=5000
ENV EXPOSE_PORT=$whoogle_port ARG twitter_alt='farside.link/nitter'
ARG youtube_alt='farside.link/invidious'
ARG twitter_alt='nitter.net' ARG instagram_alt='farside.link/bibliogram'
ENV WHOOGLE_ALT_TW=$twitter_alt ARG reddit_alt='farside.link/libreddit'
ARG youtube_alt='invidious.snopyta.org' ARG medium_alt='farside.link/scribe'
ENV WHOOGLE_ALT_YT=$youtube_alt
ARG instagram_alt='bibliogram.art/u'
ENV WHOOGLE_ALT_IG=$instagram_alt
ARG reddit_alt='libredd.it'
ENV WHOOGLE_ALT_RD=$reddit_alt
ARG translate_alt='lingva.ml' ARG translate_alt='lingva.ml'
ENV WHOOGLE_ALT_TL=$translate_alt ARG imgur_alt='imgin.voidnet.tech'
ARG medium_alt='scribe.rip' ARG wikipedia_alt='wikiless.org'
ENV WHOOGLE_ALT_MD=$medium_alt
ENV CONFIG_VOLUME=$config_dir \
WHOOGLE_USER=$username \
WHOOGLE_PASS=$password \
WHOOGLE_PROXY_USER=$proxyuser \
WHOOGLE_PROXY_PASS=$proxypass \
WHOOGLE_PROXY_TYPE=$proxytype \
WHOOGLE_PROXY_LOC=$proxyloc \
WHOOGLE_DOTENV=$whoogle_dotenv \
HTTPS_ONLY=$use_https \
EXPOSE_PORT=$whoogle_port \
WHOOGLE_ALT_TW=$twitter_alt \
WHOOGLE_ALT_YT=$youtube_alt \
WHOOGLE_ALT_IG=$instagram_alt \
WHOOGLE_ALT_RD=$reddit_alt \
WHOOGLE_ALT_MD=$medium_alt \
WHOOGLE_ALT_TL=$translate_alt \
WHOOGLE_ALT_IMG=$imgur_alt \
WHOOGLE_ALT_WIKI=$wikipedia_alt
WORKDIR /whoogle WORKDIR /whoogle
@ -72,6 +72,13 @@ COPY run .
# Allow writing symlinks to build dir # Allow writing symlinks to build dir
RUN chown 102:102 app/static/build RUN chown 102:102 app/static/build
# Create user/group to run as
RUN adduser -D -g $DOCKER_USERID -u $DOCKER_USERID $DOCKER_USER
# Fix ownership / permissions
RUN chown -R ${DOCKER_USER}:${DOCKER_USER} /whoogle /var/lib/tor
USER $DOCKER_USER:$DOCKER_USER
EXPOSE $EXPOSE_PORT EXPOSE $EXPOSE_PORT
HEALTHCHECK --interval=30s --timeout=5s \ HEALTHCHECK --interval=30s --timeout=5s \

View File

@ -22,6 +22,7 @@ Contents
6. [Manual](#f-manual) 6. [Manual](#f-manual)
7. [Docker](#g-manual-docker) 7. [Docker](#g-manual-docker)
8. [Arch/AUR](#arch-linux--arch-based-distributions) 8. [Arch/AUR](#arch-linux--arch-based-distributions)
9. [Helm/Kubernetes](#helm-chart-for-kubernetes)
4. [Environment Variables and Configuration](#environment-variables) 4. [Environment Variables and Configuration](#environment-variables)
5. [Usage](#usage) 5. [Usage](#usage)
6. [Extra Steps](#extra-steps) 6. [Extra Steps](#extra-steps)
@ -163,7 +164,7 @@ See the [available environment variables](#environment-variables) for additional
### F) Manual ### F) Manual
*Note: `Content-Security-Policy` headers are already sent by Whoogle -- you don't/shouldn't need to apply a CSP header yourself* *Note: `Content-Security-Policy` headers can be sent by Whoogle if you set `WHOOGLE_CSP`.*
Clone the repo and run the following commands to start the app in a local-only environment: Clone the repo and run the following commands to start the app in a local-only environment:
@ -178,7 +179,7 @@ pip install -r requirements.txt
See the [available environment variables](#environment-variables) for additional configuration. See the [available environment variables](#environment-variables) for additional configuration.
#### systemd Configuration #### systemd Configuration
After building the virtual environment, you can add the following to `/lib/systemd/system/whoogle.service` to set up a Whoogle Search systemd service: After building the virtual environment, you can add something like the following to `/lib/systemd/system/whoogle.service` to set up a Whoogle Search systemd service:
```ini ```ini
[Unit] [Unit]
@ -196,18 +197,26 @@ Description=Whoogle
# Site alternative configurations, uncomment to enable # Site alternative configurations, uncomment to enable
# Note: If not set, the feature will still be available # Note: If not set, the feature will still be available
# with default values. # with default values.
#Environment=WHOOGLE_ALT_TW=nitter.net #Environment=WHOOGLE_ALT_TW=farside.link/nitter
#Environment=WHOOGLE_ALT_YT=invidious.snopyta.org #Environment=WHOOGLE_ALT_YT=farside.link/invidious
#Environment=WHOOGLE_ALT_IG=bibliogram.art/u #Environment=WHOOGLE_ALT_IG=farside.link/bibliogram/u
#Environment=WHOOGLE_ALT_RD=libredd.it #Environment=WHOOGLE_ALT_RD=farside.link/libreddit
#Environment=WHOOGLE_ALT_MD=farside.link/scribe
#Environment=WHOOGLE_ALT_TL=lingva.ml #Environment=WHOOGLE_ALT_TL=lingva.ml
#Environment=WHOOGLE_ALT_MD=scribe.rip #Environment=WHOOGLE_ALT_IMG=imgin.voidnet.tech
#Environment=WHOOGLE_ALT_WIKI=wikiless.org
# Load values from dotenv only # Load values from dotenv only
#Environment=WHOOGLE_DOTENV=1 #Environment=WHOOGLE_DOTENV=1
Type=simple Type=simple
User=<username> User=<username>
WorkingDirectory=<whoogle_directory> # If installed as a package, add:
ExecStart=<whoogle_directory>/venv/bin/python3 -um app --host 0.0.0.0 --port 5000 ExecStart=<python_install_dir>/python3 <whoogle_install_dir>/whoogle-search --host 127.0.0.1 --port 5000
# For example:
# ExecStart=/usr/bin/python3 /home/my_username/.local/bin/whoogle-search --host 127.0.0.1 --port 5000
# Otherwise if running the app from source, add:
ExecStart=<whoogle_repo_dir>/run
# For example:
# ExecStart=/var/www/whoogle-search/run
ExecReload=/bin/kill -HUP $MAINPID ExecReload=/bin/kill -HUP $MAINPID
Restart=always Restart=always
RestartSec=3 RestartSec=3
@ -287,6 +296,13 @@ You may also edit environment variables from your apps Settings tab in the He
#### Arch Linux & Arch-based Distributions #### Arch Linux & Arch-based Distributions
There is an [AUR package available](https://aur.archlinux.org/packages/whoogle-git/), as well as a pre-built and daily updated package available at [Chaotic-AUR](https://chaotic.cx). There is an [AUR package available](https://aur.archlinux.org/packages/whoogle-git/), as well as a pre-built and daily updated package available at [Chaotic-AUR](https://chaotic.cx).
#### Helm chart for Kubernetes
To use the Kubernetes Helm Chart:
1. Ensure you have [Helm](https://helm.sh/docs/intro/install/) `>=3.0.0` installed
2. Clone this repository
3. Update [charts/whoogle/values.yaml](./charts/whoogle/values.yaml) as desired
4. Run `helm install whoogle ./charts/whoogle`
#### Using your own server, or alternative container deployment #### Using your own server, or alternative container deployment
There are other methods for deploying docker containers that are well outlined in [this article](https://rollout.io/blog/the-shortlist-of-docker-hosting/), but there are too many to describe set up for each here. Generally it should be about the same amount of effort as the Heroku deployment. There are other methods for deploying docker containers that are well outlined in [this article](https://rollout.io/blog/the-shortlist-of-docker-hosting/), but there are too many to describe set up for each here. Generally it should be about the same amount of effort as the Heroku deployment.
@ -320,8 +336,12 @@ There are a few optional environment variables available for customizing a Whoog
| WHOOGLE_ALT_RD | The reddit.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_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_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_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_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 |
### Config Environment Variables ### Config Environment Variables
These environment variables allow setting default config values, but can be overwritten manually by using the home page config menu. These allow a shortcut for destroying/rebuilding an instance to the same config state every time. These environment variables allow setting default config values, but can be overwritten manually by using the home page config menu. These allow a shortcut for destroying/rebuilding an instance to the same config state every time.
@ -488,25 +508,33 @@ A lot of the app currently piggybacks on Google's existing support for fetching
## Public Instances ## Public Instances
*Note: Use public instances at your own discretion. Maintainers of Whoogle do not personally validate the integrity of these instances, and popular public instances are more likely to be rate-limited or blocked.* *Note: Use public instances at your own discretion. The maintainers of Whoogle are only responsible for https://whoogle.fossho.st, and do not personally validate the integrity of any other instances. Popular public instances are more likely to be rate-limited or blocked.*
| Website | Country | Language | Cloudflare | | Website | Country | Language | Cloudflare |
|-|-|-|-| |-|-|-|-|
| [https://whoogle.fossho.st](https://whoogle.fossho.st) | 🇺🇸 US | Multi-choice | |
| [https://search.albony.xyz](https://search.albony.xyz/) | 🇮🇳 IN | Multi-choice | |
| [https://whoogle.sdf.org](https://whoogle.sdf.org) | 🇺🇸 US | Multi-choice | | [https://whoogle.sdf.org](https://whoogle.sdf.org) | 🇺🇸 US | Multi-choice |
| [https://whoogle.kavin.rocks](https://whoogle.kavin.rocks) | 🇮🇳 IN | Unknown | ✅ |
| [https://search.garudalinux.org](https://search.garudalinux.org) | 🇩🇪 DE | Multi-choice | | | [https://search.garudalinux.org](https://search.garudalinux.org) | 🇩🇪 DE | Multi-choice | |
| [https://whooglesearch.net](https://whooglesearch.net) | 🇩🇪 DE | Spanish | | | [https://whooglesearch.net](https://whooglesearch.net) | 🇩🇪 DE | Spanish | |
| [https://search.flawcra.cc](https://search.flawcra.cc) |🇩🇪 DE | Unknown | ✅ |
| [https://search.exonip.de](https://search.exonip.de) | 🇳🇱 NL | Multi-choice | | | [https://search.exonip.de](https://search.exonip.de) | 🇳🇱 NL | Multi-choice | |
| [https://s.alefvanoon.xyz](https://s.alefvanoon.xyz) | 🇺🇸 US | English | ✅ | | [https://s.alefvanoon.xyz](https://s.alefvanoon.xyz) | 🇺🇸 US | Multi-choice | ✅ |
| [https://search.flux.industries](https://search.flux.industries) | 🇩🇪 DE | German | ✅ | | [https://www.whooglesearch.ml](https://www.whooglesearch.ml) | 🇺🇸 US | English | |
| [http://whoogledq5f5wly5p4i2ohnvjwlihnlg4oajjum2oeddfwqdwupbuhqd.onion](http://whoogledq5f5wly5p4i2ohnvjwlihnlg4oajjum2oeddfwqdwupbuhqd.onion) | 🇮🇳 IN | Unknown | | | [https://search.sethforprivacy.com](https://search.sethforprivacy.com) | 🇩🇪 DE | English | |
| [https://whoogle.dcs0.hu](https://whoogle.dcs0.hu) | 🇭🇺 HU | Multi-choice | ✅ |
* A checkmark in the "Cloudflare" category here refers to the use of the reverse proxy, [Cloudflare](https://cloudflare). 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. * A checkmark in the "Cloudflare" category here refers to the use of the reverse proxy, [Cloudflare](https://cloudflare.com). The checkmark will not be listed for a site which uses Cloudflare DNS but rather the proxying service which grants Cloudflare the ability to monitor traffic to the website.
#### Onion Instances
| Website | Country | Language |
|-|-|-|
| [http://whoglqjdkgt2an4tdepberwqz3hk7tjo4kqgdnuj77rt7nshw2xqhqad.onion](http://whoglqjdkgt2an4tdepberwqz3hk7tjo4kqgdnuj77rt7nshw2xqhqad.onion) | 🇺🇸 US | Multi-choice
| [http://nuifgsnbb2mcyza74o7illtqmuaqbwu4flam3cdmsrnudwcmkqur37qd.onion](http://nuifgsnbb2mcyza74o7illtqmuaqbwu4flam3cdmsrnudwcmkqur37qd.onion) | 🇩🇪 DE | English
## Screenshots ## Screenshots
#### Desktop #### Desktop
![Whoogle Desktop](docs/screenshot_desktop.jpg) ![Whoogle Desktop](docs/screenshot_desktop.png)
#### Mobile #### Mobile
![Whoogle Mobile](docs/screenshot_mobile.jpg) ![Whoogle Mobile](docs/screenshot_mobile.png)

View File

@ -47,22 +47,27 @@
}, },
"WHOOGLE_ALT_TW": { "WHOOGLE_ALT_TW": {
"description": "The site to use as a replacement for twitter.com when site alternatives are enabled in the config.", "description": "The site to use as a replacement for twitter.com when site alternatives are enabled in the config.",
"value": "nitter.net", "value": "farside.link/nitter",
"required": false "required": false
}, },
"WHOOGLE_ALT_YT": { "WHOOGLE_ALT_YT": {
"description": "The site to use as a replacement for youtube.com when site alternatives are enabled in the config.", "description": "The site to use as a replacement for youtube.com when site alternatives are enabled in the config.",
"value": "invidious.snopyta.org", "value": "farside.link/invidious",
"required": false "required": false
}, },
"WHOOGLE_ALT_IG": { "WHOOGLE_ALT_IG": {
"description": "The site to use as a replacement for instagram.com when site alternatives are enabled in the config.", "description": "The site to use as a replacement for instagram.com when site alternatives are enabled in the config.",
"value": "bibliogram.art/u", "value": "farside.link/bibliogram/u",
"required": false "required": false
}, },
"WHOOGLE_ALT_RD": { "WHOOGLE_ALT_RD": {
"description": "The site to use as a replacement for reddit.com when site alternatives are enabled in the config.", "description": "The site to use as a replacement for reddit.com when site alternatives are enabled in the config.",
"value": "libredd.it", "value": "farside.link/libreddit",
"required": false
},
"WHOOGLE_ALT_MD": {
"description": "The site to use as a replacement for medium.com when site alternatives are enabled in the config.",
"value": "farside.link/scribe",
"required": false "required": false
}, },
"WHOOGLE_ALT_TL": { "WHOOGLE_ALT_TL": {
@ -70,9 +75,14 @@
"value": "lingva.ml", "value": "lingva.ml",
"required": false "required": false
}, },
"WHOOGLE_ALT_MD": { "WHOOGLE_ALT_IMG": {
"description": "The site to use as a replacement for medium.com when site alternatives are enabled in the config.", "description": "The site to use as a replacement for imgur.com when site alternatives are enabled in the config.",
"value": "scribe.rip", "value": "imgin.voidnet.tech",
"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": "wikiless.org",
"required": false "required": false
}, },
"WHOOGLE_MINIMAL": { "WHOOGLE_MINIMAL": {

View File

@ -15,16 +15,21 @@ app = Flask(__name__, static_folder=os.path.dirname(
os.path.abspath(__file__)) + '/static') os.path.abspath(__file__)) + '/static')
# Load .env file if enabled # Load .env file if enabled
if os.getenv("WHOOGLE_DOTENV", ''): if os.getenv('WHOOGLE_DOTENV', ''):
dotenv_path = '../whoogle.env' dotenv_path = '../whoogle.env'
load_dotenv(os.path.join(os.path.dirname(os.path.abspath(__file__)), load_dotenv(os.path.join(os.path.dirname(os.path.abspath(__file__)),
dotenv_path)) dotenv_path))
app.default_key = generate_user_key() app.default_key = generate_user_key()
app.no_cookie_ips = []
app.config['SECRET_KEY'] = os.urandom(32) app.config['SECRET_KEY'] = os.urandom(32)
app.config['SESSION_TYPE'] = 'filesystem' app.config['SESSION_TYPE'] = 'filesystem'
app.config['VERSION_NUMBER'] = '0.6.0' app.config['SESSION_COOKIE_SAMESITE'] = 'strict'
if os.getenv('HTTPS_ONLY'):
app.config['SESSION_COOKIE_NAME'] = '__Secure-session'
app.config['SESSION_COOKIE_SECURE'] = True
app.config['VERSION_NUMBER'] = '0.7.0'
app.config['APP_ROOT'] = os.getenv( app.config['APP_ROOT'] = os.getenv(
'APP_ROOT', 'APP_ROOT',
os.path.dirname(os.path.abspath(__file__))) os.path.dirname(os.path.abspath(__file__)))
@ -38,9 +43,11 @@ app.config['LANGUAGES'] = json.load(open(
os.path.join(app.config['STATIC_FOLDER'], 'settings/languages.json'), os.path.join(app.config['STATIC_FOLDER'], 'settings/languages.json'),
encoding='utf-8')) encoding='utf-8'))
app.config['COUNTRIES'] = json.load(open( app.config['COUNTRIES'] = json.load(open(
os.path.join(app.config['STATIC_FOLDER'], 'settings/countries.json'))) os.path.join(app.config['STATIC_FOLDER'], 'settings/countries.json'),
encoding='utf-8'))
app.config['TRANSLATIONS'] = json.load(open( app.config['TRANSLATIONS'] = json.load(open(
os.path.join(app.config['STATIC_FOLDER'], 'settings/translations.json'))) os.path.join(app.config['STATIC_FOLDER'], 'settings/translations.json'),
encoding='utf-8'))
app.config['THEMES'] = json.load(open( app.config['THEMES'] = json.load(open(
os.path.join(app.config['STATIC_FOLDER'], 'settings/themes.json'))) os.path.join(app.config['STATIC_FOLDER'], 'settings/themes.json')))
app.config['HEADER_TABS'] = json.load(open( app.config['HEADER_TABS'] = json.load(open(
@ -61,6 +68,8 @@ app.config['BANG_PATH'] = os.getenv(
app.config['BANG_FILE'] = os.path.join( app.config['BANG_FILE'] = os.path.join(
app.config['BANG_PATH'], app.config['BANG_PATH'],
'bangs.json') 'bangs.json')
app.config['RELEASES_URL'] = 'https://github.com/' \
'benbusby/whoogle-search/releases'
# The alternative to Google Translate is treated a bit differently than other # The alternative to Google Translate is treated a bit differently than other
# social media site alternatives, in that it is used for any translation # social media site alternatives, in that it is used for any translation

View File

@ -1,3 +1,5 @@
from app.models.config import Config
from app.models.endpoint import Endpoint
from app.request import VALID_PARAMS, MAPS_URL from app.request import VALID_PARAMS, MAPS_URL
from app.utils.misc import read_config_bool from app.utils.misc import read_config_bool
from app.utils.results import * from app.utils.results import *
@ -44,18 +46,8 @@ class Filter:
# type result (such as "people also asked", "related searches", etc) # type result (such as "people also asked", "related searches", etc)
RESULT_CHILD_LIMIT = 7 RESULT_CHILD_LIMIT = 7
def __init__(self, user_key: str, mobile=False, config=None) -> None: def __init__(self, user_key: str, config: Config, mobile=False) -> None:
if config is None: self.config = config
config = {}
self.near = config['near'] if 'near' in config else ''
self.dark = config['dark'] if 'dark' in config else False
self.nojs = config['nojs'] if 'nojs' in config else False
self.new_tab = config['new_tab'] if 'new_tab' in config else False
self.alt_redirect = config['alts'] if 'alts' in config else False
self.block_title = (
config['block_title'] if 'block_title' in config else '')
self.block_url = (
config['block_url'] if 'block_url' in config else '')
self.mobile = mobile self.mobile = mobile
self.user_key = user_key self.user_key = user_key
self.main_divs = ResultSet('') self.main_divs = ResultSet('')
@ -68,16 +60,6 @@ class Filter:
def elements(self): def elements(self):
return self._elements return self._elements
def reskin(self, page: str) -> str:
# Aesthetic only re-skinning
if self.dark:
page = page.replace(
'fff', '000').replace(
'202124', 'ddd').replace(
'1967D2', '3b85ea')
return page
def encrypt_path(self, path, is_element=False) -> str: def encrypt_path(self, path, is_element=False) -> str:
# Encrypts path to avoid plaintext results in logs # Encrypts path to avoid plaintext results in logs
if is_element: if is_element:
@ -109,7 +91,7 @@ class Filter:
input_form = soup.find('form') input_form = soup.find('form')
if input_form is not None: if input_form is not None:
input_form['method'] = 'POST' input_form['method'] = 'GET' if self.config.get_only else 'POST'
# Ensure no extra scripts passed through # Ensure no extra scripts passed through
for script in soup('script'): for script in soup('script'):
@ -143,9 +125,7 @@ class Filter:
_ = div.decompose() if len(div_ads) else None _ = div.decompose() if len(div_ads) else None
def remove_block_titles(self) -> None: def remove_block_titles(self) -> None:
if not self.main_divs: if not self.main_divs or not self.config.block_title:
return
if self.block_title == '':
return return
block_title = re.compile(self.block_title) block_title = re.compile(self.block_title)
for div in [_ for _ in self.main_divs.find_all('div', recursive=True)]: for div in [_ for _ in self.main_divs.find_all('div', recursive=True)]:
@ -154,9 +134,7 @@ class Filter:
_ = div.decompose() if len(block_divs) else None _ = div.decompose() if len(block_divs) else None
def remove_block_url(self) -> None: def remove_block_url(self) -> None:
if not self.main_divs: if not self.main_divs or not self.config.block_url:
return
if self.block_url == '':
return return
block_url = re.compile(self.block_url) block_url = re.compile(self.block_url)
for div in [_ for _ in self.main_divs.find_all('div', recursive=True)]: for div in [_ for _ in self.main_divs.find_all('div', recursive=True)]:
@ -211,10 +189,17 @@ class Filter:
# Find and decompose the first element with an inner HTML text val. # Find and decompose the first element with an inner HTML text val.
# This typically extracts the title of the section (i.e. "Related # This typically extracts the title of the section (i.e. "Related
# Searches", "People also ask", etc) # Searches", "People also ask", etc)
# If there are more than one child tags with text
# parenthesize the rest except the first
label = 'Collapsed Results' label = 'Collapsed Results'
subtitle = None
for elem in result_children: for elem in result_children:
if elem.text: if elem.text:
label = elem.text content = list(elem.strings)
label = content[0]
if len(content) > 1:
subtitle = '<span> (' + \
''.join(content[1:]) + ')</span>'
elem.decompose() elem.decompose()
break break
@ -229,6 +214,11 @@ class Filter:
details = BeautifulSoup(features='html.parser').new_tag('details') details = BeautifulSoup(features='html.parser').new_tag('details')
summary = BeautifulSoup(features='html.parser').new_tag('summary') summary = BeautifulSoup(features='html.parser').new_tag('summary')
summary.string = label summary.string = label
if subtitle:
soup = BeautifulSoup(subtitle, 'html.parser')
summary.append(soup)
details.append(summary) details.append(summary)
if parent and not minimal_mode: if parent and not minimal_mode:
@ -254,14 +244,14 @@ class Filter:
if src.startswith(LOGO_URL): if src.startswith(LOGO_URL):
# Re-brand with Whoogle logo # Re-brand with Whoogle logo
element.replace_with(BeautifulSoup( element.replace_with(BeautifulSoup(
render_template('logo.html', dark=self.dark), render_template('logo.html'),
features='html.parser')) features='html.parser'))
return return
elif src.startswith(GOOG_IMG) or GOOG_STATIC in src: elif src.startswith(GOOG_IMG) or GOOG_STATIC in src:
element['src'] = BLANK_B64 element['src'] = BLANK_B64
return return
element['src'] = 'element?url=' + self.encrypt_path( element['src'] = f'{Endpoint.element}?url=' + self.encrypt_path(
src, src,
is_element=True) + '&type=' + urlparse.quote(mime) is_element=True) + '&type=' + urlparse.quote(mime)
@ -353,10 +343,10 @@ class Filter:
link['href'] = filter_link_args(q) link['href'] = filter_link_args(q)
# Add no-js option # Add no-js option
if self.nojs: if self.config.nojs:
append_nojs(link) append_nojs(link)
if self.new_tab: if self.config.new_tab:
link['target'] = '_blank' link['target'] = '_blank'
else: else:
if href.startswith(MAPS_URL): if href.startswith(MAPS_URL):
@ -366,7 +356,7 @@ class Filter:
link['href'] = href link['href'] = href
# Replace link location if "alts" config is enabled # Replace link location if "alts" config is enabled
if self.alt_redirect: if self.config.alts:
# Search and replace all link descriptions # Search and replace all link descriptions
# with alternative location # with alternative location
link['href'] = get_site_alt(link['href']) link['href'] = get_site_alt(link['href'])
@ -409,7 +399,12 @@ class Filter:
for item in results_all: for item in results_all:
urls = item.find('a')['href'].split('&imgrefurl=') urls = item.find('a')['href'].split('&imgrefurl=')
img_url = urlparse.unquote(urls[0].replace('/imgres?imgurl=', '')) # Skip urls that are not two-element lists
if len(urls) != 2:
continue
img_url = urlparse.unquote(urls[0].replace(
f'/{Endpoint.imgres}?imgurl=', ''))
try: try:
# Try to strip out only the necessary part of the web page link # Try to strip out only the necessary part of the web page link

View File

@ -17,8 +17,8 @@ class Config:
self.block = os.getenv('WHOOGLE_CONFIG_BLOCK', '') self.block = os.getenv('WHOOGLE_CONFIG_BLOCK', '')
self.block_title = os.getenv('WHOOGLE_CONFIG_BLOCK_TITLE', '') self.block_title = os.getenv('WHOOGLE_CONFIG_BLOCK_TITLE', '')
self.block_url = os.getenv('WHOOGLE_CONFIG_BLOCK_URL', '') self.block_url = os.getenv('WHOOGLE_CONFIG_BLOCK_URL', '')
self.ctry = os.getenv('WHOOGLE_CONFIG_COUNTRY', '') self.country = os.getenv('WHOOGLE_CONFIG_COUNTRY', '')
self.theme = os.getenv('WHOOGLE_CONFIG_THEME', '') self.theme = os.getenv('WHOOGLE_CONFIG_THEME', 'system')
self.safe = read_config_bool('WHOOGLE_CONFIG_SAFE') self.safe = read_config_bool('WHOOGLE_CONFIG_SAFE')
self.dark = read_config_bool('WHOOGLE_CONFIG_DARK') # deprecated self.dark = read_config_bool('WHOOGLE_CONFIG_DARK') # deprecated
self.alts = read_config_bool('WHOOGLE_CONFIG_ALTS') self.alts = read_config_bool('WHOOGLE_CONFIG_ALTS')
@ -33,9 +33,13 @@ class Config:
self.safe_keys = [ self.safe_keys = [
'lang_search', 'lang_search',
'lang_interface', 'lang_interface',
'ctry', 'country',
'dark', 'theme',
'theme' 'alts',
'new_tab',
'view_image',
'block',
'safe'
] ]
# Skip setting custom config if there isn't one # Skip setting custom config if there isn't one
@ -105,5 +109,26 @@ class Config:
for param_key in params.keys(): for param_key in params.keys():
if not self.is_safe_key(param_key): if not self.is_safe_key(param_key):
continue continue
self[param_key] = params.get(param_key) param_val = params.get(param_key)
if param_val == 'off':
param_val = False
elif param_val.isdigit():
param_val = int(param_val)
self[param_key] = param_val
return self return self
def to_params(self) -> str:
"""Generates a set of safe params for using in Whoogle URLs
Returns:
str -- a set of URL parameters
"""
param_str = ''
for safe_key in self.safe_keys:
if not self[safe_key]:
continue
param_str = param_str + f'&{safe_key}={self[safe_key]}'
return param_str

23
app/models/endpoint.py Normal file
View File

@ -0,0 +1,23 @@
from enum import Enum
class Endpoint(Enum):
autocomplete = 'autocomplete'
home = 'home'
healthz = 'healthz'
session = 'session'
config = 'config'
opensearch = 'opensearch.xml'
search = 'search'
search_html = 'search.html'
url = 'url'
imgres = 'imgres'
element = 'element'
window = 'window'
def __str__(self):
return self.value
def in_path(self, path: str) -> bool:
return path.startswith(self.value) or \
path.startswith(f'/{self.value}')

View File

@ -59,7 +59,7 @@ def gen_user_agent(is_mobile) -> str:
return DESKTOP_UA.format("Mozilla", linux, firefox) return DESKTOP_UA.format("Mozilla", linux, firefox)
def gen_query(query, args, config, near_city=None) -> str: def gen_query(query, args, config) -> str:
param_dict = {key: '' for key in VALID_PARAMS} param_dict = {key: '' for key in VALID_PARAMS}
# Use :past(hour/day/week/month/year) if available # Use :past(hour/day/week/month/year) if available
@ -96,8 +96,8 @@ def gen_query(query, args, config, near_city=None) -> str:
param_dict['start'] = '&start=' + args.get('start') param_dict['start'] = '&start=' + args.get('start')
# Search for results near a particular city, if available # Search for results near a particular city, if available
if near_city: if config.near:
param_dict['near'] = '&near=' + urlparse.quote(near_city) param_dict['near'] = '&near=' + urlparse.quote(config.near)
# Set language for results (lr) if source isn't set, otherwise use the # Set language for results (lr) if source isn't set, otherwise use the
# result language param provided in the results # result language param provided in the results
@ -107,19 +107,25 @@ def gen_query(query, args, config, near_city=None) -> str:
[_ for _ in lang if not _.isdigit()] [_ for _ in lang if not _.isdigit()]
)) if lang else '' )) if lang else ''
else: else:
param_dict['lr'] = '&lr=' + ( param_dict['lr'] = (
config.lang_search if config.lang_search else '' '&lr=' + config.lang_search
) ) if config.lang_search else ''
# 'nfpr' defines the exclusion of results from an auto-corrected query # 'nfpr' defines the exclusion of results from an auto-corrected query
if 'nfpr' in args: if 'nfpr' in args:
param_dict['nfpr'] = '&nfpr=' + args.get('nfpr') param_dict['nfpr'] = '&nfpr=' + args.get('nfpr')
param_dict['cr'] = ('&cr=' + config.ctry) if config.ctry else '' # 'chips' is used in image tabs to pass the optional 'filter' to add to the
param_dict['hl'] = '&hl=' + ( # given search term
config.lang_interface.replace('lang_', '') if 'chips' in args:
if config.lang_interface else '' param_dict['chips'] = '&chips=' + args.get('chips')
)
param_dict['gl'] = (
'&gl=' + config.country
) if config.country else ''
param_dict['hl'] = (
'&hl=' + config.lang_interface.replace('lang_', '')
) if config.lang_interface else ''
param_dict['safe'] = '&safe=' + ('active' if config.safe else 'off') param_dict['safe'] = '&safe=' + ('active' if config.safe else 'off')
# Block all sites specified in the user config # Block all sites specified in the user config
@ -213,16 +219,23 @@ class Request:
list: The list of matches for possible search suggestions list: The list of matches for possible search suggestions
""" """
ac_query = dict(hl=self.language, q=query) ac_query = dict(q=query)
if self.language:
ac_query['hl'] = self.language
response = self.send(base_url=AUTOCOMPLETE_URL, response = self.send(base_url=AUTOCOMPLETE_URL,
query=urlparse.urlencode(ac_query)).text query=urlparse.urlencode(ac_query)).text
if not response: if not response:
return [] return []
try:
root = ET.fromstring(response) root = ET.fromstring(response)
return [_.attrib['data'] for _ in return [_.attrib['data'] for _ in
root.findall('.//suggestion/[@data]')] root.findall('.//suggestion/[@data]')]
except ET.ParseError:
# Malformed XML response
return []
def send(self, base_url='', query='', attempt=0, def send(self, base_url='', query='', attempt=0,
force_mobile=False) -> Response: force_mobile=False) -> Response:
@ -274,14 +287,19 @@ class Request:
# Make sure that the tor connection is valid, if enabled # Make sure that the tor connection is valid, if enabled
if self.tor: if self.tor:
try:
tor_check = requests.get('https://check.torproject.org/', tor_check = requests.get('https://check.torproject.org/',
proxies=self.proxies, headers=headers) proxies=self.proxies, headers=headers)
self.tor_valid = 'Congratulations' in tor_check.text self.tor_valid = 'Congratulations' in tor_check.text
if not self.tor_valid: if not self.tor_valid:
raise TorError( raise TorError(
"Tor connection succeeded, but the connection could not " "Tor connection succeeded, but the connection could "
"be validated by torproject.org", "not be validated by torproject.org",
disable=True)
except ConnectionError:
raise TorError(
"Error raised during Tor connection validation",
disable=True) disable=True)
response = requests.get( response = requests.get(

View File

@ -1,30 +1,45 @@
import argparse import argparse
import base64 import base64
import html
import io import io
import json import json
import pickle import pickle
import urllib.parse as urlparse import urllib.parse as urlparse
import uuid import uuid
from datetime import timedelta
from functools import wraps from functools import wraps
import waitress import waitress
from app import app from app import app
from app.models.config import Config from app.models.config import Config
from app.models.endpoint import Endpoint
from app.request import Request, TorError from app.request import Request, TorError
from app.utils.bangs import resolve_bang from app.utils.bangs import resolve_bang
from app.utils.misc import read_config_bool, get_client_ip from app.utils.misc import read_config_bool, get_client_ip, get_request_url
from app.utils.results import add_ip_card, bold_search_terms, get_tabs_content from app.utils.results import add_ip_card, bold_search_terms,\
add_currency_card, check_currency, get_tabs_content
from app.utils.search import * from app.utils.search import *
from app.utils.session import generate_user_key, valid_user_session from app.utils.session import generate_user_key, valid_user_session
from bs4 import BeautifulSoup as bsoup from bs4 import BeautifulSoup as bsoup
from flask import jsonify, make_response, request, redirect, render_template, \ from flask import jsonify, make_response, request, redirect, render_template, \
send_file, session, url_for send_file, session, url_for
from requests import exceptions from requests import exceptions, get
from requests.models import PreparedRequest
# Load DDG bang json files only on init # Load DDG bang json files only on init
bang_json = json.load(open(app.config['BANG_FILE'])) bang_json = json.load(open(app.config['BANG_FILE']))
# Check the newest version of WHOOGLE
update = bsoup(get(app.config['RELEASES_URL']).text, 'html.parser')
newest_version = update.select_one('[class="Link--primary"]').string[1:]
current_version = int(''.join(filter(str.isdigit,
app.config['VERSION_NUMBER'])))
newest_version = int(''.join(filter(str.isdigit, newest_version)))
newest_version = '' if current_version >= newest_version \
else newest_version
ac_var = 'WHOOGLE_AUTOCOMPLETE'
autocomplete_enabled = os.getenv(ac_var, '1')
def auth_required(f): def auth_required(f):
@wraps(f) @wraps(f)
@ -46,40 +61,91 @@ def auth_required(f):
return decorated return decorated
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:
session.pop('_permanent', None)
g.session_key = app.default_key
# Clear out old sessions
invalid_sessions = []
for user_session in os.listdir(app.config['SESSION_FILE_DIR']):
session_path = os.path.join(
app.config['SESSION_FILE_DIR'],
user_session)
try:
with open(session_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):
pass
for invalid_session in invalid_sessions:
try:
os.remove(invalid_session)
except FileNotFoundError:
# Don't throw error if the invalid session has been removed
pass
return f(*args, **kwargs)
return decorated
@app.before_request @app.before_request
def before_request_func(): def before_request_func():
g.request_params = ( g.request_params = (
request.args if request.method == 'GET' else request.form request.args if request.method == 'GET' else request.form
) )
g.cookies_disabled = False
# 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 # Generate session values for user if unavailable
if not valid_user_session(session): if (not valid_user_session(session) and
session['config'] = json.load(open(app.config['DEFAULT_CONFIG'])) \ 'cookies_disabled' not in request.args):
if os.path.exists(app.config['DEFAULT_CONFIG']) else {} session['config'] = default_config
session['uuid'] = str(uuid.uuid4()) session['uuid'] = str(uuid.uuid4())
session['key'] = generate_user_key(True) session['key'] = generate_user_key()
# Flag cookies as possibly disabled in order to prevent against
# unnecessary session directory expansion
g.cookies_disabled = True
# Handle https upgrade
if needs_https(request.url):
return redirect(
request.url.replace('http://', 'https://', 1),
code=308)
# 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']) 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)
if not g.user_config.url: if not g.user_config.url:
g.user_config.url = request.url_root.replace( g.user_config.url = get_request_url(request.url_root)
'http://',
'https://') if os.getenv('HTTPS_ONLY', False) else request.url_root
g.user_request = Request( g.user_request = Request(
request.headers.get('User-Agent'), request.headers.get('User-Agent'),
request.url_root, get_request_url(request.url_root),
config=g.user_config) config=g.user_config)
g.app_location = g.user_config.url g.app_location = g.user_config.url
@ -87,22 +153,14 @@ def before_request_func():
@app.after_request @app.after_request
def after_request_func(resp): def after_request_func(resp):
# Check if address consistently has cookies blocked, resp.headers['X-Content-Type-Options'] = 'nosniff'
# in which case start removing session files after creation. resp.headers['X-Frame-Options'] = 'DENY'
#
# Note: This is primarily done to prevent overpopulation of session
# directories, since browsers that block cookies will still trigger
# Flask's session creation routine with every request.
if g.cookies_disabled and request.remote_addr not in app.no_cookie_ips:
app.no_cookie_ips.append(request.remote_addr)
elif g.cookies_disabled and request.remote_addr in app.no_cookie_ips:
session_list = list(session.keys())
for key in session_list:
session.pop(key)
if os.getenv('WHOOGLE_CSP', False):
resp.headers['Content-Security-Policy'] = app.config['CSP'] resp.headers['Content-Security-Policy'] = app.config['CSP']
if os.environ.get('HTTPS_ONLY', False): if os.environ.get('HTTPS_ONLY', False):
resp.headers['Content-Security-Policy'] += 'upgrade-insecure-requests' resp.headers['Content-Security-Policy'] += \
'upgrade-insecure-requests'
return resp return resp
@ -113,22 +171,28 @@ def unknown_page(e):
return redirect(g.app_location) return redirect(g.app_location)
@app.route('/healthz', methods=['GET']) @app.route(f'/{Endpoint.healthz}', methods=['GET'])
def healthz(): def healthz():
return '' return ''
@app.route('/home', methods=['GET']) @app.route(f'/{Endpoint.session}/<session_id>', methods=['GET', 'PUT', 'POST'])
def home(): def session_check(session_id):
return redirect(url_for('.index')) 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('/', methods=['GET'])
@app.route(f'/{Endpoint.home}', methods=['GET'])
@auth_required @auth_required
def index(): def index():
# Reset keys
session['key'] = generate_user_key(g.cookies_disabled)
# Redirect if an error was raised # Redirect if an error was raised
if 'error_message' in session and session['error_message']: if 'error_message' in session and session['error_message']:
error_message = session['error_message'] error_message = session['error_message']
@ -136,22 +200,27 @@ def index():
return render_template('error.html', error_message=error_message) return render_template('error.html', error_message=error_message)
return render_template('index.html', return render_template('index.html',
newest_version=newest_version,
languages=app.config['LANGUAGES'], languages=app.config['LANGUAGES'],
countries=app.config['COUNTRIES'], countries=app.config['COUNTRIES'],
themes=app.config['THEMES'], themes=app.config['THEMES'],
autocomplete_enabled=autocomplete_enabled,
translation=app.config['TRANSLATIONS'][ translation=app.config['TRANSLATIONS'][
g.user_config.get_localization_lang() g.user_config.get_localization_lang()
], ],
logo=render_template( logo=render_template(
'logo.html', 'logo.html',
dark=g.user_config.dark), dark=g.user_config.dark),
config_disabled=app.config['CONFIG_DISABLE'], config_disabled=(
app.config['CONFIG_DISABLE'] or
not valid_user_session(session) or
'cookies_disabled' in request.args),
config=g.user_config, config=g.user_config,
tor_available=int(os.environ.get('TOR_AVAILABLE')), tor_available=int(os.environ.get('TOR_AVAILABLE')),
version_number=app.config['VERSION_NUMBER']) version_number=app.config['VERSION_NUMBER'])
@app.route('/opensearch.xml', methods=['GET']) @app.route(f'/{Endpoint.opensearch}', methods=['GET'])
def opensearch(): def opensearch():
opensearch_url = g.app_location opensearch_url = g.app_location
if opensearch_url.endswith('/'): if opensearch_url.endswith('/'):
@ -171,7 +240,7 @@ def opensearch():
), 200, {'Content-Disposition': 'attachment; filename="opensearch.xml"'} ), 200, {'Content-Disposition': 'attachment; filename="opensearch.xml"'}
@app.route('/search.html', methods=['GET']) @app.route(f'/{Endpoint.search_html}', methods=['GET'])
def search_html(): def search_html():
search_url = g.app_location search_url = g.app_location
if search_url.endswith('/'): if search_url.endswith('/'):
@ -179,9 +248,8 @@ def search_html():
return render_template('search.html', url=search_url) return render_template('search.html', url=search_url)
@app.route('/autocomplete', methods=['GET', 'POST']) @app.route(f'/{Endpoint.autocomplete}', methods=['GET', 'POST'])
def autocomplete(): def autocomplete():
ac_var = 'WHOOGLE_AUTOCOMPLETE'
if os.getenv(ac_var) and not read_config_bool(ac_var): if os.getenv(ac_var) and not read_config_bool(ac_var):
return jsonify({}) return jsonify({})
@ -212,14 +280,14 @@ def autocomplete():
]) ])
@app.route('/search', methods=['GET', 'POST']) @app.route(f'/{Endpoint.search}', methods=['GET', 'POST'])
@session_required
@auth_required @auth_required
def search(): def search():
# Update user config if specified in search args # Update user config if specified in search args
g.user_config = g.user_config.from_params(g.request_params) g.user_config = g.user_config.from_params(g.request_params)
search_util = Search(request, g.user_config, session, search_util = Search(request, g.user_config, g.session_key)
cookies_disabled=g.cookies_disabled)
query = search_util.new_search_query() query = search_util.new_search_query()
bang = resolve_bang(query=query, bangs_dict=bang_json) bang = resolve_bang(query=query, bangs_dict=bang_json)
@ -228,7 +296,7 @@ def search():
# Redirect to home if invalid/blank search # Redirect to home if invalid/blank search
if not query: if not query:
return redirect('/') return redirect(url_for('.index'))
# Generate response and number of external elements from the page # Generate response and number of external elements from the page
try: try:
@ -250,7 +318,16 @@ def search():
translate_to = localization_lang.replace('lang_', '') translate_to = localization_lang.replace('lang_', '')
# Return 503 if temporarily blocked by captcha # Return 503 if temporarily blocked by captcha
resp_code = 503 if has_captcha(str(response)) else 200 if has_captcha(str(response)):
return render_template(
'error.html',
blocked=True,
error_message=translation['ratelimit'],
translation=translation,
farside='https://farside.link',
config=g.user_config,
query=urlparse.unquote(query),
params=g.user_config.to_params()), 503
response = bold_search_terms(response, query) response = bold_search_terms(response, query)
# Feature to display IP address # Feature to display IP address
@ -264,11 +341,19 @@ def search():
search_util.search_type, search_util.search_type,
translation) translation)
# Feature to display currency_card
conversion = check_currency(str(response))
if conversion:
html_soup = bsoup(str(response), 'html.parser')
response = add_currency_card(html_soup, conversion)
return render_template( return render_template(
'display.html', 'display.html',
newest_version=newest_version,
query=urlparse.unquote(query), query=urlparse.unquote(query),
search_type=search_util.search_type, search_type=search_util.search_type,
config=g.user_config, config=g.user_config,
autocomplete_enabled=autocomplete_enabled,
lingva_url=app.config['TRANSLATE_URL'], lingva_url=app.config['TRANSLATE_URL'],
translation=translation, translation=translation,
translate_to=translate_to, translate_to=translate_to,
@ -292,10 +377,13 @@ def search():
tabs=tabs)), resp_code tabs=tabs)), resp_code
@app.route('/config', methods=['GET', 'POST', 'PUT']) @app.route(f'/{Endpoint.config}', methods=['GET', 'POST', 'PUT'])
@session_required
@auth_required @auth_required
def config(): def config():
config_disabled = app.config['CONFIG_DISABLE'] config_disabled = (
app.config['CONFIG_DISABLE'] or
not valid_user_session(session))
if request.method == 'GET': if request.method == 'GET':
return json.dumps(g.user_config.__dict__) return json.dumps(g.user_config.__dict__)
elif request.method == 'PUT' and not config_disabled: elif request.method == 'PUT' and not config_disabled:
@ -322,18 +410,14 @@ def config():
app.config['CONFIG_PATH'], app.config['CONFIG_PATH'],
request.args.get('name')), 'wb')) request.args.get('name')), 'wb'))
# Overwrite default config if user has cookies disabled
if g.cookies_disabled:
open(app.config['DEFAULT_CONFIG'], 'w').write(
json.dumps(config_data, indent=4))
session['config'] = config_data session['config'] = config_data
return redirect(config_data['url']) return redirect(config_data['url'])
else: else:
return redirect(url_for('.index'), code=403) return redirect(url_for('.index'), code=403)
@app.route('/url', methods=['GET']) @app.route(f'/{Endpoint.url}', methods=['GET'])
@session_required
@auth_required @auth_required
def url(): def url():
if 'url' in request.args: if 'url' in request.args:
@ -348,16 +432,18 @@ def url():
error_message='Unable to resolve query: ' + q) error_message='Unable to resolve query: ' + q)
@app.route('/imgres') @app.route(f'/{Endpoint.imgres}')
@session_required
@auth_required @auth_required
def imgres(): def imgres():
return redirect(request.args.get('imgurl')) return redirect(request.args.get('imgurl'))
@app.route('/element') @app.route(f'/{Endpoint.element}')
@session_required
@auth_required @auth_required
def element(): def element():
cipher_suite = Fernet(session['key']) cipher_suite = Fernet(g.session_key)
src_url = cipher_suite.decrypt(request.args.get('url').encode()).decode() src_url = cipher_suite.decrypt(request.args.get('url').encode()).decode()
src_type = request.args.get('type') src_type = request.args.get('type')
@ -376,7 +462,7 @@ def element():
return send_file(io.BytesIO(empty_gif), mimetype='image/gif') return send_file(io.BytesIO(empty_gif), mimetype='image/gif')
@app.route('/window') @app.route(f'/{Endpoint.window}')
@auth_required @auth_required
def window(): def window():
get_body = g.user_request.send(base_url=request.args.get('location')).text get_body = g.user_request.send(base_url=request.args.get('location')).text
@ -457,7 +543,8 @@ def run_app() -> None:
os.environ['WHOOGLE_PROXY_TYPE'] = args.proxytype os.environ['WHOOGLE_PROXY_TYPE'] = args.proxytype
os.environ['WHOOGLE_PROXY_LOC'] = args.proxyloc os.environ['WHOOGLE_PROXY_LOC'] = args.proxyloc
os.environ['HTTPS_ONLY'] = '1' if args.https_only else '' if args.https_only:
os.environ['HTTPS_ONLY'] = '1'
if args.debug: if args.debug:
app.run(host=args.host, port=args.port, debug=args.debug) app.run(host=args.host, port=args.port, debug=args.debug)

View File

@ -138,10 +138,14 @@ select {
color: var(--whoogle-dark-contrast-text) !important; color: var(--whoogle-dark-contrast-text) !important;
} }
#gh-link { .link {
color: var(--whoogle-dark-contrast-text); color: var(--whoogle-dark-contrast-text);
} }
.link-color {
color: var(--whoogle-dark-result-url) !important;
}
.autocomplete-items { .autocomplete-items {
border: 1px solid var(--whoogle-dark-element-bg); border: 1px solid var(--whoogle-dark-element-bg);
} }
@ -187,6 +191,10 @@ path {
color: var(--whoogle-dark-text) !important; color: var(--whoogle-dark-text) !important;
} }
.ip-text-div{ .ip-text-div, .update_available, .cb_label, .cb {
color: var(--whoogle-dark-secondary-text) !important; color: var(--whoogle-dark-secondary-text) !important;
} }
.cb:focus {
color: var(--whoogle-dark-contrast-text) !important;
}

9
app/static/css/error.css Normal file
View File

@ -0,0 +1,9 @@
html {
font-size: 1.3rem;
}
@media (max-width: 1000px) {
html {
font-size: 3rem;
}
}

View File

@ -12,3 +12,30 @@
height: 40px; height: 40px;
width: 50px; width: 50px;
} }
.ZINbbc.xpd.O9g5cc.uUPGi input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.cb {
width: 40%;
overflow: hidden;
text-align: left;
line-height: 28px;
background: transparent;
border-radius: 6px;
border: 1px solid #5f6368;
font-size: 14px !important;
height: 36px;
padding: 0 0 0 12px;
margin: 10px 10px 10px 0;
}
.conversion_box {
margin-top: 15px;
}
.ZINbbc.xpd.O9g5cc.uUPGi input:focus-visible {
outline: 0;
}

View File

@ -125,10 +125,14 @@ input {
color: var(--whoogle-contrast-text); color: var(--whoogle-contrast-text);
} }
#gh-link { .link {
color: var(--whoogle-element-bg); color: var(--whoogle-element-bg);
} }
.link-color {
color: var(--whoogle-result-url) !important;
}
.autocomplete-items { .autocomplete-items {
border: 1px solid var(--whoogle-element-bg); border: 1px solid var(--whoogle-element-bg);
} }
@ -175,6 +179,10 @@ path {
border-bottom: 0px; border-bottom: 0px;
} }
.ip-text-div{ .ip-text-div, .update_available, .cb_label, .cb {
color: var(--whoogle-secondary-text) !important; color: var(--whoogle-secondary-text) !important;
} }
.cb:focus {
color: var(--whoogle-text) !important;
}

View File

@ -12,6 +12,7 @@ a {
@media (max-width: 1000px) { @media (max-width: 1000px) {
svg { svg {
margin-top: .7em; margin-top: .3em;
height: 70%;
} }
} }

View File

@ -61,6 +61,15 @@ body {
-webkit-appearance: none; -webkit-appearance: none;
} }
.config-options {
max-height: 370px;
overflow-y: scroll;
}
.config-buttons {
max-height: 30px;
}
.config-div { .config-div {
padding: 5px; padding: 5px;
} }
@ -102,7 +111,6 @@ button::-moz-focus-inner {
} }
.open { .open {
overflow-y: scroll;
padding-bottom: 20px; padding-bottom: 20px;
} }
@ -136,6 +144,7 @@ footer {
.whoogle-svg { .whoogle-svg {
width: 80%; width: 80%;
height: initial;
display: block; display: block;
margin: auto; margin: auto;
padding-bottom: 10px; padding-bottom: 10px;
@ -168,3 +177,10 @@ details summary {
padding: 10px; padding: 10px;
font-weight: bold; font-weight: bold;
} }
/* Mobile styles */
@media (max-width: 1000px) {
select {
width: 100%;
}
}

View File

@ -26,6 +26,10 @@ details summary {
font-weight: bold; font-weight: bold;
} }
details summary span {
font-weight: normal;
}
#lingva-iframe { #lingva-iframe {
width: 100%; width: 100%;
height: 650px; height: 650px;

View File

@ -0,0 +1,9 @@
const convert = (n1, n2, conversionFactor) => {
// id's for currency input boxes
let id1 = "cb" + n1;
let id2 = "cb" + n2;
// getting the value of the input box that just got filled
let inputBox = document.getElementById(id1).value;
// updating the other input box after conversion
document.getElementById(id2).value = ((inputBox * conversionFactor).toFixed(2));
}

View File

@ -1,248 +1,247 @@
[ [
{"name": "-------", "value": ""}, {"name": "-------", "value": ""},
{"name": "Afghanistan", "value": "countryAF"}, {"name": "Afghanistan", "value": "AF"},
{"name": "Albania", "value": "countryAL"}, {"name": "Albania", "value": "AL"},
{"name": "Algeria", "value": "countryDZ"}, {"name": "Algeria", "value": "DZ"},
{"name": "American Samoa", "value": "countryAS"}, {"name": "American Samoa", "value": "AS"},
{"name": "Andorra", "value": "countryAD"}, {"name": "Andorra", "value": "AD"},
{"name": "Angola", "value": "countryAO"}, {"name": "Angola", "value": "AO"},
{"name": "Anguilla", "value": "countryAI"}, {"name": "Anguilla", "value": "AI"},
{"name": "Antarctica", "value": "countryAQ"}, {"name": "Antarctica", "value": "AQ"},
{"name": "Antigua and Barbuda", "value": "countryAG"}, {"name": "Antigua and Barbuda", "value": "AG"},
{"name": "Argentina", "value": "countryAR"}, {"name": "Argentina", "value": "AR"},
{"name": "Armenia", "value": "countryAM"}, {"name": "Armenia", "value": "AM"},
{"name": "Aruba", "value": "countryAW"}, {"name": "Aruba", "value": "AW"},
{"name": "Australia", "value": "countryAU"}, {"name": "Australia", "value": "AU"},
{"name": "Austria", "value": "countryAT"}, {"name": "Austria", "value": "AT"},
{"name": "Azerbaijan", "value": "countryAZ"}, {"name": "Azerbaijan", "value": "AZ"},
{"name": "Bahamas", "value": "countryBS"}, {"name": "Bahamas", "value": "BS"},
{"name": "Bahrain", "value": "countryBH"}, {"name": "Bahrain", "value": "BH"},
{"name": "Bangladesh", "value": "countryBD"}, {"name": "Bangladesh", "value": "BD"},
{"name": "Barbados", "value": "countryBB"}, {"name": "Barbados", "value": "BB"},
{"name": "Belarus", "value": "countryBY"}, {"name": "Belarus", "value": "BY"},
{"name": "Belgium", "value": "countryBE"}, {"name": "Belgium", "value": "BE"},
{"name": "Belize", "value": "countryBZ"}, {"name": "Belize", "value": "BZ"},
{"name": "Benin", "value": "countryBJ"}, {"name": "Benin", "value": "BJ"},
{"name": "Bermuda", "value": "countryBM"}, {"name": "Bermuda", "value": "BM"},
{"name": "Bhutan", "value": "countryBT"}, {"name": "Bhutan", "value": "BT"},
{"name": "Bolivia", "value": "countryBO"}, {"name": "Bolivia", "value": "BO"},
{"name": "Bosnia and Herzegovina", "value": "countryBA"}, {"name": "Bosnia and Herzegovina", "value": "BA"},
{"name": "Botswana", "value": "countryBW"}, {"name": "Botswana", "value": "BW"},
{"name": "Bouvet Island", "value": "countryBV"}, {"name": "Bouvet Island", "value": "BV"},
{"name": "Brazil", "value": "countryBR"}, {"name": "Brazil", "value": "BR"},
{"name": "British Indian Ocean Territory", "value": "countryIO"}, {"name": "British Indian Ocean Territory", "value": "IO"},
{"name": "Brunei Darussalam", "value": "countryBN"}, {"name": "Brunei Darussalam", "value": "BN"},
{"name": "Bulgaria", "value": "countryBG"}, {"name": "Bulgaria", "value": "BG"},
{"name": "Burkina Faso", "value": "countryBF"}, {"name": "Burkina Faso", "value": "BF"},
{"name": "Burundi", "value": "countryBI"}, {"name": "Burundi", "value": "BI"},
{"name": "Cambodia", "value": "countryKH"}, {"name": "Cambodia", "value": "KH"},
{"name": "Cameroon", "value": "countryCM"}, {"name": "Cameroon", "value": "CM"},
{"name": "Canada", "value": "countryCA"}, {"name": "Canada", "value": "CA"},
{"name": "Cape Verde", "value": "countryCV"}, {"name": "Cape Verde", "value": "CV"},
{"name": "Cayman Islands", "value": "countryKY"}, {"name": "Cayman Islands", "value": "KY"},
{"name": "Central African Republic", "value": "countryCF"}, {"name": "Central African Republic", "value": "CF"},
{"name": "Chad", "value": "countryTD"}, {"name": "Chad", "value": "TD"},
{"name": "Chile", "value": "countryCL"}, {"name": "Chile", "value": "CL"},
{"name": "China", "value": "countryCN"}, {"name": "China", "value": "CN"},
{"name": "Christmas Island", "value": "countryCX"}, {"name": "Christmas Island", "value": "CX"},
{"name": "Cocos (Keeling) Islands", "value": "countryCC"}, {"name": "Cocos (Keeling) Islands", "value": "CC"},
{"name": "Colombia", "value": "countryCO"}, {"name": "Colombia", "value": "CO"},
{"name": "Comoros", "value": "countryKM"}, {"name": "Comoros", "value": "KM"},
{"name": "Congo", "value": "countryCG"}, {"name": "Congo", "value": "CG"},
{"name": "Congo, Democratic Republic of the", "value": "countryCD"}, {"name": "Congo, Democratic Republic of the", "value": "CD"},
{"name": "Cook Islands", "value": "countryCK"}, {"name": "Cook Islands", "value": "CK"},
{"name": "Costa Rica", "value": "countryCR"}, {"name": "Costa Rica", "value": "CR"},
{"name": "Cote D\"ivoire", "value": "countryCI"}, {"name": "Cote D'ivoire", "value": "CI"},
{"name": "Croatia (Hrvatska)", "value": "countryHR"}, {"name": "Croatia (Hrvatska)", "value": "HR"},
{"name": "Cuba", "value": "countryCU"}, {"name": "Cuba", "value": "CU"},
{"name": "Cyprus", "value": "countryCY"}, {"name": "Cyprus", "value": "CY"},
{"name": "Czech Republic", "value": "countryCZ"}, {"name": "Czech Republic", "value": "CZ"},
{"name": "Denmark", "value": "countryDK"}, {"name": "Denmark", "value": "DK"},
{"name": "Djibouti", "value": "countryDJ"}, {"name": "Djibouti", "value": "DJ"},
{"name": "Dominica", "value": "countryDM"}, {"name": "Dominica", "value": "DM"},
{"name": "Dominican Republic", "value": "countryDO"}, {"name": "Dominican Republic", "value": "DO"},
{"name": "East Timor", "value": "countryTP"}, {"name": "East Timor", "value": "TP"},
{"name": "Ecuador", "value": "countryEC"}, {"name": "Ecuador", "value": "EC"},
{"name": "Egypt", "value": "countryEG"}, {"name": "Egypt", "value": "EG"},
{"name": "El Salvador", "value": "countrySV"}, {"name": "El Salvador", "value": "SV"},
{"name": "Equatorial Guinea", "value": "countryGQ"}, {"name": "Equatorial Guinea", "value": "GQ"},
{"name": "Eritrea", "value": "countryER"}, {"name": "Eritrea", "value": "ER"},
{"name": "Estonia", "value": "countryEE"}, {"name": "Estonia", "value": "EE"},
{"name": "Ethiopia", "value": "countryET"}, {"name": "Ethiopia", "value": "ET"},
{"name": "European Union", "value": "countryEU"}, {"name": "European Union", "value": "EU"},
{"name": "Falkland Islands (Malvinas)", "value": "countryFK"}, {"name": "Falkland Islands (Malvinas)", "value": "FK"},
{"name": "Faroe Islands", "value": "countryFO"}, {"name": "Faroe Islands", "value": "FO"},
{"name": "Fiji", "value": "countryFJ"}, {"name": "Fiji", "value": "FJ"},
{"name": "Finland", "value": "countryFI"}, {"name": "Finland", "value": "FI"},
{"name": "France", "value": "countryFR"}, {"name": "France", "value": "FR"},
{"name": "France, Metropolitan", "value": "countryFX"}, {"name": "France, Metropolitan", "value": "FX"},
{"name": "French Guiana", "value": "countryGF"}, {"name": "French Guiana", "value": "GF"},
{"name": "French Polynesia", "value": "countryPF"}, {"name": "French Polynesia", "value": "PF"},
{"name": "French Southern Territories", "value": "countryTF"}, {"name": "French Southern Territories", "value": "TF"},
{"name": "Gabon", "value": "countryGA"}, {"name": "Gabon", "value": "GA"},
{"name": "Gambia", "value": "countryGM"}, {"name": "Gambia", "value": "GM"},
{"name": "Georgia", "value": "countryGE"}, {"name": "Georgia", "value": "GE"},
{"name": "Germany", "value": "countryDE"}, {"name": "Germany", "value": "DE"},
{"name": "Ghana", "value": "countryGH"}, {"name": "Ghana", "value": "GH"},
{"name": "Gibraltar", "value": "countryGI"}, {"name": "Gibraltar", "value": "GI"},
{"name": "Greece", "value": "countryGR"}, {"name": "Greece", "value": "GR"},
{"name": "Greenland", "value": "countryGL"}, {"name": "Greenland", "value": "GL"},
{"name": "Grenada", "value": "countryGD"}, {"name": "Grenada", "value": "GD"},
{"name": "Guadeloupe", "value": "countryGP"}, {"name": "Guadeloupe", "value": "GP"},
{"name": "Guam", "value": "countryGU"}, {"name": "Guam", "value": "GU"},
{"name": "Guatemala", "value": "countryGT"}, {"name": "Guatemala", "value": "GT"},
{"name": "Guinea", "value": "countryGN"}, {"name": "Guinea", "value": "GN"},
{"name": "Guinea-Bissau", "value": "countryGW"}, {"name": "Guinea-Bissau", "value": "GW"},
{"name": "Guyana", "value": "countryGY"}, {"name": "Guyana", "value": "GY"},
{"name": "Haiti", "value": "countryHT"}, {"name": "Haiti", "value": "HT"},
{"name": "Heard Island and Mcdonald Islands", "value": "countryHM"}, {"name": "Heard Island and Mcdonald Islands", "value": "HM"},
{"name": "Holy See (Vatican City State)", "value": "countryVA"}, {"name": "Holy See (Vatican City State)", "value": "VA"},
{"name": "Honduras", "value": "countryHN"}, {"name": "Honduras", "value": "HN"},
{"name": "Hong Kong", "value": "countryHK"}, {"name": "Hong Kong", "value": "HK"},
{"name": "Hungary", "value": "countryHU"}, {"name": "Hungary", "value": "HU"},
{"name": "Iceland", "value": "countryIS"}, {"name": "Iceland", "value": "IS"},
{"name": "India", "value": "countryIN"}, {"name": "India", "value": "IN"},
{"name": "Indonesia", "value": "countryID"}, {"name": "Indonesia", "value": "ID"},
{"name": "Iran, Islamic Republic of", "value": "countryIR"}, {"name": "Iran, Islamic Republic of", "value": "IR"},
{"name": "Iraq", "value": "countryIQ"}, {"name": "Iraq", "value": "IQ"},
{"name": "Ireland", "value": "countryIE"}, {"name": "Ireland", "value": "IE"},
{"name": "Israel", "value": "countryIL"}, {"name": "Israel", "value": "IL"},
{"name": "Italy", "value": "countryIT"}, {"name": "Italy", "value": "IT"},
{"name": "Jamaica", "value": "countryJM"}, {"name": "Jamaica", "value": "JM"},
{"name": "Japan", "value": "countryJP"}, {"name": "Japan", "value": "JP"},
{"name": "Jordan", "value": "countryJO"}, {"name": "Jordan", "value": "JO"},
{"name": "Kazakhstan", "value": "countryKZ"}, {"name": "Kazakhstan", "value": "KZ"},
{"name": "Kenya", "value": "countryKE"}, {"name": "Kenya", "value": "KE"},
{"name": "Kiribati", "value": "countryKI"}, {"name": "Kiribati", "value": "KI"},
{"name": "Korea, Democratic People\"s Republic of", {"name": "Korea, Democratic People's Republic of", "value": "KP"},
"value": "countryKP"}, {"name": "Korea, Republic of", "value": "KR"},
{"name": "Korea, Republic of", "value": "countryKR"}, {"name": "Kuwait", "value": "KW"},
{"name": "Kuwait", "value": "countryKW"}, {"name": "Kyrgyzstan", "value": "KG"},
{"name": "Kyrgyzstan", "value": "countryKG"}, {"name": "Lao People's Democratic Republic", "value": "LA"},
{"name": "Lao People\"s Democratic Republic", "value": "countryLA"}, {"name": "Latvia", "value": "LV"},
{"name": "Latvia", "value": "countryLV"}, {"name": "Lebanon", "value": "LB"},
{"name": "Lebanon", "value": "countryLB"}, {"name": "Lesotho", "value": "LS"},
{"name": "Lesotho", "value": "countryLS"}, {"name": "Liberia", "value": "LR"},
{"name": "Liberia", "value": "countryLR"}, {"name": "Libyan Arab Jamahiriya", "value": "LY"},
{"name": "Libyan Arab Jamahiriya", "value": "countryLY"}, {"name": "Liechtenstein", "value": "LI"},
{"name": "Liechtenstein", "value": "countryLI"}, {"name": "Lithuania", "value": "LT"},
{"name": "Lithuania", "value": "countryLT"}, {"name": "Luxembourg", "value": "LU"},
{"name": "Luxembourg", "value": "countryLU"}, {"name": "Macao", "value": "MO"},
{"name": "Macao", "value": "countryMO"},
{"name": "Macedonia, the Former Yugosalv Republic of", {"name": "Macedonia, the Former Yugosalv Republic of",
"value": "countryMK"}, "value": "MK"},
{"name": "Madagascar", "value": "countryMG"}, {"name": "Madagascar", "value": "MG"},
{"name": "Malawi", "value": "countryMW"}, {"name": "Malawi", "value": "MW"},
{"name": "Malaysia", "value": "countryMY"}, {"name": "Malaysia", "value": "MY"},
{"name": "Maldives", "value": "countryMV"}, {"name": "Maldives", "value": "MV"},
{"name": "Mali", "value": "countryML"}, {"name": "Mali", "value": "ML"},
{"name": "Malta", "value": "countryMT"}, {"name": "Malta", "value": "MT"},
{"name": "Marshall Islands", "value": "countryMH"}, {"name": "Marshall Islands", "value": "MH"},
{"name": "Martinique", "value": "countryMQ"}, {"name": "Martinique", "value": "MQ"},
{"name": "Mauritania", "value": "countryMR"}, {"name": "Mauritania", "value": "MR"},
{"name": "Mauritius", "value": "countryMU"}, {"name": "Mauritius", "value": "MU"},
{"name": "Mayotte", "value": "countryYT"}, {"name": "Mayotte", "value": "YT"},
{"name": "Mexico", "value": "countryMX"}, {"name": "Mexico", "value": "MX"},
{"name": "Micronesia, Federated States of", "value": "countryFM"}, {"name": "Micronesia, Federated States of", "value": "FM"},
{"name": "Moldova, Republic of", "value": "countryMD"}, {"name": "Moldova, Republic of", "value": "MD"},
{"name": "Monaco", "value": "countryMC"}, {"name": "Monaco", "value": "MC"},
{"name": "Mongolia", "value": "countryMN"}, {"name": "Mongolia", "value": "MN"},
{"name": "Montserrat", "value": "countryMS"}, {"name": "Montserrat", "value": "MS"},
{"name": "Morocco", "value": "countryMA"}, {"name": "Morocco", "value": "MA"},
{"name": "Mozambique", "value": "countryMZ"}, {"name": "Mozambique", "value": "MZ"},
{"name": "Myanmar", "value": "countryMM"}, {"name": "Myanmar", "value": "MM"},
{"name": "Namibia", "value": "countryNA"}, {"name": "Namibia", "value": "NA"},
{"name": "Nauru", "value": "countryNR"}, {"name": "Nauru", "value": "NR"},
{"name": "Nepal", "value": "countryNP"}, {"name": "Nepal", "value": "NP"},
{"name": "Netherlands", "value": "countryNL"}, {"name": "Netherlands", "value": "NL"},
{"name": "Netherlands Antilles", "value": "countryAN"}, {"name": "Netherlands Antilles", "value": "AN"},
{"name": "New Caledonia", "value": "countryNC"}, {"name": "New Caledonia", "value": "NC"},
{"name": "New Zealand", "value": "countryNZ"}, {"name": "New Zealand", "value": "NZ"},
{"name": "Nicaragua", "value": "countryNI"}, {"name": "Nicaragua", "value": "NI"},
{"name": "Niger", "value": "countryNE"}, {"name": "Niger", "value": "NE"},
{"name": "Nigeria", "value": "countryNG"}, {"name": "Nigeria", "value": "NG"},
{"name": "Niue", "value": "countryNU"}, {"name": "Niue", "value": "NU"},
{"name": "Norfolk Island", "value": "countryNF"}, {"name": "Norfolk Island", "value": "NF"},
{"name": "Northern Mariana Islands", "value": "countryMP"}, {"name": "Northern Mariana Islands", "value": "MP"},
{"name": "Norway", "value": "countryNO"}, {"name": "Norway", "value": "NO"},
{"name": "Oman", "value": "countryOM"}, {"name": "Oman", "value": "OM"},
{"name": "Pakistan", "value": "countryPK"}, {"name": "Pakistan", "value": "PK"},
{"name": "Palau", "value": "countryPW"}, {"name": "Palau", "value": "PW"},
{"name": "Palestinian Territory", "value": "countryPS"}, {"name": "Palestinian Territory", "value": "PS"},
{"name": "Panama", "value": "countryPA"}, {"name": "Panama", "value": "PA"},
{"name": "Papua New Guinea", "value": "countryPG"}, {"name": "Papua New Guinea", "value": "PG"},
{"name": "Paraguay", "value": "countryPY"}, {"name": "Paraguay", "value": "PY"},
{"name": "Peru", "value": "countryPE"}, {"name": "Peru", "value": "PE"},
{"name": "Philippines", "value": "countryPH"}, {"name": "Philippines", "value": "PH"},
{"name": "Pitcairn", "value": "countryPN"}, {"name": "Pitcairn", "value": "PN"},
{"name": "Poland", "value": "countryPL"}, {"name": "Poland", "value": "PL"},
{"name": "Portugal", "value": "countryPT"}, {"name": "Portugal", "value": "PT"},
{"name": "Puerto Rico", "value": "countryPR"}, {"name": "Puerto Rico", "value": "PR"},
{"name": "Qatar", "value": "countryQA"}, {"name": "Qatar", "value": "QA"},
{"name": "Reunion", "value": "countryRE"}, {"name": "Reunion", "value": "RE"},
{"name": "Romania", "value": "countryRO"}, {"name": "Romania", "value": "RO"},
{"name": "Russian Federation", "value": "countryRU"}, {"name": "Russian Federation", "value": "RU"},
{"name": "Rwanda", "value": "countryRW"}, {"name": "Rwanda", "value": "RW"},
{"name": "Saint Helena", "value": "countrySH"}, {"name": "Saint Helena", "value": "SH"},
{"name": "Saint Kitts and Nevis", "value": "countryKN"}, {"name": "Saint Kitts and Nevis", "value": "KN"},
{"name": "Saint Lucia", "value": "countryLC"}, {"name": "Saint Lucia", "value": "LC"},
{"name": "Saint Pierre and Miquelon", "value": "countryPM"}, {"name": "Saint Pierre and Miquelon", "value": "PM"},
{"name": "Saint Vincent and the Grenadines", "value": "countryVC"}, {"name": "Saint Vincent and the Grenadines", "value": "VC"},
{"name": "Samoa", "value": "countryWS"}, {"name": "Samoa", "value": "WS"},
{"name": "San Marino", "value": "countrySM"}, {"name": "San Marino", "value": "SM"},
{"name": "Sao Tome and Principe", "value": "countryST"}, {"name": "Sao Tome and Principe", "value": "ST"},
{"name": "Saudi Arabia", "value": "countrySA"}, {"name": "Saudi Arabia", "value": "SA"},
{"name": "Senegal", "value": "countrySN"}, {"name": "Senegal", "value": "SN"},
{"name": "Serbia and Montenegro", "value": "countryCS"}, {"name": "Serbia and Montenegro", "value": "CS"},
{"name": "Seychelles", "value": "countrySC"}, {"name": "Seychelles", "value": "SC"},
{"name": "Sierra Leone", "value": "countrySL"}, {"name": "Sierra Leone", "value": "SL"},
{"name": "Singapore", "value": "countrySG"}, {"name": "Singapore", "value": "SG"},
{"name": "Slovakia", "value": "countrySK"}, {"name": "Slovakia", "value": "SK"},
{"name": "Slovenia", "value": "countrySI"}, {"name": "Slovenia", "value": "SI"},
{"name": "Solomon Islands", "value": "countrySB"}, {"name": "Solomon Islands", "value": "SB"},
{"name": "Somalia", "value": "countrySO"}, {"name": "Somalia", "value": "SO"},
{"name": "South Africa", "value": "countryZA"}, {"name": "South Africa", "value": "ZA"},
{"name": "South Georgia and the South Sandwich Islands", {"name": "South Georgia and the South Sandwich Islands",
"value": "countryGS"}, "value": "GS"},
{"name": "Spain", "value": "countryES"}, {"name": "Spain", "value": "ES"},
{"name": "Sri Lanka", "value": "countryLK"}, {"name": "Sri Lanka", "value": "LK"},
{"name": "Sudan", "value": "countrySD"}, {"name": "Sudan", "value": "SD"},
{"name": "Suriname", "value": "countrySR"}, {"name": "Suriname", "value": "SR"},
{"name": "Svalbard and Jan Mayen", "value": "countrySJ"}, {"name": "Svalbard and Jan Mayen", "value": "SJ"},
{"name": "Swaziland", "value": "countrySZ"}, {"name": "Swaziland", "value": "SZ"},
{"name": "Sweden", "value": "countrySE"}, {"name": "Sweden", "value": "SE"},
{"name": "Switzerland", "value": "countryCH"}, {"name": "Switzerland", "value": "CH"},
{"name": "Syrian Arab Republic", "value": "countrySY"}, {"name": "Syrian Arab Republic", "value": "SY"},
{"name": "Taiwan", "value": "countryTW"}, {"name": "Taiwan", "value": "TW"},
{"name": "Tajikistan", "value": "countryTJ"}, {"name": "Tajikistan", "value": "TJ"},
{"name": "Tanzania, United Republic of", "value": "countryTZ"}, {"name": "Tanzania, United Republic of", "value": "TZ"},
{"name": "Thailand", "value": "countryTH"}, {"name": "Thailand", "value": "TH"},
{"name": "Togo", "value": "countryTG"}, {"name": "Togo", "value": "TG"},
{"name": "Tokelau", "value": "countryTK"}, {"name": "Tokelau", "value": "TK"},
{"name": "Tonga", "value": "countryTO"}, {"name": "Tonga", "value": "TO"},
{"name": "Trinidad and Tobago", "value": "countryTT"}, {"name": "Trinidad and Tobago", "value": "TT"},
{"name": "Tunisia", "value": "countryTN"}, {"name": "Tunisia", "value": "TN"},
{"name": "Turkey", "value": "countryTR"}, {"name": "Turkey", "value": "TR"},
{"name": "Turkmenistan", "value": "countryTM"}, {"name": "Turkmenistan", "value": "TM"},
{"name": "Turks and Caicos Islands", "value": "countryTC"}, {"name": "Turks and Caicos Islands", "value": "TC"},
{"name": "Tuvalu", "value": "countryTV"}, {"name": "Tuvalu", "value": "TV"},
{"name": "Uganda", "value": "countryUG"}, {"name": "Uganda", "value": "UG"},
{"name": "Ukraine", "value": "countryUA"}, {"name": "Ukraine", "value": "UA"},
{"name": "United Arab Emirates", "value": "countryAE"}, {"name": "United Arab Emirates", "value": "AE"},
{"name": "United Kingdom", "value": "countryUK"}, {"name": "United Kingdom", "value": "UK"},
{"name": "United States", "value": "countryUS"}, {"name": "United States", "value": "US"},
{"name": "United States Minor Outlying Islands", "value": "countryUM"}, {"name": "United States Minor Outlying Islands", "value": "UM"},
{"name": "Uruguay", "value": "countryUY"}, {"name": "Uruguay", "value": "UY"},
{"name": "Uzbekistan", "value": "countryUZ"}, {"name": "Uzbekistan", "value": "UZ"},
{"name": "Vanuatu", "value": "countryVU"}, {"name": "Vanuatu", "value": "VU"},
{"name": "Venezuela", "value": "countryVE"}, {"name": "Venezuela", "value": "VE"},
{"name": "Vietnam", "value": "countryVN"}, {"name": "Vietnam", "value": "VN"},
{"name": "Virgin Islands, British", "value": "countryVG"}, {"name": "Virgin Islands, British", "value": "VG"},
{"name": "Virgin Islands, U.S.", "value": "countryVI"}, {"name": "Virgin Islands, U.S.", "value": "VI"},
{"name": "Wallis and Futuna", "value": "countryWF"}, {"name": "Wallis and Futuna", "value": "WF"},
{"name": "Western Sahara", "value": "countryEH"}, {"name": "Western Sahara", "value": "EH"},
{"name": "Yemen", "value": "countryYE"}, {"name": "Yemen", "value": "YE"},
{"name": "Yugoslavia", "value": "countryYU"}, {"name": "Yugoslavia", "value": "YU"},
{"name": "Zambia", "value": "countryZM"}, {"name": "Zambia", "value": "ZM"},
{"name": "Zimbabwe", "value": "countryZW"} {"name": "Zimbabwe", "value": "ZW"}
] ]

View File

@ -2,8 +2,7 @@
"lang_en": { "lang_en": {
"search": "Search", "search": "Search",
"config": "Configuration", "config": "Configuration",
"config-country": "Filter Results by Country", "config-country": "Set Country",
"config-country-help": "Note: If enabled, a website will only appear in the search results if it is *hosted* in the selected country.",
"config-lang": "Interface Language", "config-lang": "Interface Language",
"config-lang-search": "Search Language", "config-lang-search": "Search Language",
"config-near": "Near", "config-near": "Near",
@ -35,6 +34,8 @@
"light": "light", "light": "light",
"dark": "dark", "dark": "dark",
"system": "system", "system": "system",
"ratelimit": "Instance has been ratelimited",
"continue-search": "Continue your search with ",
"all": "All", "all": "All",
"images": "Images", "images": "Images",
"maps": "Maps", "maps": "Maps",
@ -45,8 +46,7 @@
"lang_nl": { "lang_nl": {
"search": "Zoeken", "search": "Zoeken",
"config": "Instellingen", "config": "Instellingen",
"config-country": "Filter zoek resultaten bij land", "config-country": "Land instellen",
"config-country-help": "Let op: Als je dit aanzet zal alleen website die gehost worden in het land weergegeven worden.",
"config-lang": "Taal instellingen", "config-lang": "Taal instellingen",
"config-lang-search": "Zoek taal", "config-lang-search": "Zoek taal",
"config-near": "Dichtbij", "config-near": "Dichtbij",
@ -77,13 +77,14 @@
"translate": "vertalen", "translate": "vertalen",
"light": "helder", "light": "helder",
"dark": "donker", "dark": "donker",
"system": "systeeminstellingen" "system": "systeeminstellingen",
"ratelimit": "Instantie is beperkt in snelheid",
"continue-search": "Ga verder met zoeken met "
}, },
"lang_de": { "lang_de": {
"search": "Suchen", "search": "Suchen",
"config": "Einstellungen", "config": "Einstellungen",
"config-country": "Ergebnisse nach Land filtern", "config-country": "Land einstellen",
"config-country-help": "Hinweis: Wenn aktiv, wird eine Webseite nur angezeigt, wenn sie auch in dem jeweiligen Land *gehosted* wird.",
"config-lang": "Oberflächen-Sprache", "config-lang": "Oberflächen-Sprache",
"config-lang-search": "Such-Sprache", "config-lang-search": "Such-Sprache",
"config-near": "In der Nähe von", "config-near": "In der Nähe von",
@ -114,13 +115,14 @@
"translate": "Übersetzen", "translate": "Übersetzen",
"light": "hell", "light": "hell",
"dark": "dunkel", "dark": "dunkel",
"system": "Systemeinstellung" "system": "Systemeinstellung",
"ratelimit": "Instanz wurde ratenbegrenzt",
"continue-search": "Setzen Sie Ihre Suche fort mit "
}, },
"lang_es": { "lang_es": {
"search": "Buscar", "search": "Buscar",
"config": "Configuración", "config": "Configuración",
"config-country": "Filtrar Resultados por País", "config-country": "Establecer País",
"config-country-help": "Nota: Si está habilitado, un sitio web solo aparecerá en los resultados de búsqueda si está alojado en ese país.",
"config-lang": "Idioma de Interfaz", "config-lang": "Idioma de Interfaz",
"config-lang-search": "Idioma de Búsqueda", "config-lang-search": "Idioma de Búsqueda",
"config-near": "Cerca", "config-near": "Cerca",
@ -151,13 +153,14 @@
"translate": "traducir", "translate": "traducir",
"light": "brillante", "light": "brillante",
"dark": "oscuro", "dark": "oscuro",
"system": "configuración del sistema" "system": "configuración del sistema",
"ratelimit": "La instancia ha sido ratelimited",
"continue-search": "Continúe su búsqueda con "
}, },
"lang_it": { "lang_it": {
"search": "Cerca", "search": "Cerca",
"config": "Impostazioni", "config": "Impostazioni",
"config-country": "Filtra risultati per paese", "config-country": "Imposta Paese",
"config-country-help": "Nota: se abilitato, il sito sarà presente tra i risultati se e soltanto se il server risiede nel paese selezionato",
"config-lang": "Lingua dell'interfaccia", "config-lang": "Lingua dell'interfaccia",
"config-lang-search": "Lingua della ricerca", "config-lang-search": "Lingua della ricerca",
"config-near": "Vicino", "config-near": "Vicino",
@ -188,13 +191,14 @@
"translate": "tradurre", "translate": "tradurre",
"light": "luminoso", "light": "luminoso",
"dark": "notte", "dark": "notte",
"system": "impostazioni di sistema" "system": "impostazioni di sistema",
"ratelimit": "L'istanza è stata limitata alla velocità",
"continue-search": "Continua la tua ricerca con "
}, },
"lang_pt": { "lang_pt": {
"search": "Pesquisar", "search": "Pesquisar",
"config": "Configuração", "config": "Configuração",
"config-country": "Filtrar Resultados por País", "config-country": "Definir País",
"config-country-help": "Observação: Se ativado, um site só aparecerá nos resultados da pesquisa se estiver *hospedado* no país selecionado.",
"config-lang": "Idioma da Interface", "config-lang": "Idioma da Interface",
"config-lang-search": "Idioma da Pesquisa", "config-lang-search": "Idioma da Pesquisa",
"config-near": "Perto", "config-near": "Perto",
@ -225,13 +229,52 @@
"translate": "traduzir", "translate": "traduzir",
"light": "brilhante", "light": "brilhante",
"dark": "escuro", "dark": "escuro",
"system": "configuração de sistema" "system": "configuração de sistema",
"ratelimit": "A instância foi limitada pela taxa",
"continue-search": "Continue sua pesquisa com "
},
"lang_ru": {
"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": "Показывать ссылки NoJS",
"config-dark": "Темный режим",
"config-safe": "Безопасный поиск",
"config-alts": "Заменить ссылки на социальные сети",
"config-alts-help": "Замена ссылкок Twitter, YouTube, Instagram и т.д. на альтернативы, уважающие конфиденциальность.",
"config-new-tab": "Открывать ссылки в новой вкладке",
"config-images": "Поиск полноразмерных изображений",
"config-images-help": "(Экспериментально) Добавляет опцию 'Просмотр изображения' к поиску изображений в ПК-режиме. Это приведет к тому, что миниатюры изображений будут иметь более низкое разрешение.",
"config-tor": "Использовать Tor",
"config-get-only": "Только GET-запросы",
"config-url": "Корневой URL-адрес",
"config-css": "Пользовательский CSS",
"load": "Загрузить",
"apply": "Применить",
"save-as": "Сохранить как...",
"github-link": "Посмотреть в GitHub",
"translate": "перевести",
"light": "светлое",
"dark": "темное",
"system": "системное",
"ratelimit": "Число экземпляров ограничено",
"continue-search": "Продолжайте поиск с "
}, },
"lang_zh-CN": { "lang_zh-CN": {
"search": "搜索", "search": "搜索",
"config": "配置", "config": "配置",
"config-country": "按国家过滤搜索结果", "config-country": "设置国家",
"config-country-help": "注意:启用后,只有在所选国家*部署*的网站会出现在搜索结果中。",
"config-lang": "界面语言", "config-lang": "界面语言",
"config-lang-search": "搜索语言", "config-lang-search": "搜索语言",
"config-near": "接近", "config-near": "接近",
@ -262,13 +305,14 @@
"translate": "翻译", "translate": "翻译",
"light": "明亮的", "light": "明亮的",
"dark": "黑暗的", "dark": "黑暗的",
"system": "系统设置" "system": "系统设置",
"ratelimit": "实例已被限速",
"continue-search": "继续搜索 "
}, },
"lang_si": { "lang_si": {
"search": "සොයන්න", "search": "සොයන්න",
"config": "වින්‍යාසය", "config": "වින්‍යාසය",
"config-country": "රට අනුව ප්‍රතිඵල පෙරන්න", "config-country": "රට සකසන්න",
"config-country-help": "සටහන: සබල කර ඇත්නම්, වියමන අඩවියක් සෙවුම් ප්‍රතිඵලවල දිස්වන්නේ එය තෝරාගත් රටෙහි සිට *සත්කාරකත්වය* දරන්නේ නම් පමණි.",
"config-lang": "අතුරු මුහුණතෙහි භාෂාව", "config-lang": "අතුරු මුහුණතෙහි භාෂාව",
"config-lang-search": "සෙවුම් භාෂාව", "config-lang-search": "සෙවුම් භාෂාව",
"config-near": "ආසන්න", "config-near": "ආසන්න",
@ -299,13 +343,14 @@
"translate": "පරිවර්තනය කරන්න", "translate": "පරිවර්තනය කරන්න",
"light": "දීප්තිමත්", "light": "දීප්තිමත්",
"dark": "අඳුරු", "dark": "අඳුරු",
"system": "පද්ධතිය" "system": "පද්ධතිය",
"ratelimit": "උදාහරණය අනුපාත කර ඇත",
"continue-search": "සමඟ ඔබේ සෙවීම දිගටම කරගෙන යන්න"
}, },
"lang_fr": { "lang_fr": {
"search": "Chercher", "search": "Chercher",
"config": "Configuration", "config": "Configuration",
"config-country": "Filter les Résultats par Pays", "config-country": "Définir le pays",
"config-country-help": "Note : Si activé, un site web va uniquement apparaitre dans les résultat de la recherche si il est *hébérgé* dans le pays sélectionné.",
"config-lang": "Langage de l'Interface", "config-lang": "Langage de l'Interface",
"config-lang-search": "Langage de Recherche", "config-lang-search": "Langage de Recherche",
"config-near": "Proche", "config-near": "Proche",
@ -336,13 +381,14 @@
"translate": "Traduire", "translate": "Traduire",
"light": "clair", "light": "clair",
"dark": "sombre", "dark": "sombre",
"system": "système" "system": "système",
"ratelimit": "Le débit de l'instance a été limité",
"continue-search": "Continuez votre recherche avec "
}, },
"lang_fa": { "lang_fa": {
"search": "جستجو", "search": "جستجو",
"config": "پیکربندی", "config": "پیکربندی",
"config-country": "فیلتر نتایج بر اساس کشور", "config-country": "کشور را تنظیم کنید",
"config-country-help": "توجه: در صورت فعال بودن، وبسایت تنها در صورتی نمایش داده می‌شود که *در کشور انتخابی میزبانی شده باشد*.",
"config-lang": "زبان رابط کاربری", "config-lang": "زبان رابط کاربری",
"config-lang-search": "زبان جستجو", "config-lang-search": "زبان جستجو",
"config-near": "نزدیک", "config-near": "نزدیک",
@ -373,13 +419,14 @@
"translate": "ترجمه", "translate": "ترجمه",
"light": "روشن", "light": "روشن",
"dark": "تیره", "dark": "تیره",
"system": "سیستم" "system": "سیستم",
"ratelimit": "نمونه با نرخ محدود شده است",
"continue-search": "جستجوی خود را با "
}, },
"lang_cs": { "lang_cs": {
"search": "Hledat", "search": "Hledat",
"config": "Konfigurace", "config": "Konfigurace",
"config-country": "Filtrovat výsledky podle země", "config-country": "Nastavte zemi",
"config-country-help": "Poznámka: Pokud je povoleno, webová stránka se objeví ve výsledcích vyhledávání, pouze pokud je *hostována* ve vybrané zemi.",
"config-lang": "Jazyk rozhraní", "config-lang": "Jazyk rozhraní",
"config-lang-search": "Jazyk vyhledávání", "config-lang-search": "Jazyk vyhledávání",
"config-near": "Poblíž", "config-near": "Poblíž",
@ -410,13 +457,14 @@
"translate": "Přeložit", "translate": "Přeložit",
"light": "Světlý", "light": "Světlý",
"dark": "Tmavý", "dark": "Tmavý",
"system": "Systémový" "system": "Systémový",
"ratelimit": "Instance byla omezena sazbou",
"continue-search": "Pokračujte ve vyhledávání pomocí "
}, },
"lang_zh-TW": { "lang_zh-TW": {
"search": "搜尋", "search": "搜尋",
"config": "設定", "config": "設定",
"config-country": "依國家過濾結果", "config-country": "設置國家",
"config-country-help": "注意:一經套用,只有在部署在指定國家內的網站會出現在搜尋結果中。",
"config-lang": "界面語言", "config-lang": "界面語言",
"config-lang-search": "搜尋語言", "config-lang-search": "搜尋語言",
"config-near": "接近", "config-near": "接近",
@ -447,13 +495,14 @@
"translate": "翻譯", "translate": "翻譯",
"light": "明亮的", "light": "明亮的",
"dark": "黑暗的", "dark": "黑暗的",
"system": "依系統" "system": "依系統",
"ratelimit": "實例已被限速",
"continue-search": "繼續搜索 "
}, },
"lang_bg": { "lang_bg": {
"search": "Търсене", "search": "Търсене",
"config": "Конфигурация", "config": "Конфигурация",
"config-country": "Филтрирай резултатите по държави", "config-country": "Задайте държава",
"config-country-help": "Забележка: Ако това е разрешено, уебсайтoвете ще се показват в резултатите от търсенето, само ако са * хоствани * в избраната държава.",
"config-lang": "Език на интерфейса", "config-lang": "Език на интерфейса",
"config-lang-search": "Език за търсене", "config-lang-search": "Език за търсене",
"config-near": "Близо до", "config-near": "Близо до",
@ -484,13 +533,14 @@
"translate": "превод", "translate": "превод",
"light": "светла", "light": "светла",
"dark": "тъмна", "dark": "тъмна",
"system": "системна" "system": "системна",
"ratelimit": "Екземплярът е с ограничена скорост",
"continue-search": "Продължете търсенето си с "
}, },
"lang_hi": { "lang_hi": {
"search": "खोज", "search": "खोज",
"config": "कॉन्फ़िगरेशन", "config": "कॉन्फ़िगरेशन",
"config-country": "देश के अनुसार परिणाम फ़िल्टर करें", "config-country": "देश सेट करें",
"config-country-help": "नोट: यदि सक्षम है, तो कोई वेबसाइट खोज परिणामों में केवल तभी दिखाई देगी जब वह चयनित देश में *होस्ट* हो।",
"config-lang": "इंटरफ़ेस भाषा", "config-lang": "इंटरफ़ेस भाषा",
"config-lang-search": "खोज की भाषा", "config-lang-search": "खोज की भाषा",
"config-near": "पास", "config-near": "पास",
@ -521,6 +571,46 @@
"translate": "अनुवाद करना", "translate": "अनुवाद करना",
"light": "रोशनी", "light": "रोशनी",
"dark": "अंधेरा", "dark": "अंधेरा",
"system": "प्रणाली" "system": "प्रणाली",
"ratelimit": "इंस्टेंस को सीमित कर दिया गया है",
"continue-search": "के साथ अपनी खोज जारी रखें "
},
"lang_ja": {
"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": "正規表現を使用します",
"config-block-url": "でブロック",
"config-block-url-help": "正規表現を使用",
"config-theme": "テーマ",
"config-nojs": "非JSリンクを表示",
"config-dark": "ダークモード",
"config-safe": "セーフサーチ",
"config-alts": "ソーシャルメディアのリンクを置き換え",
"config-alts-help": "Twitter/YouTube/Instagramなどのリンクを、プライバシーを尊重した代替サイトに置き換えます。",
"config-new-tab": "新しいタブでリンクを開く",
"config-images": "フルサイズの画像を検索",
"config-images-help": "(実験的) デスクトップの画像検索に「画像を表示」オプションを追加します。これにより、画像検索結果のサムネイルの解像度が低くなります。",
"config-tor": "Torを使用",
"config-get-only": "GETリクエストのみ",
"config-url": "ルートURL",
"config-css": "カスタムCSS",
"load": "読み込み",
"apply": "反映",
"save-as": "名前を付けて保存",
"github-link": "Githubで確認",
"translate": "翻訳",
"light": "ライト",
"dark": "ダーク",
"system": "自動",
"ratelimit": "インスタンスはレート制限されています",
"continue-search": "で検索を続ける "
} }
} }

View File

@ -5,6 +5,7 @@
<link rel="search" href="opensearch.xml" type="application/opensearchdescription+xml" title="Whoogle Search"> <link rel="search" href="opensearch.xml" type="application/opensearchdescription+xml" title="Whoogle Search">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="referrer" content="no-referrer"> <meta name="referrer" content="no-referrer">
<link rel="stylesheet" href="{{ cb_url('logo.css') }}">
<link rel="stylesheet" href="{{ cb_url('input.css') }}"> <link rel="stylesheet" href="{{ cb_url('input.css') }}">
<link rel="stylesheet" href="{{ cb_url('search.css') }}"> <link rel="stylesheet" href="{{ cb_url('search.css') }}">
<link rel="stylesheet" href="{{ cb_url('header.css') }}"> <link rel="stylesheet" href="{{ cb_url('header.css') }}">
@ -33,13 +34,11 @@
{% endif %} {% endif %}
{{ response|safe }} {{ response|safe }}
</body> </body>
<footer> {% include 'footer.html' %}
<p class="footer"> {% if autocomplete_enabled == '1' %}
Whoogle Search v{{ version_number }} || <script src="{{ cb_url('autocomplete.js') }}"></script>
<a id="gh-link" href="https://github.com/benbusby/whoogle-search">{{ translation['github-link'] }}</a> {% endif %}
</p>
</footer>
<script src="{{ cb_url('autocomplete.js') }}"></script>
<script src="{{ cb_url('utils.js') }}"></script> <script src="{{ cb_url('utils.js') }}"></script>
<script src="{{ cb_url('keyboard.js') }}"></script> <script src="{{ cb_url('keyboard.js') }}"></script>
<script src="{{ cb_url('currency.js') }}"></script>
</html> </html>

View File

@ -1,6 +1,40 @@
<h1>Error</h1> {% if config.theme %}
<hr> {% if config.theme == 'system' %}
<p> <style>
Error: "{{ error_message|safe }}" @import "{{ cb_url('light-theme.css') }}" screen;
</p> @import "{{ cb_url('dark-theme.css') }}" screen and (prefers-color-scheme: dark);
<a href="/">Return Home</a> </style>
{% else %}
<link rel="stylesheet" href="{{ cb_url(config.theme + '-theme.css') }}"/>
{% endif %}
{% else %}
<link rel="stylesheet" href="{{ cb_url(('dark' if config.dark else 'light') + '-theme.css') }}"/>
{% endif %}
<link rel="stylesheet" href="{{ cb_url('main.css') }}">
<link rel="stylesheet" href="{{ cb_url('error.css') }}">
<style>{{ config.style }}</style>
<div>
<h1>Error</h1>
<p>
{{ error_message|safe }}
</p>
<hr>
<p>
{% if blocked is defined %}
<h4>{{ translation['continue-search'] }} <a class="link" href="https://github.com/benbusby/farside">Farside</a>!</h4>
Whoogle:
<br>
<a class="link-color" href="{{farside}}/whoogle/search?q={{query}}{{params}}">
{{farside}}/whoogle/search?q={{query}}{{params}}
</a>
<br><br>
Searx:
<br>
<a class="link-color" href="{{farside}}/searx/search?q={{query}}">
{{farside}}/searx/search?q={{query}}
</a>
<hr>
{% endif %}
</p>
<a class="link" href="home">Return Home</a>
</div>

View File

@ -0,0 +1,9 @@
<footer>
<p class="footer">
Whoogle Search v{{ version_number }} ||
<a class="link" href="https://github.com/benbusby/whoogle-search">{{ translation['github-link'] }}</a>
{% if newest_version %}
|| <span class="update_available">Update Available 🟢</span>
{% endif %}
</p>
</footer>

View File

@ -17,10 +17,13 @@
<meta name="referrer" content="no-referrer"> <meta name="referrer" content="no-referrer">
<meta name="msapplication-TileColor" content="#ffffff"> <meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="static/img/favicon/ms-icon-144x144.png"> <meta name="msapplication-TileImage" content="static/img/favicon/ms-icon-144x144.png">
<script type="text/javascript" src="{{ cb_url('autocomplete.js') }}"></script> {% if autocomplete_enabled == '1' %}
<script src="{{ cb_url('autocomplete.js') }}"></script>
{% endif %}
<script type="text/javascript" src="{{ cb_url('controller.js') }}"></script> <script type="text/javascript" src="{{ cb_url('controller.js') }}"></script>
<link rel="search" href="opensearch.xml" type="application/opensearchdescription+xml" title="Whoogle Search"> <link rel="search" href="opensearch.xml" type="application/opensearchdescription+xml" title="Whoogle Search">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{{ cb_url('logo.css') }}">
{% if config.theme %} {% if config.theme %}
{% if config.theme == 'system' %} {% if config.theme == 'system' %}
<style> <style>
@ -84,19 +87,19 @@
<div class="content"> <div class="content">
<div class="config-fields"> <div class="config-fields">
<form id="config-form" action="config" method="post"> <form id="config-form" action="config" method="post">
<div class="config-div config-div-ctry"> <div class="config-options">
<label for="config-ctry">{{ translation['config-country'] }}: </label> <div class="config-div config-div-country">
<select name="ctry" id="config-ctry"> <label for="config-country">{{ translation['config-country'] }}: </label>
{% for ctry in countries %} <select name="country" id="config-country">
<option value="{{ ctry.value }}" {% for country in countries %}
{% if ctry.value in config.ctry %} <option value="{{ country.value }}"
{% if country.value in config.country %}
selected selected
{% endif %}> {% endif %}>
{{ ctry.name }} {{ country.name }}
</option> </option>
{% endfor %} {% endfor %}
</select> </select>
<div><span class="info-text"> — {{ translation['config-country-help'] }}</span></div>
</div> </div>
<div class="config-div config-div-lang"> <div class="config-div config-div-lang">
<label for="config-lang-interface">{{ translation['config-lang'] }}: </label> <label for="config-lang-interface">{{ translation['config-lang'] }}: </label>
@ -222,7 +225,8 @@
{{ config.style.replace('\t', '') }} {{ config.style.replace('\t', '') }}
</textarea> </textarea>
</div> </div>
<div class="config-div"> </div>
<div class="config-div config-buttons">
<input type="submit" id="config-load" value="{{ translation['load'] }}">&nbsp; <input type="submit" id="config-load" value="{{ translation['load'] }}">&nbsp;
<input type="submit" id="config-submit" value="{{ translation['apply'] }}">&nbsp; <input type="submit" id="config-submit" value="{{ translation['apply'] }}">&nbsp;
<input type="submit" id="config-save" value="{{ translation['save-as'] }}"> <input type="submit" id="config-save" value="{{ translation['save-as'] }}">
@ -232,11 +236,6 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
<footer> {% include 'footer.html' %}
<p class="footer">
Whoogle Search v{{ version_number }} ||
<a id="gh-link" href="https://github.com/benbusby/whoogle-search">{{ translation['github-link'] }}</a>
</p>
</footer>
</body> </body>
</html> </html>

View File

@ -1,4 +1,3 @@
<link rel="stylesheet" href="{{ cb_url('logo.css') }}">
<svg id="Layer_1" class="whoogle-svg" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1028 254"> <svg id="Layer_1" class="whoogle-svg" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1028 254">
<defs> <defs>
<style> <style>
@ -17,4 +16,3 @@
<path class="cls-1" d="M950.51,539.43c-.31,20.82-10.91,37.89-28,44.71-25.32,10.11-53.89-7-57.87-34.41-1.51-10.43-1.06-20.59,2.68-30.44,7.08-18.66,25.09-29.59,45-27.58,17.76,1.79,33.92,17.68,36.86,36.35C949.79,531.82,950.08,535.64,950.51,539.43Z" transform="translate(-446 -413)"></path> <path class="cls-1" d="M950.51,539.43c-.31,20.82-10.91,37.89-28,44.71-25.32,10.11-53.89-7-57.87-34.41-1.51-10.43-1.06-20.59,2.68-30.44,7.08-18.66,25.09-29.59,45-27.58,17.76,1.79,33.92,17.68,36.86,36.35C949.79,531.82,950.08,535.64,950.51,539.43Z" transform="translate(-446 -413)"></path>
<path class="cls-1" d="M1099.71,539.39c-.39,22.14-11.74,39.51-30.16,45.6-25.8,8.54-53.64-10.27-55.87-37.67-.78-9.54-.55-18.93,3-28,7.25-18.72,24.95-29.59,45-27.62,17.2,1.68,33.14,16.78,36.57,34.84C1099,530.77,1099.23,535.1,1099.71,539.39Z" transform="translate(-446 -413)"></path> <path class="cls-1" d="M1099.71,539.39c-.39,22.14-11.74,39.51-30.16,45.6-25.8,8.54-53.64-10.27-55.87-37.67-.78-9.54-.55-18.93,3-28,7.25-18.72,24.95-29.59,45-27.62,17.2,1.68,33.14,16.78,36.57,34.84C1099,530.77,1099.23,535.1,1099.71,539.39Z" transform="translate(-446 -413)"></path>
</svg> </svg>
</a>

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@ -23,3 +23,10 @@ def get_client_ip(r: Request) -> str:
return r.environ['REMOTE_ADDR'] return r.environ['REMOTE_ADDR']
else: else:
return r.environ['HTTP_X_FORWARDED_FOR'] return r.environ['HTTP_X_FORWARDED_FOR']
def get_request_url(url: str) -> str:
if os.getenv('HTTPS_ONLY', False):
return url.replace('http://', 'https://', 1)
return url

View File

@ -1,3 +1,4 @@
from app.models.endpoint import Endpoint
from bs4 import BeautifulSoup, NavigableString from bs4 import BeautifulSoup, NavigableString
import copy import copy
import html import html
@ -24,14 +25,16 @@ BLACKLIST = [
] ]
SITE_ALTS = { SITE_ALTS = {
'twitter.com': os.getenv('WHOOGLE_ALT_TW', 'nitter.net'), 'twitter.com': os.getenv('WHOOGLE_ALT_TW', 'farside.link/nitter'),
'youtube.com': os.getenv('WHOOGLE_ALT_YT', 'invidious.snopyta.org'), 'youtube.com': os.getenv('WHOOGLE_ALT_YT', 'farside.link/invidious'),
'instagram.com': os.getenv('WHOOGLE_ALT_IG', 'bibliogram.art/u'), 'instagram.com': os.getenv('WHOOGLE_ALT_IG', 'farside.link/bibliogram/u'),
'reddit.com': os.getenv('WHOOGLE_ALT_RD', 'libredd.it'), 'reddit.com': os.getenv('WHOOGLE_ALT_RD', 'farside.link/libreddit'),
**dict.fromkeys([ **dict.fromkeys([
'medium.com', 'medium.com',
'levelup.gitconnected.com' 'levelup.gitconnected.com'
], os.getenv('WHOOGLE_ALT_MD', 'scribe.rip')) ], os.getenv('WHOOGLE_ALT_MD', 'farside.link/scribe')),
'imgur.com': os.getenv('WHOOGLE_ALT_IMG', 'imgin.voidnet.tech'),
'wikipedia.com': os.getenv('WHOOGLE_ALT_WIKI', 'wikiless.org')
} }
@ -178,7 +181,7 @@ def append_nojs(result: BeautifulSoup) -> None:
""" """
nojs_link = BeautifulSoup(features='html.parser').new_tag('a') nojs_link = BeautifulSoup(features='html.parser').new_tag('a')
nojs_link['href'] = '/window?location=' + result['href'] nojs_link['href'] = f'/{Endpoint.window}?location=' + result['href']
nojs_link.string = ' NoJS Link' nojs_link.string = ' NoJS Link'
result.append(nojs_link) result.append(nojs_link)
@ -225,6 +228,110 @@ def add_ip_card(html_soup: BeautifulSoup, ip: str) -> BeautifulSoup:
return html_soup return html_soup
def check_currency(response: str) -> dict:
"""Check whether the results have currency conversion
Args:
response: Search query Result
Returns:
dict: Consists of currency names and values
"""
soup = BeautifulSoup(response, 'html.parser')
currency_link = soup.find('a', {'href': 'https://g.co/gfd'})
if currency_link:
while 'class' not in currency_link.attrs or \
'ZINbbc' not in currency_link.attrs['class']:
currency_link = currency_link.parent
currency_link = currency_link.find_all(class_='BNeawe')
currency1 = currency_link[0].text
currency2 = currency_link[1].text
currency1 = currency1.rstrip('=').split(' ', 1)
currency2 = currency2.split(' ', 1)
if currency2[0][-3] == ',':
currency1[0] = currency1[0].replace('.', '')
currency1[0] = currency1[0].replace(',', '.')
currency2[0] = currency2[0].replace('.', '')
currency2[0] = currency2[0].replace(',', '.')
else:
currency1[0] = currency1[0].replace(',', '')
currency2[0] = currency2[0].replace(',', '')
return {'currencyValue1': float(currency1[0]),
'currencyLabel1': currency1[1],
'currencyValue2': float(currency2[0]),
'currencyLabel2': currency2[1]
}
return {}
def add_currency_card(soup: BeautifulSoup,
conversion_details: dict) -> BeautifulSoup:
"""Adds the currency conversion boxes
to response of the search query
Args:
soup: Parsed search result
conversion_details: Dictionary of currency
related information
Returns:
BeautifulSoup
"""
# Element before which the code will be changed
# (This is the 'disclaimer' link)
element1 = soup.find('a', {'href': 'https://g.co/gfd'})
while 'class' not in element1.attrs or \
'nXE3Ob' not in element1.attrs['class']:
element1 = element1.parent
# Creating the conversion factor
conversion_factor = (conversion_details['currencyValue1'] /
conversion_details['currencyValue2'])
# Creating a new div for the input boxes
conversion_box = soup.new_tag('div')
conversion_box['class'] = 'conversion_box'
# Currency to be converted from
input_box1 = soup.new_tag('input')
input_box1['id'] = 'cb1'
input_box1['type'] = 'number'
input_box1['class'] = 'cb'
input_box1['value'] = conversion_details['currencyValue1']
input_box1['oninput'] = f'convert(1, 2, {1 / conversion_factor})'
label_box1 = soup.new_tag('label')
label_box1['for'] = 'cb1'
label_box1['class'] = 'cb_label'
label_box1.append(conversion_details['currencyLabel1'])
br = soup.new_tag('br')
# Currency to be converted to
input_box2 = soup.new_tag('input')
input_box2['id'] = 'cb2'
input_box2['type'] = 'number'
input_box2['class'] = 'cb'
input_box2['value'] = conversion_details['currencyValue2']
input_box2['oninput'] = f'convert(2, 1, {conversion_factor})'
label_box2 = soup.new_tag('label')
label_box2['for'] = 'cb2'
label_box2['class'] = 'cb_label'
label_box2.append(conversion_details['currencyLabel2'])
conversion_box.append(input_box1)
conversion_box.append(label_box1)
conversion_box.append(br)
conversion_box.append(input_box2)
conversion_box.append(label_box2)
element1.insert_before(conversion_box)
return soup
def get_tabs_content(tabs: dict, def get_tabs_content(tabs: dict,
full_query: str, full_query: str,
search_type: str, search_type: str,

View File

@ -52,16 +52,15 @@ class Search:
Attributes: Attributes:
request: the incoming flask request request: the incoming flask request
config: the current user config settings config: the current user config settings
session: the flask user session session_key: the flask user fernet key
""" """
def __init__(self, request, config, session_key, cookies_disabled=False):
def __init__(self, request, config, session, cookies_disabled=False):
method = request.method method = request.method
self.request_params = request.args if method == 'GET' else request.form self.request_params = request.args if method == 'GET' else request.form
self.user_agent = request.headers.get('User-Agent') self.user_agent = request.headers.get('User-Agent')
self.feeling_lucky = False self.feeling_lucky = False
self.config = config self.config = config
self.session = session self.session_key = session_key
self.query = '' self.query = ''
self.cookies_disabled = cookies_disabled self.cookies_disabled = cookies_disabled
self.search_type = self.request_params.get( self.search_type = self.request_params.get(
@ -96,7 +95,7 @@ class Search:
else: else:
# Attempt to decrypt if this is an internal link # Attempt to decrypt if this is an internal link
try: try:
q = Fernet(self.session['key']).decrypt(q.encode()).decode() q = Fernet(self.session_key).decrypt(q.encode()).decode()
except InvalidToken: except InvalidToken:
pass pass
@ -115,7 +114,7 @@ class Search:
""" """
mobile = 'Android' in self.user_agent or 'iPhone' in self.user_agent mobile = 'Android' in self.user_agent or 'iPhone' in self.user_agent
content_filter = Filter(self.session['key'], content_filter = Filter(self.session_key,
mobile=mobile, mobile=mobile,
config=self.config) config=self.config)
full_query = gen_query(self.query, full_query = gen_query(self.query,
@ -134,17 +133,15 @@ class Search:
force_mobile=view_image) force_mobile=view_image)
# Produce cleanable html soup from response # Produce cleanable html soup from response
html_soup = bsoup(content_filter.reskin(get_body.text), 'html.parser') html_soup = bsoup(get_body.text, 'html.parser')
# Replace current soup if view_image is active # Replace current soup if view_image is active
if view_image: if view_image:
html_soup = content_filter.view_image(html_soup) html_soup = content_filter.view_image(html_soup)
# Indicate whether or not a Tor connection is active # Indicate whether or not a Tor connection is active
tor_banner = bsoup('', 'html.parser')
if g.user_request.tor_valid: if g.user_request.tor_valid:
tor_banner = bsoup(TOR_BANNER, 'html.parser') html_soup.insert(0, bsoup(TOR_BANNER, 'html.parser'))
html_soup.insert(0, tor_banner)
if self.feeling_lucky: if self.feeling_lucky:
return get_first_link(html_soup) return get_first_link(html_soup)

View File

@ -4,7 +4,7 @@ from flask import current_app as app
REQUIRED_SESSION_VALUES = ['uuid', 'config', 'key'] REQUIRED_SESSION_VALUES = ['uuid', 'config', 'key']
def generate_user_key(cookies_disabled=False) -> bytes: def generate_user_key() -> bytes:
"""Generates a key for encrypting searches and element URLs """Generates a key for encrypting searches and element URLs
Args: Args:
@ -16,9 +16,6 @@ def generate_user_key(cookies_disabled=False) -> bytes:
str: A unique Fernet key str: A unique Fernet key
""" """
if cookies_disabled:
return app.default_key
# Generate/regenerate unique key per user # Generate/regenerate unique key per user
return Fernet.generate_key() return Fernet.generate_key()

View File

@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

23
charts/whoogle/Chart.yaml Normal file
View File

@ -0,0 +1,23 @@
apiVersion: v2
name: whoogle
description: A self hosted search engine on Kubernetes
type: application
version: 0.1.0
appVersion: 0.7.0
icon: https://github.com/benbusby/whoogle-search/raw/main/app/static/img/favicon/favicon-96x96.png
sources:
- https://github.com/benbusby/whoogle-search
- https://gitlab.com/benbusby/whoogle-search
- https://gogs.benbusby.com/benbusby/whoogle-search
keywords:
- whoogle
- degoogle
- search
- google
- search-engine
- privacy
- tor
- python

View File

@ -0,0 +1,22 @@
1. Get the application URL by running these commands:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
{{- range .paths }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
{{- end }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "whoogle.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "whoogle.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "whoogle.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "whoogle.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
{{- end }}

View File

@ -0,0 +1,62 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "whoogle.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "whoogle.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "whoogle.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "whoogle.labels" -}}
helm.sh/chart: {{ include "whoogle.chart" . }}
{{ include "whoogle.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "whoogle.selectorLabels" -}}
app.kubernetes.io/name: {{ include "whoogle.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "whoogle.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "whoogle.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,72 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "whoogle.fullname" . }}
labels:
{{- include "whoogle.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "whoogle.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "whoogle.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.image.pullSecrets }}
imagePullSecrets:
{{- range .}}
- name: {{ . }}
{{- end }}
{{- end }}
serviceAccountName: {{ include "whoogle.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: whoogle
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
{{- with .Values.conf }}
env:
{{- range $k,$v := . }}
{{- if $v }}
- name: {{ $k }}
value: {{ tpl (toString $v) $ | quote }}
{{- end }}
{{- end }}
{{- end }}
ports:
- name: http
containerPort: {{ default 5000 .Values.conf.EXPOSE_PORT }}
protocol: TCP
livenessProbe:
httpGet:
path: /
port: http
readinessProbe:
httpGet:
path: /
port: http
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@ -0,0 +1,28 @@
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "whoogle.fullname" . }}
labels:
{{- include "whoogle.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "whoogle.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,61 @@
{{- if .Values.ingress.enabled -}}
{{- $fullName := include "whoogle.fullname" . -}}
{{- $svcPort := .Values.service.port -}}
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
{{- end }}
{{- end }}
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1beta1
{{- else -}}
apiVersion: extensions/v1beta1
{{- end }}
kind: Ingress
metadata:
name: {{ $fullName }}
labels:
{{- include "whoogle.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
pathType: {{ .pathType }}
{{- end }}
backend:
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
service:
name: {{ $fullName }}
port:
number: {{ $svcPort }}
{{- else }}
serviceName: {{ $fullName }}
servicePort: {{ $svcPort }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "whoogle.fullname" . }}
labels:
{{- include "whoogle.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "whoogle.selectorLabels" . | nindent 4 }}

View File

@ -0,0 +1,12 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "whoogle.serviceAccountName" . }}
labels:
{{- include "whoogle.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,15 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "whoogle.fullname" . }}-test-connection"
labels:
{{- include "whoogle.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "whoogle.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never

110
charts/whoogle/values.yaml Normal file
View File

@ -0,0 +1,110 @@
# Default values for whoogle.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
nameOverride: ""
fullnameOverride: ""
replicaCount: 1
image:
repository: benbusby/whoogle-search
pullPolicy: IfNotPresent
# Overrides the image tag whose default is the chart appVersion.
tag: ""
pullSecrets: []
# - my-image-pull-secret
serviceAccount:
# Specifies whether a service account should be created
create: true
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ""
conf: {}
# WHOOGLE_DOTENV: "" # Load environment variables in whoogle.env
# WHOOGLE_USER: "" # The username for basic auth. WHOOGLE_PASS must also be set if used.
# WHOOGLE_PASS: "" # The password for basic auth. WHOOGLE_USER must also be set if used.
# WHOOGLE_PROXY_USER: "" # The username of the proxy server.
# WHOOGLE_PROXY_PASS: "" # The password of the proxy server.
# WHOOGLE_PROXY_TYPE: "" # The type of the proxy server. Can be "socks5", "socks4", or "http".
# WHOOGLE_PROXY_LOC: "" # The location of the proxy server (host or ip).
# EXPOSE_PORT: "" # The port where Whoogle will be exposed. (default 5000)
# HTTPS_ONLY: "" # Enforce HTTPS. (See https://github.com/benbusby/whoogle-search#https-enforcement)
# WHOOGLE_ALT_TW: "" # The twitter.com alternative to use when site alternatives are enabled in the config.
# WHOOGLE_ALT_YT: "" # The youtube.com alternative to use when site alternatives are enabled in the config.
# WHOOGLE_ALT_IG: "" # The instagram.com alternative to use when site alternatives are enabled in the config.
# WHOOGLE_ALT_RD: "" # The reddit.com alternative to use when site alternatives are enabled in the config.
# WHOOGLE_ALT_TL: "" # The Google Translate alternative to use. This is used for all "translate ____" searches.
# WHOOGLE_ALT_MD: "" # The medium.com alternative to use when site alternatives are enabled in the config.
# WHOOGLE_ALT_IMG: "" # The imgur.com alternative to use when site alternatives are enabled in the config.
# WHOOGLE_ALT_WIKI: "" # The wikipedia.com alternative to use when site alternatives are enabled in the config.
# WHOOGLE_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)
podAnnotations: {}
podSecurityContext: {}
# fsGroup: 2000
securityContext:
runAsUser: 0
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
service:
type: ClusterIP
port: 5000
ingress:
enabled: false
className: ""
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
hosts:
- host: whoogle.example.com
paths:
- path: /
pathType: ImplementationSpecific
tls: []
# - secretName: chart-example-tls
# hosts:
# - whoogle.example.com
resources: {}
# requests:
# cpu: 100m
# memory: 128Mi
# limits:
# cpu: 100m
# memory: 128Mi
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 100
targetCPUUtilizationPercentage: 80
# targetMemoryUtilizationPercentage: 80
nodeSelector: {}
tolerations: []
affinity: {}

View File

@ -32,12 +32,14 @@ services:
# Site alternative configurations, uncomment to enable # Site alternative configurations, uncomment to enable
# Note: If not set, the feature will still be available # Note: If not set, the feature will still be available
# with default values. # with default values.
#- WHOOGLE_ALT_TW=nitter.net #- WHOOGLE_ALT_TW=farside.link/nitter
#- WHOOGLE_ALT_YT=invidious.snopyta.org #- WHOOGLE_ALT_YT=farside.link/invidious
#- WHOOGLE_ALT_IG=bibliogram.art/u #- WHOOGLE_ALT_IG=farside.link/bibliogram/u
#- WHOOGLE_ALT_RD=libredd.it #- WHOOGLE_ALT_RD=farside.link/libreddit
#- WHOOGLE_ALT_MD=farside.link/scribe
#- WHOOGLE_ALT_TL=lingva.ml #- WHOOGLE_ALT_TL=lingva.ml
#- WHOOGLE_ALT_MD=scribe.rip #- WHOOGLE_ALT_IMG=imgin.voidnet.tech
#- WHOOGLE_ALT_WIKI=wikiless.org
#env_file: # Alternatively, load variables from whoogle.env #env_file: # Alternatively, load variables from whoogle.env
#- whoogle.env #- whoogle.env
ports: ports:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 KiB

BIN
docs/screenshot_desktop.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

BIN
docs/screenshot_mobile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

9
misc/instances.txt Normal file
View File

@ -0,0 +1,9 @@
https://s.alefvanoon.xyz
https://search.albony.xyz
https://search.exonip.de
https://search.garudalinux.org
https://search.sethforprivacy.com
https://whoogle.fossho.st
https://whooglesearch.net
https://www.whooglesearch.ml
https://whoogle.dcs0.hu

View File

@ -3,5 +3,9 @@
if [ "$(whoami)" != "root" ]; then if [ "$(whoami)" != "root" ]; then
tor -f /etc/tor/torrc tor -f /etc/tor/torrc
else else
if (grep alpine /etc/os-release >/dev/null); then
rc-service tor start
else
service tor start service tor start
fi
fi fi

View File

@ -7,7 +7,7 @@ chardet==3.0.4
click==8.0.3 click==8.0.3
cryptography==3.3.2 cryptography==3.3.2
Flask==1.1.1 Flask==1.1.1
Flask-Session==0.3.2 Flask-Session==0.4.0
idna==2.9 idna==2.9
itsdangerous==1.1.0 itsdangerous==1.1.0
Jinja2==2.11.3 Jinja2==2.11.3
@ -17,7 +17,7 @@ packaging==20.4
pluggy==0.13.1 pluggy==0.13.1
py==1.10.0 py==1.10.0
pycodestyle==2.6.0 pycodestyle==2.6.0
pycparser==2.19 pycparser==2.21
pyOpenSSL==19.1.0 pyOpenSSL==19.1.0
pyparsing==2.4.7 pyparsing==2.4.7
PySocks==1.7.1 PySocks==1.7.1

View File

@ -13,7 +13,7 @@ setuptools.setup(
author='Ben Busby', author='Ben Busby',
author_email='contact@benbusby.com', author_email='contact@benbusby.com',
name='whoogle-search', name='whoogle-search',
version='0.6.0' + optional_dev_tag, version='0.7.0' + optional_dev_tag,
include_package_data=True, include_package_data=True,
install_requires=requirements, install_requires=requirements,
description='Self-hosted, ad-free, privacy-respecting metasearch engine', description='Self-hosted, ad-free, privacy-respecting metasearch engine',

View File

@ -9,7 +9,7 @@ demo_config = {
'nojs': str(random.getrandbits(1)), 'nojs': str(random.getrandbits(1)),
'lang_interface': random.choice(app.config['LANGUAGES'])['value'], 'lang_interface': random.choice(app.config['LANGUAGES'])['value'],
'lang_search': random.choice(app.config['LANGUAGES'])['value'], 'lang_search': random.choice(app.config['LANGUAGES'])['value'],
'ctry': random.choice(app.config['COUNTRIES'])['value'] 'country': random.choice(app.config['COUNTRIES'])['value']
} }

View File

@ -1,12 +1,16 @@
from app.models.endpoint import Endpoint
def test_autocomplete_get(client): def test_autocomplete_get(client):
rv = client.get('/autocomplete?q=green+eggs+and') rv = client.get(f'/{Endpoint.autocomplete}?q=green+eggs+and')
assert rv._status_code == 200 assert rv._status_code == 200
assert len(rv.data) >= 1 assert len(rv.data) >= 1
assert b'green eggs and ham' in rv.data assert b'green eggs and ham' in rv.data
def test_autocomplete_post(client): def test_autocomplete_post(client):
rv = client.post('/autocomplete', data=dict(q='the+cat+in+the')) rv = client.post(f'/{Endpoint.autocomplete}',
data=dict(q='the+cat+in+the'))
assert rv._status_code == 200 assert rv._status_code == 200
assert len(rv.data) >= 1 assert len(rv.data) >= 1
assert b'the cat in the hat' in rv.data assert b'the cat in the hat' in rv.data

View File

@ -1,6 +1,7 @@
from cryptography.fernet import Fernet from cryptography.fernet import Fernet
from app import app from app import app
from app.models.endpoint import Endpoint
from app.utils.session import generate_user_key, valid_user_session from app.utils.session import generate_user_key, valid_user_session
@ -37,13 +38,13 @@ def test_query_decryption(client):
rv = client.get('/') rv = client.get('/')
cookie = rv.headers['Set-Cookie'] cookie = rv.headers['Set-Cookie']
rv = client.get('/search?q=test+1', headers={'Cookie': cookie}) rv = client.get(f'/{Endpoint.search}?q=test+1', headers={'Cookie': cookie})
assert rv._status_code == 200 assert rv._status_code == 200
with client.session_transaction() as session: with client.session_transaction() as session:
assert valid_user_session(session) assert valid_user_session(session)
rv = client.get('/search?q=test+2', headers={'Cookie': cookie}) rv = client.get(f'/{Endpoint.search}?q=test+2', headers={'Cookie': cookie})
assert rv._status_code == 200 assert rv._status_code == 200
with client.session_transaction() as session: with client.session_transaction() as session:

View File

@ -1,5 +1,7 @@
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from app.filter import Filter from app.filter import Filter
from app.models.config import Config
from app.models.endpoint import Endpoint
from app.utils.session import generate_user_key from app.utils.session import generate_user_key
from datetime import datetime from datetime import datetime
from dateutil.parser import * from dateutil.parser import *
@ -10,7 +12,7 @@ from test.conftest import demo_config
def get_search_results(data): def get_search_results(data):
secret_key = generate_user_key() secret_key = generate_user_key()
soup = Filter(user_key=secret_key).clean( soup = Filter(user_key=secret_key, config=Config(**demo_config)).clean(
BeautifulSoup(data, 'html.parser')) BeautifulSoup(data, 'html.parser'))
main_divs = soup.find('div', {'id': 'main'}) main_divs = soup.find('div', {'id': 'main'})
@ -30,7 +32,7 @@ def get_search_results(data):
def test_get_results(client): def test_get_results(client):
rv = client.get('/search?q=test') rv = client.get(f'/{Endpoint.search}?q=test')
assert rv._status_code == 200 assert rv._status_code == 200
# Depending on the search, there can be more # Depending on the search, there can be more
@ -41,7 +43,7 @@ def test_get_results(client):
def test_post_results(client): def test_post_results(client):
rv = client.post('/search', data=dict(q='test')) rv = client.post(f'/{Endpoint.search}', data=dict(q='test'))
assert rv._status_code == 200 assert rv._status_code == 200
# Depending on the search, there can be more # Depending on the search, there can be more
@ -52,7 +54,7 @@ def test_post_results(client):
def test_translate_search(client): def test_translate_search(client):
rv = client.post('/search', data=dict(q='translate hola')) rv = client.post(f'/{Endpoint.search}', data=dict(q='translate hola'))
assert rv._status_code == 200 assert rv._status_code == 200
# Pretty weak test, but better than nothing # Pretty weak test, but better than nothing
@ -62,7 +64,7 @@ def test_translate_search(client):
def test_block_results(client): def test_block_results(client):
rv = client.post('/search', data=dict(q='pinterest')) rv = client.post(f'/{Endpoint.search}', data=dict(q='pinterest'))
assert rv._status_code == 200 assert rv._status_code == 200
has_pinterest = False has_pinterest = False
@ -74,28 +76,17 @@ def test_block_results(client):
assert has_pinterest assert has_pinterest
demo_config['block'] = 'pinterest.com' demo_config['block'] = 'pinterest.com'
rv = client.post('/config', data=demo_config) rv = client.post(f'/{Endpoint.config}', data=demo_config)
assert rv._status_code == 302 assert rv._status_code == 302
rv = client.post('/search', data=dict(q='pinterest')) rv = client.post(f'/{Endpoint.search}', data=dict(q='pinterest'))
assert rv._status_code == 200 assert rv._status_code == 200
for link in BeautifulSoup(rv.data, 'html.parser').find_all('a', href=True): for link in BeautifulSoup(rv.data, 'html.parser').find_all('a', href=True):
assert 'pinterest.com' not in urlparse(link['href']).netloc result_site = urlparse(link['href']).netloc
if not result_site:
continue
# TODO: Unit test the site alt method instead -- the results returned assert result_site not in 'pinterest.com'
# are too unreliable for this test in particular.
# def test_site_alts(client):
# rv = client.post('/search', data=dict(q='twitter official account'))
# assert b'twitter.com/Twitter' in rv.data
# client.post('/config', data=dict(alts=True))
# assert json.loads(client.get('/config').data)['alts']
# rv = client.post('/search', data=dict(q='twitter official account'))
# assert b'twitter.com/Twitter' not in rv.data
# assert b'nitter.net/Twitter' in rv.data
def test_recent_results(client): def test_recent_results(client):
@ -106,7 +97,7 @@ def test_recent_results(client):
} }
for time, num_days in times.items(): for time, num_days in times.items():
rv = client.post('/search', data=dict(q='test :' + time)) rv = client.post(f'/{Endpoint.search}', data=dict(q='test :' + time))
result_divs = get_search_results(rv.data) result_divs = get_search_results(rv.data)
current_date = datetime.now() current_date = datetime.now()

View File

@ -1,4 +1,5 @@
from app import app from app import app
from app.models.endpoint import Endpoint
import json import json
@ -11,47 +12,47 @@ def test_main(client):
def test_search(client): def test_search(client):
rv = client.get('/search?q=test') rv = client.get(f'/{Endpoint.search}?q=test')
assert rv._status_code == 200 assert rv._status_code == 200
def test_feeling_lucky(client): def test_feeling_lucky(client):
rv = client.get('/search?q=!%20test') rv = client.get(f'/{Endpoint.search}?q=!%20test')
assert rv._status_code == 303 assert rv._status_code == 303
def test_ddg_bang(client): def test_ddg_bang(client):
# Bang at beginning of query # Bang at beginning of query
rv = client.get('/search?q=!gh%20whoogle') rv = client.get(f'/{Endpoint.search}?q=!gh%20whoogle')
assert rv._status_code == 302 assert rv._status_code == 302
assert rv.headers.get('Location').startswith('https://github.com') assert rv.headers.get('Location').startswith('https://github.com')
# Move bang to end of query # Move bang to end of query
rv = client.get('/search?q=github%20!w') rv = client.get(f'/{Endpoint.search}?q=github%20!w')
assert rv._status_code == 302 assert rv._status_code == 302
assert rv.headers.get('Location').startswith('https://en.wikipedia.org') assert rv.headers.get('Location').startswith('https://en.wikipedia.org')
# Move bang to middle of query # Move bang to middle of query
rv = client.get('/search?q=big%20!r%20chungus') rv = client.get(f'/{Endpoint.search}?q=big%20!r%20chungus')
assert rv._status_code == 302 assert rv._status_code == 302
assert rv.headers.get('Location').startswith('https://www.reddit.com') assert rv.headers.get('Location').startswith('https://www.reddit.com')
# Move '!' to end of the bang # Move '!' to end of the bang
rv = client.get('/search?q=gitlab%20w!') rv = client.get(f'/{Endpoint.search}?q=gitlab%20w!')
assert rv._status_code == 302 assert rv._status_code == 302
assert rv.headers.get('Location').startswith('https://en.wikipedia.org') assert rv.headers.get('Location').startswith('https://en.wikipedia.org')
# Ensure bang is case insensitive # Ensure bang is case insensitive
rv = client.get('/search?q=!GH%20whoogle') rv = client.get(f'/{Endpoint.search}?q=!GH%20whoogle')
assert rv._status_code == 302 assert rv._status_code == 302
assert rv.headers.get('Location').startswith('https://github.com') assert rv.headers.get('Location').startswith('https://github.com')
def test_config(client): def test_config(client):
rv = client.post('/config', data=demo_config) rv = client.post(f'/{Endpoint.config}', data=demo_config)
assert rv._status_code == 302 assert rv._status_code == 302
rv = client.get('/config') rv = client.get(f'/{Endpoint.config}')
assert rv._status_code == 200 assert rv._status_code == 200
config = json.loads(rv.data) config = json.loads(rv.data)
@ -62,15 +63,15 @@ def test_config(client):
app.config['CONFIG_DISABLE'] = 1 app.config['CONFIG_DISABLE'] = 1
dark_mod = not demo_config['dark'] dark_mod = not demo_config['dark']
demo_config['dark'] = dark_mod demo_config['dark'] = dark_mod
rv = client.post('/config', data=demo_config) rv = client.post(f'/{Endpoint.config}', data=demo_config)
assert rv._status_code == 403 assert rv._status_code == 403
rv = client.get('/config') rv = client.get(f'/{Endpoint.config}')
config = json.loads(rv.data) config = json.loads(rv.data)
assert config['dark'] != dark_mod assert config['dark'] != dark_mod
def test_opensearch(client): def test_opensearch(client):
rv = client.get('/opensearch.xml') rv = client.get(f'/{Endpoint.opensearch}')
assert rv._status_code == 200 assert rv._status_code == 200
assert '<ShortName>Whoogle</ShortName>' in str(rv.data) assert '<ShortName>Whoogle</ShortName>' in str(rv.data)

View File

@ -7,25 +7,28 @@
# - docker-compose: Uncomment the env_file option # - docker-compose: Uncomment the env_file option
# - docker: Add "--env-file ./whoogle.env" to your build command # - docker: Add "--env-file ./whoogle.env" to your build command
#WHOOGLE_ALT_TW=nitter.net #WHOOGLE_ALT_TW=farside.link/nitter
#WHOOGLE_ALT_YT=invidious.snopyta.org #WHOOGLE_ALT_YT=farside.link/invidious
#WHOOGLE_ALT_IG=bibliogram.art/u #WHOOGLE_ALT_IG=farside.link/bibliogram/u
#WHOOGLE_ALT_RD=libredd.it #WHOOGLE_ALT_RD=farside.link/libreddit
#WHOOGLE_ALT_MD=farside.link/scribe
#WHOOGLE_ALT_TL=lingva.ml #WHOOGLE_ALT_TL=lingva.ml
#WHOOGLE_ALT_MD=scribe.rip #WHOOGLE_ALT_IMG=imgin.voidnet.tech
#WHOOGLE_ALT_WIKI=wikiless.org
#WHOOGLE_USER="" #WHOOGLE_USER=""
#WHOOGLE_PASS="" #WHOOGLE_PASS=""
#WHOOGLE_PROXY_USER="" #WHOOGLE_PROXY_USER=""
#WHOOGLE_PROXY_PASS="" #WHOOGLE_PROXY_PASS=""
#WHOOGLE_PROXY_TYPE="" #WHOOGLE_PROXY_TYPE=""
#WHOOGLE_PROXY_LOC="" #WHOOGLE_PROXY_LOC=""
#WHOOGLE_CSP=1
#HTTPS_ONLY=1 #HTTPS_ONLY=1
# Restrict results to only those near a particular city # Restrict results to only those near a particular city
#WHOOGLE_CONFIG_NEAR=denver #WHOOGLE_CONFIG_NEAR=denver
# See app/static/settings/countries.json for values # See app/static/settings/countries.json for values
#WHOOGLE_CONFIG_COUNTRY=countryUK #WHOOGLE_CONFIG_COUNTRY=US
# See app/static/settings/languages.json for values # See app/static/settings/languages.json for values
#WHOOGLE_CONFIG_LANGUAGE=lang_en #WHOOGLE_CONFIG_LANGUAGE=lang_en
@ -60,6 +63,18 @@
# Search using GET requests only (exposes query in logs) # Search using GET requests only (exposes query in logs)
#WHOOGLE_CONFIG_GET_ONLY=1 #WHOOGLE_CONFIG_GET_ONLY=1
# Remove everything except basic result cards from all search queries
#WHOOGLE_MINIMAL=0
# Set the number of results per page
#WHOOGLE_RESULTS_PER_PAGE=10
# Controls visibility of autocomplete/search suggestions
#WHOOGLE_AUTOCOMPLETE=1
# The port where Whoogle will be exposed
#EXPOSE_PORT=5000
# Set instance URL # Set instance URL
#WHOOGLE_CONFIG_URL=https://<whoogle url>/ #WHOOGLE_CONFIG_URL=https://<whoogle url>/