Compare commits

..

No commits in common. "main" and "v0.5.0" have entirely different histories.
main ... v0.5.0

89 changed files with 1425 additions and 5875 deletions

View File

@ -1,3 +1,2 @@
.git/
venv/
test/

View File

@ -1,38 +0,0 @@
---
name: New theme
about: Create a new theme for Whoogle
title: "[THEME] <your theme name>"
labels: theme
assignees: benbusby
---
Use the following template to design your theme, replacing the blank spaces with the colors of your choice.
```css
:root {
/* LIGHT THEME COLORS */
--whoogle-logo: #______;
--whoogle-page-bg: #______;
--whoogle-element-bg: #______;
--whoogle-text: #______;
--whoogle-contrast-text: #______;
--whoogle-secondary-text: #______;
--whoogle-result-bg: #______;
--whoogle-result-title: #______;
--whoogle-result-url: #______;
--whoogle-result-visited: #______;
/* DARK THEME COLORS */
--whoogle-dark-logo: #______;
--whoogle-dark-page-bg: #______;
--whoogle-dark-element-bg: #______;
--whoogle-dark-text: #______;
--whoogle-dark-contrast-text: #______;
--whoogle-dark-secondary-text: #______;
--whoogle-dark-result-bg: #______;
--whoogle-dark-result-title: #______;
--whoogle-dark-result-url: #______;
--whoogle-dark-result-visited: #______;
}
```

View File

@ -1,22 +1,13 @@
name: buildx
on:
workflow_run:
workflows: ["docker_main"]
branches: [main]
types:
- completed
push:
tags:
- '*'
branches: develop
jobs:
on-success:
build:
runs-on: ubuntu-latest
steps:
- name: Wait for tests to succeed
if: ${{ github.event.workflow_run.conclusion != 'success' && startsWith(github.ref, 'refs/tags') != true }}
run: exit 1
- name: checkout code
uses: actions/checkout@v2
- name: install buildx
@ -24,36 +15,14 @@ jobs:
uses: crazy-max/ghaction-docker-buildx@v1
with:
version: latest
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to ghcr.io
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: log in to docker hub
run: |
echo "${{ secrets.DOCKER_PASSWORD }}" | \
docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
- name: build and push the image
if: startsWith(github.ref, 'refs/heads/main') && github.actor == 'benbusby'
run: |
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
docker buildx ls
docker buildx build --push \
--tag benbusby/whoogle-search:latest \
--platform linux/amd64,linux/arm/v7,linux/arm64 .
docker buildx build --push \
--tag ghcr.io/benbusby/whoogle-search:latest \
--platform linux/amd64,linux/arm/v7,linux/arm64 .
- name: build and push tag
if: startsWith(github.ref, 'refs/tags')
run: |
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
docker buildx ls
docker buildx build --push \
--tag benbusby/whoogle-search:${GITHUB_REF#refs/*/v}\
--platform linux/amd64,linux/arm/v7,linux/arm64 .
docker buildx build --push \
--tag ghcr.io/benbusby/whoogle-search:${GITHUB_REF#refs/*/v}\
--tag benbusby/whoogle-search:buildx-experimental \
--platform linux/amd64,linux/arm/v7,linux/arm64 .

View File

@ -1,28 +0,0 @@
name: docker_main
on:
workflow_run:
workflows: ["tests"]
branches: [main]
types:
- completed
# TODO: Needs refactoring to use reusable workflows and share w/ docker_tests
jobs:
on-success:
runs-on: ubuntu-latest
steps:
- name: checkout code
uses: actions/checkout@v2
- name: build and test (docker)
run: |
docker build --tag whoogle-search:test .
docker run --publish 5000:5000 --detach --name whoogle-search-nocompose whoogle-search:test
sleep 15
docker exec whoogle-search-nocompose curl -f http://localhost:5000/healthz || exit 1
- name: build and test (docker-compose)
run: |
docker rm -f whoogle-search-nocompose
WHOOGLE_IMAGE="whoogle-search:test" docker-compose up --detach
sleep 15
docker exec whoogle-search curl -f http://localhost:5000/healthz || exit 1

View File

@ -1,26 +0,0 @@
name: docker_tests
on:
push:
branches: main
pull_request:
branches: main
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: checkout code
uses: actions/checkout@v2
- name: build and test (docker)
run: |
docker build --tag whoogle-search:test .
docker run --publish 5000:5000 --detach --name whoogle-search-nocompose whoogle-search:test
sleep 15
docker exec whoogle-search-nocompose curl -f http://localhost:5000/healthz || exit 1
- name: build and test (docker-compose)
run: |
docker rm -f whoogle-search-nocompose
WHOOGLE_IMAGE="whoogle-search:test" docker-compose up --detach
sleep 15
docker exec whoogle-search curl -f http://localhost:5000/healthz || exit 1

22
.github/workflows/pep8.yml vendored Normal file
View File

@ -0,0 +1,22 @@
name: pep8
on:
push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pycodestyle
- name: Run pycodestyle
run: |
pycodestyle --show-source --show-pep8 app/*
pycodestyle --show-source --show-pep8 test/*

View File

@ -1,68 +0,0 @@
name: pypi
on:
push:
branches: main
tags: v*
jobs:
publish-test:
name: Build and publish to TestPyPI
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.9
uses: actions/setup-python@v1
with:
python-version: 3.9
- name: Install pypa/build
run: >-
python -m
pip install
build
setuptools
--user
- name: Set dev timestamp
run: echo "DEV_BUILD=$(date +%s)" >> $GITHUB_ENV
- name: Build binary wheel and source tarball
run: >-
python -m
build
--sdist
--wheel
--outdir dist/
.
- name: Publish distribution to TestPyPI
uses: pypa/gh-action-pypi-publish@master
with:
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
repository_url: https://test.pypi.org/legacy/
publish:
name: Build and publish to PyPI
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.9
uses: actions/setup-python@v1
with:
python-version: 3.9
- name: Install pypa/build
run: >-
python -m
pip install
build
--user
- name: Build binary wheel and source tarball
run: >-
python -m
build
--sdist
--wheel
--outdir dist/
.
- name: Publish distribution to PyPI
if: startsWith(github.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@master
with:
password: ${{ secrets.PYPI_API_TOKEN }}

View File

@ -1,19 +0,0 @@
name: scan
on:
schedule:
- cron: '0 0 * * *'
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build the container image
run: |
docker build --tag whoogle-search:test .
- name: Initiate grype scan
run: |
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b .
chmod +x ./grype
./grype whoogle-search:test --only-fixed

View File

@ -1,17 +0,0 @@
name: tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Install dependencies
run: pip install --upgrade pip && pip install -r requirements.txt
- name: Run tests
run: ./run test

6
.gitignore vendored
View File

@ -4,7 +4,6 @@ __pycache__/
*.pyc
*.pem
*.conf
*.key
config.json
test/static
flask_session/
@ -13,9 +12,6 @@ app/static/custom_config
app/static/bangs
# pip stuff
/build/
build/
dist/
*.egg-info/
# env
whoogle.env

View File

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

15
.travis.yml Normal file
View File

@ -0,0 +1,15 @@
language: python
python: 3.6
before_install:
- sudo apt-get -y install libgnutls28-dev
install:
- pip install -r requirements.txt
script:
- "./run test"
deploy:
provider: pypi
user: __token__
password:
secure: WNEH2Gg84MZF/AZEberFDGPPWb4cYyHAeD/XV8En94QRSI9Aznz6qiDKOvV4eVgjMAIEW5uB3TL1LHf6KU+Hrg6SmhF7JquqP1gsBOCDNFPTljO+k2Hc53uDdSnhi/HLgY7cnFNX4lc2nNrbyxZxMHuSA2oNz/tosyNGBEeyU+JA5va7uX0albGsLiNjimO4aeau83fsI0Hn2eN6ag68pewUMXNxzpyTeO2bRcCd5d5iILs07jMVwFoC2j7W11oNqrVuSWAs8CPe4+kwvNvXWxljUGiBGppNZ7RAsKNLwi6U6kGGUTWjQm09rY/2JBpJ2WEGmIWGIrno75iiFRbjnRp3mnXPvtVTyWhh+hQIUd7bJOVKM34i9eHotYTrkMJObgW1gnRzvI9VYldtgL/iP/Isn2Pv2EeMX8V+C9/8pxv0jkQkZMnFhE6gGlzpz37zTl04B2J7xyV5znM35Lx2Pn3zxdcmdCvD3yT8I4MuBbKqq2/v4emYCfPfOmfwnS0BEVSqr9lbx4xfUZV76tcvLcj4n86DJbx77pA2Ch8FRprpOOBcf0WuqTbZp8c3mb8prFp2EupUknXu7+C2VQ6sqrnzNuDeTGm/nyjjRQ81rlvlD4tqkwsEGEDDO44FF2eUTc5D2MvoHs4cnz095FWjy63gn5IxUjhMi31b5tGRz2Q=
on:
tags: true

View File

@ -1,73 +1,60 @@
FROM python:3.11.0a5-alpine as builder
FROM python:3.8-slim as builder
RUN apk --update add \
build-base \
RUN apt-get update && apt-get install -y \
build-essential \
libxml2-dev \
libxslt-dev \
openssl-dev \
libssl-dev \
libffi-dev
COPY requirements.txt .
RUN pip install --upgrade pip
RUN pip install --prefix /install --no-warn-script-location --no-cache-dir -r requirements.txt
FROM python:3.11.0a5-alpine
FROM python:3.8-slim
RUN apk add --update --no-cache tor curl openrc libstdc++
# libcurl4-openssl-dev
RUN apt-get update && apt-get install -y \
libcurl4-openssl-dev \
tor \
curl \
&& rm -rf /var/lib/apt/lists/*
RUN apk -U upgrade
ARG DOCKER_USER=whoogle
ARG DOCKER_USERID=927
ARG config_dir=/config
RUN mkdir -p $config_dir
RUN chmod a+w $config_dir
VOLUME $config_dir
ENV CONFIG_VOLUME=$config_dir
ARG url_prefix=''
ARG username=''
ENV WHOOGLE_USER=$username
ARG password=''
ARG proxyuser=''
ARG proxypass=''
ARG proxytype=''
ARG proxyloc=''
ARG whoogle_dotenv=''
ARG use_https=''
ARG whoogle_port=5000
ARG twitter_alt='farside.link/nitter'
ARG youtube_alt='farside.link/invidious'
ARG instagram_alt='farside.link/bibliogram/u'
ARG reddit_alt='farside.link/libreddit'
ARG medium_alt='farside.link/scribe'
ARG translate_alt='farside.link/lingva'
ARG imgur_alt='farside.link/rimgo'
ARG wikipedia_alt='farside.link/wikiless'
ARG imdb_alt='farside.link/libremdb'
ARG quora_alt='farside.link/quetre'
ENV WHOOGLE_PASS=$password
ENV CONFIG_VOLUME=$config_dir \
WHOOGLE_URL_PREFIX=$url_prefix \
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 \
WHOOGLE_ALT_IMDB=$imdb_alt \
WHOOGLE_ALT_QUORA=$quora_alt
ARG proxyuser=''
ENV WHOOGLE_PROXY_USER=$proxyuser
ARG proxypass=''
ENV WHOOGLE_PROXY_PASS=$proxypass
ARG proxytype=''
ENV WHOOGLE_PROXY_TYPE=$proxytype
ARG proxyloc=''
ENV WHOOGLE_PROXY_LOC=$proxyloc
ARG whoogle_dotenv=''
ENV WHOOGLE_DOTENV=$whoogle_dotenv
ARG use_https=''
ENV HTTPS_ONLY=$use_https
ARG whoogle_port=5000
ENV EXPOSE_PORT=$whoogle_port
ARG twitter_alt='nitter.net'
ENV WHOOGLE_ALT_TW=$twitter_alt
ARG youtube_alt='invidious.snopyta.org'
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
WORKDIR /whoogle
@ -76,22 +63,11 @@ COPY misc/tor/torrc /etc/tor/torrc
COPY misc/tor/start-tor.sh misc/tor/start-tor.sh
COPY app/ app/
COPY run .
#COPY whoogle.env .
# Create user/group to run as
RUN adduser -D -g $DOCKER_USERID -u $DOCKER_USERID $DOCKER_USER
# Fix ownership / permissions
RUN chown -R ${DOCKER_USER}:${DOCKER_USER} /whoogle /var/lib/tor
# Allow writing symlinks to build dir
RUN chown $DOCKER_USERID:$DOCKER_USERID app/static/build
USER $DOCKER_USER:$DOCKER_USER
COPY whoogle.env .
EXPOSE $EXPOSE_PORT
HEALTHCHECK --interval=30s --timeout=5s \
HEALTHCHECK --interval=30s --timeout=5s \
CMD curl -f http://localhost:${EXPOSE_PORT}/healthz || exit 1
CMD misc/tor/start-tor.sh & ./run

393
README.md
View File

@ -2,18 +2,11 @@
[![Latest Release](https://img.shields.io/github/v/release/benbusby/whoogle-search)](https://github.com/benbusby/shoogle/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![tests](https://github.com/benbusby/whoogle-search/actions/workflows/tests.yml/badge.svg)](https://github.com/benbusby/whoogle-search/actions/workflows/tests.yml)
[![buildx](https://github.com/benbusby/whoogle-search/actions/workflows/buildx.yml/badge.svg)](https://github.com/benbusby/whoogle-search/actions/workflows/buildx.yml)
[![Build Status](https://travis-ci.com/benbusby/whoogle-search.svg?branch=master)](https://travis-ci.com/benbusby/whoogle-search)
[![pep8](https://github.com/benbusby/whoogle-search/workflows/pep8/badge.svg)](https://github.com/benbusby/whoogle-search/actions?query=workflow%3Apep8)
[![codebeat badge](https://codebeat.co/badges/e96cada2-fb6f-4528-8285-7d72abd74e8d)](https://codebeat.co/projects/github-com-benbusby-shoogle-master)
[![Docker Pulls](https://img.shields.io/docker/pulls/benbusby/whoogle-search)](https://hub.docker.com/r/benbusby/whoogle-search)
<table>
<tr>
<td><a href="https://sr.ht/~benbusby/whoogle-search">SourceHut</a></td>
<td><a href="https://github.com/benbusby/whoogle-search">GitHub</a></td>
</tr>
</table>
Get Google search results, but without any ads, javascript, AMP links, cookies, or IP address tracking. Easily deployable in one click as a Docker app, and customizable with a single config file. Quick and simple to implement as a primary search engine replacement on both desktop and mobile.
Contents
@ -22,32 +15,29 @@ Contents
3. [Install/Deploy](#install)
1. [Heroku Quick Deploy](#a-heroku-quick-deploy)
2. [Repl.it](#b-replit)
3. [Fly.io](#c-flyio)
4. [pipx](#d-pipx)
5. [pip](#e-pip)
6. [Manual](#f-manual)
7. [Docker](#g-manual-docker)
8. [Arch/AUR](#arch-linux--arch-based-distributions)
9. [Helm/Kubernetes](#helm-chart-for-kubernetes)
3. [pipx](#c-pipx)
4. [pip](#d-pip)
5. [Manual](#e-manual)
6. [Docker](#f-manual-docker)
7. [Arch/AUR](#arch-linux--arch-based-distributions)
4. [Environment Variables and Configuration](#environment-variables)
5. [Usage](#usage)
6. [Extra Steps](#extra-steps)
1. [Set Primary Search Engine](#set-whoogle-as-your-primary-search-engine)
2. [Prevent Downtime (Heroku Only)](#prevent-downtime-heroku-only)
3. [Manual HTTPS Enforcement](#https-enforcement)
4. [Using with Firefox Containers](#using-with-firefox-containers)
5. [Reverse Proxying](#reverse-proxying)
1. [Nginx](#nginx)
7. [Contributing](#contributing)
8. [FAQ](#faq)
9. [Public Instances](#public-instances)
10. [Screenshots](#screenshots)
7. [FAQ](#faq)
8. [Public Instances](#public-instances)
9. [Screenshots](#screenshots)
10. Mirrors (read-only)
1. [GitLab](https://gitlab.com/benbusby/whoogle-search)
2. [Gogs](https://gogs.benbusby.com/benbusby/whoogle-search)
## Features
- No ads or sponsored content
- No JavaScript\*
- No cookies\*\*
- No tracking/linking of your personal IP address\*\*\*
- No javascript
- No cookies
- No tracking/linking of your personal IP address\*
- No AMP links
- No URL tracking tags (i.e. utm=%s)
- No referrer header
@ -55,18 +45,14 @@ Contents
- Autocomplete/search suggestions
- POST request search and suggestion queries (when possible)
- View images at full res without site redirect (currently mobile only)
- Light/Dark/System theme modes (with support for [custom CSS theming](https://github.com/benbusby/whoogle-search/wiki/User-Contributed-CSS-Themes))
- Dark mode
- Randomly generated User Agent
- Easy to install/deploy
- DDG-style bang (i.e. `!<tag> <query>`) searches
- Optional location-based searching (i.e. results near \<city\>)
- Optional NoJS mode to view search results in a separate window with JavaScript blocked
- Optional NoJS mode to disable all Javascript in results
<sup>*No third party JavaScript. Whoogle can be used with JavaScript disabled, but if enabled, uses JavaScript for things like presenting search suggestions.</sup>
<sup>**No third party cookies. Whoogle uses server side cookies (sessions) to store non-sensitive configuration settings such as theme, language, etc. Just like with JavaScript, cookies can be disabled and not affect Whoogle's search functionality.</sup>
<sup>***If deployed to a remote server, or configured to send requests through a VPN, Tor, proxy, etc.</sup>
<sup>*If deployed to a remote server, or configured to send requests through a VPN, Tor, proxy, etc.</sup>
## Dependencies
If using Heroku Quick Deploy, **you can skip this section**.
@ -83,17 +69,15 @@ If using Heroku Quick Deploy, **you can skip this section**.
There are a few different ways to begin using the app, depending on your preferences:
### A) [Heroku Quick Deploy](https://heroku.com/about)
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/benbusby/whoogle-search/tree/main)
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/benbusby/whoogle-search/tree/heroku-app-beta)
*Note: Requires a (free) Heroku account*
Provides:
- Free deployment of app
- Free HTTPS url (https://\<your app name\>.herokuapp.com)
- Downtime after periods of inactivity \([solution](https://github.com/benbusby/whoogle-search#prevent-downtime-heroku-only)\)
Notes:
- Requires a (free) Heroku account
- Sometimes has issues with auto-redirecting to `https`. Make sure to navigate to the `https` version of your app before adding as a default search engine.
### B) [Repl.it](https://repl.it)
[![Run on Repl.it](https://repl.it/badge/github/benbusby/whoogle-search)](https://repl.it/github/benbusby/whoogle-search)
@ -105,22 +89,7 @@ Provides:
- Supports custom domains
- Downtime after periods of inactivity \([solution 1](https://repl.it/talk/ask/use-this-pingmat1replco-just-enter/28821/101298), [solution 2](https://repl.it/talk/learn/How-to-use-and-setup-UptimeRobot/9003)\)
### C) [Fly.io](https://fly.io)
You will need a **PAID** [Fly.io](https://fly.io) account to deploy Whoogle.
#### Install the CLI: https://fly.io/docs/hands-on/installing/
#### Deploy the app
```bash
flyctl auth login
flyctl launch --image benbusby/whoogle-search:latest
```
Your app is now available at `https://<app-name>.fly.dev`.
### D) [pipx](https://github.com/pipxproject/pipx#install-pipx)
### C) [pipx](https://github.com/pipxproject/pipx#install-pipx)
Persistent install:
`pipx install git+https://github.com/benbusby/whoogle-search.git`
@ -129,7 +98,7 @@ Sandboxed temporary instance:
`pipx run --spec git+https://github.com/benbusby/whoogle-search.git whoogle-search`
### E) pip
### D) pip
`pip install whoogle-search`
```bash
@ -156,10 +125,7 @@ optional arguments:
```
See the [available environment variables](#environment-variables) for additional configuration.
### F) Manual
*Note: `Content-Security-Policy` headers can be sent by Whoogle if you set `WHOOGLE_CSP`.*
### E) Manual
Clone the repo and run the following commands to start the app in a local-only environment:
```bash
@ -173,9 +139,9 @@ pip install -r requirements.txt
See the [available environment variables](#environment-variables) for additional configuration.
#### systemd Configuration
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:
After building the virtual environment, you can add the following to `/lib/systemd/system/whoogle.service` to set up a Whoogle Search systemd service:
```ini
```
[Unit]
Description=Whoogle
@ -186,33 +152,21 @@ Description=Whoogle
# Proxy configuration, uncomment to enable
#Environment=WHOOGLE_PROXY_USER=<proxy username>
#Environment=WHOOGLE_PROXY_PASS=<proxy password>
#Environment=WHOOGLE_PROXY_TYPE=<proxy type (http|https|proxy4|proxy5)
#Environment=WHOOGLE_PROXY_TYPE=<proxy type (http|proxy4|proxy5)
#Environment=WHOOGLE_PROXY_LOC=<proxy host/ip>
# Site alternative configurations, uncomment to enable
# Note: If not set, the feature will still be available
# with default values.
#Environment=WHOOGLE_ALT_TW=farside.link/nitter
#Environment=WHOOGLE_ALT_YT=farside.link/invidious
#Environment=WHOOGLE_ALT_IG=farside.link/bibliogram/u
#Environment=WHOOGLE_ALT_RD=farside.link/libreddit
#Environment=WHOOGLE_ALT_MD=farside.link/scribe
#Environment=WHOOGLE_ALT_TL=farside.link/lingva
#Environment=WHOOGLE_ALT_IMG=farside.link/rimgo
#Environment=WHOOGLE_ALT_WIKI=farside.link/wikiless
#Environment=WHOOGLE_ALT_IMDB=farside.link/libremdb
#Environment=WHOOGLE_ALT_QUORA=farside.link/quetre
# with default values.
#Environment=WHOOGLE_ALT_TW=nitter.net
#Environment=WHOOGLE_ALT_YT=invidious.snopyta.org
#Environment=WHOOGLE_ALT_IG=bibliogram.art/u
#Environment=WHOOGLE_ALT_RD=libredd.it
# Load values from dotenv only
#Environment=WHOOGLE_DOTENV=1
Type=simple
User=<username>
# If installed as a package, add:
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
User=root
WorkingDirectory=<whoogle_directory>
ExecStart=<whoogle_directory>/venv/bin/python3 -um app --host 0.0.0.0 --port 5000
ExecReload=/bin/kill -HUP $MAINPID
Restart=always
RestartSec=3
@ -228,52 +182,7 @@ sudo systemctl enable whoogle
sudo systemctl start whoogle
```
#### Tor Configuration *optional*
If routing your request through Tor you will need to make the following adjustments.
Due to the nature of interacting with Google through Tor we will need to be able to send signals to Tor and therefore authenticate with it.
There are two authentication methods, password and cookie. You will need to make changes to your torrc:
* Cookie
1. Uncomment or add the following lines in your torrc:
- `ControlPort 9051`
- `CookieAuthentication 1`
- `DataDirectoryGroupReadable 1`
- `CookieAuthFileGroupReadable 1`
2. Make the tor auth cookie readable:
- This is assuming that you are using a dedicated user to run whoogle. If you are using a different user replace `whoogle` with that user.
1. `chmod tor:whoogle /var/lib/tor`
2. `chmod tor:whoogle /var/lib/tor/control_auth_cookie`
3. Restart the tor service:
- `systemctl restart tor`
4. Set the Tor environment variable to 1, `WHOOGLE_CONFIG_TOR`. Refer to the [Environment Variables](#environment-variables) section for more details.
- This may be added in the systemd unit file or env file `WHOOGLE_CONFIG_TOR=1`
* Password
1. Run this command:
- `tor --hash-password {Your Password Here}`; put your password in place of `{Your Password Here}`.
- Keep the output of this command, you will be placing it in your torrc.
- Keep the password input of this command, you will be using it later.
2. Uncomment or add the following lines in your torrc:
- `ControlPort 9051`
- `HashedControlPassword {Place output here}`; put the output of the previous command in place of `{Place output here}`.
3. Now take the password from the first step and place it in the control.conf file within the whoogle working directory, ie. [misc/tor/control.conf](misc/tor/control.conf)
- If you want to place your password file in a different location set this location with the `WHOOGLE_TOR_CONF` environment variable. Refer to the [Environment Variables](#environment-variables) section for more details.
4. Heavily restrict access to control.conf to only be readable by the user running whoogle:
- `chmod 400 control.conf`
5. Finally set the Tor environment variable and use password variable to 1, `WHOOGLE_CONFIG_TOR` and `WHOOGLE_TOR_USE_PASS`. Refer to the [Environment Variables](#environment-variables) section for more details.
- These may be added to the systemd unit file or env file:
- `WHOOGLE_CONFIG_TOR=1`
- `WHOOGLE_TOR_USE_PASS=1`
### G) Manual (Docker)
### F) Manual (Docker)
1. Ensure the Docker daemon is running, and is accessible by your user account
- To add user permissions, you can execute `sudo usermod -aG docker yourusername`
- Running `docker ps` should return something besides an error. If you encounter an error saying the daemon isn't running, try `sudo systemctl start docker` (Linux) or ensure the docker tool is running (Windows/macOS).
@ -281,6 +190,8 @@ There are two authentication methods, password and cookie. You will need to make
#### Docker CLI
***Note:** For ARM machines, use the `buildx-experimental` Docker tag.*
Through Docker Hub:
```bash
docker pull benbusby/whoogle-search
@ -337,13 +248,6 @@ You may also edit environment variables from your apps Settings tab in the He
#### Arch Linux & Arch-based Distributions
There is an [AUR package available](https://aur.archlinux.org/packages/whoogle-git/), as well as a pre-built and daily updated package available at [Chaotic-AUR](https://chaotic.cx).
#### Helm chart for Kubernetes
To use the Kubernetes Helm Chart:
1. Ensure you have [Helm](https://helm.sh/docs/intro/install/) `>=3.0.0` installed
2. Clone this repository
3. Update [charts/whoogle/values.yaml](./charts/whoogle/values.yaml) as desired
4. Run `helm install whoogle ./charts/whoogle`
#### Using your own server, or alternative container deployment
There are other methods for deploying docker containers that are well outlined in [this article](https://rollout.io/blog/the-shortlist-of-docker-hosting/), but there are too many to describe set up for each here. Generally it should be about the same amount of effort as the Heroku deployment.
@ -360,58 +264,41 @@ There are a few optional environment variables available for customizing a Whoog
- With `docker-compose`: Uncomment the `env_file` option
- With `docker build/run`: Add `--env-file ./whoogle.env` to your command
| Variable | Description |
| -------------------- | ----------------------------------------------------------------------------------------- |
| WHOOGLE_URL_PREFIX | The URL prefix to use for the whoogle instance (i.e. "/whoogle") |
| WHOOGLE_DOTENV | Load environment variables in `whoogle.env` |
| WHOOGLE_USER | The username for basic auth. WHOOGLE_PASS must also be set if used. |
| WHOOGLE_PASS | The password for basic auth. WHOOGLE_USER must also be set if used. |
| WHOOGLE_PROXY_USER | The username of the proxy server. |
| WHOOGLE_PROXY_PASS | The password of the proxy server. |
| WHOOGLE_PROXY_TYPE | The type of the proxy server. Can be "socks5", "socks4", or "http". |
| WHOOGLE_PROXY_LOC | The location of the proxy server (host or ip). |
| EXPOSE_PORT | The port where Whoogle will be exposed. |
| HTTPS_ONLY | Enforce HTTPS. (See [here](https://github.com/benbusby/whoogle-search#https-enforcement)) |
| WHOOGLE_ALT_TW | The twitter.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
| WHOOGLE_ALT_YT | The youtube.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
| WHOOGLE_ALT_IG | The instagram.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
| WHOOGLE_ALT_RD | The reddit.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
| WHOOGLE_ALT_TL | The Google Translate alternative to use. This is used for all "translate ____" searches. Set to "" to disable. |
| WHOOGLE_ALT_MD | The medium.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
| WHOOGLE_ALT_IMG | The imgur.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
| WHOOGLE_ALT_WIKI | The wikipedia.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
| WHOOGLE_ALT_IMDB | The imdb.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
| WHOOGLE_ALT_QUORA | The quora.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
| WHOOGLE_AUTOCOMPLETE | Controls visibility of autocomplete/search suggestions. Default on -- use '0' to disable. |
| WHOOGLE_MINIMAL | Remove everything except basic result cards from all search queries. |
| WHOOGLE_CSP | Sets a default set of 'Content-Security-Policy' headers |
| WHOOGLE_RESULTS_PER_PAGE | Set the number of results per page |
| WHOOGLE_TOR_SERVICE | Enable/disable the Tor service on startup. Default on -- use '0' to disable. |
| WHOOGLE_TOR_USE_PASS | Use password authentication for tor control port. |
| WHOOGLE_TOR_CONF | The absolute path to the config file containing the password for the tor control port. Default: ./misc/tor/control.conf WHOOGLE_TOR_PASS must be 1 for this to work.|
| Variable | Description |
| ------------------ | ----------------------------------------------------------------------------------------- |
| 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. |
| HTTPS_ONLY | Enforce HTTPS. (See [here](https://github.com/benbusby/whoogle-search#https-enforcement)) |
| WHOOGLE_ALT_TW | The twitter.com alternative to use when site alternatives are enabled in the config. |
| WHOOGLE_ALT_YT | The youtube.com alternative to use when site alternatives are enabled in the config. |
| WHOOGLE_ALT_IG | The instagram.com alternative to use when site alternatives are enabled in the config. |
| WHOOGLE_ALT_RD | The reddit.com alternative to use when site alternatives are enabled in the config. |
### Config Environment Variables
These environment variables allow setting default config values, but can be overwritten manually by using the home page config menu. These allow a shortcut for destroying/rebuilding an instance to the same config state every time.
| Variable | Description |
| ------------------------------------ | --------------------------------------------------------------- |
| WHOOGLE_CONFIG_DISABLE | Hide config from UI and disallow changes to config by client |
| WHOOGLE_CONFIG_COUNTRY | Filter results by hosting country |
| WHOOGLE_CONFIG_LANGUAGE | Set interface language |
| WHOOGLE_CONFIG_SEARCH_LANGUAGE | Set search result language |
| WHOOGLE_CONFIG_BLOCK | Block websites from search results (use comma-separated list) |
| WHOOGLE_CONFIG_THEME | Set theme mode (light, dark, or system) |
| WHOOGLE_CONFIG_SAFE | Enable safe searches |
| WHOOGLE_CONFIG_ALTS | Use social media site alternatives (nitter, invidious, etc) |
| WHOOGLE_CONFIG_NEAR | Restrict results to only those near a particular city |
| WHOOGLE_CONFIG_TOR | Use Tor routing (if available) |
| WHOOGLE_CONFIG_NEW_TAB | Always open results in new tab |
| WHOOGLE_CONFIG_VIEW_IMAGE | Enable View Image option |
| WHOOGLE_CONFIG_GET_ONLY | Search using GET requests only |
| WHOOGLE_CONFIG_URL | The root url of the instance (`https://<your url>/`) |
| WHOOGLE_CONFIG_STYLE | The custom CSS to use for styling (should be single line) |
| WHOOGLE_CONFIG_PREFERENCES_ENCRYPTED | Encrypt preferences token, requires preferences key |
| WHOOGLE_CONFIG_PREFERENCES_KEY | Key to encrypt preferences in URL (REQUIRED to show url) |
| Variable | Description |
| ------------------------------ | --------------------------------------------------------------- |
| WHOOGLE_CONFIG_DISABLE | Hide config from UI and disallow changes to config by client |
| WHOOGLE_CONFIG_COUNTRY | Filter results by hosting country |
| WHOOGLE_CONFIG_LANGUAGE | Set interface language |
| WHOOGLE_CONFIG_SEARCH_LANGUAGE | Set search result language |
| WHOOGLE_CONFIG_BLOCK | Block websites from search results (use comma-separated list) |
| WHOOGLE_CONFIG_DARK | Enable dark theme |
| WHOOGLE_CONFIG_SAFE | Enable safe searches |
| WHOOGLE_CONFIG_ALTS | Use social media site alternatives (nitter, invidious, etc) |
| 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) |
## Usage
Same as most search engines, with the exception of filtering by time range.
@ -424,12 +311,7 @@ To filter by a range of time, append ":past <time>" to the end of your search, w
Browser settings:
- Firefox (Desktop)
- Version 89+
- Navigate to your app's url, right click the address bar, and select "Add Search Engine".
- Previous versions
- Navigate to your app's url, and click the 3 dot menu in the address bar. At the bottom, there should be an option to "Add Search Engine".
- Once you've added the new search engine, open your Firefox Preferences menu, click "Search" in the left menu, and use the available dropdown to select "Whoogle" from the list.
- **Note**: If your Whoogle instance uses Firefox Containers, you'll need to [go through the steps here](#using-with-firefox-containers) to get it working properly.
- Navigate to your app's url, and click the 3 dot menu in the address bar. At the bottom, there should be an option to "Add Search Engine". Once you've clicked this, open your Firefox Preferences menu, click "Search" in the left menu, and use the available dropdown to select "Whoogle" from the list.
- Firefox (iOS)
- In the mobile app Settings page, tap "Search" within the "General" section. There should be an option titled "Add Search Engine" to select. It should prompt you to enter a title and search query url - use the following elements to fill out the form:
- Title: "Whoogle"
@ -459,7 +341,7 @@ Browser settings:
2. Go to `Default Results` and click the `Setup fallback results` button. Click `+` and add Whoogle, then drag it to the top.
- Chrome/Chromium-based Browsers
- Automatic
- Visit the home page of your Whoogle Search instance -- this will automatically add the search engine if the [requirements](https://www.chromium.org/tab-to-search/) are met (GET request, no OnSubmit script, no path). If not, you can add it manually.
- Visit the home page of your Whoogle Search instance -- this may automatically add the search engine to your list of search engines. If not, you can add it manually.
- Manual
- Under search engines > manage search engines > add, manually enter your Whoogle instance details with a `<whoogle url>/search?q=%s` formatted search URL.
@ -483,90 +365,6 @@ Note: You should have your own domain name and [an https certificate](https://le
- Pip/Pipx: Add the `--https-only` flag to the end of the `whoogle-search` command
- Default `run` script: Modify the script locally to include the `--https-only` flag at the end of the python run command
### Using with Firefox Containers
Unfortunately, Firefox Containers do not currently pass through `POST` requests (the default) to the engine, and Firefox caches the opensearch template on initial page load. To get around this, you can take the following steps to get it working as expected:
1. Remove any existing Whoogle search engines from Firefox settings
2. Enable `GET Requests Only` in Whoogle config
3. Clear Firefox cache
4. Restart Firefox
5. Navigate to Whoogle instance and [re-add the engine](#set-whoogle-as-your-primary-search-engine)
### Reverse Proxying
#### Nginx
Here is a sample Nginx config for Whoogle:
```
server {
server_name your_domain_name.com;
access_log /dev/null;
error_log /dev/null;
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://localhost:5000;
}
}
```
You can then add SSL support using LetsEncrypt by following a guide such as [this one](https://www.nginx.com/blog/using-free-ssltls-certificates-from-lets-encrypt-with-nginx/).
## Contributing
Under the hood, Whoogle is a basic Flask app with the following structure:
- `app/`
- `routes.py`: Primary app entrypoint, contains all API routes
- `request.py`: Handles all outbound requests, including proxied/Tor connectivity
- `filter.py`: Functions and utilities used for filtering out content from upstream Google search results
- `utils/`
- `bangs.py`: All logic related to handling DDG-style "bang" queries
- `results.py`: Utility functions for interpreting/modifying individual search results
- `search.py`: Creates and handles new search queries
- `session.py`: Miscellaneous methods related to user sessions
- `templates/`
- `index.html`: The home page template
- `display.html`: The search results template
- `header.html`: A general "top of the page" query header for desktop and mobile
- `search.html`: An iframe-able search page
- `logo.html`: A template consisting mostly of the Whoogle logo as an SVG (separated to help keep `index.html` a bit cleaner)
- `opensearch.xml`: A template used for supporting [OpenSearch](https://developer.mozilla.org/en-US/docs/Web/OpenSearch).
- `imageresults.html`: An "experimental" template used for supporting the "Full Size" image feature on desktop.
- `static/<css|js>`
- CSS/Javascript files, should be self-explanatory
- `static/settings`
- Key-value JSON files for establishing valid configuration values
If you're new to the project, the easiest way to get started would be to try fixing [an open bug report](https://github.com/benbusby/whoogle-search/issues?q=is%3Aissue+is%3Aopen+label%3Abug). If there aren't any open, or if the open ones are too stale, try taking on a [feature request](https://github.com/benbusby/whoogle-search/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement). Generally speaking, if you can write something that has any potential of breaking down in the future, you should write a test for it.
The project follows the [PEP 8 Style Guide](https://www.python.org/dev/peps/pep-0008/), but is liable to change. Static typing should always be used when possible. Function documentation is greatly appreciated, and typically follows the below format:
```python
def contains(x: list, y: int) -> bool:
"""Check a list (x) for the presence of an element (y)
Args:
x: The list to inspect
y: The int to look for
Returns:
bool: True if the list contains the item, otherwise False
"""
return y in x
```
#### Translating
Whoogle currently supports translations using [`translations.json`](https://github.com/benbusby/whoogle-search/blob/main/app/static/settings/translations.json). Language values in this file need to match the "value" of the according language in [`languages.json`](https://github.com/benbusby/whoogle-search/blob/main/app/static/settings/languages.json) (i.e. "lang_en" for English, "lang_es" for Spanish, etc). After you add a new set of translations to `translations.json`, open a PR with your changes and they will be merged in as soon as possible.
## FAQ
**What's the difference between this and [Searx](https://github.com/asciimoo/searx)?**
@ -582,45 +380,18 @@ A lot of the app currently piggybacks on Google's existing support for fetching
## Public Instances
*Note: Use public instances at your own discretion. The maintainers of Whoogle do not personally validate the integrity of any other instances. Popular public instances are more likely to be rate-limited or blocked.*
| Website | Country | Language | Cloudflare |
|-|-|-|-|
| [https://search.albony.xyz](https://search.albony.xyz/) | 🇮🇳 IN | Multi-choice | |
| [https://search.garudalinux.org](https://search.garudalinux.org) | 🇫🇮 FI | Multi-choice | ✅ |
| [https://search.dr460nf1r3.org](https://search.dr460nf1r3.org) | 🇩🇪 DE | Multi-choice | ✅ |
| [https://s.tokhmi.xyz](https://s.tokhmi.xyz) | 🇺🇸 US | Multi-choice | ✅ |
| [https://www.whooglesearch.ml](https://www.whooglesearch.ml) | 🇺🇸 US | English | |
| [https://search.sethforprivacy.com](https://search.sethforprivacy.com) | 🇩🇪 DE | English | |
| [https://whoogle.dcs0.hu](https://whoogle.dcs0.hu) | 🇭🇺 HU | Multi-choice | |
| [https://whoogle.esmailelbob.xyz](https://whoogle.esmailelbob.xyz) | 🇨🇦 CA | Multi-choice | |
| [https://gowogle.voring.me](https://gowogle.voring.me) | 🇺🇸 US | Multi-choice | |
| [https://whoogle.lunar.icu](https://whoogle.lunar.icu) | 🇩🇪 DE | Multi-choice | ✅ |
| [https://whoogle.privacydev.net](https://whoogle.privacydev.net) | 🇺🇸 US | Multi-choice | |
| [https://search.wef.lol](https://search.wef.lol) | 🇮🇸 IC | Multi-choice | |
| [https://wg.vern.cc](https://wg.vern.cc) | 🇺🇸 US | English | |
* A checkmark in the "Cloudflare" category here refers to the use of the reverse proxy, [Cloudflare](https://cloudflare.com). The checkmark will not be listed for a site which uses Cloudflare DNS but rather the proxying service which grants Cloudflare the ability to monitor traffic to the website.
#### Onion Instances
| Website | Country | Language |
|-|-|-|
| [http://whoglqjdkgt2an4tdepberwqz3hk7tjo4kqgdnuj77rt7nshw2xqhqad.onion](http://whoglqjdkgt2an4tdepberwqz3hk7tjo4kqgdnuj77rt7nshw2xqhqad.onion) | 🇺🇸 US | Multi-choice
| [http://nuifgsnbb2mcyza74o7illtqmuaqbwu4flam3cdmsrnudwcmkqur37qd.onion](http://nuifgsnbb2mcyza74o7illtqmuaqbwu4flam3cdmsrnudwcmkqur37qd.onion) | 🇩🇪 DE | English
| [http://whoogle.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion](http://whoogle.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion/) | 🇺🇸 US | English |
#### I2P Instances
| Website | Country | Language |
|-|-|-|
| [http://verneks7rfjptpz5fpii7n7nrxilsidi2qxepeuuf66c3tsf4nhq.b32.i2p](http://verneks7rfjptpz5fpii7n7nrxilsidi2qxepeuuf66c3tsf4nhq.b32.i2p) | 🇺🇸 US | English |
*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.*
- [https://whoogle.sdf.org](https://whoogle.sdf.org)
- [https://whoogle.himiko.cloud](https://whoogle.himiko.cloud)
- [https://whoogle.kavin.rocks](https://whoogle.kavin.rocks) or [http://whoogledq5f5wly5p4i2ohnvjwlihnlg4oajjum2oeddfwqdwupbuhqd.onion](http://whoogledq5f5wly5p4i2ohnvjwlihnlg4oajjum2oeddfwqdwupbuhqd.onion)
- [https://search.garudalinux.org](https://search.garudalinux.org)
- [https://whooglesearch.net/](https://whooglesearch.net/)
- [https://search.flawcra.cc/](https://search.flawcra.cc/)
- [https://search.exonip.de/](https://search.exonip.de/)
## Screenshots
#### Desktop
![Whoogle Desktop](docs/screenshot_desktop.png)
![Whoogle Desktop](docs/screenshot_desktop.jpg)
#### Mobile
![Whoogle Mobile](docs/screenshot_mobile.png)
![Whoogle Mobile](docs/screenshot_mobile.jpg)

View File

@ -15,11 +15,6 @@
],
"stack": "container",
"env": {
"WHOOGLE_URL_PREFIX": {
"description": "The URL prefix to use for the whoogle instance (i.e. \"/whoogle\")",
"value": "",
"required": false
},
"WHOOGLE_USER": {
"description": "The username for basic auth. WHOOGLE_PASS must also be set if used. Leave empty to disable.",
"value": "",
@ -52,59 +47,24 @@
},
"WHOOGLE_ALT_TW": {
"description": "The site to use as a replacement for twitter.com when site alternatives are enabled in the config.",
"value": "farside.link/nitter",
"value": "nitter.net",
"required": false
},
"WHOOGLE_ALT_YT": {
"description": "The site to use as a replacement for youtube.com when site alternatives are enabled in the config.",
"value": "farside.link/invidious",
"value": "invidious.snopyta.org",
"required": false
},
"WHOOGLE_ALT_IG": {
"description": "The site to use as a replacement for instagram.com when site alternatives are enabled in the config.",
"value": "farside.link/bibliogram/u",
"value": "bibliogram.art/u",
"required": false
},
"WHOOGLE_ALT_RD": {
"description": "The site to use as a replacement for reddit.com when site alternatives are enabled in the config.",
"value": "farside.link/libreddit",
"value": "libredd.it",
"required": false
},
"WHOOGLE_ALT_MD": {
"description": "The site to use as a replacement for medium.com when site alternatives are enabled in the config.",
"value": "farside.link/scribe",
"required": false
},
"WHOOGLE_ALT_TL": {
"description": "The Google Translate alternative to use for all searches following the 'translate ___' structure.",
"value": "farside.link/lingva",
"required": false
},
"WHOOGLE_ALT_IMG": {
"description": "The site to use as a replacement for imgur.com when site alternatives are enabled in the config.",
"value": "farside.link/rimgo",
"required": false
},
"WHOOGLE_ALT_WIKI": {
"description": "The site to use as a replacement for wikipedia.com when site alternatives are enabled in the config.",
"value": "farside.link/wikiless",
"required": false
},
"WHOOGLE_ALT_IMDB": {
"description": "The site to use as a replacement for imdb.com when site alternatives are enabled in the config.",
"value": "farside.link/libremdb",
"required": false
},
"WHOOGLE_ALT_QUORA": {
"description": "The site to use as a replacement for quora.com when site alternatives are enabled in the config.",
"value": "farside.link/quetre",
"required": false
},
"WHOOGLE_MINIMAL": {
"description": "Remove everything except basic result cards from all search queries (set to 1 or leave blank)",
"value": "",
"required": false
},
"WHOOGLE_CONFIG_COUNTRY": {
"description": "[CONFIG] The country to use for restricting search results (use values from https://raw.githubusercontent.com/benbusby/whoogle-search/develop/app/static/settings/countries.json)",
"value": "",
@ -130,9 +90,9 @@
"value": "",
"required": false
},
"WHOOGLE_CONFIG_THEME": {
"description": "[CONFIG] Set theme to 'dark', 'light', or 'system'",
"value": "system",
"WHOOGLE_CONFIG_DARK": {
"description": "[CONFIG] Enable dark mode (set to 1 or leave blank)",
"value": "",
"required": false
},
"WHOOGLE_CONFIG_SAFE": {
@ -145,11 +105,6 @@
"value": "",
"required": false
},
"WHOOGLE_CONFIG_NEAR": {
"description": "[CONFIG] Restrict results to only those near a particular city",
"value": "",
"required": false
},
"WHOOGLE_CONFIG_TOR": {
"description": "[CONFIG] Use Tor, if available (set to 1 or leave blank)",
"value": "",
@ -172,18 +127,8 @@
},
"WHOOGLE_CONFIG_STYLE": {
"description": "[CONFIG] Custom CSS styling (paste in CSS or leave blank)",
"value": ":root { /* LIGHT THEME COLORS */ --whoogle-background: #d8dee9; --whoogle-accent: #2e3440; --whoogle-text: #3B4252; --whoogle-contrast-text: #eceff4; --whoogle-secondary-text: #70757a; --whoogle-result-bg: #fff; --whoogle-result-title: #4c566a; --whoogle-result-url: #81a1c1; --whoogle-result-visited: #a3be8c; /* DARK THEME COLORS */ --whoogle-dark-background: #222; --whoogle-dark-accent: #685e79; --whoogle-dark-text: #fff; --whoogle-dark-contrast-text: #000; --whoogle-dark-secondary-text: #bbb; --whoogle-dark-result-bg: #000; --whoogle-dark-result-title: #1967d2; --whoogle-dark-result-url: #4b11a8; --whoogle-dark-result-visited: #bbbbff; }",
"required": false
},
"WHOOGLE_CONFIG_PREFERENCES_ENCRYPTED": {
"description": "[CONFIG] Encrypt preferences token, requires WHOOGLE_CONFIG_PREFERENCES_KEY to be set",
"value": "",
"required": false
},
"WHOOGLE_CONFIG_PREFERENCES_KEY": {
"description": "[CONFIG] Key to encrypt preferences",
"value": "NEEDS_TO_BE_MODIFIED",
"required": false
}
}
}

View File

@ -1,171 +1,76 @@
from app.filter import clean_query
from app.request import send_tor_signal
from app.utils.session import generate_user_key
from app.utils.bangs import gen_bangs_json
from app.utils.misc import gen_file_hash, read_config_bool
from base64 import b64encode
from datetime import datetime, timedelta
from flask import Flask
from flask_session import Session
import json
import logging.config
import os
from stem import Signal
import threading
from dotenv import load_dotenv
from werkzeug.middleware.proxy_fix import ProxyFix
from app.utils.misc import read_config_bool
app = Flask(__name__, static_folder=os.path.dirname(
os.path.abspath(__file__)) + '/static')
app.wsgi_app = ProxyFix(app.wsgi_app)
dot_env_path = (
os.path.join(os.path.dirname(os.path.abspath(__file__)),
'../whoogle.env'))
# Load .env file if enabled
if read_config_bool('WHOOGLE_DOTENV'):
load_dotenv(dot_env_path)
if os.getenv("WHOOGLE_DOTENV", ''):
dotenv_path = '../whoogle.env'
load_dotenv(os.path.join(os.path.dirname(os.path.abspath(__file__)),
dotenv_path))
app.default_key = generate_user_key()
if read_config_bool('HTTPS_ONLY'):
app.config['SESSION_COOKIE_NAME'] = '__Secure-session'
app.config['SESSION_COOKIE_SECURE'] = True
app.config['VERSION_NUMBER'] = '0.7.4'
app.no_cookie_ips = []
app.config['SECRET_KEY'] = os.urandom(32)
app.config['SESSION_TYPE'] = 'filesystem'
app.config['VERSION_NUMBER'] = '0.5.0'
app.config['APP_ROOT'] = os.getenv(
'APP_ROOT',
os.path.dirname(os.path.abspath(__file__)))
app.config['STATIC_FOLDER'] = os.getenv(
'STATIC_FOLDER',
os.path.join(app.config['APP_ROOT'], 'static'))
app.config['BUILD_FOLDER'] = os.path.join(
app.config['STATIC_FOLDER'], 'build')
app.config['CACHE_BUSTING_MAP'] = {}
app.config['LANGUAGES'] = json.load(open(
os.path.join(app.config['STATIC_FOLDER'], 'settings/languages.json'),
encoding='utf-8'))
os.path.join(app.config['STATIC_FOLDER'], 'settings/languages.json')))
app.config['COUNTRIES'] = json.load(open(
os.path.join(app.config['STATIC_FOLDER'], 'settings/countries.json'),
encoding='utf-8'))
app.config['TRANSLATIONS'] = json.load(open(
os.path.join(app.config['STATIC_FOLDER'], 'settings/translations.json'),
encoding='utf-8'))
app.config['THEMES'] = json.load(open(
os.path.join(app.config['STATIC_FOLDER'], 'settings/themes.json'),
encoding='utf-8'))
app.config['HEADER_TABS'] = json.load(open(
os.path.join(app.config['STATIC_FOLDER'], 'settings/header_tabs.json'),
encoding='utf-8'))
os.path.join(app.config['STATIC_FOLDER'], 'settings/countries.json')))
app.config['CONFIG_PATH'] = os.getenv(
'CONFIG_VOLUME',
os.path.join(app.config['STATIC_FOLDER'], 'config'))
app.config['DEFAULT_CONFIG'] = os.path.join(
app.config['CONFIG_PATH'],
'config.json')
app.config['CONFIG_DISABLE'] = read_config_bool('WHOOGLE_CONFIG_DISABLE')
app.config['CONFIG_DISABLE'] = os.getenv('WHOOGLE_CONFIG_DISABLE', '')
app.config['SESSION_FILE_DIR'] = os.path.join(
app.config['CONFIG_PATH'],
'session')
app.config['MAX_SESSION_SIZE'] = 4000 # Sessions won't exceed 4KB
app.config['BANG_PATH'] = os.getenv(
'CONFIG_VOLUME',
os.path.join(app.config['STATIC_FOLDER'], 'bangs'))
app.config['BANG_FILE'] = os.path.join(
app.config['BANG_PATH'],
'bangs.json')
app.config['CSP'] = 'default-src \'none\';' \
'manifest-src \'self\';' \
'img-src \'self\' data:;' \
'style-src \'self\' \'unsafe-inline\';' \
'script-src \'self\';' \
'media-src \'self\';' \
'connect-src \'self\';' \
'form-action \'self\';'
# Ensure all necessary directories exist
if not os.path.exists(app.config['CONFIG_PATH']):
os.makedirs(app.config['CONFIG_PATH'])
if not os.path.exists(app.config['SESSION_FILE_DIR']):
os.makedirs(app.config['SESSION_FILE_DIR'])
# Generate DDG bang filter, and create path if it doesn't exist yet
if not os.path.exists(app.config['BANG_PATH']):
os.makedirs(app.config['BANG_PATH'])
if not os.path.exists(app.config['BUILD_FOLDER']):
os.makedirs(app.config['BUILD_FOLDER'])
# Session values
app_key_path = os.path.join(app.config['CONFIG_PATH'], 'whoogle.key')
if os.path.exists(app_key_path):
app.config['SECRET_KEY'] = open(app_key_path, 'r').read()
else:
app.config['SECRET_KEY'] = str(b64encode(os.urandom(32)))
with open(app_key_path, 'w') as key_file:
key_file.write(app.config['SECRET_KEY'])
key_file.close()
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=365)
# NOTE: SESSION_COOKIE_SAMESITE must be set to 'lax' to allow the user's
# previous session to persist when accessing the instance from an external
# link. Setting this value to 'strict' causes Whoogle to revalidate a new
# session, and fail, resulting in cookies being disabled.
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
# Config fields that are used to check for updates
app.config['RELEASES_URL'] = 'https://github.com/' \
'benbusby/whoogle-search/releases'
app.config['LAST_UPDATE_CHECK'] = datetime.now() - timedelta(hours=24)
app.config['HAS_UPDATE'] = ''
# The alternative to Google Translate is treated a bit differently than other
# social media site alternatives, in that it is used for any translation
# related searches.
translate_url = os.getenv('WHOOGLE_ALT_TL', 'https://farside.link/lingva')
if not translate_url.startswith('http'):
translate_url = 'https://' + translate_url
app.config['TRANSLATE_URL'] = translate_url
app.config['CSP'] = 'default-src \'none\';' \
'frame-src ' + translate_url + ';' \
'manifest-src \'self\';' \
'img-src \'self\' data:;' \
'style-src \'self\' \'unsafe-inline\';' \
'script-src \'self\';' \
'media-src \'self\';' \
'connect-src \'self\';'
# Generate DDG bang filter
if not os.path.exists(app.config['BANG_FILE']):
json.dump({}, open(app.config['BANG_FILE'], 'w'))
bangs_thread = threading.Thread(
target=gen_bangs_json,
args=(app.config['BANG_FILE'],))
bangs_thread.start()
gen_bangs_json(app.config['BANG_FILE'])
# Build new mapping of static files for cache busting
cache_busting_dirs = ['css', 'js']
for cb_dir in cache_busting_dirs:
full_cb_dir = os.path.join(app.config['STATIC_FOLDER'], cb_dir)
for cb_file in os.listdir(full_cb_dir):
# Create hash from current file state
full_cb_path = os.path.join(full_cb_dir, cb_file)
cb_file_link = gen_file_hash(full_cb_dir, cb_file)
build_path = os.path.join(app.config['BUILD_FOLDER'], cb_file_link)
try:
os.symlink(full_cb_path, build_path)
except FileExistsError:
# Symlink hasn't changed, ignore
pass
# Create mapping for relative path urls
map_path = build_path.replace(app.config['APP_ROOT'], '')
if map_path.startswith('/'):
map_path = map_path[1:]
app.config['CACHE_BUSTING_MAP'][cb_file] = map_path
# Templating functions
app.jinja_env.globals.update(clean_query=clean_query)
app.jinja_env.globals.update(
cb_url=lambda f: app.config['CACHE_BUSTING_MAP'][f])
Session(app)
# Attempt to acquire tor identity, to determine if Tor config is available
send_tor_signal(Signal.HEARTBEAT)

View File

@ -1,134 +1,28 @@
import cssutils
from app.request import VALID_PARAMS
from app.utils.results import *
from bs4 import BeautifulSoup
from bs4.element import ResultSet, Tag
from cryptography.fernet import Fernet
from flask import render_template
import re
import urllib.parse as urlparse
from urllib.parse import parse_qs
import re
from app.models.g_classes import GClasses
from app.request import VALID_PARAMS, MAPS_URL
from app.utils.misc import get_abs_url, read_config_bool
from app.utils.results import (
BLANK_B64, GOOG_IMG, GOOG_STATIC, G_M_LOGO_URL, LOGO_URL, SITE_ALTS,
has_ad_content, filter_link_args, append_anon_view, get_site_alt,
)
from app.models.endpoint import Endpoint
from app.models.config import Config
MAPS_ARGS = ['q', 'daddr']
minimal_mode_sections = ['Top stories', 'Images', 'People also ask']
unsupported_g_pages = [
'support.google.com',
'accounts.google.com',
'policies.google.com',
'google.com/preferences',
'google.com/intl',
'advanced_search',
'tbm=shop'
]
def extract_q(q_str: str, href: str) -> str:
"""Extracts the 'q' element from a result link. This is typically
either the link to a result's website, or a string.
Args:
q_str: The result link to parse
href: The full url to check for standalone 'q' elements first,
rather than parsing the whole query string and then checking.
Returns:
str: The 'q' element of the link, or an empty string
"""
return parse_qs(q_str)['q'][0] if ('&q=' in href or '?q=' in href) else ''
def build_map_url(href: str) -> str:
"""Tries to extract known args that explain the location in the url. If a
location is found, returns the default url with it. Otherwise, returns the
url unchanged.
Args:
href: The full url to check.
Returns:
str: The parsed url, or the url unchanged.
"""
# parse the url
parsed_url = parse_qs(href)
# iterate through the known parameters and try build the url
for param in MAPS_ARGS:
if param in parsed_url:
return MAPS_URL + "?q=" + parsed_url[param][0]
# query could not be extracted returning unchanged url
return href
def clean_query(query: str) -> str:
"""Strips the blocked site list from the query, if one is being
used.
Args:
query: The query string
Returns:
str: The query string without any "-site:..." filters
"""
return query[:query.find('-site:')] if '-site:' in query else query
def clean_css(css: str, page_url: str) -> str:
"""Removes all remote URLs from a CSS string.
Args:
css: The CSS string
Returns:
str: The filtered CSS, with URLs proxied through Whoogle
"""
sheet = cssutils.parseString(css)
urls = cssutils.getUrls(sheet)
for url in urls:
abs_url = get_abs_url(url, page_url)
if abs_url.startswith('data:'):
continue
css = css.replace(
url,
f'{Endpoint.element}?type=image/png&url={abs_url}'
)
return css
class Filter:
# Limit used for determining if a result is a "regular" result or a list
# type result (such as "people also asked", "related searches", etc)
RESULT_CHILD_LIMIT = 7
def __init__(self, user_key: str, mobile=False, config=None) -> None:
if config is None:
config = {}
def __init__(
self,
user_key: str,
config: Config,
root_url='',
page_url='',
query='',
mobile=False) -> None:
self.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.mobile = mobile
self.user_key = user_key
self.page_url = page_url
self.query = query
self.main_divs = ResultSet('')
self._elements = 0
self._av = set()
self.root_url = root_url[:-1] if root_url.endswith('/') else root_url
def __getitem__(self, name):
return getattr(self, name)
@ -137,6 +31,16 @@ class Filter:
def elements(self):
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:
# Encrypts path to avoid plaintext results in logs
if is_element:
@ -151,12 +55,8 @@ class Filter:
def clean(self, soup) -> BeautifulSoup:
self.main_divs = soup.find('div', {'id': 'main'})
self.remove_ads()
self.remove_block_titles()
self.remove_block_url()
self.collapse_sections()
self.update_css(soup)
self.fix_question_section()
self.update_styling(soup)
self.remove_block_tabs(soup)
for img in [_ for _ in soup.find_all('img') if 'src' in _.attrs]:
self.update_element_src(img, 'image/png')
@ -169,9 +69,7 @@ class Filter:
input_form = soup.find('form')
if input_form is not None:
input_form['method'] = 'GET' if self.config.get_only else 'POST'
# Use a relative URI for submissions
input_form['action'] = 'search'
input_form['method'] = 'POST'
# Ensure no extra scripts passed through
for script in soup('script'):
@ -187,20 +85,9 @@ class Filter:
header = soup.find('header')
if header:
header.decompose()
self.remove_site_blocks(soup)
return soup
def remove_site_blocks(self, soup) -> None:
if not self.config.block or not soup.body:
return
search_string = ' '.join(['-site:' +
_ for _ in self.config.block.split(',')])
selected = soup.body.findAll(text=re.compile(search_string))
for result in selected:
result.string.replace_with(result.string.replace(
search_string, ''))
def remove_ads(self) -> None:
"""Removes ads found in the list of search result divs
@ -215,124 +102,43 @@ class Filter:
if has_ad_content(_.text)]
_ = div.decompose() if len(div_ads) else None
def remove_block_titles(self) -> None:
if not self.main_divs or not self.config.block_title:
return
block_title = re.compile(self.block_title)
for div in [_ for _ in self.main_divs.find_all('div', recursive=True)]:
block_divs = [_ for _ in div.find_all('h3', recursive=True)
if block_title.search(_.text) is not None]
_ = div.decompose() if len(block_divs) else None
def remove_block_url(self) -> None:
if not self.main_divs or not self.config.block_url:
return
block_url = re.compile(self.block_url)
for div in [_ for _ in self.main_divs.find_all('div', recursive=True)]:
block_divs = [_ for _ in div.find_all('a', recursive=True)
if block_url.search(_.attrs['href']) is not None]
_ = div.decompose() if len(block_divs) else None
def remove_block_tabs(self, soup) -> None:
if self.main_divs:
for div in self.main_divs.find_all(
'div',
attrs={'class': f'{GClasses.main_tbm_tab}'}
):
_ = div.decompose()
else:
# when in images tab
for div in soup.find_all(
'div',
attrs={'class': f'{GClasses.images_tbm_tab}'}
):
_ = div.decompose()
def collapse_sections(self) -> None:
"""Collapses long result sections ("people also asked", "related
searches", etc) into "details" elements
def fix_question_section(self) -> None:
"""Collapses the "People Also Asked" section into a "details" element
These sections are typically the only sections in the results page that
have more than ~5 child divs within a primary result div.
are structured as <div><h2>Title</h2><div>...</div></div>, so they are
extracted by checking all result divs for h2 children.
Returns:
None (The soup object is modified directly)
"""
minimal_mode = read_config_bool('WHOOGLE_MINIMAL')
def pull_child_divs(result_div: BeautifulSoup):
try:
return result_div.findChildren(
'div', recursive=False
)[0].findChildren(
'div', recursive=False)
except IndexError:
return []
if not self.main_divs:
return
# Loop through results and check for the number of child divs in each
for result in self.main_divs.find_all():
result_children = pull_child_divs(result)
if minimal_mode:
if any(f">{x}</span" in str(s) for s in result_children
for x in minimal_mode_sections):
result.decompose()
continue
for s in result_children:
if ('Twitter ' in str(s)):
result.decompose()
continue
if len(result_children) < self.RESULT_CHILD_LIMIT:
continue
else:
if len(result_children) < self.RESULT_CHILD_LIMIT:
continue
question_divs = [_ for _ in self.main_divs.find_all(
'div', recursive=False
) if len(_.find_all('h2')) > 0]
# Find and decompose the first element with an inner HTML text val.
# This typically extracts the title of the section (i.e. "Related
# Searches", "People also ask", etc)
# If there are more than one child tags with text
# parenthesize the rest except the first
label = 'Collapsed Results'
subtitle = None
for elem in result_children:
if elem.text:
content = list(elem.strings)
label = content[0]
if len(content) > 1:
subtitle = '<span> (' + \
''.join(content[1:]) + ')</span>'
elem.decompose()
break
if len(question_divs) == 0:
return
# Create the new details element to wrap around the result's
# first parent
parent = None
idx = 0
while not parent and idx < len(result_children):
parent = result_children[idx].parent
idx += 1
# Wrap section in details element to allow collapse/expand
details = BeautifulSoup(features='html.parser').new_tag('details')
summary = BeautifulSoup(features='html.parser').new_tag('summary')
summary.string = question_divs[0].find('h2').text
question_divs[0].find('h2').decompose()
details.append(summary)
question_divs[0].wrap(details)
details = BeautifulSoup(features='html.parser').new_tag('details')
summary = BeautifulSoup(features='html.parser').new_tag('summary')
summary.string = label
for question_div in question_divs:
questions = [_ for _ in question_div.find_all(
'div', recursive=True
) if _.text.endswith('?')]
if subtitle:
soup = BeautifulSoup(subtitle, 'html.parser')
summary.append(soup)
for question in questions:
question['style'] = 'padding: 10px; font-style: italic;'
details.append(summary)
if parent and not minimal_mode:
parent.wrap(details)
elif parent and minimal_mode:
# Remove parent element from document if "minimal mode" is
# enabled
parent.decompose()
def update_element_src(self, element: Tag, mime: str, attr='src') -> None:
def update_element_src(self, element: Tag, mime: str) -> None:
"""Encrypts the original src of an element and rewrites the element src
to use the "/element?src=" pass-through.
@ -340,56 +146,26 @@ class Filter:
None (The soup element is modified directly)
"""
src = element[attr].split(' ')[0]
src = element['src']
if src.startswith('//'):
src = 'https:' + src
elif src.startswith('data:'):
return
if src.startswith(LOGO_URL):
# Re-brand with Whoogle logo
element.replace_with(BeautifulSoup(
render_template('logo.html'),
render_template('logo.html', dark=self.dark),
features='html.parser'))
return
elif src.startswith(G_M_LOGO_URL):
# Re-brand with single-letter Whoogle logo
element['src'] = 'static/img/favicon/apple-icon.png'
element.parent['href'] = 'home'
return
elif src.startswith(GOOG_IMG) or GOOG_STATIC in src:
element['src'] = BLANK_B64
return
element[attr] = f'{self.root_url}/{Endpoint.element}?url=' + (
self.encrypt_path(
src,
is_element=True
) + '&type=' + urlparse.quote(mime)
)
def update_css(self, soup) -> None:
"""Updates URLs used in inline styles to be proxied by Whoogle
using the /element endpoint.
Returns:
None (The soup element is modified directly)
"""
# Filter all <style> tags
for style in soup.find_all('style'):
style.string = clean_css(style.string, self.page_url)
# TODO: Convert remote stylesheets to style tags and proxy all
# remote requests
# for link in soup.find_all('link', attrs={'rel': 'stylesheet'}):
# print(link)
element['src'] = 'element?url=' + self.encrypt_path(
src,
is_element=True) + '&type=' + urlparse.quote(mime)
def update_styling(self, soup) -> None:
# Update CSS classes for result divs
soup = GClasses.replace_css_classes(soup)
# Remove unnecessary button(s)
for button in soup.find_all('button'):
button.decompose()
@ -412,26 +188,6 @@ class Filter:
except AttributeError:
pass
# Fix body max width on images tab
style = soup.find('style')
div = soup.find('div', attrs={'class': f'{GClasses.images_tbm_tab}'})
if style and div and not self.mobile:
css = style.string
css_html_tag = (
'html{'
'font-family: Roboto, Helvetica Neue, Arial, sans-serif;'
'font-size: 14px;'
'line-height: 20px;'
'text-size-adjust: 100%;'
'word-wrap: break-word;'
'}'
)
css = f"{css_html_tag}{css}"
css = re.sub('body{(.*?)}',
'body{padding:0 8px;margin:0 auto;max-width:736px;}',
css)
style.string = css
def update_link(self, link: Tag) -> None:
"""Update internal link paths with encrypted path, otherwise remove
unnecessary redirects and/or marketing params from the url
@ -443,63 +199,31 @@ class Filter:
None (the tag is updated directly)
"""
parsed_link = urlparse.urlparse(link['href'])
link_netloc = ''
if '/url?q=' in link['href']:
link_netloc = extract_q(parsed_link.query, link['href'])
else:
link_netloc = parsed_link.netloc
# Remove any elements that direct to unsupported Google pages
if any(url in link_netloc for url in unsupported_g_pages):
# FIXME: The "Shopping" tab requires further filtering (see #136)
# Temporarily removing all links to that tab for now.
# Replaces the /url google unsupported link to the direct url
link['href'] = link_netloc
parent = link.parent
if 'google.com/preferences?hl=' in link_netloc:
# Handle case where a search is performed in a different
# language than what is configured. This usually returns a
# div with the same classes as normal search results, but with
# a link to configure language preferences through Google.
# Since we want all language config done through Whoogle, we
# can safely decompose this element.
while parent:
p_cls = parent.attrs.get('class') or []
if f'{GClasses.result_class_a}' in p_cls:
parent.decompose()
break
parent = parent.parent
else:
# Remove cases where google links appear in the footer
while parent:
p_cls = parent.attrs.get('class') or []
if parent.name == 'footer' or f'{GClasses.footer}' in p_cls:
link.decompose()
parent = parent.parent
return
# Replace href with only the intended destination (no "utm" type tags)
href = link['href'].replace('https://www.google.com', '')
result_link = urlparse.urlparse(href)
q = extract_q(result_link.query, href)
if q.startswith('/') and q not in self.query and 'spell=1' not in href:
# Internal google links (i.e. mail, maps, etc) should still
# be forwarded to Google
link['href'] = 'https://google.com' + q
elif q.startswith('https://accounts.google.com'):
# Remove Sign-in link
if 'advanced_search' in href or 'tbm=shop' in href:
# FIXME: The "Shopping" tab requires further filtering (see #136)
# Temporarily removing all links to that tab for now.
link.decompose()
return
elif self.new_tab:
link['target'] = '_blank'
result_link = urlparse.urlparse(href)
query_link = parse_qs(
result_link.query
)['q'][0] if '?q=' in href else ''
if query_link.startswith('/'):
# Internal google links (i.e. mail, maps, etc) should still
# be forwarded to Google
link['href'] = 'https://google.com' + query_link
elif '/search?q=' in href:
# "li:1" implies the query should be interpreted verbatim,
# which is accomplished by wrapping the query in double quotes
if 'li:1' in href:
q = '"' + q + '"'
new_search = 'search?q=' + self.encrypt_path(q)
query_link = '"' + query_link + '"'
new_search = 'search?q=' + self.encrypt_path(query_link)
query_params = parse_qs(urlparse.urlparse(href).query)
for param in VALID_PARAMS:
@ -510,41 +234,16 @@ class Filter:
link['href'] = new_search
elif 'url?q=' in href:
# Strip unneeded arguments
link['href'] = filter_link_args(q)
# Add alternate viewing options for results,
# if the result doesn't already have an AV link
netloc = urlparse.urlparse(link['href']).netloc
if self.config.anon_view and netloc not in self._av:
self._av.add(netloc)
append_anon_view(link, self.config)
link['href'] = filter_link_args(query_link)
# Add no-js option
if self.nojs:
append_nojs(link)
else:
if href.startswith(MAPS_URL):
# Maps links don't work if a site filter is applied
link['href'] = build_map_url(link['href'])
elif (href.startswith('/?') or href.startswith('/search?') or
href.startswith('/imgres?')):
# make sure that tags can be clicked as relative URLs
link['href'] = href[1:]
elif href.startswith('/intl/'):
# do nothing, keep original URL for ToS
pass
elif href.startswith('/preferences'):
# there is no config specific URL, remove this
link.decompose()
return
else:
link['href'] = href
if self.config.new_tab and (
link["href"].startswith("http")
or link["href"].startswith("imgres?")
):
link["target"] = "_blank"
link['href'] = href
# Replace link location if "alts" config is enabled
if self.config.alts:
if self.alt_redirect:
# Search and replace all link descriptions
# with alternative location
link['href'] = get_site_alt(link['href'])
@ -553,15 +252,8 @@ class Filter:
if len(link_desc) == 0:
return
# Replace link description
link_desc = link_desc[0]
for site, alt in SITE_ALTS.items():
if site not in link_desc or not alt:
continue
new_desc = BeautifulSoup(features='html.parser').new_tag('div')
new_desc.string = str(link_desc).replace(site, alt)
link_desc.replace_with(new_desc)
break
# Replace link destination
link_desc[0].replace_with(get_site_alt(link_desc[0]))
def view_image(self, soup) -> BeautifulSoup:
"""Replaces the soup with a new one that handles mobile results and
@ -575,8 +267,11 @@ class Filter:
"""
# get some tags that are unchanged between mobile and pc versions
search_input = soup.find_all('td', attrs={'class': "O4cRJf"})[0]
search_options = soup.find_all('div', attrs={'class': "M7pB2"})[0]
cor_suggested = soup.find_all('table', attrs={'class': "By0U9"})
next_pages = soup.find_all('table', attrs={'class': "uZgmoc"})[0]
information = soup.find_all('div', attrs={'class': "TuS8Ad"})[0]
results = []
# find results div
@ -587,25 +282,13 @@ class Filter:
for item in results_all:
urls = item.find('a')['href'].split('&imgrefurl=')
# 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 to strip out only the necessary part of the web page link
web_page = urlparse.unquote(urls[1].split('&')[0])
except IndexError:
web_page = urlparse.unquote(urls[1])
img_url = urlparse.unquote(urls[0].replace('/imgres?imgurl=', ''))
webpage = urlparse.unquote(urls[1].split('&')[0])
img_tbn = urlparse.unquote(item.find('a').find('img')['src'])
results.append({
'domain': urlparse.urlparse(web_page).netloc,
'domain': urlparse.urlparse(webpage).netloc,
'img_url': img_url,
'web_page': web_page,
'webpage': webpage,
'img_tbn': img_tbn
})
@ -614,7 +297,12 @@ class Filter:
results=results,
view_label="View Image"),
features='html.parser')
# replace search input object
soup.find_all('td',
attrs={'class': "O4cRJf"})[0].replaceWith(search_input)
# replace search options object (All, Images, Videos, etc.)
soup.find_all('div',
attrs={'class': "M7pB2"})[0].replaceWith(search_options)
# replace correction suggested by google object if exists
if len(cor_suggested):
soup.find_all(
@ -624,4 +312,7 @@ class Filter:
# replace next page object at the bottom of the page
soup.find_all('table',
attrs={'class': "uZgmoc"})[0].replaceWith(next_pages)
# replace information about user connection at the bottom of the page
soup.find_all('div',
attrs={'class': "TuS8Ad"})[0].replaceWith(information)
return soup

View File

@ -1,17 +1,15 @@
from inspect import Attribute
from app.utils.misc import read_config_bool
from flask import current_app
import os
import re
from base64 import urlsafe_b64encode, urlsafe_b64decode
import pickle
from cryptography.fernet import Fernet
import hashlib
import brotli
class Config:
def __init__(self, **kwargs):
def read_config_bool(var: str) -> bool:
val = os.getenv(var, '0')
if val.isdigit():
return bool(int(val))
return False
app_config = current_app.config
self.url = os.getenv('WHOOGLE_CONFIG_URL', '')
self.lang_search = os.getenv('WHOOGLE_CONFIG_SEARCH_LANGUAGE', '')
@ -21,12 +19,9 @@ class Config:
open(os.path.join(app_config['STATIC_FOLDER'],
'css/variables.css')).read())
self.block = os.getenv('WHOOGLE_CONFIG_BLOCK', '')
self.block_title = os.getenv('WHOOGLE_CONFIG_BLOCK_TITLE', '')
self.block_url = os.getenv('WHOOGLE_CONFIG_BLOCK_URL', '')
self.country = os.getenv('WHOOGLE_CONFIG_COUNTRY', '')
self.theme = os.getenv('WHOOGLE_CONFIG_THEME', 'system')
self.ctry = os.getenv('WHOOGLE_CONFIG_COUNTRY', '')
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')
self.alts = read_config_bool('WHOOGLE_CONFIG_ALTS')
self.nojs = read_config_bool('WHOOGLE_CONFIG_NOJS')
self.tor = read_config_bool('WHOOGLE_CONFIG_TOR')
@ -34,25 +29,12 @@ class Config:
self.new_tab = read_config_bool('WHOOGLE_CONFIG_NEW_TAB')
self.view_image = read_config_bool('WHOOGLE_CONFIG_VIEW_IMAGE')
self.get_only = read_config_bool('WHOOGLE_CONFIG_GET_ONLY')
self.anon_view = read_config_bool('WHOOGLE_CONFIG_ANON_VIEW')
self.preferences_encrypted = read_config_bool('WHOOGLE_CONFIG_PREFERENCES_ENCRYPTED')
self.preferences_key = os.getenv('WHOOGLE_CONFIG_PREFERENCES_KEY', '')
self.accept_language = False
self.safe_keys = [
'lang_search',
'lang_interface',
'country',
'theme',
'alts',
'new_tab',
'view_image',
'block',
'safe',
'nojs',
'anon_view',
'preferences_encrypted'
'ctry',
'dark'
]
# Skip setting custom config if there isn't one
@ -81,24 +63,6 @@ class Config:
if not name.startswith("__")
and (type(attr) is bool or type(attr) is str)}
def get_attrs(self):
return {name: attr for name, attr in self.__dict__.items()
if not name.startswith("__")
and (type(attr) is bool or type(attr) is str)}
@property
def preferences(self) -> str:
# if encryption key is not set will uncheck preferences encryption
if self.preferences_encrypted:
self.preferences_encrypted = bool(self.preferences_key)
# add a tag for visibility if preferences token startswith 'e' it means
# the token is encrypted, 'u' means the token is unencrypted and can be
# used by other whoogle instances
encrypted_flag = "e" if self.preferences_encrypted else 'u'
preferences_digest = self._encode_preferences()
return f"{encrypted_flag}{preferences_digest}"
def is_safe_key(self, key) -> bool:
"""Establishes a group of config options that are safe to set
in the url.
@ -113,19 +77,6 @@ class Config:
return key in self.safe_keys
def get_localization_lang(self):
"""Returns the correct language to use for localization, but falls
back to english if not set.
Returns:
str -- the localization language string
"""
if (self.lang_interface and
self.lang_interface in current_app.config['TRANSLATIONS']):
return self.lang_interface
return 'lang_en'
def from_params(self, params) -> 'Config':
"""Modify user config with search parameters. This is primarily
used for specifying configuration on a search-by-search basis on
@ -137,74 +88,8 @@ class Config:
Returns:
Config -- a modified config object
"""
if 'preferences' in params:
params_new = self._decode_preferences(params['preferences'])
# if preferences leads to an empty dictionary it means preferences
# parameter was not decrypted successfully
if len(params_new):
params = params_new
for param_key in params.keys():
if not self.is_safe_key(param_key):
continue
param_val = params.get(param_key)
if param_val == 'off':
param_val = False
elif isinstance(param_val, str):
if param_val.isdigit():
param_val = int(param_val)
self[param_key] = param_val
self[param_key] = params.get(param_key)
return self
def to_params(self) -> str:
"""Generates a set of safe params for using in Whoogle URLs
Returns:
str -- a set of URL parameters
"""
param_str = ''
for safe_key in self.safe_keys:
if not self[safe_key]:
continue
param_str = param_str + f'&{safe_key}={self[safe_key]}'
return param_str
def _get_fernet_key(self, password: str) -> bytes:
hash_object = hashlib.md5(password.encode())
key = urlsafe_b64encode(hash_object.hexdigest().encode())
return key
def _encode_preferences(self) -> str:
encoded_preferences = brotli.compress(pickle.dumps(self.get_attrs()))
if self.preferences_encrypted:
if self.preferences_key != '':
key = self._get_fernet_key(self.preferences_key)
encoded_preferences = Fernet(key).encrypt(encoded_preferences)
encoded_preferences = brotli.compress(encoded_preferences)
return urlsafe_b64encode(encoded_preferences).decode()
def _decode_preferences(self, preferences: str) -> dict:
mode = preferences[0]
preferences = preferences[1:]
if mode == 'e': # preferences are encrypted
try:
key = self._get_fernet_key(self.preferences_key)
config = Fernet(key).decrypt(
brotli.decompress(urlsafe_b64decode(preferences.encode()))
)
config = pickle.loads(brotli.decompress(config))
except Exception:
config = {}
elif mode == 'u': # preferences are not encrypted
config = pickle.loads(
brotli.decompress(urlsafe_b64decode(preferences.encode()))
)
else: # preferences are incorrectly formatted
config = {}
return config

View File

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

View File

@ -1,46 +0,0 @@
from bs4 import BeautifulSoup
class GClasses:
"""A class for tracking obfuscated class names used in Google results that
are directly referenced in Whoogle's filtering code.
Note: Using these should be a last resort. It is always preferred to filter
results using structural cues instead of referencing class names, as these
are liable to change at any moment.
"""
main_tbm_tab = 'KP7LCb'
images_tbm_tab = 'n692Zd'
footer = 'TuS8Ad'
result_class_a = 'ZINbbc'
result_class_b = 'luh4td'
result_classes = {
result_class_a: ['Gx5Zad'],
result_class_b: ['fP1Qef']
}
@classmethod
def replace_css_classes(cls, soup: BeautifulSoup) -> BeautifulSoup:
"""Replace updated Google classes with the original class names that
Whoogle relies on for styling.
Args:
soup: The result page as a BeautifulSoup object
Returns:
BeautifulSoup: The new BeautifulSoup
"""
result_divs = soup.find_all('div', {
'class': [_ for c in cls.result_classes.values() for _ in c]
})
for div in result_divs:
new_class = ' '.join(div['class'])
for key, val in cls.result_classes.items():
new_class = ' '.join(new_class.replace(_, key) for _ in val)
div['class'] = new_class.split(' ')
return soup
def __str__(self):
return self.value

View File

@ -1,18 +1,15 @@
from app.models.config import Config
from app.utils.misc import read_config_bool
from datetime import datetime
from defusedxml import ElementTree as ET
import xml.etree.ElementTree as ET
import random
import requests
from requests import Response, ConnectionError
import urllib.parse as urlparse
import os
from stem import Signal, SocketError
from stem.connection import AuthenticationFailure
from stem.control import Controller
from stem.connection import authenticate_cookie, authenticate_password
MAPS_URL = 'https://maps.google.com/maps'
SEARCH_URL = 'https://www.google.com/search?gbv=1&q='
AUTOCOMPLETE_URL = ('https://suggestqueries.google.com/'
'complete/search?client=toolbar&')
@ -40,33 +37,13 @@ class TorError(Exception):
def send_tor_signal(signal: Signal) -> bool:
use_pass = read_config_bool('WHOOGLE_TOR_USE_PASS')
confloc = './misc/tor/control.conf'
# Check that the custom location of conf is real.
temp = os.getenv('WHOOGLE_TOR_CONF', '')
if os.path.isfile(temp):
confloc = temp
# Attempt to authenticate and send signal.
try:
with Controller.from_port(port=9051) as c:
if use_pass:
with open(confloc, "r") as conf:
# Scan for the last line of the file.
for line in conf:
pass
secret = line.strip('\n')
authenticate_password(c, password=secret)
else:
cookie_path = '/var/lib/tor/control_auth_cookie'
authenticate_cookie(c, cookie_path=cookie_path)
c.authenticate()
c.signal(signal)
os.environ['TOR_AVAILABLE'] = '1'
return True
except (SocketError, AuthenticationFailure,
ConnectionRefusedError, ConnectionError):
# TODO: Handle Tor authentication (password and cookie)
except (SocketError, ConnectionRefusedError, ConnectionError):
os.environ['TOR_AVAILABLE'] = '0'
return False
@ -82,7 +59,7 @@ def gen_user_agent(is_mobile) -> str:
return DESKTOP_UA.format("Mozilla", linux, firefox)
def gen_query(query, args, config) -> str:
def gen_query(query, args, config, near_city=None) -> str:
param_dict = {key: '' for key in VALID_PARAMS}
# Use :past(hour/day/week/month/year) if available
@ -119,8 +96,8 @@ def gen_query(query, args, config) -> str:
param_dict['start'] = '&start=' + args.get('start')
# Search for results near a particular city, if available
if config.near:
param_dict['near'] = '&near=' + urlparse.quote(config.near)
if near_city:
param_dict['near'] = '&near=' + urlparse.quote(near_city)
# Set language for results (lr) if source isn't set, otherwise use the
# result language param provided in the results
@ -138,26 +115,15 @@ def gen_query(query, args, config) -> str:
if 'nfpr' in args:
param_dict['nfpr'] = '&nfpr=' + args.get('nfpr')
# 'chips' is used in image tabs to pass the optional 'filter' to add to the
# given search term
if 'chips' in args:
param_dict['chips'] = '&chips=' + args.get('chips')
param_dict['gl'] = (
'&gl=' + config.country
) if config.country else ''
param_dict['cr'] = ('&cr=' + config.ctry) if config.ctry 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')
# Block all sites specified in the user config
unquoted_query = urlparse.unquote(query)
for blocked_site in config.block.replace(' ', '').split(','):
if not blocked_site:
continue
block = (' -site:' + blocked_site)
query += block if block not in unquoted_query else ''
for blocked in config.block.split(','):
query += (' -site:' + blocked) if blocked else ''
for val in param_dict.values():
if not val:
@ -178,48 +144,28 @@ class Request:
"""
def __init__(self, normal_ua, root_path, config: Config):
self.search_url = 'https://www.google.com/search?gbv=1&num=' + str(
os.getenv('WHOOGLE_RESULTS_PER_PAGE', 10)) + '&q='
# Send heartbeat to Tor, used in determining if the user can or cannot
# enable Tor for future requests
send_tor_signal(Signal.HEARTBEAT)
self.language = (
config.lang_search if config.lang_search else ''
)
# For setting Accept-language Header
self.lang_interface = ''
if config.accept_language:
self.lang_interface = config.lang_interface
self.mobile = bool(normal_ua) and ('Android' in normal_ua
or 'iPhone' in normal_ua)
self.language = config.lang_search
self.mobile = 'Android' in normal_ua or 'iPhone' in normal_ua
self.modified_user_agent = gen_user_agent(self.mobile)
if not self.mobile:
self.modified_user_agent_mobile = gen_user_agent(True)
# Set up proxy, if previously configured
proxy_path = os.environ.get('WHOOGLE_PROXY_LOC', '')
if proxy_path:
proxy_type = os.environ.get('WHOOGLE_PROXY_TYPE', '')
proxy_user = os.environ.get('WHOOGLE_PROXY_USER', '')
proxy_pass = os.environ.get('WHOOGLE_PROXY_PASS', '')
if os.environ.get('WHOOGLE_PROXY_LOC'):
auth_str = ''
if proxy_user:
auth_str = proxy_user + ':' + proxy_pass
if os.environ.get('WHOOGLE_PROXY_USER', ''):
auth_str = os.environ.get('WHOOGLE_PROXY_USER', '') + \
':' + os.environ.get('WHOOGLE_PROXY_PASS', '')
self.proxies = {
'https': proxy_type + '://' +
((auth_str + '@') if auth_str else '') + proxy_path,
'http': os.environ.get('WHOOGLE_PROXY_TYPE', '') + '://' +
auth_str + '@' + os.environ.get('WHOOGLE_PROXY_LOC', ''),
}
# Need to ensure both HTTP and HTTPS are in the proxy dict,
# regardless of underlying protocol
if proxy_type == 'https':
self.proxies['http'] = self.proxies['https'].replace(
'https', 'http')
else:
self.proxies['http'] = self.proxies['https']
self.proxies['https'] = self.proxies['http'].replace('http',
'https')
else:
self.proxies = {
'http': 'socks5://127.0.0.1:9050',
@ -242,25 +188,18 @@ class Request:
list: The list of matches for possible search suggestions
"""
ac_query = dict(q=query)
if self.language:
ac_query['hl'] = self.language
ac_query = dict(hl=self.language, q=query)
response = self.send(base_url=AUTOCOMPLETE_URL,
query=urlparse.urlencode(ac_query)).text
if not response:
return []
try:
root = ET.fromstring(response)
return [_.attrib['data'] for _ in
root.findall('.//suggestion/[@data]')]
except ET.ParseError:
# Malformed XML response
return []
root = ET.fromstring(response)
return [_.attrib['data'] for _ in
root.findall('.//suggestion/[@data]')]
def send(self, base_url='', query='', attempt=0,
def send(self, base_url=SEARCH_URL, query='', attempt=0,
force_mobile=False) -> Response:
"""Sends an outbound request to a URL. Optionally sends the request
using Tor, if enabled by the user.
@ -270,8 +209,6 @@ class Request:
query: The optional query string for the request
attempt: The number of attempts made for the request
(used for cycling through Tor identities, if enabled)
force_mobile: Optional flag to enable a mobile user agent
(used for fetching full size images in search results)
Returns:
Response: The Response object returned by the requests call
@ -286,12 +223,7 @@ class Request:
'User-Agent': modified_user_agent
}
# Adding the Accept-Language to the Header if possible
if self.lang_interface:
headers.update({'Accept-Language':
self.lang_interface.replace('lang_', '')
+ ';q=1.0'})
# FIXME: Should investigate this further to ensure the consent
# view is suppressed correctly
now = datetime.now()
cookies = {
@ -310,23 +242,18 @@ class Request:
# Make sure that the tor connection is valid, if enabled
if self.tor:
try:
tor_check = requests.get('https://check.torproject.org/',
proxies=self.proxies, headers=headers)
self.tor_valid = 'Congratulations' in tor_check.text
tor_check = requests.get('https://check.torproject.org/',
proxies=self.proxies, headers=headers)
self.tor_valid = 'Congratulations' in tor_check.text
if not self.tor_valid:
raise TorError(
"Tor connection succeeded, but the connection could "
"not be validated by torproject.org",
disable=True)
except ConnectionError:
if not self.tor_valid:
raise TorError(
"Error raised during Tor connection validation",
"Tor connection succeeded, but the connection could not "
"be validated by torproject.org",
disable=True)
response = requests.get(
(base_url or self.search_url) + query,
base_url + query,
proxies=self.proxies,
headers=headers,
cookies=cookies)
@ -336,6 +263,6 @@ class Request:
attempt += 1
if attempt > 10:
raise TorError("Tor query failed -- max attempts exceeded 10")
return self.send((base_url or self.search_url), query, attempt)
return self.send(base_url, query, attempt)
return response

View File

@ -2,46 +2,25 @@ import argparse
import base64
import io
import json
import os
import pickle
import urllib.parse as urlparse
import uuid
from datetime import datetime, timedelta
from functools import wraps
import waitress
from flask import jsonify, make_response, request, redirect, render_template, \
send_file, session, url_for
from requests import exceptions
from app import app
from app.models.config import Config
from app.models.endpoint import Endpoint
from app.request import Request, TorError
from app.utils.bangs import resolve_bang
from app.utils.misc import get_proxy_host_url
from app.filter import Filter
from app.utils.misc import read_config_bool, get_client_ip, get_request_url, \
check_for_update
from app.utils.results import add_ip_card, bold_search_terms,\
add_currency_card, check_currency, get_tabs_content
from app.utils.search import Search, needs_https, has_captcha
from app.utils.session import generate_user_key, valid_user_session
from bs4 import BeautifulSoup as bsoup
from flask import jsonify, make_response, request, redirect, render_template, \
send_file, session, url_for, g
from requests import exceptions
from requests.models import PreparedRequest
from cryptography.fernet import Fernet, InvalidToken
from cryptography.exceptions import InvalidSignature
from app.utils.search import *
# Load DDG bang json files only on init
bang_json = json.load(open(app.config['BANG_FILE'])) or {}
ac_var = 'WHOOGLE_AUTOCOMPLETE'
autocomplete_enabled = os.getenv(ac_var, '1')
def get_search_name(tbm):
for tab in app.config['HEADER_TABS'].values():
if tab['tbm'] == tbm:
return tab['name']
bang_json = json.load(open(app.config['BANG_FILE']))
def auth_required(f):
@ -64,110 +43,63 @@ def auth_required(f):
return decorated
def session_required(f):
@wraps(f)
def decorated(*args, **kwargs):
if (valid_user_session(session)):
g.session_key = session['key']
else:
session.pop('_permanent', None)
g.session_key = app.default_key
# Clear out old sessions
invalid_sessions = []
for user_session in os.listdir(app.config['SESSION_FILE_DIR']):
file_path = os.path.join(
app.config['SESSION_FILE_DIR'],
user_session)
try:
# Ignore files that are larger than the max session file size
if os.path.getsize(file_path) > app.config['MAX_SESSION_SIZE']:
continue
with open(file_path, 'rb') as session_file:
_ = pickle.load(session_file)
data = pickle.load(session_file)
if isinstance(data, dict) and 'valid' in data:
continue
invalid_sessions.append(file_path)
except Exception:
# Broad exception handling here due to how instances installed
# with pip seem to have issues storing unrelated files in the
# same directory as sessions
pass
for invalid_session in invalid_sessions:
try:
os.remove(invalid_session)
except FileNotFoundError:
# Don't throw error if the invalid session has been removed
pass
return f(*args, **kwargs)
return decorated
@app.before_request
def before_request_func():
global bang_json
session.permanent = True
# Check for latest version if needed
now = datetime.now()
if now - timedelta(hours=24) > app.config['LAST_UPDATE_CHECK']:
app.config['LAST_UPDATE_CHECK'] = now
app.config['HAS_UPDATE'] = check_for_update(
app.config['RELEASES_URL'],
app.config['VERSION_NUMBER'])
g.request_params = (
request.args if request.method == 'GET' else request.form
)
default_config = json.load(open(app.config['DEFAULT_CONFIG'])) \
if os.path.exists(app.config['DEFAULT_CONFIG']) else {}
g.cookies_disabled = False
# Generate session values for user if unavailable
if (not valid_user_session(session)):
session['config'] = default_config
if not valid_user_session(session):
session['config'] = json.load(open(app.config['DEFAULT_CONFIG'])) \
if os.path.exists(app.config['DEFAULT_CONFIG']) else {}
session['uuid'] = str(uuid.uuid4())
session['key'] = generate_user_key()
session['key'] = generate_user_key(True)
# Flag cookies as possibly disabled in order to prevent against
# unnecessary session directory expansion
g.cookies_disabled = True
# Handle https upgrade
if needs_https(request.url):
return redirect(
request.url.replace('http://', 'https://', 1),
code=308)
# Establish config values per user session
g.user_config = Config(**session['config'])
if not g.user_config.url:
g.user_config.url = get_request_url(request.url_root)
g.user_config.url = request.url_root.replace(
'http://',
'https://') if os.getenv('HTTPS_ONLY', False) else request.url_root
g.user_request = Request(
request.headers.get('User-Agent'),
get_request_url(request.url_root),
request.url_root,
config=g.user_config)
g.app_location = g.user_config.url
# Attempt to reload bangs json if not generated yet
if not bang_json and os.path.getsize(app.config['BANG_FILE']) > 4:
try:
bang_json = json.load(open(app.config['BANG_FILE']))
except json.decoder.JSONDecodeError:
# Ignore decoding error, can occur if file is still
# being written
pass
@app.after_request
def after_request_func(resp):
resp.headers['X-Content-Type-Options'] = 'nosniff'
resp.headers['X-Frame-Options'] = 'DENY'
# Check if address consistently has cookies blocked,
# in which case start removing session files after creation.
#
# 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']
if os.environ.get('HTTPS_ONLY', False):
resp.headers['Content-Security-Policy'] += \
'upgrade-insecure-requests'
resp.headers['Content-Security-Policy'] = app.config['CSP']
if os.environ.get('HTTPS_ONLY', False):
resp.headers['Content-Security-Policy'] += 'upgrade-insecure-requests'
return resp
@ -178,45 +110,36 @@ def unknown_page(e):
return redirect(g.app_location)
@app.route(f'/{Endpoint.healthz}', methods=['GET'])
@app.route('/healthz', methods=['GET'])
def healthz():
return ''
@app.route('/', methods=['GET'])
@app.route(f'/{Endpoint.home}', methods=['GET'])
@auth_required
def index():
# Reset keys
session['key'] = generate_user_key(g.cookies_disabled)
# Redirect if an error was raised
if 'error_message' in session and session['error_message']:
error_message = session['error_message']
session['error_message'] = ''
return render_template('error.html', error_message=error_message)
# Update user config if specified in search args
g.user_config = g.user_config.from_params(g.request_params)
return render_template('index.html',
has_update=app.config['HAS_UPDATE'],
languages=app.config['LANGUAGES'],
countries=app.config['COUNTRIES'],
themes=app.config['THEMES'],
autocomplete_enabled=autocomplete_enabled,
translation=app.config['TRANSLATIONS'][
g.user_config.get_localization_lang()
],
logo=render_template(
'logo.html',
dark=g.user_config.dark),
config_disabled=(
app.config['CONFIG_DISABLE'] or
not valid_user_session(session)),
config_disabled=app.config['CONFIG_DISABLE'],
config=g.user_config,
tor_available=int(os.environ.get('TOR_AVAILABLE')),
version_number=app.config['VERSION_NUMBER'])
@app.route(f'/{Endpoint.opensearch}', methods=['GET'])
@app.route('/opensearch.xml', methods=['GET'])
def opensearch():
opensearch_url = g.app_location
if opensearch_url.endswith('/'):
@ -232,14 +155,11 @@ def opensearch():
return render_template(
'opensearch.xml',
main_url=opensearch_url,
request_type='' if get_only else 'method="post"',
search_type=request.args.get('tbm'),
search_name=get_search_name(request.args.get('tbm')),
preferences=g.user_config.preferences
), 200, {'Content-Type': 'application/xml'}
request_type='' if get_only else 'method="post"'
), 200, {'Content-Disposition': 'attachment; filename="opensearch.xml"'}
@app.route(f'/{Endpoint.search_html}', methods=['GET'])
@app.route('/search.html', methods=['GET'])
def search_html():
search_url = g.app_location
if search_url.endswith('/'):
@ -247,11 +167,8 @@ def search_html():
return render_template('search.html', url=search_url)
@app.route(f'/{Endpoint.autocomplete}', methods=['GET', 'POST'])
@app.route('/autocomplete', methods=['GET', 'POST'])
def autocomplete():
if os.getenv(ac_var) and not read_config_bool(ac_var):
return jsonify({})
q = g.request_params.get('q')
if not q:
# FF will occasionally (incorrectly) send the q field without a
@ -279,23 +196,23 @@ def autocomplete():
])
@app.route(f'/{Endpoint.search}', methods=['GET', 'POST'])
@session_required
@app.route('/search', methods=['GET', 'POST'])
@auth_required
def search():
# Update user config if specified in search args
g.user_config = g.user_config.from_params(g.request_params)
search_util = Search(request, g.user_config, g.session_key)
search_util = Search(request, g.user_config, session,
cookies_disabled=g.cookies_disabled)
query = search_util.new_search_query()
bang = resolve_bang(query, bang_json)
if bang:
bang = resolve_bang(query=query, bangs_dict=bang_json)
if bang != '':
return redirect(bang)
# Redirect to home if invalid/blank search
if not query:
return redirect(url_for('.index'))
return redirect('/')
# Generate response and number of external elements from the page
try:
@ -310,88 +227,30 @@ def search():
if search_util.feeling_lucky:
return redirect(response, code=303)
# If the user is attempting to translate a string, determine the correct
# string for formatting the lingva.ml url
localization_lang = g.user_config.get_localization_lang()
translation = app.config['TRANSLATIONS'][localization_lang]
translate_to = localization_lang.replace('lang_', '')
# Return 503 if temporarily blocked by captcha
if has_captcha(str(response)):
return render_template(
'error.html',
blocked=True,
error_message=translation['ratelimit'],
translation=translation,
farside='https://farside.link',
config=g.user_config,
query=urlparse.unquote(query),
params=g.user_config.to_params()), 503
response = bold_search_terms(response, query)
# Feature to display IP address
if search_util.check_kw_ip():
html_soup = bsoup(str(response), 'html.parser')
response = add_ip_card(html_soup, get_client_ip(request))
# Update tabs content
tabs = get_tabs_content(app.config['HEADER_TABS'],
search_util.full_query,
search_util.search_type,
g.user_config.preferences,
translation)
# Feature to display currency_card
conversion = check_currency(str(response))
if conversion:
html_soup = bsoup(str(response), 'html.parser')
response = add_currency_card(html_soup, conversion)
preferences = g.user_config.preferences
home_url = f"home?preferences={preferences}" if preferences else "home"
resp_code = 503 if has_captcha(str(response)) else 200
return render_template(
'display.html',
has_update=app.config['HAS_UPDATE'],
query=urlparse.unquote(query),
search_type=search_util.search_type,
search_name=get_search_name(search_util.search_type),
config=g.user_config,
autocomplete_enabled=autocomplete_enabled,
lingva_url=app.config['TRANSLATE_URL'],
translation=translation,
translate_to=translate_to,
translate_str=query.replace(
'translate', ''
).replace(
translation['translate'], ''
),
is_translation=any(
_ in query.lower() for _ in [translation['translate'], 'translate']
) and not search_util.search_type, # Standard search queries only
response=response,
version_number=app.config['VERSION_NUMBER'],
search_header=render_template(
search_header=(render_template(
'header.html',
home_url=home_url,
config=g.user_config,
translation=translation,
languages=app.config['LANGUAGES'],
countries=app.config['COUNTRIES'],
logo=render_template('logo.html', dark=g.user_config.dark),
query=urlparse.unquote(query),
search_type=search_util.search_type,
mobile=g.user_request.mobile,
tabs=tabs))
mobile=g.user_request.mobile)
if 'isch' not in search_util.search_type else '')), resp_code
@app.route(f'/{Endpoint.config}', methods=['GET', 'POST', 'PUT'])
@session_required
@app.route('/config', methods=['GET', 'POST', 'PUT'])
@auth_required
def config():
config_disabled = (
app.config['CONFIG_DISABLE'] or
not valid_user_session(session))
config_disabled = app.config['CONFIG_DISABLE']
if request.method == 'GET':
return json.dumps(g.user_config.__dict__)
elif request.method == 'PUT' and not config_disabled:
@ -418,33 +277,43 @@ def config():
app.config['CONFIG_PATH'],
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
return redirect(config_data['url'])
else:
return redirect(url_for('.index'), code=403)
@app.route(f'/{Endpoint.imgres}')
@session_required
@app.route('/url', methods=['GET'])
@auth_required
def url():
if 'url' in request.args:
return redirect(request.args.get('url'))
q = request.args.get('q')
if len(q) > 0 and 'http' in q:
return redirect(q)
else:
return render_template(
'error.html',
error_message='Unable to resolve query: ' + q)
@app.route('/imgres')
@auth_required
def imgres():
return redirect(request.args.get('imgurl'))
@app.route(f'/{Endpoint.element}')
@session_required
@app.route('/element')
@auth_required
def element():
element_url = src_url = request.args.get('url')
if element_url.startswith('gAAAAA'):
try:
cipher_suite = Fernet(g.session_key)
src_url = cipher_suite.decrypt(element_url.encode()).decode()
except (InvalidSignature, InvalidToken) as e:
return render_template(
'error.html',
error_message=str(e)), 401
cipher_suite = Fernet(session['key'])
src_url = cipher_suite.decrypt(request.args.get('url').encode()).decode()
src_type = request.args.get('type')
try:
@ -462,71 +331,21 @@ def element():
return send_file(io.BytesIO(empty_gif), mimetype='image/gif')
@app.route(f'/{Endpoint.window}')
@session_required
@app.route('/window')
@auth_required
def window():
target_url = request.args.get('location')
if target_url.startswith('gAAAAA'):
cipher_suite = Fernet(g.session_key)
target_url = cipher_suite.decrypt(target_url.encode()).decode()
content_filter = Filter(
g.session_key,
root_url=request.url_root,
config=g.user_config)
target = urlparse.urlparse(target_url)
host_url = f'{target.scheme}://{target.netloc}'
get_body = g.user_request.send(base_url=target_url).text
get_body = g.user_request.send(base_url=request.args.get('location')).text
get_body = get_body.replace('src="/',
'src="' + request.args.get('location') + '"')
get_body = get_body.replace('href="/',
'href="' + request.args.get('location') + '"')
results = bsoup(get_body, 'html.parser')
src_attrs = ['src', 'href', 'srcset', 'data-srcset', 'data-src']
# Parse HTML response and replace relative links w/ absolute
for element in results.find_all():
for attr in src_attrs:
if not element.has_attr(attr) or not element[attr].startswith('/'):
continue
for script in results('script'):
script.decompose()
element[attr] = host_url + element[attr]
# Replace or remove javascript sources
for script in results.find_all('script', {'src': True}):
if 'nojs' in request.args:
script.decompose()
else:
content_filter.update_element_src(script, 'application/javascript')
# Replace all possible image attributes
img_sources = ['src', 'data-src', 'data-srcset', 'srcset']
for img in results.find_all('img'):
_ = [
content_filter.update_element_src(img, 'image/png', attr=_)
for _ in img_sources if img.has_attr(_)
]
# Replace all stylesheet sources
for link in results.find_all('link', {'href': True}):
content_filter.update_element_src(link, 'text/css', attr='href')
# Use anonymous view for all links on page
for a in results.find_all('a', {'href': True}):
a['href'] = f'{Endpoint.window}?location=' + a['href'] + (
'&nojs=1' if 'nojs' in request.args else '')
# Remove all iframes -- these are commonly used inside of <noscript> tags
# to enforce loading Google Analytics
for iframe in results.find_all('iframe'):
iframe.decompose()
return render_template(
'display.html',
response=results,
translation=app.config['TRANSLATIONS'][
g.user_config.get_localization_lang()
]
)
return render_template('display.html', response=results)
def run_app() -> None:
@ -542,11 +361,6 @@ def run_app() -> None:
default='127.0.0.1',
metavar='<ip address>',
help='Specifies the host address to use (default 127.0.0.1)')
parser.add_argument(
'--unix-socket',
default='',
metavar='</path/to/unix.sock>',
help='Listen for app on unix socket instead of host:port')
parser.add_argument(
'--debug',
default=False,
@ -592,15 +406,9 @@ def run_app() -> None:
os.environ['WHOOGLE_PROXY_TYPE'] = args.proxytype
os.environ['WHOOGLE_PROXY_LOC'] = args.proxyloc
if args.https_only:
os.environ['HTTPS_ONLY'] = '1'
os.environ['HTTPS_ONLY'] = '1' if args.https_only else ''
if args.debug:
app.run(host=args.host, port=args.port, debug=args.debug)
elif args.unix_socket:
waitress.serve(app, unix_socket=args.unix_socket)
else:
waitress.serve(
app,
listen="{}:{}".format(args.host, args.port),
url_prefix=os.environ.get('WHOOGLE_URL_PREFIX', ''))
waitress.serve(app, listen="{}:{}".format(args.host, args.port))

View File

@ -1,2 +0,0 @@
*
!.gitignore

View File

@ -22,11 +22,6 @@ li {
color: var(--whoogle-dark-text) !important;
}
.anon-view {
color: var(--whoogle-dark-text) !important;
text-decoration: underline;
}
textarea {
background: var(--whoogle-dark-page-bg) !important;
color: var(--whoogle-dark-text) !important;
@ -63,29 +58,15 @@ select {
}
.ZINbbc {
overflow: hidden;
box-shadow: 0 0 0 0 !important;
background-color: var(--whoogle-dark-result-bg) !important;
margin-bottom: 10px !important;
border-radius: 8px !important;
}
.KP7LCb {
box-shadow: 0 0 0 0 !important;
}
.BVG0Nb {
box-shadow: 0 0 0 0 !important;
background-color: var(--whoogle-dark-page-bg) !important;
}
.ZINbbc.luh4tb {
background: var(--whoogle-dark-result-bg) !important;
margin-bottom: 24px !important;
background-color: var(--whoogle-dark-result-bg) !important;
}
.bRsWnc {
background-color: var(--whoogle-dark-result-bg) !important;
background-color: var(--whoogle-dark-result-bg) !important;
}
.BVG0Nb {
background-color: var(--whoogle-dark-page-bg) !important;
}
.x54gtf {
@ -100,19 +81,9 @@ select {
background-color: var(--whoogle-dark-divider) !important;
}
.home-search {
border-color: var(--whoogle-dark-element-bg) !important;
}
.sa1toc {
background: var(--whoogle-dark-page-bg) !important;
}
#search-bar {
border-color: var(--whoogle-dark-element-bg) !important;
color: var(--whoogle-dark-text) !important;
background-color: var(--whoogle-dark-result-bg) !important;
border-bottom: 2px solid var(--whoogle-dark-element-bg);
}
#search-bar:focus {
@ -131,11 +102,11 @@ select {
}
.collapsible {
color: var(--whoogle-dark-text) !important;
color: var(--whoogle-dark-text);
}
.collapsible:after {
color: var(--whoogle-dark-text) !important;
color: var(--whoogle-dark-text);
}
.active {
@ -143,7 +114,7 @@ select {
color: var(--whoogle-dark-contrast-text) !important;
}
.content, .result-config {
.content {
background-color: var(--whoogle-dark-element-bg) !important;
color: var(--whoogle-contrast-text) !important;
}
@ -152,14 +123,10 @@ select {
color: var(--whoogle-dark-contrast-text) !important;
}
.link {
#gh-link {
color: var(--whoogle-dark-contrast-text);
}
.link-color {
color: var(--whoogle-dark-result-url) !important;
}
.autocomplete-items {
border: 1px solid var(--whoogle-dark-element-bg);
}
@ -179,40 +146,3 @@ select {
background-color: var(--whoogle-dark-element-bg) !important;
color: var(--whoogle-dark-contrast-text) !important;
}
.footer {
color: var(--whoogle-dark-text);
}
path {
fill: var(--whoogle-dark-logo);
}
.header-div {
background-color: var(--whoogle-dark-result-bg) !important;
}
#search-reset {
color: var(--whoogle-dark-text) !important;
}
.mobile-search-bar {
background-color: var(--whoogle-dark-result-bg) !important;
color: var(--whoogle-dark-text) !important;
}
.search-bar-desktop {
color: var(--whoogle-dark-text) !important;
}
.ip-text-div, .update_available, .cb_label, .cb {
color: var(--whoogle-dark-secondary-text) !important;
}
.cb:focus {
color: var(--whoogle-dark-contrast-text) !important;
}
.desktop-header, .mobile-header {
background-color: var(--whoogle-dark-result-bg) !important;
}

View File

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

View File

@ -13,18 +13,9 @@ header {
border-radius: 2px 0 0 0;
}
.result-config {
margin-bottom: 10px;
padding: 10px;
border-radius: 8px;
}
.mobile-logo {
font: 22px/36px Futura, Arial, sans-serif;
padding-left: 5px;
display: flex;
justify-content: center;
align-items: center;
}
.logo-div {
@ -80,171 +71,3 @@ header {
border-radius: 8px;
box-shadow: 0 0 6px 1px #2375e8;
}
#mobile-header-logo {
height: 1.75em;
}
.mobile-input-div {
width: 100%;
}
.mobile-search-bar {
display: block;
font-size: 16px;
padding: 0 0 0 8px;
padding-right: 0px;
-webkit-box-flex: 1;
height: 35px;
outline: none;
border: none;
width: 100%;
-webkit-tap-highlight-color: rgba(0,0,0,.00);
overflow: hidden;
border: 0px !important;
}
.autocomplete-mobile{
display: -webkit-box;
width: 100%;
}
.desktop-header-logo {
height: 1.65em;
}
.header-autocomplete {
width: 100%;
flex: 1
}
a {
color: #1967D2;
text-decoration: none;
tap-highlight-color: rgba(0, 0, 0, .10);
}
.header-tab-div {
border-radius: 0 0 8px 8px;
box-shadow: 0 2px 3px rgba(32, 33, 36, 0.18);
overflow: hidden;
margin-bottom: 10px;
}
.header-tab-div-2 {
border-top: 1px solid #dadce0;
height: 39px;
overflow: hidden;
}
.header-tab-div-3 {
height: 51px;
overflow-x: auto;
overflow-y: hidden;
}
.desktop-header {
height: 39px;
display: box;
display: flex;
width: 100%;
}
.header-tab {
box-pack: justify;
font-size: 14px;
line-height: 37px;
justify-content: space-between;
}
.desktop-header a, .desktop-header span {
color: #70757a;
display: block;
flex: none;
padding: 0 16px;
text-align: center;
text-transform: uppercase;
}
span.header-tab-span {
border-bottom: 2px solid #4285f4;
color: #4285f4;
font-weight: bold;
}
.mobile-header {
height: 39px;
display: box;
display: flex;
overflow-x: scroll;
width: 100%;
padding-left: 12px;
}
.mobile-header a, .mobile-header span {
color: #70757a;
text-decoration: none;
display: inline-block;
/* padding: 8px 12px 8px 12px; */
}
span.mobile-tab-span {
border-bottom: 2px solid #202124;
color: #202124;
height: 26px;
/* margin: 0 12px; */
/* padding: 0; */
}
.desktop-header input {
margin: 2px 4px 2px 8px;
}
a.header-tab-a:visited {
color: #70757a;
}
.header-tab-div-end {
border-left: 1px solid rgba(0, 0, 0, 0.12);
}
.adv-search {
font-size: 30px;
}
.adv-search:hover {
cursor: pointer;
}
#adv-search-toggle {
display: none;
}
.result-collapsible {
max-height: 0px;
overflow: hidden;
transition: max-height .25s linear;
}
.search-bar-input {
display: block;
font-size: 16px;
padding: 0 0 0 8px;
flex: 1;
height: 35px;
outline: none;
border: none;
width: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
overflow: hidden;
}
#result-country {
max-width: 200px;
}
@media (max-width: 801px) {
.header-tab-div {
margin-bottom: 10px !important
}
}

View File

@ -12,30 +12,3 @@
height: 40px;
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

@ -22,11 +22,6 @@ li {
color: var(--whoogle-text) !important;
}
.anon-view {
color: var(--whoogle-text) !important;
text-decoration: underline;
}
textarea {
background: var(--whoogle-page-bg) !important;
color: var(--whoogle-text) !important;
@ -38,24 +33,11 @@ select {
}
.ZINbbc {
overflow: hidden;
background-color: var(--whoogle-result-bg) !important;
margin-bottom: 10px !important;
border-radius: 8px !important;
box-shadow: 0 1px 6px rgba(32,33,36,0.28) !important;
}
.BVG0Nb {
background-color: var(--whoogle-result-bg) !important;
}
.ZINbbc.luh4tb {
background: var(--whoogle-result-bg) !important;
margin-bottom: 24px !important;
background-color: var(--whoogle-result-bg) !important;
}
.bRsWnc {
background-color: var(--whoogle-result-bg) !important;
background-color: var(--whoogle-result-bg) !important;
}
.x54gtf {
@ -98,7 +80,7 @@ input {
}
.home-search {
border-color: var(--whoogle-element-bg) !important;
border: 3px solid var(--whoogle-element-bg) !important;
}
.search-container {
@ -129,7 +111,7 @@ input {
color: var(--whoogle-contrast-text) !important;
}
.content, .result-config {
.content {
background-color: var(--whoogle-element-bg) !important;
color: var(--whoogle-contrast-text) !important;
}
@ -138,14 +120,10 @@ input {
color: var(--whoogle-contrast-text);
}
.link {
#gh-link {
color: var(--whoogle-element-bg);
}
.link-color {
color: var(--whoogle-result-url) !important;
}
.autocomplete-items {
border: 1px solid var(--whoogle-element-bg);
}
@ -164,42 +142,3 @@ input {
background-color: var(--whoogle-element-bg) !important;
color: var(--whoogle-contrast-text) !important;
}
.footer {
color: var(--whoogle-text);
}
path {
fill: var(--whoogle-logo);
}
.header-div {
background-color: var(--whoogle-result-bg) !important;
}
#search-reset {
color: var(--whoogle-text) !important;
}
.mobile-search-bar {
background-color: var(--whoogle-result-bg) !important;
color: var(--whoogle-text) !important;
}
.search-bar-desktop {
background-color: var(--whoogle-result-bg) !important;
color: var(--whoogle-text);
border-bottom: 0px;
}
.ip-text-div, .update_available, .cb_label, .cb {
color: var(--whoogle-secondary-text) !important;
}
.cb:focus {
color: var(--whoogle-text) !important;
}
.desktop-header, .mobile-header {
background-color: var(--whoogle-result-bg) !important;
}

View File

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

View File

@ -13,11 +13,6 @@ body {
max-height: 500px;
}
.home-search {
background: transparent !important;
border: 3px solid;
}
.search-container {
background: transparent !important;
width: 80%;
@ -61,15 +56,6 @@ body {
-webkit-appearance: none;
}
.config-options {
max-height: 370px;
overflow-y: scroll;
}
.config-buttons {
max-height: 30px;
}
.config-div {
padding: 5px;
}
@ -144,7 +130,6 @@ footer {
.whoogle-svg {
width: 80%;
height: initial;
display: block;
margin: auto;
padding-bottom: 10px;
@ -177,14 +162,3 @@ details summary {
padding: 10px;
font-weight: bold;
}
/* Mobile styles */
@media (max-width: 1000px) {
select {
width: 100%;
}
#search-bar {
font-size: 20px;
}
}

View File

@ -1,12 +1,3 @@
body {
display: block !important;
margin: auto !important;
}
.vvjwJb {
font-size: 16px !important;
}
.autocomplete {
position: relative;
display: inline-block;
@ -31,41 +22,6 @@ body {
}
details summary {
margin-bottom: 20px;
padding: 10px;
font-weight: bold;
padding-left: 10px;
}
details summary span {
font-weight: normal;
}
#lingva-iframe {
width: 100%;
height: 650px;
border: 0;
}
.ip-address-div {
padding-bottom: 0 !important;
}
.ip-text-div {
padding-top: 0 !important;
}
.footer {
text-align: center;
}
@media (min-width: 801px) {
body {
min-width: 736px !important;
}
}
@media (max-width: 801px) {
details summary {
margin-bottom: 10px !important
}
}

View File

@ -3,7 +3,7 @@
/* LIGHT THEME COLORS */
--whoogle-logo: #685e79;
--whoogle-page-bg: #ffffff;
--whoogle-element-bg: #4285f4;
--whoogle-element-bg: #685e79;
--whoogle-text: #000000;
--whoogle-contrast-text: #ffffff;
--whoogle-secondary-text: #70757a;
@ -11,44 +11,18 @@
--whoogle-result-title: #1967d2;
--whoogle-result-url: #0d652d;
--whoogle-result-visited: #4b11a8;
--whoogle-divider: #dfe1e5;
/* DARK THEME COLORS */
--whoogle-dark-logo: #685e79;
--whoogle-dark-page-bg: #101020;
--whoogle-dark-element-bg: #4285f4;
--whoogle-dark-text: #ffffff;
--whoogle-dark-contrast-text: #ffffff;
--whoogle-dark-secondary-text: #bbbbbb;
--whoogle-dark-result-bg: #212131;
--whoogle-dark-result-title: #64a7f6;
--whoogle-dark-result-url: #34a853;
--whoogle-dark-result-visited: #bbbbff;
}
#whoogle-w {
fill: #4285f4;
}
#whoogle-h {
fill: #ea4335;
}
#whoogle-o-1 {
fill: #fbbc05;
}
#whoogle-o-2 {
fill: #4285f4;
}
#whoogle-g {
fill: #34a853;
}
#whoogle-l {
fill: #ea4335;
}
#whoogle-e {
fill: #fbbc05;
--whoogle-dark-logo: #888888;
--whoogle-dark-page-bg: #080808;
--whoogle-dark-element-bg: #111111;
--whoogle-dark-text: #dddddd;
--whoogle-dark-contrast-text: #aaaaaa;
--whoogle-dark-secondary-text: #8a8b8c;
--whoogle-dark-result-bg: #111111;
--whoogle-dark-result-title: #dddddd;
--whoogle-dark-result-url: #eceff4;
--whoogle-dark-result-visited: #959595;
--whoogle-dark-divider: #111111;
}

View File

@ -1,9 +1,4 @@
let searchInput;
let currentFocus;
let originalSearch;
let autocompleteResults;
const handleUserInput = () => {
const handleUserInput = searchBar => {
let xhrRequest = new XMLHttpRequest();
xhrRequest.open("POST", "autocomplete");
xhrRequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
@ -14,119 +9,118 @@ const handleUserInput = () => {
}
// Fill autocomplete with fetched results
autocompleteResults = JSON.parse(xhrRequest.responseText)[1];
updateAutocompleteList();
let autocompleteResults = JSON.parse(xhrRequest.responseText);
autocomplete(searchBar, autocompleteResults[1]);
};
xhrRequest.send('q=' + searchInput.value);
xhrRequest.send('q=' + searchBar.value);
};
const closeAllLists = el => {
// Close all autocomplete suggestions
let suggestions = document.getElementsByClassName("autocomplete-items");
for (let i = 0; i < suggestions.length; i++) {
if (el !== suggestions[i] && el !== searchInput) {
suggestions[i].parentNode.removeChild(suggestions[i]);
const autocomplete = (searchInput, autocompleteResults) => {
let currentFocus;
let originalSearch;
searchInput.addEventListener("input", function () {
let autocompleteList, autocompleteItem, i, val = this.value;
closeAllLists();
if (!val || !autocompleteResults) {
return false;
}
}
};
const removeActive = suggestion => {
// Remove "autocomplete-active" class from previously active suggestion
for (let i = 0; i < suggestion.length; i++) {
suggestion[i].classList.remove("autocomplete-active");
}
};
currentFocus = -1;
autocompleteList = document.createElement("div");
autocompleteList.setAttribute("id", this.id + "-autocomplete-list");
autocompleteList.setAttribute("class", "autocomplete-items");
this.parentNode.appendChild(autocompleteList);
const addActive = (suggestion) => {
// Handle navigation outside of suggestion list
if (!suggestion || !suggestion[currentFocus]) {
if (currentFocus >= suggestion.length) {
// Move selection back to the beginning
currentFocus = 0;
} else if (currentFocus < 0) {
// Retrieve original search and remove active suggestion selection
currentFocus = -1;
searchInput.value = originalSearch;
removeActive(suggestion);
return;
for (i = 0; i < autocompleteResults.length; i++) {
if (autocompleteResults[i].substr(0, val.length).toUpperCase() === val.toUpperCase()) {
autocompleteItem = document.createElement("div");
autocompleteItem.innerHTML = "<strong>" + autocompleteResults[i].substr(0, val.length) + "</strong>";
autocompleteItem.innerHTML += autocompleteResults[i].substr(val.length);
autocompleteItem.innerHTML += "<input type=\"hidden\" value=\"" + autocompleteResults[i] + "\">";
autocompleteItem.addEventListener("click", function () {
searchInput.value = this.getElementsByTagName("input")[0].value;
closeAllLists();
document.getElementById("search-form").submit();
});
autocompleteList.appendChild(autocompleteItem);
}
}
});
searchInput.addEventListener("keydown", function (e) {
let suggestion = document.getElementById(this.id + "-autocomplete-list");
if (suggestion) suggestion = suggestion.getElementsByTagName("div");
if (e.keyCode === 40) { // down
e.preventDefault();
currentFocus++;
addActive(suggestion);
} else if (e.keyCode === 38) { //up
e.preventDefault();
currentFocus--;
addActive(suggestion);
} else if (e.keyCode === 13) { // enter
e.preventDefault();
if (currentFocus > -1) {
if (suggestion) suggestion[currentFocus].click();
}
} else {
return;
originalSearch = document.getElementById("search-bar").value;
}
}
});
removeActive(suggestion);
suggestion[currentFocus].classList.add("autocomplete-active");
const addActive = suggestion => {
let searchBar = document.getElementById("search-bar");
// Autofill search bar with suggestion content (minus the "bang name" if using a bang operator)
let searchContent = suggestion[currentFocus].textContent;
if (searchContent.indexOf('(') > 0) {
searchInput.value = searchContent.substring(0, searchContent.indexOf('('));
} else {
searchInput.value = searchContent;
}
searchInput.focus();
};
const autocompleteInput = (e) => {
// Handle navigation between autocomplete suggestions
let suggestion = document.getElementById(this.id + "-autocomplete-list");
if (suggestion) suggestion = suggestion.getElementsByTagName("div");
if (e.keyCode === 40) { // down
e.preventDefault();
currentFocus++;
addActive(suggestion);
} else if (e.keyCode === 38) { //up
e.preventDefault();
currentFocus--;
addActive(suggestion);
} else if (e.keyCode === 13) { // enter
e.preventDefault();
if (currentFocus > -1) {
if (suggestion) suggestion[currentFocus].click();
// Handle navigation outside of suggestion list
if (!suggestion || !suggestion[currentFocus]) {
if (currentFocus >= suggestion.length) {
// Move selection back to the beginning
currentFocus = 0;
} else if (currentFocus < 0) {
// Retrieve original search and remove active suggestion selection
currentFocus = -1;
searchBar.value = originalSearch;
removeActive(suggestion);
return;
} else {
return;
}
}
} else {
originalSearch = searchInput.value;
}
};
const updateAutocompleteList = () => {
let autocompleteList, autocompleteItem, i;
let val = originalSearch;
closeAllLists();
removeActive(suggestion);
suggestion[currentFocus].classList.add("autocomplete-active");
if (!val || !autocompleteResults) {
return false;
}
currentFocus = -1;
autocompleteList = document.createElement("div");
autocompleteList.setAttribute("id", this.id + "-autocomplete-list");
autocompleteList.setAttribute("class", "autocomplete-items");
searchInput.parentNode.appendChild(autocompleteList);
for (i = 0; i < autocompleteResults.length; i++) {
if (autocompleteResults[i].substr(0, val.length).toUpperCase() === val.toUpperCase()) {
autocompleteItem = document.createElement("div");
autocompleteItem.innerHTML = "<strong>" + autocompleteResults[i].substr(0, val.length) + "</strong>";
autocompleteItem.innerHTML += autocompleteResults[i].substr(val.length);
autocompleteItem.innerHTML += "<input type=\"hidden\" value=\"" + autocompleteResults[i] + "\">";
autocompleteItem.addEventListener("click", function () {
searchInput.value = this.getElementsByTagName("input")[0].value;
closeAllLists();
document.getElementById("search-form").submit();
});
autocompleteList.appendChild(autocompleteItem);
// Autofill search bar with suggestion content (minus the "bang name" if using a bang operator)
let searchContent = suggestion[currentFocus].textContent;
if (searchContent.indexOf('(') > 0) {
searchBar.value = searchContent.substring(0, searchContent.indexOf('('));
} else {
searchBar.value = searchContent;
}
}
};
document.addEventListener("DOMContentLoaded", function() {
searchInput = document.getElementById("search-bar");
searchInput.addEventListener("keydown", (event) => autocompleteInput(event));
searchBar.focus();
};
const removeActive = suggestion => {
for (let i = 0; i < suggestion.length; i++) {
suggestion[i].classList.remove("autocomplete-active");
}
};
const closeAllLists = el => {
let suggestions = document.getElementsByClassName("autocomplete-items");
for (let i = 0; i < suggestions.length; i++) {
if (el !== suggestions[i] && el !== searchInput) {
suggestions[i].parentNode.removeChild(suggestions[i]);
}
}
};
// Close lists and search when user selects a suggestion
document.addEventListener("click", function (e) {
closeAllLists(e.target);
});
});
};

View File

@ -2,8 +2,6 @@ const setupSearchLayout = () => {
// Setup search field
const searchBar = document.getElementById("search-bar");
const searchBtn = document.getElementById("search-submit");
const arrowKeys = [37, 38, 39, 40];
let searchValue = searchBar.value;
// Automatically focus on search field
searchBar.focus();
@ -13,9 +11,8 @@ const setupSearchLayout = () => {
if (event.keyCode === 13) {
event.preventDefault();
searchBtn.click();
} else if (searchBar.value !== searchValue && !arrowKeys.includes(event.keyCode)) {
searchValue = searchBar.value;
handleUserInput();
} else {
handleUserInput(searchBar);
}
});
};
@ -29,7 +26,7 @@ const setupConfigLayout = () => {
if (content.style.maxHeight) {
content.style.maxHeight = null;
} else {
content.style.maxHeight = "400px";
content.style.maxHeight = content.scrollHeight + "px";
}
content.classList.toggle("open");

View File

@ -1,9 +0,0 @@
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,46 +1,11 @@
document.addEventListener("DOMContentLoaded", () => {
const advSearchToggle = document.getElementById("adv-search-toggle");
const advSearchDiv = document.getElementById("adv-search-div");
const searchBar = document.getElementById("search-bar");
const countrySelect = document.getElementById("result-country");
const arrowKeys = [37, 38, 39, 40];
let searchValue = searchBar.value;
countrySelect.onchange = () => {
let str = window.location.href;
n = str.lastIndexOf("/search");
if (n > 0) {
str = str.substring(0, n) +
`/search?q=${searchBar.value}&country=${countrySelect.value}`;
window.location.href = str;
}
}
const toggleAdvancedSearch = on => {
if (on) {
advSearchDiv.style.maxHeight = "70px";
searchBar.addEventListener("keyup", function (event) {
if (event.keyCode !== 13) {
handleUserInput(searchBar);
} else {
advSearchDiv.style.maxHeight = "0px";
}
localStorage.advSearchToggled = on;
}
try {
toggleAdvancedSearch(JSON.parse(localStorage.advSearchToggled));
} catch (error) {
console.warn("Did not recover advanced search toggle state");
}
advSearchToggle.onclick = () => {
toggleAdvancedSearch(advSearchToggle.checked);
}
searchBar.addEventListener("keyup", function(event) {
if (event.keyCode === 13) {
document.getElementById("search-form").submit();
} else if (searchBar.value !== searchValue && !arrowKeys.includes(event.keyCode)) {
searchValue = searchBar.value;
handleUserInput();
}
});
});

View File

@ -1,58 +1,44 @@
(function () {
let searchBar, results;
let shift = false;
const keymap = {
ArrowUp: goUp,
ArrowDown: goDown,
ShiftTab: goUp,
Tab: goDown,
k: goUp,
j: goDown,
'/': focusSearch,
};
let activeIdx = -1;
let searchBar, results;
const keymap = {
ArrowUp: goUp,
ArrowDown: goDown,
k: goUp,
j: goDown,
'/': focusSearch,
};
let activeIdx = -1;
document.addEventListener('DOMContentLoaded', () => {
searchBar = document.querySelector('#search-bar');
results = document.querySelectorAll('#main>div>div>div>a');
});
document.addEventListener('DOMContentLoaded', () => {
searchBar = document.querySelector('#search-bar');
results = document.querySelectorAll('#main>div>div>div>a');
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Shift') {
shift = true;
}
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT') return true;
if (typeof keymap[e.key] === 'function') {
e.preventDefault();
keymap[e.key]();
}
});
if (e.target.tagName === 'INPUT') return true;
if (typeof keymap[e.key] === 'function') {
e.preventDefault();
function goUp () {
if (activeIdx > 0) focusResult(activeIdx - 1);
else focusSearch();
}
keymap[`${shift && e.key == 'Tab' ? 'Shift' : ''}${e.key}`]();
}
});
function goDown () {
if (activeIdx < results.length - 1) focusResult(activeIdx + 1);
}
document.addEventListener('keyup', (e) => {
if (e.key === 'Shift') {
shift = false;
}
});
function focusResult (idx) {
activeIdx = idx;
results[activeIdx].scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
results[activeIdx].focus();
}
function goUp () {
if (activeIdx > 0) focusResult(activeIdx - 1);
else focusSearch();
}
function goDown () {
if (activeIdx < results.length - 1) focusResult(activeIdx + 1);
}
function focusResult (idx) {
activeIdx = idx;
results[activeIdx].scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
results[activeIdx].focus();
}
function focusSearch () {
activeIdx = -1;
searchBar.focus();
}
function focusSearch () {
activeIdx = -1;
searchBar.focus();
}
}());

View File

@ -1,10 +1,6 @@
const checkForTracking = () => {
const mainDiv = document.getElementById("main");
const searchBar = document.getElementById("search-bar");
// some pages (e.g. images) do not have these
if (!mainDiv || !searchBar)
return;
const query = searchBar.value.replace(/\s+/g, '');
const query = document.getElementById("search-bar").value.replace(/\s+/g, '');
// Note: regex functions for checking for tracking queries were derived
// from here -- https://stackoverflow.com/questions/619977
@ -63,14 +59,11 @@ document.addEventListener("DOMContentLoaded", function() {
checkForTracking();
// Clear input if reset button tapped
const searchBar = document.getElementById("search-bar");
const search = document.getElementById("search-bar");
const resetBtn = document.getElementById("search-reset");
// some pages (e.g. images) do not have these
if (!searchBar || !resetBtn)
return;
resetBtn.addEventListener("click", event => {
event.preventDefault();
searchBar.value = "";
searchBar.focus();
search.value = "";
search.focus();
});
});

View File

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

View File

@ -1,32 +0,0 @@
{
"all": {
"tbm": null,
"href": "search?q={query}",
"name": "All",
"selected": true
},
"images": {
"tbm": "isch",
"href": "search?q={query}",
"name": "Images",
"selected": false
},
"maps": {
"tbm": null,
"href": "https://maps.google.com/maps?q={query}",
"name": "Maps",
"selected": false
},
"videos": {
"tbm": "vid",
"href": "search?q={query}",
"name": "Videos",
"selected": false
},
"news": {
"tbm": "nws",
"href": "search?q={query}",
"name": "News",
"selected": false
}
}

View File

@ -1,53 +1,49 @@
[
{"name": "-------", "value": ""},
{"name": "Default (none specified)", "value": ""},
{"name": "English", "value": "lang_en"},
{"name": "Afrikaans (Afrikaans)", "value": "lang_af"},
{"name": "Arabic (عربى)", "value": "lang_ar"},
{"name": "Armenian (հայերեն)", "value": "lang_hy"},
{"name": "Belarusian (Беларуская)", "value": "lang_be"},
{"name": "Bulgarian (български)", "value": "lang_bg"},
{"name": "Catalan (Català)", "value": "lang_ca"},
{"name": "Chinese, Simplified (简体中文)", "value": "lang_zh-CN"},
{"name": "Chinese, Traditional (正體中文)", "value": "lang_zh-TW"},
{"name": "Croatian (Hrvatski)", "value": "lang_hr"},
{"name": "Czech (čeština)", "value": "lang_cs"},
{"name": "Danish (Dansk)", "value": "lang_da"},
{"name": "Dutch (Nederlands)", "value": "lang_nl"},
{"name": "Esperanto (Esperanto)", "value": "lang_eo"},
{"name": "Estonian (Eestlane)", "value": "lang_et"},
{"name": "Filipino (Pilipino)", "value": "lang_tl"},
{"name": "Finnish (Suomalainen)", "value": "lang_fi"},
{"name": "French (Français)", "value": "lang_fr"},
{"name": "German (Deutsch)", "value": "lang_de"},
{"name": "Greek (Ελληνικά)", "value": "lang_el"},
{"name": "Hebrew (עִברִית)", "value": "lang_iw"},
{"name": "Hindi (हिंदी)", "value": "lang_hi"},
{"name": "Hungarian (Magyar)", "value": "lang_hu"},
{"name": "Icelandic (Íslenska)", "value": "lang_is"},
{"name": "Indonesian (Indonesian)", "value": "lang_id"},
{"name": "Italian (Italiano)", "value": "lang_it"},
{"name": "Japanese (日本語)", "value": "lang_ja"},
{"name": "Korean (한국어)", "value": "lang_ko"},
{"name": "Kurdish (Kurdî)", "value": "lang_ku"},
{"name": "Latvian (Latvietis)", "value": "lang_lv"},
{"name": "Lithuanian (Lietuvis)", "value": "lang_lt"},
{"name": "Norwegian (Norwegian)", "value": "lang_no"},
{"name": "Persian (فارسی)", "value": "lang_fa"},
{"name": "Polish (Polskie)", "value": "lang_pl"},
{"name": "Portuguese (Português)", "value": "lang_pt"},
{"name": "Romanian (Română)", "value": "lang_ro"},
{"name": "Russian (русский)", "value": "lang_ru"},
{"name": "Serbian (Српски)", "value": "lang_sr"},
{"name": "Sinhala (සිංහල)", "value": "lang_si"},
{"name": "Slovak (Slovák)", "value": "lang_sk"},
{"name": "Slovenian (Slovenščina)", "value": "lang_sl"},
{"name": "Spanish (Español)", "value": "lang_es"},
{"name": "Swahili (Kiswahili)", "value": "lang_sw"},
{"name": "Swedish (Svenska)", "value": "lang_sv"},
{"name": "Thai (ไทย)", "value": "lang_th"},
{"name": "Turkish (Türk)", "value": "lang_tr"},
{"name": "Ukrainian (Український)", "value": "lang_uk"},
{"name": "Vietnamese (Tiếng Việt)", "value": "lang_vi"},
{"name": "Xhosa (isiXhosa)", "value": "lang_xh"},
{"name": "Zulu (isiZulu)", "value": "lang_zu"}
{"name": "Afrikaans", "value": "lang_af"},
{"name": "Arabic", "value": "lang_ar"},
{"name": "Armenian", "value": "lang_hy"},
{"name": "Belarusian", "value": "lang_be"},
{"name": "Bulgarian", "value": "lang_bg"},
{"name": "Catalan", "value": "lang_ca"},
{"name": "Chinese (Simplified)", "value": "lang_zh-CN"},
{"name": "Chinese (Traditional)", "value": "lang_zh-TW"},
{"name": "Croatian", "value": "lang_hr"},
{"name": "Czech", "value": "lang_cs"},
{"name": "Danish", "value": "lang_da"},
{"name": "Dutch", "value": "lang_nl"},
{"name": "Esperanto", "value": "lang_eo"},
{"name": "Estonian", "value": "lang_et"},
{"name": "Filipino", "value": "lang_tl"},
{"name": "Finnish", "value": "lang_fi"},
{"name": "French", "value": "lang_fr"},
{"name": "German", "value": "lang_de"},
{"name": "Greek", "value": "lang_el"},
{"name": "Hebrew", "value": "lang_iw"},
{"name": "Hindi", "value": "lang_hi"},
{"name": "Hungarian", "value": "lang_hu"},
{"name": "Icelandic", "value": "lang_is"},
{"name": "Indonesian", "value": "lang_id"},
{"name": "Italian", "value": "lang_it"},
{"name": "Japanese", "value": "lang_ja"},
{"name": "Korean", "value": "lang_ko"},
{"name": "Latvian", "value": "lang_lv"},
{"name": "Lithuanian", "value": "lang_lt"},
{"name": "Norwegian", "value": "lang_no"},
{"name": "Persian", "value": "lang_fa"},
{"name": "Polish", "value": "lang_pl"},
{"name": "Portuguese", "value": "lang_pt"},
{"name": "Romanian", "value": "lang_ro"},
{"name": "Russian", "value": "lang_ru"},
{"name": "Serbian", "value": "lang_sr"},
{"name": "Slovak", "value": "lang_sk"},
{"name": "Slovenian", "value": "lang_sl"},
{"name": "Spanish", "value": "lang_es"},
{"name": "Swahili", "value": "lang_sw"},
{"name": "Swedish", "value": "lang_sv"},
{"name": "Thai", "value": "lang_th"},
{"name": "Turkish", "value": "lang_tr"},
{"name": "Ukrainian", "value": "lang_uk"},
{"name": "Vietnamese", "value": "lang_vi"}
]

View File

@ -1,5 +0,0 @@
[
"light",
"dark",
"system"
]

View File

@ -1,933 +0,0 @@
{
"lang_en": {
"search": "Search",
"config": "Configuration",
"config-country": "Country",
"config-lang": "Interface Language",
"config-lang-search": "Search Language",
"config-near": "Near",
"config-near-help": "City Name",
"config-block": "Block",
"config-block-help": "Comma-separated site list",
"config-block-title": "Block by Title",
"config-block-title-help": "Use regex",
"config-block-url": "Block by URL",
"config-block-url-help": "Use regex",
"config-theme": "Theme",
"config-nojs": "Remove Javascript in Anonymous View",
"config-anon-view": "Show Anonymous View Links",
"config-dark": "Dark Mode",
"config-safe": "Safe Search",
"config-alts": "Replace Social Media Links",
"config-alts-help": "Replaces Twitter/YouTube/Instagram/etc links with privacy respecting alternatives.",
"config-new-tab": "Open Links in New Tab",
"config-images": "Full Size Image Search",
"config-images-help": "(Experimental) Adds the 'View Image' option to desktop image searches. This will cause image result thumbnails to be lower resolution.",
"config-tor": "Use Tor",
"config-get-only": "GET Requests Only",
"config-url": "Root URL",
"config-pref-url": "Preferences URL",
"config-pref-encryption": "Encrypt Preferences",
"config-pref-help": "Requires WHOOGLE_CONFIG_PREFERENCES_KEY, otherwise this will be ignored.",
"config-css": "Custom CSS",
"load": "Load",
"apply": "Apply",
"save-as": "Save As...",
"github-link": "View on GitHub",
"translate": "translate",
"light": "light",
"dark": "dark",
"system": "system",
"ratelimit": "Instance has been ratelimited",
"continue-search": "Continue your search with Farside",
"all": "All",
"images": "Images",
"maps": "Maps",
"videos": "Videos",
"news": "News",
"books": "Books",
"anon-view": "Anonymous View"
},
"lang_nl": {
"search": "Zoeken",
"config": "Instellingen",
"config-country": "Land instellen",
"config-lang": "Taal instellingen",
"config-lang-search": "Zoek taal",
"config-near": "Dichtbij",
"config-near-help": "Stad",
"config-block": "Blok",
"config-block-help": "Lijst met sites met kommas onderscheiden",
"config-block-title": "Blokkeren op titel",
"config-block-title-help": "Gebruik regex",
"config-block-url": "Blokkeren op URL",
"config-block-url-help": "Gebruik regex",
"config-theme": "Thema",
"config-nojs": "Javascript verwijderen in anonieme weergave",
"config-anon-view": "Toon anonieme links bekijken",
"config-dark": "Donkere Modus",
"config-safe": "Veilig zoeken",
"config-alts": "Social Media Links Vervangen",
"config-alts-help": "Vervang Twitter/YouTube/Instagram/etc links met privacy gerespecteerde alternatieve.",
"config-new-tab": "Open Links in New Tab",
"config-images": "Volledige Grote Afbeelding Zoeken",
"config-images-help": "(Expirimenteel) Voegt de optie 'View Image' toe aan desktop afbeeldingen zoeken. Dit zorgt ervoor dat de voorbeeld foto's kleiner zijn.",
"config-tor": "Gebruik Tor",
"config-get-only": "Alleen GET Requests",
"config-url": "Root URL",
"config-pref-url": "Voorkeurs URL",
"config-pref-encryption": "Versleutel voorkeuren",
"config-pref-help": "Vereist WHOOGLE_CONFIG_PREFERENCES_KEY, anders wordt dit genegeerd.",
"config-css": "Eigen CSS",
"load": "Laden",
"apply": "Opslaan",
"save-as": "Opslaan Als...",
"github-link": "Bekijk op GitHub",
"translate": "vertalen",
"light": "helder",
"dark": "donker",
"system": "systeeminstellingen",
"ratelimit": "Instantie is beperkt in snelheid",
"continue-search": "Ga verder met zoeken met Farside",
"all": "Alle",
"images": "Afbeeldingen",
"maps": "Maps",
"videos": "Videos",
"news": "Nieuws",
"books": "Boeken",
"anon-view": "Anonieme Weergave"
},
"lang_de": {
"search": "Suchen",
"config": "Einstellungen",
"config-country": "Land einstellen",
"config-lang": "Oberflächen-Sprache",
"config-lang-search": "Such-Sprache",
"config-near": "In der Nähe von",
"config-near-help": "Stadt-Name",
"config-block": "Block",
"config-block-help": "Komma-getrennte Liste von Seiten",
"config-block-title": "Nach Titel blockieren",
"config-block-title-help": "Regex verwenden",
"config-block-url": "Nach URL blockieren",
"config-block-url-help": "Regex verwenden",
"config-theme": "Thema",
"config-nojs": "Entfernen Sie Javascript in der anonymen Ansicht",
"config-anon-view": "Anonyme Ansichtslinks anzeigen",
"config-dark": "Dark Mode",
"config-safe": "Sicheres Suchen",
"config-alts": "Social-Media-Links ersetzen",
"config-alts-help": "Ersetzt Twitter/YouTube/Instagram/etc Links mit Alternativen, welche die Privatsphäre respektieren.",
"config-new-tab": "Links in neuen Tabs öffnen",
"config-images": "Bilder-Suche in Vollbild",
"config-images-help": "(Experimentell) Fügt 'View Image'-Einstellung zu Dekstop Bilder-Suchen hinzu. Dadurch werden Thumbnails in niedrigerer Auflösung angezeigt.",
"config-tor": "Tor benutzen",
"config-get-only": "Auschließlich GET-Anfragen",
"config-url": "Root URL",
"config-pref-url": "Einstellungs URL",
"config-pref-encryption": "Einstellungen verschlüsseln",
"config-pref-help": "Erfordert WHOOGLE_CONFIG_PREFERENCES_KEY, sonst wird dies ignoriert.",
"config-css": "Custom CSS",
"load": "Laden",
"apply": "Übernehmen",
"save-as": "Speichern unter...",
"github-link": "Auf GitHub öffnen",
"translate": "Übersetzen",
"light": "hell",
"dark": "dunkel",
"system": "Systemeinstellung",
"ratelimit": "Instanz wurde ratenbegrenzt",
"continue-search": "Setzen Sie Ihre Suche fort mit Farside",
"all": "Alle",
"images": "Bilder",
"maps": "Maps",
"videos": "Videos",
"news": "Nachrichten",
"books": "Bücher",
"anon-view": "Anonyme Ansicht"
},
"lang_es": {
"search": "Buscar",
"config": "Configuración",
"config-country": "Establecer País",
"config-lang": "Idioma de Interfaz",
"config-lang-search": "Idioma de Búsqueda",
"config-near": "Cerca",
"config-near-help": "Nombre de la Ciudad",
"config-block": "Bloquear",
"config-block-help": "Lista de sitios separados por comas",
"config-block-title": "Bloquear por título",
"config-block-title-help": "Usar expresiones regulares",
"config-block-url": "Bloquear por URL",
"config-block-url-help": "Usar expresiones regulares",
"config-theme": "Tema",
"config-nojs": "Eliminar Javascript en vista anónima",
"config-anon-view": "Mostrar enlaces de vista anónima",
"config-dark": "Modo Oscuro",
"config-safe": "Búsqueda Segura",
"config-alts": "Reemplazar Enlaces de Redes Sociales",
"config-alts-help": "Reemplaza los enlaces de Twitter/YouTube/Instagram/etc con alternativas que respetan la privacidad.",
"config-new-tab": "Abrir enlaces en una pestaña nueva",
"config-images": "Búsqueda de imágenes a tamaño completo",
"config-images-help": "(Experimental) Agrega la opción 'Ver imagen' a las búsquedas de imágenes de escritorio. Esto hará que las miniaturas de los resultados de la imagen aparezcan con una resolución más baja.",
"config-tor": "Usa Tor",
"config-get-only": "GET solo solicitudes",
"config-url": "URL raíz",
"config-pref-url": "URL de preferencias",
"config-pref-encryption": "Cifrar preferencias",
"config-pref-help": "Requiere WHOOGLE_CONFIG_PREFERENCES_KEY; de lo contrario, se ignorará.",
"config-css": "CSS personalizado",
"load": "Cargar",
"apply": "Aplicar",
"save-as": "Guardar como...",
"github-link": "Ver en GitHub",
"translate": "traducir",
"light": "brillante",
"dark": "oscuro",
"system": "configuración del sistema",
"ratelimit": "La instancia ha sido ratelimited",
"continue-search": "Continúe su búsqueda con Farside",
"all": "Todo",
"images": "Imágenes",
"maps": "Maps",
"videos": "Vídeos",
"news": "Noticias",
"books": "Libros",
"anon-view": "Vista Anónima"
},
"lang_it": {
"search": "Cerca",
"config": "Impostazioni",
"config-country": "Imposta Paese",
"config-lang": "Lingua dell'interfaccia",
"config-lang-search": "Lingua della ricerca",
"config-near": "Vicino",
"config-near-help": "Nome della città",
"config-block": "Blocca",
"config-block-help": "Lista di siti separati da virgole",
"config-block-title": "Blocca per titolo",
"config-block-title-help": "Usa regex",
"config-block-url": "Blocca per url",
"config-block-url-help": "Usa regex",
"config-theme": "Tema",
"config-nojs": "Rimuovere Javascript in visualizzazione anonima",
"config-anon-view": "Mostra collegamenti di visualizzazione anonimi",
"config-dark": "Modalità Notte",
"config-safe": "Ricerca Sicura",
"config-alts": "Sostituisci link dei social",
"config-alts-help": "Sostituisci link di Twitter/YouTube/Instagram/etc con alternative che rispettano la privacy.",
"config-new-tab": "Apri i link in una nuova scheda",
"config-images": "Ricerca Immagini",
"config-images-help": "(Sperimentale) Aggiunge la modalità 'Ricerca Immagini'. Questo ridurrà drasticamente la qualità delle miniature durante la ricerca.",
"config-tor": "Usa Tor",
"config-get-only": "Utilizza solo richieste GET",
"config-url": "Root URL",
"config-pref-url": "URL delle preferenze",
"config-pref-encryption": "Crittografa le preferenze",
"config-pref-help": "Richiede WHOOGLE_CONFIG_PREFERENCES_KEY, altrimenti verrà ignorato.",
"config-css": "CSS Personalizzato",
"load": "Carica",
"apply": "Applica",
"save-as": "Salva Come...",
"github-link": "Guarda su GitHub",
"translate": "tradurre",
"light": "luminoso",
"dark": "notte",
"system": "impostazioni di sistema",
"ratelimit": "L'istanza è stata limitata alla velocità",
"continue-search": "Continua la tua ricerca con Farside",
"all": "Tutti",
"images": "Immagini",
"maps": "Maps",
"videos": "Video",
"news": "Notizie",
"books": "Libri",
"anon-view": "Vista Anonima"
},
"lang_pt": {
"search": "Pesquisar",
"config": "Configuração",
"config-country": "Definir País",
"config-lang": "Idioma da Interface",
"config-lang-search": "Idioma da Pesquisa",
"config-near": "Perto",
"config-near-help": "Nome da Cidade",
"config-block": "Bloquear",
"config-block-help": "Lista de sites separados por vírgulas",
"config-block-title": "Bloco por título",
"config-block-title-help": "Use regex",
"config-block-url": "Bloquear por url",
"config-block-url-help": "Use regex",
"config-theme": "Tema",
"config-nojs": "Remover Javascript na visualização anônima",
"config-anon-view": "Mostrar links de visualização anônimos",
"config-dark": "Modo Escuro",
"config-safe": "Pesquisa Segura",
"config-alts": "Substituir Links de Redes Sociais",
"config-alts-help": "Substitui os links do Twitter/YouTube/Instagram/etc. por alternativas que respeitam sua privacidade.",
"config-new-tab": "Abrir Links em Nova Aba",
"config-images": "Pesquisa de Imagem em Tamanho Real",
"config-images-help": "(Experimental) Adiciona a opção 'Mostrar Imagem' às pesquisas de imagens no modo 'para computador'. Isso fará com que as miniaturas do resultado da imagem sejam de menor resolução.",
"config-tor": "Usar Tor",
"config-get-only": "Apenas Pedidos GET",
"config-url": "URL Fonte",
"config-pref-url": "URL de preferências",
"config-pref-encryption": "Criptografar preferências",
"config-pref-help": "Requer WHOOGLE_CONFIG_PREFERENCES_KEY, caso contrário, será ignorado.",
"config-css": "CSS Personalizado",
"load": "Carregar",
"apply": "Aplicar",
"save-as": "Guardar Como...",
"github-link": "Ver no GitHub",
"translate": "traduzir",
"light": "brilhante",
"dark": "escuro",
"system": "configuração de sistema",
"ratelimit": "A instância foi limitada pela taxa",
"continue-search": "Continue sua pesquisa com Farside",
"all": "Todas",
"images": "Imagens",
"maps": "Maps",
"videos": "Vídeos",
"news": "Notícias",
"books": "Livros",
"anon-view": "Visualização Anônima"
},
"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": "Удалить Javascript в анонимном просмотре",
"config-anon-view": "Показать ссылки для анонимного просмотра",
"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-pref-url": "URL-адрес настроек",
"config-pref-encryption": "Зашифровать настройки",
"config-pref-help": "Требуется WHOOGLE_CONFIG_PREFERENCES_KEY, иначе это будет проигнорировано.",
"config-css": "Пользовательский CSS",
"load": "Загрузить",
"apply": "Применить",
"save-as": "Сохранить как...",
"github-link": "Посмотреть на GitHub",
"translate": "перевести",
"light": "светлая",
"dark": "тёмная",
"system": "системная",
"ratelimit": "Инстанс был ограничен по операциям",
"continue-search": "Продолжить поиск с Farside",
"all": "Все",
"images": "Картинки",
"maps": "Карты",
"videos": "Видео",
"news": "Новости",
"books": "Книги",
"anon-view": "Анонимный просмотр"
},
"lang_zh-CN": {
"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": "在匿名视图中删除 Javascript",
"config-anon-view": "显示匿名查看链接",
"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-pref-url": "首选项网址",
"config-pref-encryption": "加密首选项",
"config-pref-help": "需要 WHOOGLE_CONFIG_PREFERENCES_KEY否则将被忽略。",
"config-css": "自定义 CSS",
"load": "载入",
"apply": "应用",
"save-as": "另存为...",
"github-link": "在 GitHub 上查看",
"translate": "翻译",
"light": "明亮的",
"dark": "黑暗的",
"system": "系统设置",
"ratelimit": "实例已被限速",
"continue-search": "继续搜索 Farside",
"all": "全部",
"images": "图片",
"maps": "地图",
"videos": "视频",
"news": "新闻",
"books": "书籍",
"anon-view": "匿名视图"
},
"lang_si": {
"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": "Anonymous View හි Javascript ඉවත් කරන්න",
"config-anon-view": "නිර්නාමික බලන්න සබැඳි පෙන්වන්න",
"config-dark": "අඳුරු ආකාරය",
"config-safe": "ආරක්‍ෂිත සෙවුම",
"config-alts": "සමාජ මාධ්‍ය සබැඳි ප්‍රතිස්ථාපනය කරන්න",
"config-alts-help": "ට්විටර්/යූ ටියුබ්/ඉන්ස්ටග්‍රෑම් ආදී සබැඳි පෞද්ගලිකත්වයට ගරු කරන විකල්ප සමඟ ප්‍රතිස්ථාපනය කරයි.",
"config-new-tab": "නව තීරුවකින් සබැඳි විවෘත කරන්න",
"config-images": "පූර්ණ ප්‍රමාණයේ රූප සෙවීම",
"config-images-help": "(පර්යේෂණාත්මක) මේස පරිගණකවල රූප සෙවීමට 'රූපය බලන්න' විකල්පය එකතු කරයි. මෙය රූප ප්‍රතිඵල සංක්ෂිප්තවල අඩු විභේදනයක් ඇති කිරීමට හේතු වේ.",
"config-tor": "ටෝර් භාවිතා කරන්න",
"config-get-only": "ඉල්ලීම් පමණක් ලබා ගන්න",
"config-url": "ඒ.ස.නි.(URL) මූලය",
"config-pref-url": "මනාප URL",
"config-pref-encryption": "මනාප සංකේතනය කරන්න",
"config-pref-help": "WHOOGLE_CONFIG_PREFERENCES_KEY අවශ්‍ය වේ, එසේ නොමැතිනම් මෙය නොසලකා හරිනු ඇත.",
"config-css": "අභිරුචි සීඑස්එස්",
"load": "පූරනය කරන්න",
"apply": "යොදන්න",
"save-as": "...ලෙස සුරකින්න",
"github-link": "ගිට්හබ් හි බලන්න",
"translate": "පරිවර්තනය කරන්න",
"light": "දීප්තිමත්",
"dark": "අඳුරු",
"system": "පද්ධතිය",
"ratelimit": "සේවාදායකය අනුපාතනය කර ඇත",
"continue-search": "Farside සමඟ ඔබගේ සෙවුම කරගෙන යන්න",
"all": "සියල්ල",
"images": "රූප",
"maps": "සිතියම්",
"videos": "වීඩියෝ",
"news": "අනුරූප",
"books": "පොත්",
"anon-view": "නිර්නාමික දසුන"
},
"lang_fr": {
"search": "Chercher",
"config": "Configuration",
"config-country": "Définir le pays",
"config-lang": "Langage de l'Interface",
"config-lang-search": "Langage de Recherche",
"config-near": "Proche",
"config-near-help": "Nom de ville",
"config-block": "Bloquer",
"config-block-help": "Liste de sites séparés pas des virgules",
"config-block-title": "Bloquer par titre",
"config-block-title-help": "Utiliser l'expression régulière",
"config-block-url": "Bloquer par URL",
"config-block-url-help": "Utiliser l'expression régulière",
"config-theme": "Theme",
"config-nojs": "Supprimer Javascript dans la vue anonyme",
"config-anon-view": "Afficher les liens de vue anonymes",
"config-dark": "Mode Sombre",
"config-safe": "Recherche sécurisée",
"config-alts": "Remplacer les liens des réseaux sociaux",
"config-alts-help": "Remplacer les liens Twitter/YouTube/Instagram/etc avec leurs alternatives respectueuses de la vie privée.",
"config-new-tab": "Ouvrir les Liens dans un Nouveau Onglet",
"config-images": "Recherche d'image en plein écran",
"config-images-help": "(Expérimental) Ajouter l'option 'Voir Image' aux recherches d'images sur ordinateur. Les vignettes des résultats d'image seront de plus faible résolution.",
"config-tor": "Utiliser Tor",
"config-get-only": "Requêtes GET seulement",
"config-url": "URL de la racine",
"config-pref-url": "URL des préférences",
"config-pref-encryption": "Chiffrer les préférences",
"config-pref-help": "Nécessite WHOOGLE_CONFIG_PREFERENCES_KEY, sinon cela sera ignoré.",
"config-css": "CSS Personalisé",
"load": "Charger",
"apply": "Appliquer",
"save-as": "Sauvegarder comme...",
"github-link": "Voir sur GitHub",
"translate": "Traduire",
"light": "clair",
"dark": "sombre",
"system": "système",
"ratelimit": "Le débit de l'instance a été limité",
"continue-search": "Continuez votre recherche avec Farside",
"all": "Tous",
"images": "Images",
"maps": "Maps",
"videos": "Vidéos",
"news": "Actualités",
"books": "Livres",
"anon-view": "Vue anonyme"
},
"lang_fa": {
"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": "بلوک بر اساس URL",
"config-block-url-help": "از عبارت منظم استفاده کنید",
"config-theme": "پوسته",
"config-nojs": "جاوا اسکریپت را در نمای ناشناس حذف کنید",
"config-anon-view": "نمایش پیوندهای مشاهده ناشناس",
"config-dark": "حالت تاریک",
"config-safe": "جستجوی امن",
"config-alts": "جایگزینی پیوند‌های شبکه‌های اجتماعی",
"config-alts-help": "لینک‌های توییتر، یوتیوب، اینستاگرام و... را با جایگزین‌هایی که به حریم خصوصی احترام می‌گذارند جایگزین می‌کند.",
"config-new-tab": "باز کردن پیوند‌ها در تب جدید",
"config-images": "جستجوی تصویر در اندازه‌ی کامل",
"config-images-help": "(تجربی) گزینه‌ی \"مشاهده‌ی تصویر\" را به جستجو‌های تصویر میزکار اضافه می‌کند. این باعث می‌شود تصاویر کوچک وضوح و حجم کمتری داشته باشند.",
"config-tor": "استفاده از تور",
"config-get-only": "فقط درخواست‌های GET",
"config-url": "آدرس ریشه‌ی سایت",
"config-pref-url": "URL تنظیمات برگزیده",
"config-pref-encryption": "رمزگذاری تنظیمات برگزیده",
"config-pref-help": "به WHOOGLE_CONFIG_PREFERENCES_KEY نیاز دارد، در غیر این صورت نادیده گرفته خواهد شد.",
"config-css": "CSS دلخواه",
"load": "بارگذاری",
"apply": "تایید",
"save-as": "ذخیره به عنوان...",
"github-link": "نمایش در گیت‌هاب",
"translate": "ترجمه",
"light": "روشن",
"dark": "تیره",
"system": "سیستم",
"ratelimit": "نمونه با نرخ محدود شده است",
"continue-search": "Farside جستجوی خود را با ",
"all": "همه",
"images": "تصاویر",
"maps": "نقشه‌ها",
"videos": "ویدئوها",
"news": "اخبار",
"books": "کتاب‌ها",
"anon-view": "نمای ناشناس"
},
"lang_cs": {
"search": "Hledat",
"config": "Konfigurace",
"config-country": "Nastavte zemi",
"config-lang": "Jazyk rozhraní",
"config-lang-search": "Jazyk vyhledávání",
"config-near": "Poblíž",
"config-near-help": "Název města",
"config-block": "Blokovat",
"config-block-help": "Čárkami oddělený seznam stránek",
"config-block-title": "Blokovat podle názvu",
"config-block-title-help": "Použijte regulární výraz",
"config-block-url": "Blokovat podle adresy URL",
"config-block-url-help": "Použijte regulární výraz",
"config-theme": "Motiv",
"config-nojs": "Odeberte Javascript v anonymním zobrazení",
"config-anon-view": "Zobrazit odkazy anonymního zobrazení",
"config-dark": "Tmavý motiv",
"config-safe": "Bezpečné vyhledávání",
"config-alts": "Nahradit odkazy na sociální média",
"config-alts-help": "Nahradí odkazy na Twitter, YouTube, Instagram atd. alternativami respektujícími soukromí.",
"config-new-tab": "Otevírat odkazy na novém listu",
"config-images": "Vyhledávání obrázků v plné velikosti",
"config-images-help": "(Experimentální) Přidá volbu Zobrazit obrázek do vyhledávání obrázků na ploše. Způsobí to, že náhledy výsledků vyhledávání obrázků budou mít nižší rozlišení.",
"config-tor": "Používat Tor",
"config-get-only": "Pouze požadavky GET",
"config-url": "Kořenová adresa URL",
"config-pref-url": "Adresa URL předvoleb",
"config-pref-encryption": "Předvolby šifrování",
"config-pref-help": "Vyžaduje WHOOGLE_CONFIG_PREFERENCES_KEY, jinak bude ignorována.",
"config-css": "Vlastní CSS",
"load": "Načíst",
"apply": "Použít",
"save-as": "Uložit jako...",
"github-link": "Zobrazit na GitHub",
"translate": "Přeložit",
"light": "Světlý",
"dark": "Tmavý",
"system": "Systémový",
"ratelimit": "Instance byla omezena sazbou",
"continue-search": "Pokračujte ve vyhledávání pomocí Farside",
"all": "Vše",
"images": "Obrázky",
"maps": "Mapy",
"videos": "Videa",
"news": "Zprávy",
"books": "Knihy",
"anon-view": "Anonymní pohled"
},
"lang_zh-TW": {
"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": "於匿名檢視中刪除 JavaScript",
"config-anon-view": "顯示匿名檢視鏈接",
"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": "首頁網址",
"config-pref-url": "首選項網址",
"config-pref-encryption": "加密首選項",
"config-pref-help": "需要 WHOOGLE_CONFIG_PREFERENCES_KEY否則將被忽略。",
"config-css": "自定 CSS",
"load": "載入",
"apply": "套用",
"save-as": "另存為...",
"github-link": "在 GitHub 上查看",
"translate": "翻譯",
"light": "明亮的",
"dark": "黑暗的",
"system": "依系統",
"ratelimit": "實例已被限速",
"continue-search": "繼續搜索 Farside",
"all": "全部",
"images": "圖片",
"maps": "地圖",
"videos": "影片",
"news": "新聞",
"books": "書籍",
"anon-view": "匿名檢視"
},
"lang_bg": {
"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": "Блокиране по url",
"config-block-url-help": "Използвайте регулярно изражение",
"config-theme": "Стил",
"config-nojs": "Премахнете Javascript в анонимен изглед",
"config-anon-view": "Показване на анонимни връзки за преглед",
"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-pref-url": "URL адрес на предпочитанията",
"config-pref-encryption": "Шифроване на предпочитанията",
"config-pref-help": "Изисква WHOOGLE_CONFIG_PREFERENCES_KEY, в противен случай това ще бъде игнорирано.",
"config-css": "Персонализиран CSS",
"load": "Зареди",
"apply": "Приложи",
"save-as": "Запис като...",
"github-link": "Вижте в GitHub",
"translate": "превод",
"light": "светла",
"dark": "тъмна",
"system": "системна",
"ratelimit": "Екземплярът е с ограничена скорост",
"continue-search": "Продължете търсенето си с Farside",
"all": "Всичкo",
"images": "Изображения",
"maps": "Видеоклипове",
"videos": "Новини",
"news": "Карти",
"books": "Книги",
"anon-view": "Анонимен изглед"
},
"lang_hi": {
"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": "url द्वारा अवरोधित करें",
"config-block-url-help": "रेगेक्स का प्रयोग करें",
"config-theme": "विषय",
"config-nojs": "अनाम दृश्य में जावास्क्रिप्ट निकालें",
"config-anon-view": "बेनामी देखें लिंक दिखाएं",
"config-dark": "डार्क मोड",
"config-safe": "सुरक्षित खोज",
"config-alts": "सोशल मीडिया लिंक बदलें",
"config-alts-help": "गोपनीयता का सम्मान करने वाले विकल्पों के साथ ट्विटर/यूट्यूब/इंस्टाग्राम/आदि लिंक को बदल देता है।",
"config-new-tab": "नए टैब में लिंक खोलें",
"config-images": "पूर्ण आकार छवि खोज",
"config-images-help": "(Experimental) डेस्कटॉप छवि खोजों में 'छवि देखें' विकल्प जोड़ता है। इससे छवि परिणाम थंबनेल कम रिज़ॉल्यूशन वाले होंगे।",
"config-tor": "TOR का प्रयोग करें",
"config-get-only": "केवल GET अनुरोध",
"config-url": "रूट यूआरएल",
"config-pref-url": "वरीयताएँ URL",
"config-pref-encryption": "एन्क्रिप्ट प्राथमिकताएं",
"config-pref-help": "WHOOGLE_CONFIG_PREFERENCES_KEY की आवश्यकता है, अन्यथा इसे अनदेखा कर दिया जाएगा।",
"config-css": "कस्टम सीएसएस",
"load": "भार",
"apply": "लागू करना",
"save-as": "के रूप रक्षित करें...",
"github-link": "गिटहब पर देखें",
"translate": "अनुवाद करना",
"light": "रोशनी",
"dark": "अंधेरा",
"system": "प्रणाली",
"ratelimit": "इंस्टेंस को सीमित कर दिया गया है",
"continue-search": "के साथ अपनी खोज जारी रखें Farside",
"all": "सभी",
"images": "इमेज",
"maps": "वीडियो",
"videos": "मैप",
"news": "समाचार",
"books": "किताबें",
"anon-view": "अनाम दृश्य"
},
"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": "匿名ビューでJavascriptを削除する",
"config-anon-view": "匿名のビューリンクを表示する",
"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-pref-url": "設定 URL",
"config-pref-encryption": "設定を暗号化する",
"config-pref-help": "WHOOGLE_CONFIG_PREFERENCES_KEY が必要です。それ以外の場合、これは無視されます。",
"config-css": "カスタムCSS",
"load": "読み込み",
"apply": "反映",
"save-as": "名前を付けて保存",
"github-link": "Githubで確認",
"translate": "翻訳",
"light": "ライト",
"dark": "ダーク",
"system": "自動",
"ratelimit": "インスタンスはレート制限されています",
"continue-search": "で検索を続ける Farside",
"all": "すべて",
"images": "画像",
"maps": "地図",
"videos": "動画",
"news": "ニュース",
"books": "書籍",
"anon-view": "匿名ビュー"
},
"lang_ko": {
"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": "URL로 차단",
"config-block-url-help": "정규 표현식 사용",
"config-theme": "테마",
"config-nojs": "익명 보기에서 Javascript 제거",
"config-anon-view": "익명 보기 링크 표시",
"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-pref-url": "환경설정 URL",
"config-pref-encryption": "암호화 환경 설정",
"config-pref-help": "WHOOGLE_CONFIG_PREFERENCES_KEY이 필요합니다. 그렇지 않으면 무시됩니다.",
"config-css": "커스텀 CSS",
"load": "불러오기",
"apply": "적용",
"save-as": "다른 이름으로 저장...",
"github-link": "깃허브에서 보기",
"translate": "번역",
"light": "라이트",
"dark": "다크",
"system": "시스템",
"ratelimit": "인스턴스가 속도 제한되었습니다.",
"continue-search": "Farside로 검색 계속하기",
"all": "전체",
"images": "이미지",
"maps": "지도",
"videos": "동영상",
"news": "뉴스",
"books": "도서",
"anon-view": "익명 보기"
},
"lang_ku": {
"search": "Lêgerîn",
"config": "Pevsazî",
"config-country": "Welat",
"config-lang": "Zimanê Navrûyê",
"config-lang-search": "Zimanê Lêgerînê",
"config-near": "Nêzîk",
"config-near-help": "Navê Bajêr",
"config-block": "Astengkirin",
"config-block-help": "Lîsteya malperê ya ji hev veqetandî bi rêya bêhnok",
"config-block-title": "Bi ya Sernavê Asteng bike",
"config-block-title-help": "regex bi kar bîne",
"config-block-url": "Bi ya URL asteng bike",
"config-block-url-help": "regex bi kar bîne",
"config-theme": "Rûkar",
"config-nojs": "Javascript Rake di Nîşandanên Nenenas de",
"config-anon-view": "Girêdanên Nenas Nîşan bide",
"config-dark": "Awaya Tarî",
"config-safe": "Lêgerîna Parastî",
"config-alts": "Girêdanên Medya Civakî Biguherîne",
"config-alts-help": "Girêdanên Twitter/YouTube/Instagram/hwd biguherîne bi alternatîvên ku ji taybetiyê re rêzê digrin.",
"config-new-tab": "Girêdanan di Rûgereke Nû de Veke",
"config-images": "Lêgerîna Wêne bi Mezinahiya Tevahî",
"config-images-help": "(Ezmûnî) Vebijêrka 'Wêneyê Nîşan bide' tevlî lêgerînên wêneyê yê sermaseyê bike. Ev ê bibe sedem ku encamê çareseriya wêneyn nîşanê kêmtir bibe.",
"config-tor": "Tor bi kar bîne",
"config-get-only": "Daxwazan bi Dest Bixe",
"config-url": "Reha URL",
"config-pref-url": "Preferences URL",
"config-pref-encryption": "Vebijêrkên şîfre bikin",
"config-pref-help": "WHOOGLE_CONFIG_PREFERENCES_KEY hewce dike, wekî din ev ê were paşguh kirin.",
"config-css": "CSS kesane bike",
"load": "Bar bike",
"apply": "Bisepîne",
"save-as": "Biparêze wekî...",
"github-link": "Li ser GitHub Nîşan bide",
"translate": "werger",
"light": "ronî",
"dark": "tarî",
"system": "pergal",
"ratelimit": "Mînak bi rêjeya sînorkirî ye",
"continue-search": "Lêgerîna xwe bi Farside bidomîne",
"all": "Hemû",
"images": "Wêne",
"maps": "Nexşe",
"videos": "Vîdyo",
"news": "Nûçe",
"books": "Pirtûk",
"anon-view": "Dîtina Nenas"
},
"lang_th": {
"search": "ค้นหา",
"config": "กำหนดค่า",
"config-country": "ประเทศ",
"config-lang": "ภาษาหน้าอินเตอร์เฟซ",
"config-lang-search": "ค้นหาในภาษา",
"config-near": "รอบๆ",
"config-near-help": "ชื่อเมือง",
"config-block": "บล็อค",
"config-block-help": "รายการเว็บไซต์คั่นด้วยเครื่องหมายจุลภาค(,)",
"config-block-title": "บล็อกตามหัวชื่อเว็บไซต์",
"config-block-title-help": "ใช้ regex",
"config-block-url": "บล็อกตาม URL",
"config-block-url-help": "ใช้ regex",
"config-theme": "ธีม",
"config-nojs": "ลบ Javascript ในมุมมองที่ไม่ระบุตัวตน",
"config-anon-view": "แสดงลิงค์ในมุมมองไม่ระบุตัวตน",
"config-dark": "โหมดมืด",
"config-safe": "ค้นหาแบบปลอดภัย",
"config-alts": "แทนที่ลิงก์โซเชียลมีเดีย",
"config-alts-help": "แทนที่ลิงก์ Twitter/YouTube/Instagram/อื่นๆ ตามความเป็นส่วนตัวด้วยทางเลือกอื่น",
"config-new-tab": "เปิดลิงก์ในแท็บใหม่",
"config-images": "ค้นหารูปภาพขนาดเต็ม",
"config-images-help": "(ตัวอย่าง) เพิ่มตัวเลือก 'ดูภาพ' ในการค้นหารูปภาพบนเดสก์ท็อป ซึ่งจะทำให้ภาพขนาดย่อมีความละเอียดต่ำ",
"config-tor": "ใช้ Tor",
"config-get-only": "รับคำขอเท่านั้น",
"config-url": "URL หลัก",
"config-pref-url": "URL การตั้งค่า",
"config-pref-encryption": "เข้ารหัสการตั้งค่า",
"config-pref-help": "จำเป็นต้องมี WHOOGLE_CONFIG_PREFERENCES_KEY ไม่เช่นนั้นจะถูกละเว้นไป",
"config-css": "กำหนด CSS เอง",
"load": "โหลด",
"apply": "ยอมรับ",
"save-as": "บันทึกเป็น...",
"github-link": "ดูบน GitHub",
"translate": "แปลภาษา",
"light": "สว่าง",
"dark": "มืด",
"system": "ระบบ",
"ratelimit": "คำขอร้องจะถูกจำกัดจำนวน",
"continue-search": "ค้นหาต่อไปด้วย Farside",
"all": "ทั้งหมด",
"images": "รูปภาพ",
"maps": "แผนที่",
"videos": "วิดีโอ",
"news": "ข่าว",
"books": "หนังสือ",
"anon-view": "มุมมองที่ไม่ระบุตัวตน"
}
}

View File

@ -1,48 +1,29 @@
<html>
<head>
<link rel="shortcut icon" href="static/img/favicon.ico" type="image/x-icon">
<link rel="icon" href="static/img/favicon.ico" type="image/x-icon">
{% if not search_type %}
<head>
<link rel="shortcut icon" href="static/img/favicon.ico" type="image/x-icon">
<link rel="icon" href="static/img/favicon.ico" type="image/x-icon">
<link rel="search" href="opensearch.xml" type="application/opensearchdescription+xml" title="Whoogle Search">
{% else %}
<link rel="search" href="opensearch.xml?tbm={{ search_type }}" type="application/opensearchdescription+xml" title="Whoogle Search ({{ search_name }})">
{% endif %}
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="referrer" content="no-referrer">
<link rel="stylesheet" href="{{ cb_url('logo.css') }}">
<link rel="stylesheet" href="{{ cb_url('input.css') }}">
<link rel="stylesheet" href="{{ cb_url('search.css') }}">
<link rel="stylesheet" href="{{ cb_url('header.css') }}">
{% if config.theme %}
{% if config.theme == 'system' %}
<style>
@import "{{ cb_url('light-theme.css') }}" screen;
@import "{{ cb_url('dark-theme.css') }}" screen and (prefers-color-scheme: dark);
</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 %}
<style>{{ config.style }}</style>
<title>{{ clean_query(query) }} - Whoogle Search</title>
</head>
<body>
{{ search_header|safe }}
{% if is_translation %}
<iframe
id="lingva-iframe"
src="{{ lingva_url }}/auto/{{ translate_to }}/{{ translate_str }}">
</iframe>
{% endif %}
{{ response|safe }}
</body>
{% include 'footer.html' %}
{% if autocomplete_enabled == '1' %}
<script src="{{ cb_url('autocomplete.js') }}"></script>
{% endif %}
<script src="{{ cb_url('utils.js') }}"></script>
<script src="{{ cb_url('keyboard.js') }}"></script>
<script src="{{ cb_url('currency.js') }}"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="referrer" content="no-referrer">
<link rel="stylesheet" href="static/css/input.css">
<link rel="stylesheet" href="static/css/search.css">
<link rel="stylesheet" href="static/css/variables.css">
<link rel="stylesheet" href="static/css/header.css">
<link rel="stylesheet" href="static/css/{{ 'dark' if config.dark else 'light' }}-theme.css"/>
<style>{{ config.style }}</style>
<title>{{ query }} - Whoogle Search</title>
</head>
<body>
{{ search_header|safe }}
{{ response|safe }}
</body>
<footer>
<p style="color: {{ 'var(--whoogle-dark-text)' if config.dark else 'var(--whoogle-text)' }};">
Whoogle Search v{{ version_number }} ||
<a id="gh-link" href="https://github.com/benbusby/whoogle-search">View on GitHub</a>
</p>
</footer>
<script src="static/js/autocomplete.js"></script>
<script src="static/js/utils.js"></script>
<script src="static/js/keyboard.js"></script>
</html>

View File

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

View File

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

View File

@ -1,94 +1,64 @@
{% if mobile %}
<header>
<div class="header-div">
<form class="search-form header"
id="search-form"
method="{{ 'GET' if config.get_only else 'POST' }}">
<a class="logo-link mobile-logo" href="{{ home_url }}">
<div id="mobile-header-logo">
<div style="background-color: {{ 'var(--whoogle-dark-result-bg)' if config.dark else 'var(--whoogle-result-bg)' }} !important;" class="bz1lBb">
<form class="search-form Pg70bf" id="search-form" method="POST">
<a class="logo-link mobile-logo"
href="/"
style="display:flex; justify-content:center; align-items:center;">
<div style="height: 1.75em;">
{{ logo|safe }}
</div>
</a>
<div class="H0PQec mobile-input-div">
<div class="autocomplete-mobile esbc autocomplete">
{% if config.preferences %}
<input type="hidden" name="preferences" value="{{ config.preferences }}" />
{% endif %}
<input
id="search-bar"
class="mobile-search-bar"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
class="search-bar-input"
name="q"
type="text"
value="{{ clean_query(query) }}"
dir="auto">
<input id="search-reset" type="reset" value="x">
<div class="H0PQec" style="width: 100%;">
<div class="sbc esbc autocomplete">
<input
id="search-bar"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
class="noHIxc"
name="q"
style="background-color: {{ 'var(--whoogle-dark-result-bg)' if config.dark else 'var(--whoogle-result-bg)' }} !important;
color: {{ 'var(--whoogle-dark-text)' if config.dark else 'var(--whoogle-text)' }};
type="text"
value="{{ query[:query.find('-site:')] if '-site:' in query else query }}">
<input style="color: {{ 'var(--whoogle-dark-text)' if config.dark else 'var(--whoogle-text)' }}" id="search-reset" type="reset" value="x">
<input name="tbm" value="{{ search_type }}" style="display: none">
<input name="country" value="{{ config.country }}" style="display: none;">
<input type="submit" style="display: none;">
<div class="sc"></div>
</div>
</div>
</form>
</div>
<div>
<div class="header-tab-div">
<div class="header-tab-div-2">
<div class="header-tab-div-3">
<div class="mobile-header header-tab">
{% for tab_id, tab_content in tabs.items() %}
{% if tab_content['selected'] %}
<span class="mobile-tab-span">{{ tab_content['name'] }}</span>
{% else %}
<a class="header-tab-a" href="{{ tab_content['href'] }}">{{ tab_content['name'] }}</a>
{% endif %}
{% endfor %}
<label for="adv-search-toggle" id="adv-search-label" class="adv-search"></label>
<input id="adv-search-toggle" type="checkbox">
<div class="header-tab-div-end"></div>
</div>
</div>
</div>
</div>
<div class="" id="s">
</div>
</header>
{% else %}
<header>
<div class="logo-div">
<a class="logo-link" href="{{ home_url }}">
<div class="desktop-header-logo">
<a class="logo-link" href="/">
<div style="height: 1.65em;">
{{ logo|safe }}
</div>
</a>
</div>
<div class="search-div">
<form id="search-form"
class="search-form"
id="sf"
method="{{ 'GET' if config.get_only else 'POST' }}">
<div class="autocomplete header-autocomplete">
<form id="search-form" class="search-form" id="sf" method="POST">
<div class="autocomplete" style="width: 100%; flex: 1">
<div style="width: 100%; display: flex">
{% if config.preferences %}
<input type="hidden" name="preferences" value="{{ config.preferences }}" />
{% endif %}
<input
id="search-bar"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
class="search-bar-desktop search-bar-input"
name="q"
spellcheck="false"
type="text"
value="{{ clean_query(query) }}"
dir="auto">
<input
id="search-bar"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
class="search-bar-desktop noHIxc"
name="q"
spellcheck="false"
type="text"
value="{{ query[:query.find('-site:')] if '-site:' in query else query }}"
style="background-color: {{ 'var(--whoogle-dark-result-bg)' if config.dark else 'var(--whoogle-result-bg)' }} !important;
color: {{ 'var(--whoogle-dark-text)' if config.dark else 'var(--whoogle-text)' }};
border-bottom: {{ '2px solid var(--whoogle-dark-element-bg)' if config.dark else '0px' }};">
<input name="tbm" value="{{ search_type }}" style="display: none">
<input name="country" value="{{ config.country }}" style="display: none;">
<input type="submit" style="display: none;">
<div class="sc"></div>
</div>
@ -96,46 +66,6 @@
</form>
</div>
</header>
<div>
<div class="header-tab-div">
<div class="header-tab-div-2">
<div class="header-tab-div-3">
<div class="desktop-header header-tab">
{% for tab_id, tab_content in tabs.items() %}
{% if tab_content['selected'] %}
<span class="header-tab-span">{{ tab_content['name'] }}</span>
{% else %}
<a class="header-tab-a" href="{{ tab_content['href'] }}">{{ tab_content['name'] }}</a>
{% endif %}
{% endfor %}
<label for="adv-search-toggle" id="adv-search-label" class="adv-search"></label>
<input id="adv-search-toggle" type="checkbox">
<div class="header-tab-div-end"></div>
</div>
</div>
</div>
</div>
<div class="" id="s">
</div>
{% endif %}
<div class="result-collapsible" id="adv-search-div">
<div class="result-config">
<label for="config-country">{{ translation['config-country'] }}: </label>
<select name="country" id="result-country">
{% for country in countries %}
<option value="{{ country.value }}"
{% if (
config.country != '' and config.country in country.value
) or (
config.country == '' and country.value == '')
%}
selected
{% endif %}>
{{ country.name }}
</option>
{% endfor %}
</select>
</div>
</div>
<script type="text/javascript" src="{{ cb_url('header.js') }}"></script>
<script type="text/javascript" src="static/js/header.js"></script>

View File

@ -1,390 +1,116 @@
<div>
<!DOCTYPE html>
<html>
<head>
<meta content="application/xhtml+xml; charset=utf-8" http-equiv="Content-Type"/>
<meta content="no-cache" name="Cache-Control"/>
<title>
</title>
<style>
html {
font-family: Roboto, Helvetica Neue, Arial, sans-serif;
font-size: 14px;
line-height: 20px;
text-size-adjust: 100%;
color: #3c4043;
word-wrap: break-word;
background-color: #fff;
}
body {
padding: 0 8px;
margin: 0 auto;
max-width: 736px;
}
a {
text-decoration: none;
color: inherit;
}
a:hover {
text-decoration: underline;
}
a img {
border: 0;
}
.FbhRzb {
border-left: thin solid #dadce0;
border-right: thin solid #dadce0;
border-top: thin solid #dadce0;
height: 40px;
overflow: hidden;
}
.n692Zd {
margin-bottom: 10px;
}
.cvifge {
height: 40px;
border-spacing: 0;
width: 100%;
}
.QvGUP {
height: 40px;
padding: 0 8px 0 8px;
vertical-align: top;
}
.O4cRJf {
height: 40px;
width: 100%;
padding: 0;
padding-right: 16px;
}
.O1ePr {
height: 40px;
padding: 0;
vertical-align: top;
}
.kgJEQe {
height: 36px;
width: 98px;
vertical-align: top;
margin-top: 4px;
}
.lXLRf {
vertical-align: top;
}
.MhzMZd {
border: 0;
vertical-align: middle;
font-size: 14px;
height: 40px;
padding: 0;
width: 100%;
padding-left: 16px;
}
.xB0fq {
height: 40px;
border: none;
font-size: 14px;
background-color: #4285f4;
color: #fff;
padding: 0 16px;
margin: 0;
vertical-align: top;
cursor: pointer;
}
.xB0fq:focus {
border: 1px solid #000;
}
.M7pB2 {
border: thin solid #dadce0;
margin: 0 0 3px 0;
font-size: 13px;
font-weight: 500;
height: 40px;
}
.euZec {
width: 100%;
height: 40px;
text-align: center;
border-spacing: 0;
}
table.euZec td {
padding: 0;
width: 25%;
}
.QIqI7 {
display: inline-block;
padding-top: 4px;
font-weight: bold;
color: #4285f4;
}
.EY24We {
border-bottom: 2px solid #4285f4;
}
.CsQyDc {
display: inline-block;
color: #70757a;
}
.TuS8Ad {
font-size: 14px;
}
.HddGcc {
padding: 8px;
color: #70757a;
}
.dzp8ae {
font-weight: bold;
color: #3c4043;
}
.rEM8G {
color: #70757a;
}
.bookcf {
table-layout: fixed;
width: 100%;
border-spacing: 0;
}
.InWNIe {
text-align: center;
}
.uZgmoc {
border: thin solid #dadce0;
color: #70757a;
font-size: 14px;
text-align: center;
table-layout: fixed;
width: 100%;
}
.frGj1b {
display: block;
padding: 12px 0 12px 0;
width: 100%;
}
.BnJWBc {
text-align: center;
padding: 6px 0 13px 0;
height: 35px;
}
.e3goi {
vertical-align: top;
padding: 0;
height: 180px;
}
.GpQGbf {
margin: auto;
border-collapse: collapse;
border-spacing: 0;
width: 100%;
}
.X6ZCif {
color: #202124;
font-size: 11px;
line-height: 16px;
display: inline-block;
padding-top: 2px;
overflow: hidden;
padding-bottom: 4px;
width: 100%;
}
.TwVfHd {
border-radius: 16px;
border: thin solid #dadce0;
display: inline-block;
padding: 8px 8px;
margin-right: 8px;
margin-bottom: 4px;
}
.yekiAe {
background-color: #dadce0;
}
.svla5d {
width: 100%;
}
.ezO2md {
border: thin solid #dadce0;
padding: 12px 16px 12px 16px;
margin-bottom: 10px;
font-family: Roboto, Helvetica, Arial, sans-serif;
}
.TxbwNb {
border-spacing: 0;
}
.K35ahc {
width: 100%;
}
.owohpf {
text-align: center;
}
.RAyV4b {
width: 162px;
height: 140px;
line-height: 140px;
overflow: "hidden";
text-align: center;
}
.t0fcAb {
text-align: center;
margin: auto;
vertical-align: middle;
width: 100%;
height: 100%;
object-fit: contain;
}
.Tor4Ec {
padding-top: 2px;
padding-bottom: 8px;
}
.fYyStc {
word-break: break-word;
}
.ynsChf {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.Fj3V3b {
color: #1967d2;
font-size: 14px;
line-height: 20px;
}
.FrIlee {
color: #202124;
font-size: 11px;
line-height: 16px;
}
.F9iS2e {
color: #70757a;
font-size: 11px;
line-height: 16px;
}
.WMQ2Le {
color: #70757a;
font-size: 12px;
line-height: 16px;
}
.x3G5ab {
color: #202124;
font-size: 12px;
line-height: 16px;
}
.fuLhoc {
color: #1967d2;
font-size: 18px;
line-height: 24px;
}
.epoveb {
font-size: 32px;
line-height: 40px;
font-weight: 400;
color: #202124;
}
.dXDvrc {
color: #0d652d;
font-size: 14px;
line-height: 20px;
word-wrap: break-word;
}
.dloBPe {
font-weight: bold;
}
.YVIcad {
color: #70757a;
}
.JkVVdd {
color: #ea4335;
}
.oXZRFd {
color: #ea4335;
}
.MQHtg {
color: #fbbc04;
}
.pyMRrb {
color: #1e8e3e;
}
.EtTZid {
color: #1e8e3e;
}
.M3vVJe {
color: #1967d2;
}
.qXLe6d {
display: block;
}
.NHQNef {
font-style: italic;
}
.Cb8Z7c {
white-space: pre;
}
a.ZWRArf {
text-decoration: none;
}
a .CVA68e:hover {
text-decoration: underline;
}
a{text-decoration:none;color:inherit}a:hover{text-decoration:underline}a img{border:0}body{font-family:Roboto,Helvetica,Arial,sans-serif;padding:8px;margin:0 auto;max-width:700px;min-width:240px;}.FbhRzb{border-left:thin solid #dadce0;border-right:thin solid #dadce0;border-top:thin solid #dadce0;height:40px;overflow:hidden}.n692Zd{margin-bottom:10px}.cvifge{height:40px;border-spacing:0;width:100%;}.QvGUP{height:40px;padding:0 8px 0 8px;vertical-align:top}.O4cRJf{height:40px;width:100%;padding:0;padding-right:16px}.O1ePr{height:40px;padding:0;vertical-align:top}.kgJEQe{height:36px;width:98px;vertical-align:top;margin-top:4px}.lXLRf{vertical-align:top}.MhzMZd{border:0;vertical-align:middle;font-size:14px;height:40px;padding:0;width:100%;padding-left:16px}.xB0fq{height:40px;border:none;font-size:14px;background-color:#4285f4;color:#fff;padding:0 16px;margin:0;vertical-align:top;cursor:pointer}.xB0fq:focus{border:1px solid #000}.M7pB2{border:thin solid #dadce0;margin:0 0 3px 0;font-size:13px;font-weight:500;height:40px}.euZec{width:100%;height:40px;text-align:center;border-spacing:0}table.euZec td{padding:0;width:25%}.QIqI7{display:inline-block;padding-top:4px;font-weight:bold;color:#4285f4}.EY24We{border-bottom:2px solid #4285f4}.CsQyDc{display:inline-block;color:#70757a}.TuS8Ad{font-size:14px}.HddGcc{padding:8px;color:#70757a}.dzp8ae{font-weight:bold;color:#3c4043}.rEM8G{color:#70757a}.bookcf{table-layout:fixed;width:100%;border-spacing:0}.InWNIe{text-align:center}.uZgmoc{border:thin solid #dadce0;color:#70757a;font-size:14px;text-align:center;table-layout:fixed;width:100%}.frGj1b{display:block;padding:12px 0 12px 0;width:100%}.BnJWBc{text-align:center;padding:6px 0 13px 0;height:35px}.e3goi{vertical-align:top;padding:0;height:180px}.GpQGbf{margin:auto;border-collapse:collapse;border-spacing:0;width:100%}
</style>
</head>
<body>
<style>
.X6ZCif{color:#202124;font-size:11px;line-height:16px;display:inline-block;padding-top:2px;overflow:hidden;padding-bottom:4px;width:100%}.TwVfHd{border-radius:16px;border:thin solid #dadce0;display:inline-block;padding:8px 8px;margin-right:8px;margin-bottom:4px}.yekiAe{background-color:#dadce0}.svla5d{width:100%}.ezO2md{border:thin solid #dadce0;padding:12px 16px 12px 16px;margin-bottom:10px;font-family:Roboto,Helvetica,Arial,sans-serif}.lIMUZd{font-family:Roboto,Helvetica,Arial,sans-serif}.TxbwNb{border-spacing:0}.K35ahc{width:100%}.owohpf{text-align:center}.RAyV4b{width:162px;height:140px;line-height:140px;overflow:'hidden';text-align:center;}.t0fcAb{text-align:center;margin:auto;vertical-align:middle;width:100%;height:100%;object-fit: contain}.Tor4Ec{padding-top:2px;padding-bottom:8px;}.fYyStc{word-break:break-word}.ynsChf{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.Fj3V3b{color:#1967D2;font-size:14px;line-height:20px}.FrIlee{color:#202124;font-size:11px;line-height:16px}.F9iS2e{color:#70757a;font-size:11px;line-height:16px}.WMQ2Le{color:#70757a;font-size:12px;line-height:16px}.x3G5ab{color:#202124;font-size:12px;line-height:16px}.fuLhoc{color:#1967D2;font-size:18px;line-height:24px}.epoveb{font-size:32px;line-height:40px;font-weight:400;color:#202124}.dXDvrc{color:#0d652d;font-size:14px;line-height:20px;word-wrap:break-word}.dloBPe{font-weight:bold}.YVIcad{color:#70757a}.JkVVdd{color:#ea4335}.oXZRFd{color:#ea4335}.MQHtg{color:#fbbc04}.pyMRrb{color:#1e8e3e}.EtTZid{color:#1e8e3e}.M3vVJe{color:#1967D2}.qXLe6d{display:block}.NHQNef{font-style:italic}.Cb8Z7c{white-space:pre}a.ZWRArf{text-decoration:none}a .CVA68e:hover{text-decoration:underline}
</style>
<div class="n692Zd">
<div class="BnJWBc">
<a class="lXLRf" href="/?safe=off&amp;gbv=1&amp;output=images&amp;ie=UTF-8&amp;tbm=isch&amp;sa=X&amp;ved=0ahUKEwjhh7TZyd_vAhWShf0HHeYzCmsQPAgC">
<img alt="Google" class="kgJEQe" src="/images/branding/searchlogo/1x/googlelogo_desk_heirloom_color_150x55dp.gif"/>
</a>
</div>
<div class="FbhRzb">
<form action="/search">
<input name="safe" type="hidden" value="off"/>
<input name="gbv" type="hidden" value="1"/>
<input name="ie" type="hidden" value="ISO-8859-1"/>
<input name="tbm" type="hidden" value="isch"/>
<input name="oq" type="hidden"/>
<input name="aqs" type="hidden"/>
<table class="cvifge">
<tr>
<td class="O4cRJf">
<!-- search input -->
</td>
</tr>
</table>
</form>
</div>
<div class="M7pB2">
<!-- search options -->
</div>
</div>
<!-- <div class="X6ZCif"> Not present in mobile
</div> -->
<div>
<div>
<div>
<div class="lIMUZd">
<table class="By0U9">
<!-- correction suggested -->
</table>
</div>
<div class="lIMUZd">
<table class="By0U9">
<!-- correction suggested -->
</table>
</div>
</div>
</div>
<table class="GpQGbf">
{% for i in range((length // 4) + 1) %}
<tr>
{% for j in range([length - (i*4), 4]|min) %}
<td align="center" class="e3goi">
<td align="center" class="e3goi">
<div class="svla5d">
<div>
<div class="lIMUZd">
<div>
<table class="TxbwNb">
<tr>
<td>
<a href="{{ results[(i*4)+j].web_page }}">
<div class="RAyV4b">
<img
alt=""
class="t0fcAb"
src="{{ results[(i*4)+j].img_tbn }}"
/>
</div>
</a>
</td>
</tr>
<tr>
<td>
<a href="{{ results[(i*4)+j].web_page }}">
<div class="Tor4Ec">
<span class="qXLe6d x3G5ab">
<span class="fYyStc">
{{ results[(i*4)+j].domain }}
</span>
</span>
</div>
</a>
<a href="{{ results[(i*4)+j].img_url }}">
<div class="Tor4Ec">
<span class="qXLe6d F9iS2e">
<span class="fYyStc"> {{ view_label }} </span>
</span>
</div>
</a>
</td>
</tr>
</table>
</div>
<div class="lIMUZd">
<div>
<table class="TxbwNb">
<tr>
<td>
<a href="{{ results[(i*4)+j].webpage }}">
<div class="RAyV4b">
<img alt="" class="t0fcAb" src="{{ results[(i*4)+j].img_tbn }}"/>
</div>
</a>
</td>
</tr>
<tr>
<td>
<a href="{{ results[(i*4)+j].webpage }}">
<div class="Tor4Ec">
<span class="qXLe6d x3G5ab">
<span class="fYyStc">
{{ results[(i*4)+j].domain }}
</span>
</span>
</div>
</a>
<a href="{{ results[(i*4)+j].img_url }}">
<div class="Tor4Ec">
<span class="qXLe6d F9iS2e">
<span class="fYyStc">
{{ view_label }}
</span>
</span>
</div>
</a>
</td>
</tr>
</table>
</div>
</div>
</div>
</div>
</td>
{% endfor %}
</tr>
</td>
{% endfor %}
</tr>
{% endfor %}
</table>
</div>
<table class="uZgmoc">
<!-- next page object -->
</table>
<br />
</div>
<br/>
<div class="TuS8Ad">
<!-- information about user connection -->
<div>
</div>
</body>
</html>

View File

@ -1,260 +1,183 @@
<html style="background: #000;">
<head>
<link rel="apple-touch-icon" sizes="57x57" href="static/img/favicon/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="static/img/favicon/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="static/img/favicon/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="static/img/favicon/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="static/img/favicon/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="static/img/favicon/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="static/img/favicon/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="static/img/favicon/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="static/img/favicon/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="static/img/favicon/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="static/img/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="static/img/favicon/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="static/img/favicon/favicon-16x16.png">
<link rel="manifest" href="static/img/favicon/manifest.json">
<meta name="referrer" content="no-referrer">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="static/img/favicon/ms-icon-144x144.png">
{% if autocomplete_enabled == '1' %}
<script src="{{ cb_url('autocomplete.js') }}"></script>
{% endif %}
<script type="text/javascript" src="{{ cb_url('controller.js') }}"></script>
<link rel="search" href="opensearch.xml" type="application/opensearchdescription+xml" title="Whoogle Search">
<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 == 'system' %}
<style>
@import "{{ cb_url('light-theme.css') }}" screen;
@import "{{ cb_url('dark-theme.css') }}" screen and (prefers-color-scheme: dark);
</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') }}">
<noscript>
<style>
#main {
display: inherit !important;
}
.content {
max-height: 400px;
padding: 18px;
border-radius: 10px;
overflow-y: scroll;
}
.collapsible {
display: none;
}
</style>
</noscript>
<style>{{ config.style }}</style>
<title>Whoogle Search</title>
</head>
<body id="main">
<div class="search-container">
<div class="logo-container">
{{ logo|safe }}
</div>
<form id="search-form" action="search" method="{{ 'get' if config.get_only else 'post' }}">
<div class="search-fields">
<div class="autocomplete">
{% if config.preferences %}
<input type="hidden" name="preferences" value="{{ config.preferences }}" />
{% endif %}
<input
type="text"
name="q"
id="search-bar"
class="home-search"
autofocus="autofocus"
autocapitalize="none"
spellcheck="false"
autocorrect="off"
autocomplete="off"
dir="auto">
</div>
<input type="submit" id="search-submit" value="{{ translation['search'] }}">
</div>
</form>
{% if not config_disabled %}
<br/>
<button id="config-collapsible" class="collapsible">{{ translation['config'] }}</button>
<div class="content">
<div class="config-fields">
<form id="config-form" action="config" method="post">
<div class="config-options">
<div class="config-div config-div-country">
<label for="config-country">{{ translation['config-country'] }}: </label>
<select name="country" id="config-country">
{% for country in countries %}
<option value="{{ country.value }}"
{% if (
config.country != '' and config.country in country.value
) or (
config.country == '' and country.value == '')
%}
selected
{% endif %}>
{{ country.name }}
</option>
{% endfor %}
</select>
</div>
<div class="config-div config-div-lang">
<label for="config-lang-interface">{{ translation['config-lang'] }}: </label>
<select name="lang_interface" id="config-lang-interface">
{% for lang in languages %}
<option value="{{ lang.value }}"
{% if lang.value in config.lang_interface %}
selected
{% endif %}>
{{ lang.name }}
</option>
{% endfor %}
</select>
</div>
<div class="config-div config-div-search-lang">
<label for="config-lang-search">{{ translation['config-lang-search'] }}: </label>
<select name="lang_search" id="config-lang-search">
{% for lang in languages %}
<option value="{{ lang.value }}"
{% if lang.value in config.lang_search %}
selected
{% endif %}>
{{ lang.name }}
</option>
{% endfor %}
</select>
</div>
<div class="config-div config-div-near">
<label for="config-near">{{ translation['config-near'] }}: </label>
<input type="text" name="near" id="config-near"
placeholder="{{ translation['config-near-help'] }}" value="{{ config.near }}">
</div>
<div class="config-div config-div-block">
<label for="config-block">{{ translation['config-block'] }}: </label>
<input type="text" name="block" id="config-block"
placeholder="{{ translation['config-block-help'] }}" value="{{ config.block }}">
</div>
<div class="config-div config-div-block">
<label for="config-block-title">{{ translation['config-block-title'] }}: </label>
<input type="text" name="block_title" id="config-block"
placeholder="{{ translation['config-block-title-help'] }}"
value="{{ config.block_title }}">
</div>
<div class="config-div config-div-block">
<label for="config-block-url">{{ translation['config-block-url'] }}: </label>
<input type="text" name="block_url" id="config-block"
placeholder="{{ translation['config-block-url-help'] }}" value="{{ config.block_url }}">
</div>
<div class="config-div config-div-anon-view">
<label for="config-anon-view">{{ translation['config-anon-view'] }}: </label>
<input type="checkbox" name="anon_view" id="config-anon-view" {{ 'checked' if config.anon_view else '' }}>
</div>
<div class="config-div config-div-nojs">
<label for="config-nojs">{{ translation['config-nojs'] }}: </label>
<input type="checkbox" name="nojs" id="config-nojs" {{ 'checked' if config.nojs else '' }}>
</div>
<div class="config-div config-div-theme">
<label for="config-theme">{{ translation['config-theme'] }}: </label>
<select name="theme" id="config-theme">
{% for theme in themes %}
<option value="{{ theme }}"
{% if theme in config.theme %}
selected
{% endif %}>
{{ translation[theme].capitalize() }}
</option>
{% endfor %}
</select>
</div>
<!-- DEPRECATED -->
<!--<div class="config-div config-div-dark">-->
<!--<label for="config-dark">{{ translation['config-dark'] }}: </label>-->
<!--<input type="checkbox" name="dark" id="config-dark" {{ 'checked' if config.dark else '' }}>-->
<!--</div>-->
<div class="config-div config-div-safe">
<label for="config-safe">{{ translation['config-safe'] }}: </label>
<input type="checkbox" name="safe" id="config-safe" {{ 'checked' if config.safe else '' }}>
</div>
<div class="config-div config-div-alts">
<label class="tooltip" for="config-alts">{{ translation['config-alts'] }}: </label>
<input type="checkbox" name="alts" id="config-alts" {{ 'checked' if config.alts else '' }}>
<div><span class="info-text"> — {{ translation['config-alts-help'] }}</span></div>
</div>
<div class="config-div config-div-new-tab">
<label for="config-new-tab">{{ translation['config-new-tab'] }}: </label>
<input type="checkbox" name="new_tab"
id="config-new-tab" {{ 'checked' if config.new_tab else '' }}>
</div>
<div class="config-div config-div-view-image">
<label for="config-view-image">{{ translation['config-images'] }}: </label>
<input type="checkbox" name="view_image"
id="config-view-image" {{ 'checked' if config.view_image else '' }}>
<div><span class="info-text"> — {{ translation['config-images-help'] }}</span></div>
</div>
<div class="config-div config-div-tor">
<label for="config-tor">{{ translation['config-tor'] }}: {{ '' if tor_available else 'Unavailable' }}</label>
<input type="checkbox" name="tor"
id="config-tor" {{ '' if tor_available else 'hidden' }} {{ 'checked' if config.tor else '' }}>
</div>
<div class="config-div config-div-get-only">
<label for="config-get-only">{{ translation['config-get-only'] }}: </label>
<input type="checkbox" name="get_only"
id="config-get-only" {{ 'checked' if config.get_only else '' }}>
</div>
<div class="config-div config-div-accept-language">
<label for="config-accept-language">Set Accept-Language: </label>
<input type="checkbox" name="accept_language"
id="config-accept-language" {{ 'checked' if config.accept_language else '' }}>
</div>
<div class="config-div config-div-root-url">
<label for="config-url">{{ translation['config-url'] }}: </label>
<input type="text" name="url" id="config-url" value="{{ config.url }}">
</div>
<div class="config-div config-div-custom-css">
<a id="css-link"
href="https://github.com/benbusby/whoogle-search/wiki/User-Contributed-CSS-Themes">
{{ translation['config-css'] }}:
</a>
<textarea
name="style"
id="config-style"
autocapitalize="off"
autocomplete="off"
spellcheck="false"
autocorrect="off"
value="">
{{ config.style.replace('\t', '') }}
</textarea>
</div>
<div class="config-div config-div-pref-url">
<label for="config-pref-encryption">{{ translation['config-pref-encryption'] }}: </label>
<input type="checkbox" name="preferences_encrypted"
id="config-pref-encryption" {{ 'checked' if config.preferences_encrypted and config.preferences_key else '' }}>
<div><span class="info-text"> — {{ translation['config-pref-help'] }}</span></div>
<label for="config-pref-url">{{ translation['config-pref-url'] }}: </label>
<input type="text" name="pref-url" id="config-pref-url" value="{{ config.url }}?preferences={{ config.preferences }}">
</div>
</div>
<div class="config-div config-buttons">
<input type="submit" id="config-load" value="{{ translation['load'] }}">&nbsp;
<input type="submit" id="config-submit" value="{{ translation['apply'] }}">&nbsp;
<input type="submit" id="config-save" value="{{ translation['save-as'] }}">
</div>
</form>
</div>
</div>
{% endif %}
</div>
{% include 'footer.html' %}
</body>
<html>
<head>
<link rel="apple-touch-icon" sizes="57x57" href="static/img/favicon/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="static/img/favicon/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="static/img/favicon/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="static/img/favicon/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="static/img/favicon/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="static/img/favicon/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="static/img/favicon/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="static/img/favicon/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="static/img/favicon/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="static/img/favicon/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="static/img/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="static/img/favicon/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="static/img/favicon/favicon-16x16.png">
<link rel="manifest" href="static/img/favicon/manifest.json">
<meta name="referrer" content="no-referrer">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="static/img/favicon/ms-icon-144x144.png">
<script type="text/javascript" src="static/js/autocomplete.js"></script>
<script type="text/javascript" src="static/js/controller.js"></script>
<link rel="search" href="opensearch.xml" type="application/opensearchdescription+xml" title="Whoogle Search">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="static/css/variables.css">
<link rel="stylesheet" href="static/css/main.css">
<link rel="stylesheet" href="static/css/{{ 'dark' if config.dark else 'light' }}-theme.css"/>
<noscript>
<style>
#main { display: inherit !important; }
.content { max-height: 720px; padding: 18px; border-radius: 10px; }
.collapsible { display: none; }
</style>
</noscript>
<style>{{ config.style }}</style>
<title>Whoogle Search</title>
</head>
<body id="main" style="display: none; background-color: {{ '#000' if config.dark else '#fff' }}">
<div class="search-container">
<div class="logo-container">
{{ logo|safe }}
</div>
<form id="search-form" action="search" method="{{ 'get' if config.get_only else 'post' }}">
<div class="search-fields">
<div class="autocomplete">
<input
type="text"
name="q"
id="search-bar"
class="home-search"
autofocus="autofocus"
autocapitalize="none"
spellcheck="false"
autocorrect="off"
autocomplete="off">
</div>
<input type="submit" id="search-submit" value="Search">
</div>
</form>
{% if not config_disabled %}
<br/>
<button id="config-collapsible" class="collapsible">Configuration</button>
<div class="content">
<div class="config-fields">
<form id="config-form" action="config" method="post">
<div class="config-div config-div-ctry">
<label for="config-ctry">Filter Results by Country: </label>
<select name="ctry" id="config-ctry">
{% for ctry in countries %}
<option value="{{ ctry.value }}"
{% if ctry.value in config.ctry %}
selected
{% endif %}>
{{ ctry.name }}
</option>
{% endfor %}
</select>
<div><span class="info-text"> — Note: If enabled, a website will only appear in the results if it is *hosted* in the selected country.</span></div>
</div>
<div class="config-div config-div-lang">
<label for="config-lang-interface">Interface Language: </label>
<select name="lang_interface" id="config-lang-interface">
{% for lang in languages %}
<option value="{{ lang.value }}"
{% if lang.value in config.lang_interface %}
selected
{% endif %}>
{{ lang.name }}
</option>
{% endfor %}
</select>
</div>
<div class="config-div config-div-search-lang">
<label for="config-lang-search">Search Language: </label>
<select name="lang_search" id="config-lang-search">
{% for lang in languages %}
<option value="{{ lang.value }}"
{% if lang.value in config.lang_search %}
selected
{% endif %}>
{{ lang.name }}
</option>
{% endfor %}
</select>
</div>
<div class="config-div config-div-near">
<label for="config-near">Near: </label>
<input type="text" name="near" id="config-near" placeholder="City Name" value="{{ config.near }}">
</div>
<div class="config-div config-div-block">
<label for="config-block">Block: </label>
<input type="text" name="block" id="config-block" placeholder="Comma-separated site list" value="{{ config.block }}">
</div>
<div class="config-div config-div-nojs">
<label for="config-nojs">Show NoJS Links: </label>
<input type="checkbox" name="nojs" id="config-nojs" {{ 'checked' if config.nojs else '' }}>
</div>
<div class="config-div config-div-dark">
<label for="config-dark">Dark Mode: </label>
<input type="checkbox" name="dark" id="config-dark" {{ 'checked' if config.dark else '' }}>
</div>
<div class="config-div config-div-safe">
<label for="config-safe">Safe Search: </label>
<input type="checkbox" name="safe" id="config-safe" {{ 'checked' if config.safe else '' }}>
</div>
<div class="config-div config-div-alts">
<label class="tooltip" for="config-alts">Replace Social Media Links: </label>
<input type="checkbox" name="alts" id="config-alts" {{ 'checked' if config.alts else '' }}>
<div><span class="info-text"> — Replaces Twitter/YouTube/Instagram/Reddit links
with Nitter/Invidious/Bibliogram/Libreddit links.</span></div>
</div>
<div class="config-div config-div-new-tab">
<label for="config-new-tab">Open Links in New Tab: </label>
<input type="checkbox" name="new_tab" id="config-new-tab" {{ 'checked' if config.new_tab else '' }}>
</div>
<div class="config-div config-div-view-image">
<label for="config-view-image">Full Size Image Search: </label>
<input type="checkbox" name="view_image" id="config-view-image" {{ 'checked' if config.view_image else '' }}>
<div><span class="info-text"> — (Experimental) Adds the "View Image" option on desktop to view full size images in search results.
This will cause image result thumbnails to be lower resolution.</span></div>
</div>
<div class="config-div config-div-tor">
<label for="config-tor">Use Tor: {{ '' if tor_available else 'Unavailable' }}</label>
<input type="checkbox" name="tor" id="config-tor" {{ '' if tor_available else 'hidden' }} {{ 'checked' if config.tor else '' }}>
</div>
<div class="config-div config-div-get-only">
<label for="config-get-only">GET Requests Only: </label>
<input type="checkbox" name="get_only" id="config-get-only" {{ 'checked' if config.get_only else '' }}>
</div>
<div class="config-div config-div-root-url">
<label for="config-url">Root URL: </label>
<input type="text" name="url" id="config-url" value="{{ config.url }}">
</div>
<div class="config-div config-div-custom-css">
<label for="config-style">Custom CSS:</label>
<textarea
name="style"
id="config-style"
autocapitalize="off"
autocomplete="off"
spellcheck="false"
autocorrect="off"
value="">
{{ config.style }}
</textarea>
</div>
<div class="config-div">
<input type="submit" id="config-load" value="Load">&nbsp;
<input type="submit" id="config-submit" value="Apply">&nbsp;
<input type="submit" id="config-save" value="Save As...">
</div>
</form>
</div>
</div>
{% endif %}
</div>
<footer>
<p style="color: {{ 'var(--whoogle-dark-text)' if config.dark else 'var(--whoogle-text)' }};">
Whoogle Search v{{ version_number }} ||
<a id="gh-link" href="https://github.com/benbusby/whoogle-search">View on GitHub</a>
</p>
</footer>
</body>
</html>

View File

@ -1,4 +1,10 @@
<link rel="stylesheet" href="static/css/logo.css">
<svg id="Layer_1" class="whoogle-svg" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1028 254">
<style>
path {
fill: {{ 'var(--whoogle-dark-logo)' if dark else 'var(--whoogle-logo)' }};
}
</style>
<defs>
<style>
</style>
@ -16,3 +22,4 @@
<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>
</svg>
</a>

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
<form id="search-form" action="search" method="post">
<form id="search-form" action="{{ url }}/search" method="post">
<input
type="text"
name="q"
@ -8,7 +8,6 @@
spellcheck="false"
autocorrect="off"
placeholder="Whoogle Search"
autocomplete="off"
dir="auto">
autocomplete="off">
<input type="submit" style="width: 9%" id="search-submit" value="Search">
</form>

View File

@ -1,6 +1,5 @@
import json
import requests
import urllib.parse as urlparse
DDG_BANGS = 'https://duckduckgo.com/bang.v255.js'
@ -36,7 +35,6 @@ def gen_bangs_json(bangs_file: str) -> None:
}
json.dump(bangs_data, open(bangs_file, 'w'))
print('* Finished creating ddg bangs json')
def resolve_bang(query: str, bangs_dict: dict) -> str:
@ -53,38 +51,11 @@ def resolve_bang(query: str, bangs_dict: dict) -> str:
wasn't a match or didn't contain a bang operator
"""
split_query = query.split(' ')
for operator in bangs_dict.keys():
if operator not in split_query:
continue
#if ! not in query simply return (speed up processing)
if '!' not in query:
return ''
split_query = query.strip().split(' ')
# look for operator in query if one is found, list operator should be of
# length 1, operator should not be case-sensitive here to remove it later
operator = [
word
for word in split_query
if word.lower() in bangs_dict
]
if len(operator) == 1:
# get operator
operator = operator[0]
# removes operator from query
split_query.remove(operator)
# rebuild the query string
bang_query = ' '.join(split_query).strip()
# Check if operator is a key in bangs and get bang if exists
bang = bangs_dict.get(operator.lower(), None)
if bang:
bang_url = bang['url']
if bang_query:
return bang_url.replace('{}', bang_query, 1)
else:
parsed_url = urlparse.urlparse(bang_url)
return f'{parsed_url.scheme}://{parsed_url.netloc}'
return bangs_dict[operator]['url'].format(
query.replace(operator, '').strip())
return ''

View File

@ -1,72 +0,0 @@
from bs4 import BeautifulSoup as bsoup
from flask import Request
import hashlib
import os
from requests import exceptions, get
from urllib.parse import urlparse
def gen_file_hash(path: str, static_file: str) -> str:
file_contents = open(os.path.join(path, static_file), 'rb').read()
file_hash = hashlib.md5(file_contents).hexdigest()[:8]
filename_split = os.path.splitext(static_file)
return filename_split[0] + '.' + file_hash + filename_split[-1]
def read_config_bool(var: str) -> bool:
val = os.getenv(var, '0')
# user can specify one of the following values as 'true' inputs (all
# variants with upper case letters will also work):
# ('true', 't', '1', 'yes', 'y')
val = val.lower() in ('true', 't', '1', 'yes', 'y')
return val
def get_client_ip(r: Request) -> str:
if r.environ.get('HTTP_X_FORWARDED_FOR') is None:
return r.environ['REMOTE_ADDR']
else:
return r.environ['HTTP_X_FORWARDED_FOR']
def get_request_url(url: str) -> str:
if os.getenv('HTTPS_ONLY', False):
return url.replace('http://', 'https://', 1)
return url
def get_proxy_host_url(r: Request, default: str, root=False) -> str:
scheme = r.headers.get('X-Forwarded-Proto', 'https')
http_host = r.headers.get('X-Forwarded-Host')
if http_host:
return f'{scheme}://{http_host}{r.full_path if not root else "/"}'
return default
def check_for_update(version_url: str, current: str) -> int:
# Check for the latest version of Whoogle
try:
update = bsoup(get(version_url).text, 'html.parser')
latest = update.select_one('[class="Link--primary"]').string[1:]
current = int(''.join(filter(str.isdigit, current)))
latest = int(''.join(filter(str.isdigit, latest)))
has_update = '' if current >= latest else latest
except (exceptions.ConnectionError, AttributeError):
# Ignore failures, assume current version is up to date
has_update = ''
return has_update
def get_abs_url(url, page_url):
# Creates a valid absolute URL using a partial or relative URL
if url.startswith('//'):
return f'https:{url}'
elif url.startswith('/'):
return f'{urlparse(page_url).netloc}{url}'
elif url.startswith('./'):
return f'{page_url}{url[2:]}'
return url

View File

@ -1,90 +1,35 @@
from app.models.config import Config
from app.models.endpoint import Endpoint
from bs4 import BeautifulSoup, NavigableString
import copy
from flask import current_app
import html
from bs4 import BeautifulSoup
import os
import urllib.parse as urlparse
from urllib.parse import parse_qs
import re
SKIP_ARGS = ['ref_src', 'utm']
SKIP_PREFIX = ['//www.', '//mobile.', '//m.']
GOOG_STATIC = 'www.gstatic.com'
G_M_LOGO_URL = 'https://www.gstatic.com/m/images/icons/googleg.gif'
GOOG_IMG = '/images/branding/searchlogo/1x/googlelogo'
LOGO_URL = GOOG_IMG + '_desk'
BLANK_B64 = ('data:image/png;base64,'
'iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAD0lEQVR42mNkw'
'AIYh7IgAAVVAAuInjI5AAAAAElFTkSuQmCC')
# Ad keywords
BLACKLIST = [
'ad', 'ads', 'anuncio', 'annuncio', 'annonce', 'Anzeige', '广告', '廣告',
'Reklama', 'Реклама', 'Anunț', '광고', 'annons', 'Annonse', 'Iklan',
'広告', 'Augl.', 'Mainos', 'Advertentie', 'إعلان', 'Գովազդ', 'विज्ञापन',
'Reklam', 'آگهی', 'Reklāma', 'Reklaam', 'Διαφήμιση', 'מודעה', 'Hirdetés',
'Anúncio', 'Quảng cáo','โฆษณา'
'ad', 'anuncio', 'annuncio', 'annonce', 'Anzeige', '广告', '廣告', 'Reklama',
'Реклама', 'Anunț', '광고', 'annons', 'Annonse', 'Iklan', '広告', 'Augl.',
'Mainos', 'Advertentie', 'إعلان', 'Գովազդ', 'विज्ञापन', 'Reklam', 'آگهی',
'Reklāma', 'Reklaam', 'Διαφήμιση', 'מודעה', 'Hirdetés', 'Anúncio'
]
SITE_ALTS = {
'twitter.com': os.getenv('WHOOGLE_ALT_TW', 'farside.link/nitter'),
'youtube.com': os.getenv('WHOOGLE_ALT_YT', 'farside.link/invidious'),
'instagram.com': os.getenv('WHOOGLE_ALT_IG', 'farside.link/bibliogram/u'),
'reddit.com': os.getenv('WHOOGLE_ALT_RD', 'farside.link/libreddit'),
**dict.fromkeys([
'medium.com',
'levelup.gitconnected.com'
], os.getenv('WHOOGLE_ALT_MD', 'farside.link/scribe')),
'imgur.com': os.getenv('WHOOGLE_ALT_IMG', 'farside.link/rimgo'),
'wikipedia.org': os.getenv('WHOOGLE_ALT_WIKI', 'farside.link/wikiless'),
'imdb.com': os.getenv('WHOOGLE_ALT_IMDB', 'farside.link/libremdb'),
'quora.com': os.getenv('WHOOGLE_ALT_QUORA', 'farside.link/quetre')
'twitter.com': os.getenv('WHOOGLE_ALT_TW', 'nitter.net'),
'youtube.com': os.getenv('WHOOGLE_ALT_YT', 'invidious.snopyta.org'),
'instagram.com': os.getenv('WHOOGLE_ALT_IG', 'bibliogram.art/u'),
'reddit.com': os.getenv('WHOOGLE_ALT_RD', 'libredd.it')
}
def bold_search_terms(response: str, query: str) -> BeautifulSoup:
"""Wraps all search terms in bold tags (<b>). If any terms are wrapped
in quotes, only that exact phrase will be made bold.
Args:
response: The initial response body for the query
query: The original search query
Returns:
BeautifulSoup: modified soup object with bold items
"""
response = BeautifulSoup(response, 'html.parser')
def replace_any_case(element: NavigableString, target_word: str) -> None:
# Replace all instances of the word, but maintaining the same case in
# the replacement
if len(element) == len(target_word):
return
if not re.match('.*[a-zA-Z0-9].*', target_word) or (
element.parent and element.parent.name == 'style'):
return
element.replace_with(BeautifulSoup(
re.sub(fr'\b((?![{{}}<>-]){target_word}(?![{{}}<>-]))\b',
r'<b>\1</b>',
html.escape(element),
flags=re.I), 'html.parser')
)
# Split all words out of query, grouping the ones wrapped in quotes
for word in re.split(r'\s+(?=[^"]*(?:"[^"]*"[^"]*)*$)', query):
word = re.sub(r'[^A-Za-z0-9 ]+', '', word)
target = response.find_all(
text=re.compile(r'' + re.escape(word), re.I))
for nav_str in target:
replace_any_case(nav_str, word)
return response
def has_ad_content(element: str) -> bool:
"""Inspects an HTML element for ad related content
@ -95,8 +40,7 @@ def has_ad_content(element: str) -> bool:
bool: True/False for the element containing an ad
"""
element_str = ''.join(filter(str.isalpha, element))
return (element_str.upper() in (value.upper() for value in BLACKLIST)
return (element.upper() in (value.upper() for value in BLACKLIST)
or '' in element)
@ -128,38 +72,17 @@ def get_site_alt(link: str) -> str:
str: An updated (or ignored) result link
"""
# Need to replace full hostname with alternative to encapsulate
# subdomains as well
parsed_link = urlparse.urlparse(link)
hostname = parsed_link.hostname
for site_key in SITE_ALTS.keys():
if not hostname or site_key not in hostname or not SITE_ALTS[site_key]:
if site_key not in link:
continue
# Wikipedia -> Wikiless replacements require the subdomain (if it's
# a 2-char language code) to be passed as a URL param to Wikiless
# in order to preserve the language setting.
params = ''
if 'wikipedia' in hostname:
subdomain = hostname.split('.')[0]
if len(subdomain) == 2:
params = f'?lang={subdomain}'
parsed_alt = urlparse.urlparse(SITE_ALTS[site_key])
link = link.replace(hostname, SITE_ALTS[site_key]) + params
# If a scheme is specified in the alternative, this results in a
# replaced link that looks like "https://http://altservice.tld".
# In this case, we can remove the original scheme from the result
# and use the one specified for the alt.
if parsed_alt.scheme:
link = '//'.join(link.split('//')[1:])
for prefix in SKIP_PREFIX:
link = link.replace(prefix, '//')
link = link.replace(site_key, SITE_ALTS[site_key])
break
for prefix in SKIP_PREFIX:
link = link.replace(prefix, '//')
return link
@ -207,224 +130,8 @@ def append_nojs(result: BeautifulSoup) -> None:
"""
nojs_link = BeautifulSoup(features='html.parser').new_tag('a')
nojs_link['href'] = f'{Endpoint.window}?nojs=1&location=' + result['href']
nojs_link.string = ' NoJS Link'
nojs_link['href'] = '/window?location=' + result['href']
nojs_link['style'] = 'display:block;width:100%;'
nojs_link.string = 'NoJS Link: ' + nojs_link['href']
result.append(BeautifulSoup('<br><hr><br>', 'html.parser'))
result.append(nojs_link)
def append_anon_view(result: BeautifulSoup, config: Config) -> None:
"""Appends an 'anonymous view' for a search result, where all site
contents are viewed through Whoogle as a proxy.
Args:
result: The search result to append an anon view link to
nojs: Remove Javascript from Anonymous View
Returns:
None
"""
av_link = BeautifulSoup(features='html.parser').new_tag('a')
nojs = 'nojs=1' if config.nojs else 'nojs=0'
location = f'location={result["href"]}'
av_link['href'] = f'{Endpoint.window}?{nojs}&{location}'
translation = current_app.config['TRANSLATIONS'][
config.get_localization_lang()
]
av_link.string = f'{translation["anon-view"]}'
av_link['class'] = 'anon-view'
result.append(av_link)
def add_ip_card(html_soup: BeautifulSoup, ip: str) -> BeautifulSoup:
"""Adds the client's IP address to the search results
if query contains keywords
Args:
html_soup: The parsed search result containing the keywords
ip: ip address of the client
Returns:
BeautifulSoup
"""
main_div = html_soup.select_one('#main')
if main_div:
# HTML IP card tag
ip_tag = html_soup.new_tag('div')
ip_tag['class'] = 'ZINbbc xpd O9g5cc uUPGi'
# For IP Address html tag
ip_address = html_soup.new_tag('div')
ip_address['class'] = 'kCrYT ip-address-div'
ip_address.string = ip
# Text below the IP address
ip_text = html_soup.new_tag('div')
ip_text.string = 'Your public IP address'
ip_text['class'] = 'kCrYT ip-text-div'
# Adding all the above html tags to the IP card
ip_tag.append(ip_address)
ip_tag.append(ip_text)
# Insert the element at the top of the result list
main_div.insert_before(ip_tag)
return html_soup
def check_currency(response: str) -> dict:
"""Check whether the results have currency conversion
Args:
response: Search query Result
Returns:
dict: Consists of currency names and values
"""
soup = BeautifulSoup(response, 'html.parser')
currency_link = soup.find('a', {'href': 'https://g.co/gfd'})
if currency_link:
while 'class' not in currency_link.attrs or \
'ZINbbc' not in currency_link.attrs['class']:
if currency_link.parent:
currency_link = currency_link.parent
else:
return {}
currency_link = currency_link.find_all(class_='BNeawe')
currency1 = currency_link[0].text
currency2 = currency_link[1].text
currency1 = currency1.rstrip('=').split(' ', 1)
currency2 = currency2.split(' ', 1)
# Handle differences in currency formatting
# i.e. "5.000" vs "5,000"
if currency2[0][-3] == ',':
currency1[0] = currency1[0].replace('.', '')
currency1[0] = currency1[0].replace(',', '.')
currency2[0] = currency2[0].replace('.', '')
currency2[0] = currency2[0].replace(',', '.')
else:
currency1[0] = currency1[0].replace(',', '')
currency2[0] = currency2[0].replace(',', '')
currency1_value = float(re.sub(r'[^\d\.]', '', currency1[0]))
currency1_label = currency1[1]
currency2_value = float(re.sub(r'[^\d\.]', '', currency2[0]))
currency2_label = currency2[1]
return {'currencyValue1': currency1_value,
'currencyLabel1': currency1_label,
'currencyValue2': currency2_value,
'currencyLabel2': currency2_label
}
return {}
def add_currency_card(soup: BeautifulSoup,
conversion_details: dict) -> BeautifulSoup:
"""Adds the currency conversion boxes
to response of the search query
Args:
soup: Parsed search result
conversion_details: Dictionary of currency
related information
Returns:
BeautifulSoup
"""
# Element before which the code will be changed
# (This is the 'disclaimer' link)
element1 = soup.find('a', {'href': 'https://g.co/gfd'})
while 'class' not in element1.attrs or \
'nXE3Ob' not in element1.attrs['class']:
element1 = element1.parent
# Creating the conversion factor
conversion_factor = (conversion_details['currencyValue1'] /
conversion_details['currencyValue2'])
# Creating a new div for the input boxes
conversion_box = soup.new_tag('div')
conversion_box['class'] = 'conversion_box'
# Currency to be converted from
input_box1 = soup.new_tag('input')
input_box1['id'] = 'cb1'
input_box1['type'] = 'number'
input_box1['class'] = 'cb'
input_box1['value'] = conversion_details['currencyValue1']
input_box1['oninput'] = f'convert(1, 2, {1 / conversion_factor})'
label_box1 = soup.new_tag('label')
label_box1['for'] = 'cb1'
label_box1['class'] = 'cb_label'
label_box1.append(conversion_details['currencyLabel1'])
br = soup.new_tag('br')
# Currency to be converted to
input_box2 = soup.new_tag('input')
input_box2['id'] = 'cb2'
input_box2['type'] = 'number'
input_box2['class'] = 'cb'
input_box2['value'] = conversion_details['currencyValue2']
input_box2['oninput'] = f'convert(2, 1, {conversion_factor})'
label_box2 = soup.new_tag('label')
label_box2['for'] = 'cb2'
label_box2['class'] = 'cb_label'
label_box2.append(conversion_details['currencyLabel2'])
conversion_box.append(input_box1)
conversion_box.append(label_box1)
conversion_box.append(br)
conversion_box.append(input_box2)
conversion_box.append(label_box2)
element1.insert_before(conversion_box)
return soup
def get_tabs_content(tabs: dict,
full_query: str,
search_type: str,
preferences: str,
translation: dict) -> dict:
"""Takes the default tabs content and updates it according to the query.
Args:
tabs: The default content for the tabs
full_query: The original search query
search_type: The current search_type
translation: The translation to get the names of the tabs
Returns:
dict: contains the name, the href and if the tab is selected or not
"""
tabs = copy.deepcopy(tabs)
for tab_id, tab_content in tabs.items():
# update name to desired language
if tab_id in translation:
tab_content['name'] = translation[tab_id]
# update href with query
query = full_query.replace(f'&tbm={search_type}', '')
if tab_content['tbm'] is not None:
query = f"{query}&tbm={tab_content['tbm']}"
if preferences:
query = f"{query}&preferences={preferences}"
tab_content['href'] = tab_content['href'].format(query=query)
# update if selected tab (default all tab is selected)
if tab_content['tbm'] == search_type:
tabs['all']['selected'] = False
tab_content['selected'] = True
return tabs

View File

@ -1,14 +1,10 @@
import os
import re
from typing import Any
from app.filter import Filter
from app.filter import Filter, get_first_link
from app.request import gen_query
from app.utils.misc import get_proxy_host_url
from app.utils.results import get_first_link
from bs4 import BeautifulSoup as bsoup
from cryptography.fernet import Fernet, InvalidToken
from flask import g
from typing import Any, Tuple
import os
TOR_BANNER = '<hr><h1 style="text-align: center">You are using Tor</h1><hr>'
CAPTCHA = 'div class="g-recaptcha"'
@ -54,16 +50,15 @@ class Search:
Attributes:
request: the incoming flask request
config: the current user config settings
session_key: the flask user fernet key
session: the flask user session
"""
def __init__(self, request, config, session_key, cookies_disabled=False):
def __init__(self, request, config, session, cookies_disabled=False):
method = request.method
self.request = request
self.request_params = request.args if method == 'GET' else request.form
self.user_agent = request.headers.get('User-Agent')
self.feeling_lucky = False
self.config = config
self.session_key = session_key
self.session = session
self.query = ''
self.cookies_disabled = cookies_disabled
self.search_type = self.request_params.get(
@ -98,7 +93,7 @@ class Search:
else:
# Attempt to decrypt if this is an internal link
try:
q = Fernet(self.session_key).decrypt(q.encode()).decode()
q = Fernet(self.session['key']).decrypt(q.encode()).decode()
except InvalidToken:
pass
@ -116,21 +111,14 @@ class Search:
"""
mobile = 'Android' in self.user_agent or 'iPhone' in self.user_agent
# reconstruct url if X-Forwarded-Host header present
root_url = get_proxy_host_url(
self.request,
self.request.url_root,
root=True)
content_filter = Filter(self.session_key,
root_url=root_url,
content_filter = Filter(self.session['key'],
mobile=mobile,
config=self.config,
query=self.query)
config=self.config)
full_query = gen_query(self.query,
self.request_params,
self.config)
self.full_query = full_query
self.config,
content_filter.near)
# force mobile search when view image is true and
# the request is not already made by a mobile
@ -142,15 +130,17 @@ class Search:
force_mobile=view_image)
# Produce cleanable html soup from response
html_soup = bsoup(get_body.text, 'html.parser')
html_soup = bsoup(content_filter.reskin(get_body.text), 'html.parser')
# Replace current soup if view_image is active
if view_image:
html_soup = content_filter.view_image(html_soup)
# Indicate whether or not a Tor connection is active
tor_banner = bsoup('', 'html.parser')
if g.user_request.tor_valid:
html_soup.insert(0, bsoup(TOR_BANNER, 'html.parser'))
tor_banner = bsoup(TOR_BANNER, 'html.parser')
html_soup.insert(0, tor_banner)
if self.feeling_lucky:
return get_first_link(html_soup)
@ -163,21 +153,9 @@ class Search:
self.request_params.to_dict(flat=True).items()
if self.config.is_safe_key(k))
for link in formatted_results.find_all('a', href=True):
link['rel'] = "nofollow noopener noreferrer"
if 'search?' not in link['href'] or link['href'].index(
'search?') > 1:
continue
link['href'] += param_str
return str(formatted_results)
def check_kw_ip(self) -> re.Match:
"""Checks for keywords related to 'my ip' in the query
Returns:
bool
"""
return re.search("([^a-z0-9]|^)my *[^a-z0-9] *(ip|internet protocol)" +
"($|( *[^a-z0-9] *(((addres|address|adres|" +
"adress)|a)? *$)))", self.query.lower())

View File

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

View File

@ -1,23 +0,0 @@
# 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/

View File

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

View File

@ -1,22 +0,0 @@
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

@ -1,62 +0,0 @@
{{/*
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

@ -1,72 +0,0 @@
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

@ -1,28 +0,0 @@
{{- 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

@ -1,61 +0,0 @@
{{- 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

@ -1,15 +0,0 @@
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

@ -1,12 +0,0 @@
{{- 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

@ -1,15 +0,0 @@
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

View File

@ -1,115 +0,0 @@
# Default values for whoogle.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
nameOverride: ""
fullnameOverride: ""
replicaCount: 1
image:
repository: benbusby/whoogle-search
pullPolicy: IfNotPresent
# Overrides the image tag whose default is the chart appVersion.
tag: ""
pullSecrets: []
# - my-image-pull-secret
serviceAccount:
# Specifies whether a service account should be created
create: true
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ""
conf: {}
# WHOOGLE_URL_PREFIX: "" # The URL prefix to use for the whoogle instance (i.e. "/whoogle")
# WHOOGLE_DOTENV: "" # Load environment variables in whoogle.env
# WHOOGLE_USER: "" # The username for basic auth. WHOOGLE_PASS must also be set if used.
# WHOOGLE_PASS: "" # The password for basic auth. WHOOGLE_USER must also be set if used.
# WHOOGLE_PROXY_USER: "" # The username of the proxy server.
# WHOOGLE_PROXY_PASS: "" # The password of the proxy server.
# WHOOGLE_PROXY_TYPE: "" # The type of the proxy server. Can be "socks5", "socks4", or "http".
# WHOOGLE_PROXY_LOC: "" # The location of the proxy server (host or ip).
# EXPOSE_PORT: "" # The port where Whoogle will be exposed. (default 5000)
# HTTPS_ONLY: "" # Enforce HTTPS. (See https://github.com/benbusby/whoogle-search#https-enforcement)
# WHOOGLE_ALT_TW: "" # The twitter.com alternative to use when site alternatives are enabled in the config.
# WHOOGLE_ALT_YT: "" # The youtube.com alternative to use when site alternatives are enabled in the config.
# WHOOGLE_ALT_IG: "" # The instagram.com alternative to use when site alternatives are enabled in the config.
# WHOOGLE_ALT_RD: "" # The reddit.com alternative to use when site alternatives are enabled in the config.
# WHOOGLE_ALT_TL: "" # The Google Translate alternative to use. This is used for all "translate ____" searches.
# WHOOGLE_ALT_MD: "" # The medium.com alternative to use when site alternatives are enabled in the config.
# WHOOGLE_ALT_IMG: "" # The imgur.com alternative to use when site alternatives are enabled in the config.
# WHOOGLE_ALT_WIKI: "" # The wikipedia.com alternative to use when site alternatives are enabled in the config.
# WHOOGLE_ALT_IMDB: "" # The imdb.com alternative to use. Set to "" to continue using imdb.com when site alternatives are enabled.
# WHOOGLE_ALT_QUORA: "" # The quora.com alternative to use. Set to "" to continue using quora.com when site alternatives are enabled.
# WHOOGLE_AUTOCOMPLETE: "" # Controls visibility of autocomplete/search suggestions. Default on -- use '0' to disable
# WHOOGLE_MINIMAL: "" # Remove everything except basic result cards from all search queries.
# WHOOGLE_CONFIG_DISABLE: "" # Hide config from UI and disallow changes to config by client
# WHOOGLE_CONFIG_COUNTRY: "" # Filter results by hosting country
# WHOOGLE_CONFIG_LANGUAGE: "" # Set interface language
# WHOOGLE_CONFIG_SEARCH_LANGUAGE: "" # Set search result language
# WHOOGLE_CONFIG_BLOCK: "" # Block websites from search results (use comma-separated list)
# WHOOGLE_CONFIG_THEME: "" # Set theme mode (light, dark, or system)
# WHOOGLE_CONFIG_SAFE: "" # Enable safe searches
# WHOOGLE_CONFIG_ALTS: "" # Use social media site alternatives (nitter, invidious, etc)
# WHOOGLE_CONFIG_NEAR: "" # Restrict results to only those near a particular city
# WHOOGLE_CONFIG_TOR: "" # Use Tor routing (if available)
# WHOOGLE_CONFIG_NEW_TAB: "" # Always open results in new tab
# WHOOGLE_CONFIG_VIEW_IMAGE: "" # Enable View Image option
# WHOOGLE_CONFIG_GET_ONLY: "" # Search using GET requests only
# WHOOGLE_CONFIG_URL: "" # The root url of the instance (https://<your url>/)
# WHOOGLE_CONFIG_STYLE: "" # The custom CSS to use for styling (should be single line)
# WHOOGLE_CONFIG_PREFERENCES_ENCRYPTED: "" # Encrypt preferences token, requires key
# WHOOGLE_CONFIG_PREFERENCES_KEY: "" # Key to encrypt preferences in URL (REQUIRED to show url)
podAnnotations: {}
podSecurityContext: {}
# fsGroup: 2000
securityContext:
runAsUser: 0
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
service:
type: ClusterIP
port: 5000
ingress:
enabled: false
className: ""
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
hosts:
- host: whoogle.example.com
paths:
- path: /
pathType: ImplementationSpecific
tls: []
# - secretName: chart-example-tls
# hosts:
# - whoogle.example.com
resources: {}
# requests:
# cpu: 100m
# memory: 128Mi
# limits:
# cpu: 100m
# memory: 128Mi
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 100
targetCPUUtilizationPercentage: 80
# targetMemoryUtilizationPercentage: 80
nodeSelector: {}
tolerations: []
affinity: {}

View File

@ -1,80 +0,0 @@
# can't use mem_limit in a 3.x docker-compose file in non swarm mode
# see https://github.com/docker/compose/issues/4513
version: "2.4"
services:
traefik:
image: "traefik:v2.7"
container_name: "traefik"
command:
#- "--log.level=DEBUG"
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.myresolver.acme.tlschallenge=true"
#- "--certificatesresolvers.myresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"
- "--certificatesresolvers.myresolver.acme.email=change@domain.name"
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
ports:
- "443:443"
- "8080:8080"
volumes:
- "./letsencrypt:/letsencrypt"
- "/var/run/docker.sock:/var/run/docker.sock:ro"
whoogle-search:
labels:
- "traefik.enable=true"
- "traefik.http.routers.whoami.rule=Host(`change.host.name`)"
- "traefik.http.routers.whoami.entrypoints=websecure"
- "traefik.http.routers.whoami.tls.certresolver=myresolver"
- "traefik.http.services.whoogle-search.loadbalancer.server.port=5000"
image: ${WHOOGLE_IMAGE:-benbusby/whoogle-search}
container_name: whoogle-search
restart: unless-stopped
pids_limit: 50
mem_limit: 256mb
memswap_limit: 256mb
# user debian-tor from tor package
user: whoogle
security_opt:
- no-new-privileges
cap_drop:
- ALL
tmpfs:
- /config/:size=10M,uid=927,gid=927,mode=1700
- /var/lib/tor/:size=15M,uid=927,gid=927,mode=1700
- /run/tor/:size=1M,uid=927,gid=927,mode=1700
environment: # Uncomment to configure environment variables
# Basic auth configuration, uncomment to enable
#- WHOOGLE_USER=<auth username>
#- WHOOGLE_PASS=<auth password>
# Proxy configuration, uncomment to enable
#- WHOOGLE_PROXY_USER=<proxy username>
#- WHOOGLE_PROXY_PASS=<proxy password>
#- WHOOGLE_PROXY_TYPE=<proxy type (http|https|socks4|socks5)
#- WHOOGLE_PROXY_LOC=<proxy host/ip>
# Site alternative configurations, uncomment to enable
# Note: If not set, the feature will still be available
# with default values.
#- WHOOGLE_ALT_TW=farside.link/nitter
#- WHOOGLE_ALT_YT=farside.link/invidious
#- WHOOGLE_ALT_IG=farside.link/bibliogram/u
#- WHOOGLE_ALT_RD=farside.link/libreddit
#- WHOOGLE_ALT_MD=farside.link/scribe
#- WHOOGLE_ALT_TL=farside.link/lingva
#- WHOOGLE_ALT_IMG=farside.link/rimgo
#- WHOOGLE_ALT_WIKI=farside.link/wikiless
#- WHOOGLE_ALT_IMDB=farside.link/libremdb
#- WHOOGLE_ALT_QUORA=farside.link/quetre
# - WHOOGLE_CONFIG_DISABLE=1
# - WHOOGLE_CONFIG_SEARCH_LANGUAGE=lang_en
# - WHOOGLE_CONFIG_GET_ONLY=1
# - WHOOGLE_CONFIG_COUNTRY=FR
# - WHOOGLE_CONFIG_PREFERENCES_ENCRYPTED=1
# - WHOOGLE_CONFIG_PREFERENCES_KEY="NEEDS_TO_BE_MODIFIED"
#env_file: # Alternatively, load variables from whoogle.env
#- whoogle.env
ports:
- 8000:5000

View File

@ -1,25 +1,26 @@
# can't use mem_limit in a 3.x docker-compose file in non swarm mode
# cant use mem_limit in a 3.x docker-compose file in non swarm mode
# see https://github.com/docker/compose/issues/4513
version: "2.4"
services:
whoogle-search:
image: ${WHOOGLE_IMAGE:-benbusby/whoogle-search}
image: benbusby/whoogle-search
container_name: whoogle-search
restart: unless-stopped
restart: on-failure:5
pids_limit: 50
mem_limit: 256mb
memswap_limit: 256mb
# user debian-tor from tor package
user: whoogle
user: '102'
security_opt:
- no-new-privileges
cap_drop:
- ALL
read_only: true
tmpfs:
- /config/:size=10M,uid=927,gid=927,mode=1700
- /var/lib/tor/:size=15M,uid=927,gid=927,mode=1700
- /run/tor/:size=1M,uid=927,gid=927,mode=1700
- /config/:size=10M,uid=102,gid=102,mode=1700
- /var/lib/tor/:size=10M,uid=102,gid=102,mode=1700
- /run/tor/:size=1M,uid=102,gid=102,mode=1700
#environment: # Uncomment to configure environment variables
# Basic auth configuration, uncomment to enable
#- WHOOGLE_USER=<auth username>
@ -27,22 +28,17 @@ services:
# Proxy configuration, uncomment to enable
#- WHOOGLE_PROXY_USER=<proxy username>
#- WHOOGLE_PROXY_PASS=<proxy password>
#- WHOOGLE_PROXY_TYPE=<proxy type (http|https|socks4|socks5)
#- WHOOGLE_PROXY_TYPE=<proxy type (http|socks4|socks5)
#- WHOOGLE_PROXY_LOC=<proxy host/ip>
# Site alternative configurations, uncomment to enable
# Note: If not set, the feature will still be available
# with default values.
#- WHOOGLE_ALT_TW=farside.link/nitter
#- WHOOGLE_ALT_YT=farside.link/invidious
#- WHOOGLE_ALT_IG=farside.link/bibliogram/u
#- WHOOGLE_ALT_RD=farside.link/libreddit
#- WHOOGLE_ALT_MD=farside.link/scribe
#- WHOOGLE_ALT_TL=farside.link/lingva
#- WHOOGLE_ALT_IMG=farside.link/rimgo
#- WHOOGLE_ALT_WIKI=farside.link/wikiless
#- WHOOGLE_ALT_IMDB=farside.link/libremdb
#- WHOOGLE_ALT_QUORA=farside.link/quetre
# with default values.
#- WHOOGLE_ALT_TW=nitter.net
#- WHOOGLE_ALT_YT=invidious.snopyta.org
#- WHOOGLE_ALT_IG=bibliogram.art/u
#- WHOOGLE_ALT_RD=libredd.it
#env_file: # Alternatively, load variables from whoogle.env
#- whoogle.env
ports:
- 5000:5000
restart: unless-stopped

BIN
docs/screenshot_desktop.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

BIN
docs/screenshot_mobile.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

View File

View File

@ -1,13 +0,0 @@
https://gowogle.voring.me
https://s.tokhmi.xyz
https://search.albony.xyz
https://search.garudalinux.org
https://search.dr460nf1r3.org
https://search.sethforprivacy.com
https://whoogle.fossho.st
https://www.whooglesearch.ml
https://whoogle.dcs0.hu
https://whoogle.esmailelbob.xyz
https://whoogle.lunar.icu
https://whoogle.privacydev.net
https://search.wef.lol

View File

@ -1 +0,0 @@
# Place password here. Keep this safe.

View File

@ -1,16 +1,7 @@
#!/bin/sh
if [ "$WHOOGLE_TOR_SERVICE" == "0" ]; then
echo "Skipping Tor startup..."
exit 0
fi
#!/bin/bash
if [ "$(whoami)" != "root" ]; then
tor -f /etc/tor/torrc
else
if (grep alpine /etc/os-release >/dev/null); then
rc-service tor start
else
service tor start
fi
service tor start
fi

View File

@ -6,4 +6,3 @@ CookieAuthFileGroupReadable 1
ExtORPortCookieAuthFileGroupReadable 1
CacheDirectoryGroupReadable 1
CookieAuthFile /var/lib/tor/control_auth_cookie
Log debug-notice file /dev/null

View File

@ -1,15 +1,14 @@
attrs==19.3.0
beautifulsoup4==4.10.0
brotli==1.0.9
cachelib==0.4.1
beautifulsoup4==4.8.2
bs4==0.0.1
cachelib==0.1
certifi==2020.4.5.1
cffi==1.15.0
cffi==1.13.2
chardet==3.0.4
click==8.0.3
Click==7.0
cryptography==3.3.2
cssutils==2.4.0
defusedxml==0.7.1
Flask==1.1.1
Flask-Session==0.3.2
idna==2.9
itsdangerous==1.1.0
Jinja2==2.11.3
@ -19,17 +18,17 @@ packaging==20.4
pluggy==0.13.1
py==1.10.0
pycodestyle==2.6.0
pycparser==2.21
pycparser==2.19
pyOpenSSL==19.1.0
pyparsing==2.4.7
PySocks==1.7.1
pytest==6.2.5
pytest==5.4.1
python-dateutil==2.8.1
requests==2.25.1
requests==2.23.0
soupsieve==1.9.5
stem==1.8.0
urllib3==1.26.5
waitress==2.1.2
urllib3==1.25.9
waitress==1.4.3
wcwidth==0.1.9
Werkzeug==0.16.0
python-dotenv==0.16.0

24
run
View File

@ -1,36 +1,26 @@
#!/bin/sh
#!/bin/bash
# Usage:
# ./run # Runs the full web app
# ./run test # Runs the testing suite
set -e
set -euo pipefail
SCRIPT_DIR="$(CDPATH= command cd -- "$(dirname -- "$0")" && pwd -P)"
SCRIPT_DIR="$(builtin cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
# Set directory to serve static content from
SUBDIR="${1:-app}"
export APP_ROOT="$SCRIPT_DIR/$SUBDIR"
export STATIC_FOLDER="$APP_ROOT/static"
# Clear out build directory
rm -f "$SCRIPT_DIR"/app/static/build/*.js
rm -f "$SCRIPT_DIR"/app/static/build/*.css
# Check for regular vs test run
if [ "$SUBDIR" = "test" ]; then
if [[ "$SUBDIR" == "test" ]]; then
# Set up static files for testing
rm -rf "$STATIC_FOLDER"
ln -s "$SCRIPT_DIR/app/static" "$STATIC_FOLDER"
pytest -sv
else
mkdir -p "$STATIC_FOLDER"
if [ ! -z "$UNIX_SOCKET" ]; then
python3 -um app \
--unix-socket "$UNIX_SOCKET"
else
python3 -um app \
--host "${ADDRESS:-0.0.0.0}" \
--port "${PORT:-"${EXPOSE_PORT:-5000}"}"
fi
python3 -um app \
--host "${ADDRESS:-0.0.0.0}" \
--port "${PORT:-"${EXPOSE_PORT:-5000}"}"
fi

View File

@ -1,39 +0,0 @@
[metadata]
name = whoogle-search
url = https://github.com/benbusby/whoogle-search
description = Self-hosted, ad-free, privacy-respecting metasearch engine
long_description = file: README.md
long_description_content_type = text/markdown
keywords = search, metasearch, flask, adblock, degoogle, privacy
author = Ben Busby
author_email = contact@benbusby.com
license = MIT
classifiers =
Programming Language :: Python :: 3
License :: OSI Approved :: MIT License
Operating System :: OS Independent
[options]
packages = find:
include_package_data = True
install_requires=
beautifulsoup4
cssutils
cryptography
defusedxml
Flask
Flask-Session
python-dotenv
requests
stem
waitress
[options.extras_require]
test =
pytest
python-dateutil
dev = pycodestyle
[options.entry_points]
console_scripts =
whoogle-search = app.routes:run_app

View File

@ -1,8 +1,29 @@
import os
import setuptools
optional_dev_tag = ''
if os.getenv('DEV_BUILD'):
optional_dev_tag = '.dev' + os.getenv('DEV_BUILD')
long_description = open('README.md', 'r').read()
setuptools.setup(version='0.7.4' + optional_dev_tag)
requirements = list(open('requirements.txt', 'r'))
setuptools.setup(
author='Ben Busby',
author_email='benbusby@protonmail.com',
name='whoogle-search',
version='0.5.0',
include_package_data=True,
install_requires=requirements,
description='Self-hosted, ad-free, privacy-respecting metasearch engine',
long_description=long_description,
long_description_content_type='text/markdown',
url='https://github.com/benbusby/whoogle-search',
entry_points={
'console_scripts': [
'whoogle-search=app.routes:run_app',
]
},
packages=setuptools.find_packages(),
classifiers=[
'Programming Language :: Python :: 3',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
],
)

View File

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

View File

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

View File

@ -1,24 +1,8 @@
from cryptography.fernet import Fernet
from app import app
from app.models.endpoint import Endpoint
from app.utils.session import generate_user_key, valid_user_session
JAPAN_PREFS = 'uG-gGIJwHdqxl6DrS3mnu_511HlQcRpxYlG03Xs-' \
+ '_znXNiJWI9nLOkRLkiiFwIpeUYMTGfUF5-t9fP5DGmzDLEt04DCx703j3nPf' \
+ '29v_RWkU7gXw_44m2oAFIaKGmYlu4Z0bKyu9k5WXfL9Dy6YKKnpcR5CiaFsG' \
+ 'rccNRkAPYm-eYGAFUV8M59f8StsGd_M-gHKGS9fLok7EhwBWjHxBJ2Kv8hsT' \
+ '87zftP2gMJOevTdNnezw2Y5WOx-ZotgeheCW1BYCFcRqatlov21PHp22NGVG' \
+ '8ZuBNAFW0bE99WSdyT7dUIvzeWCLJpbdSsq-3FUUZkxbRdFYlGd8vY1UgVAp' \
+ 'OSie2uAmpgLFXygO-VfNBBZ68Q7gAap2QtzHCiKD5cFYwH3LPgVJ-DoZvJ6k' \
+ 'alt34TaYiJphgiqFKV4SCeVmLWTkr0SF3xakSR78yYJU_d41D2ng-TojA9XZ' \
+ 'uR2ZqjSvPKOWvjimu89YhFOgJxG1Po8Henj5h9OL9VXXvdvlJwBSAKw1E3FV' \
+ '7UHWiglMxPblfxqou1cYckMYkFeIMCD2SBtju68mBiQh2k328XRPTsQ_ocby' \
+ 'cgVKnleGperqbD6crRk3Z9xE5sVCjujn9JNVI-7mqOITMZ0kntq9uJ3R5n25' \
+ 'Vec0TJ0P19nEtvjY0nJIrIjtnBg=='
def test_generate_user_keys():
key = generate_user_key()
assert Fernet(key)
@ -31,18 +15,6 @@ def test_valid_session(client):
assert valid_user_session(session)
def test_valid_translation_keys(client):
valid_lang_keys = [_['value'] for _ in app.config['LANGUAGES']]
en_keys = app.config['TRANSLATIONS']['lang_en'].keys()
for translation_key in app.config['TRANSLATIONS']:
# Ensure the translation is using a valid language value
assert translation_key in valid_lang_keys
# Ensure all translations match the same size/content of the original
# English translation
assert app.config['TRANSLATIONS'][translation_key].keys() == en_keys
def test_query_decryption(client):
# FIXME: Handle decryption errors in search.py and rewrite test
# This previously was used to test swapping decryption keys between
@ -52,27 +24,14 @@ def test_query_decryption(client):
rv = client.get('/')
cookie = rv.headers['Set-Cookie']
rv = client.get(f'/{Endpoint.search}?q=test+1', headers={'Cookie': cookie})
rv = client.get('/search?q=test+1', headers={'Cookie': cookie})
assert rv._status_code == 200
with client.session_transaction() as session:
assert valid_user_session(session)
rv = client.get(f'/{Endpoint.search}?q=test+2', headers={'Cookie': cookie})
rv = client.get('/search?q=test+2', headers={'Cookie': cookie})
assert rv._status_code == 200
with client.session_transaction() as session:
assert valid_user_session(session)
def test_prefs_url(client):
base_url = f'/{Endpoint.search}?q=wikipedia'
rv = client.get(base_url)
assert rv._status_code == 200
assert b'wikipedia.org' in rv.data
assert b'ja.wikipedia.org' not in rv.data
rv = client.get(f'{base_url}&preferences={JAPAN_PREFS}')
assert rv._status_code == 200
assert b'ja.wikipedia.org' in rv.data

View File

@ -1,10 +1,8 @@
from bs4 import BeautifulSoup
from app.filter import Filter
from app.models.config import Config
from app.models.endpoint import Endpoint
from app.utils.session import generate_user_key
from datetime import datetime
from dateutil.parser import ParserError, parse
from dateutil.parser import *
from urllib.parse import urlparse
from test.conftest import demo_config
@ -12,7 +10,7 @@ from test.conftest import demo_config
def get_search_results(data):
secret_key = generate_user_key()
soup = Filter(user_key=secret_key, config=Config(**demo_config)).clean(
soup = Filter(user_key=secret_key).clean(
BeautifulSoup(data, 'html.parser'))
main_divs = soup.find('div', {'id': 'main'})
@ -32,39 +30,27 @@ def get_search_results(data):
def test_get_results(client):
rv = client.get(f'/{Endpoint.search}?q=test')
rv = client.get('/search?q=test')
assert rv._status_code == 200
# Depending on the search, there can be more
# than 10 result divs
results = get_search_results(rv.data)
assert len(results) >= 10
assert len(results) <= 15
assert len(get_search_results(rv.data)) >= 10
assert len(get_search_results(rv.data)) <= 15
def test_post_results(client):
rv = client.post(f'/{Endpoint.search}', data=dict(q='test'))
rv = client.post('/search', data=dict(q='test'))
assert rv._status_code == 200
# Depending on the search, there can be more
# than 10 result divs
results = get_search_results(rv.data)
assert len(results) >= 10
assert len(results) <= 15
def test_translate_search(client):
rv = client.post(f'/{Endpoint.search}', data=dict(q='translate hola'))
assert rv._status_code == 200
# Pretty weak test, but better than nothing
str_data = str(rv.data)
assert 'iframe' in str_data
assert '/auto/en/ hola' in str_data
assert len(get_search_results(rv.data)) >= 10
assert len(get_search_results(rv.data)) <= 15
def test_block_results(client):
rv = client.post(f'/{Endpoint.search}', data=dict(q='pinterest'))
rv = client.post('/search', data=dict(q='pinterest'))
assert rv._status_code == 200
has_pinterest = False
@ -76,27 +62,28 @@ def test_block_results(client):
assert has_pinterest
demo_config['block'] = 'pinterest.com'
rv = client.post(f'/{Endpoint.config}', data=demo_config)
rv = client.post('/config', data=demo_config)
assert rv._status_code == 302
rv = client.post(f'/{Endpoint.search}', data=dict(q='pinterest'))
rv = client.post('/search', data=dict(q='pinterest'))
assert rv._status_code == 200
for link in BeautifulSoup(rv.data, 'html.parser').find_all('a', href=True):
result_site = urlparse(link['href']).netloc
if not result_site:
continue
assert result_site not in 'pinterest.com'
assert 'pinterest.com' not in urlparse(link['href']).netloc
def test_view_my_ip(client):
rv = client.post(f'/{Endpoint.search}', data=dict(q='my ip address'))
assert rv._status_code == 200
# TODO: Unit test the site alt method instead -- the results returned
# 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
# Pretty weak test, but better than nothing
str_data = str(rv.data)
assert 'Your public IP address' in str_data
assert '127.0.0.1' in str_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):
@ -107,7 +94,7 @@ def test_recent_results(client):
}
for time, num_days in times.items():
rv = client.post(f'/{Endpoint.search}', data=dict(q='test :' + time))
rv = client.post('/search', data=dict(q='test :' + time))
result_divs = get_search_results(rv.data)
current_date = datetime.now()
@ -122,23 +109,3 @@ def test_recent_results(client):
assert (current_date - date).days <= (num_days + 5)
except ParserError:
pass
def test_leading_slash_search(client):
# Ensure searches with a leading slash are interpreted
# correctly as queries and not endpoints
q = '/test'
rv = client.get(f'/{Endpoint.search}?q={q}')
assert rv._status_code == 200
soup = Filter(
user_key=generate_user_key(),
config=Config(**demo_config),
query=q
).clean(BeautifulSoup(rv.data, 'html.parser'))
for link in soup.find_all('a', href=True):
if 'start=' not in link['href']:
continue
assert link['href'].startswith(f'{Endpoint.search}')

View File

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

View File

@ -1,39 +1,23 @@
# ----------------------------------
# Rename to "whoogle.env" before use
# ----------------------------------
# You can set Whoogle environment variables here, but must
# modify your deployment to enable these values:
# - Local: Set WHOOGLE_DOTENV=1
# - docker-compose: Uncomment the env_file option
# - docker: Add "--env-file ./whoogle.env" to your build command
#WHOOGLE_ALT_TW=farside.link/nitter
#WHOOGLE_ALT_YT=farside.link/invidious
#WHOOGLE_ALT_IG=farside.link/bibliogram/u
#WHOOGLE_ALT_RD=farside.link/libreddit
#WHOOGLE_ALT_MD=farside.link/scribe
#WHOOGLE_ALT_TL=farside.link/lingva
#WHOOGLE_ALT_IMG=farside.link/rimgo
#WHOOGLE_ALT_WIKI=farside.link/wikiless
#WHOOGLE_ALT_IMDB=farside.link/libremdb
#WHOOGLE_ALT_QUORA=farside.link/quetre
#WHOOGLE_ALT_TW=nitter.net
#WHOOGLE_ALT_YT=invidious.snopyta.org
#WHOOGLE_ALT_IG=bibliogram.art/u
#WHOOGLE_ALT_RD=libredd.it
#WHOOGLE_USER=""
#WHOOGLE_PASS=""
#WHOOGLE_PROXY_USER=""
#WHOOGLE_PROXY_PASS=""
#WHOOGLE_PROXY_TYPE=""
#WHOOGLE_PROXY_LOC=""
#WHOOGLE_CSP=1
#HTTPS_ONLY=1
# The URL prefix to use for the whoogle instance (i.e. "/whoogle")
#WHOOGLE_URL_PREFIX=""
# Restrict results to only those near a particular city
#WHOOGLE_CONFIG_NEAR=denver
# See app/static/settings/countries.json for values
#WHOOGLE_CONFIG_COUNTRY=US
#WHOOGLE_CONFIG_COUNTRY=countryUK
# See app/static/settings/languages.json for values
#WHOOGLE_CONFIG_LANGUAGE=lang_en
@ -47,8 +31,8 @@
# Block websites from search results (comma-separated list)
#WHOOGLE_CONFIG_BLOCK=pinterest.com,whitehouse.gov
# Theme (light, dark, or system)
#WHOOGLE_CONFIG_THEME=system
# Dark mode
#WHOOGLE_CONFIG_DARK=1
# Safe search mode
#WHOOGLE_CONFIG_SAFE=1
@ -63,31 +47,13 @@
#WHOOGLE_CONFIG_NEW_TAB=1
# Enable View Image option
#WHOOGLE_CONFIG_VIEW_IMAGE=1
#WHOOGLE_CONFIG_VIEW_IMAGE=1
# Search using GET requests only (exposes query in logs)
#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
#WHOOGLE_CONFIG_URL=https://<whoogle url>/
# Set custom CSS styling/theming
#WHOOGLE_CONFIG_STYLE=":root { /* LIGHT THEME COLORS */ --whoogle-background: #d8dee9; --whoogle-accent: #2e3440; --whoogle-text: #3B4252; --whoogle-contrast-text: #eceff4; --whoogle-secondary-text: #70757a; --whoogle-result-bg: #fff; --whoogle-result-title: #4c566a; --whoogle-result-url: #81a1c1; --whoogle-result-visited: #a3be8c; /* DARK THEME COLORS */ --whoogle-dark-background: #222; --whoogle-dark-accent: #685e79; --whoogle-dark-text: #fff; --whoogle-dark-contrast-text: #000; --whoogle-dark-secondary-text: #bbb; --whoogle-dark-result-bg: #000; --whoogle-dark-result-title: #1967d2; --whoogle-dark-result-url: #4b11a8; --whoogle-dark-result-visited: #bbbbff; }"
# Enable preferences encryption (requires key)
#WHOOGLE_CONFIG_PREFERENCES_ENCRYPTED=1
# Set Key to encode config in url
#WHOOGLE_CONFIG_PREFERENCES_KEY="NEEDS_TO_BE_MODIFIED"