Compare commits
No commits in common. "c690c1d4217992e7557c261092385b7236af3b78" and "606d2c1dd62bcf938ab7262187949bac79f063fa" have entirely different histories.
c690c1d421
...
606d2c1dd6
BIN
.DS_Store
vendored
|
@ -1,3 +0,0 @@
|
||||||
.git/
|
|
||||||
venv/
|
|
||||||
test/
|
|
9
.github/FUNDING.yml
vendored
|
@ -1,9 +0,0 @@
|
||||||
# These are supported funding model platforms
|
|
||||||
github: benbusby
|
|
||||||
ko_fi: benbusby
|
|
||||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
|
||||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
|
||||||
liberapay: # Replace with a single Liberapay username
|
|
||||||
issuehunt: # Replace with a single IssueHunt username
|
|
||||||
otechie: # Replace with a single Otechie username
|
|
||||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
|
45
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -1,45 +0,0 @@
|
||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a bug report to help fix an issue with Whoogle
|
|
||||||
title: "[BUG] <brief bug description>"
|
|
||||||
labels: bug
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Describe the bug**
|
|
||||||
A clear and concise description of what the bug is.
|
|
||||||
|
|
||||||
**To Reproduce**
|
|
||||||
Steps to reproduce the behavior:
|
|
||||||
1. Go to '...'
|
|
||||||
2. Click on '....'
|
|
||||||
3. Scroll down to '....'
|
|
||||||
4. See error
|
|
||||||
|
|
||||||
**Deployment Method**
|
|
||||||
- [ ] Heroku (one-click deploy)
|
|
||||||
- [ ] Docker
|
|
||||||
- [ ] `run` executable
|
|
||||||
- [ ] pip/pipx
|
|
||||||
- [ ] Other: [describe setup]
|
|
||||||
|
|
||||||
**Version of Whoogle Search**
|
|
||||||
- [ ] Latest build from [source] (i.e. GitHub, Docker Hub, pip, etc)
|
|
||||||
- [ ] Version [version number]
|
|
||||||
- [ ] Not sure
|
|
||||||
|
|
||||||
|
|
||||||
**Desktop (please complete the following information):**
|
|
||||||
- OS: [e.g. iOS]
|
|
||||||
- Browser [e.g. chrome, safari]
|
|
||||||
- Version [e.g. 22]
|
|
||||||
|
|
||||||
**Smartphone (please complete the following information):**
|
|
||||||
- Device: [e.g. iPhone6]
|
|
||||||
- OS: [e.g. iOS8.1]
|
|
||||||
- Browser [e.g. stock browser, safari]
|
|
||||||
- Version [e.g. 22]
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context about the problem here.
|
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
@ -1,20 +0,0 @@
|
||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest a feature that would improve Whoogle
|
|
||||||
title: "[FEATURE] <description of feature>"
|
|
||||||
labels: enhancement
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<!--
|
|
||||||
DO NOT REQUEST UI/THEME/GUI/APPEARANCE IMPROVEMENTS HERE
|
|
||||||
THESE SHOULD GO IN ISSUE #60
|
|
||||||
REQUESTING A NEW FEATURE SHOULD BE STRICTLY RELATED TO NEW FUNCTIONALITY
|
|
||||||
-->
|
|
||||||
|
|
||||||
**Describe the feature you'd like to see added**
|
|
||||||
A short description of the feature, and what it would accomplish.
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context or screenshots about the feature request here.
|
|
38
.github/ISSUE_TEMPLATE/new-theme.md
vendored
|
@ -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: #______;
|
|
||||||
}
|
|
||||||
```
|
|
10
.github/ISSUE_TEMPLATE/question.md
vendored
|
@ -1,10 +0,0 @@
|
||||||
---
|
|
||||||
name: Question
|
|
||||||
about: Ask a (simple) question about Whoogle
|
|
||||||
title: "[QUESTION] <question here>"
|
|
||||||
labels: question
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Type out your question here. Please make sure that this is a topic that isn't already covered in the README.
|
|
59
.github/workflows/buildx.yml
vendored
|
@ -1,59 +0,0 @@
|
||||||
name: buildx
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows: ["docker_main"]
|
|
||||||
branches: [main]
|
|
||||||
types:
|
|
||||||
- completed
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- '*'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
on-success:
|
|
||||||
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
|
|
||||||
id: buildx
|
|
||||||
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: 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/arm64 .
|
|
||||||
docker buildx build --push \
|
|
||||||
--tag ghcr.io/benbusby/whoogle-search:latest \
|
|
||||||
--platform linux/amd64,linux/arm64 .
|
|
||||||
- name: build and push tag
|
|
||||||
if: startsWith(github.ref, 'refs/tags')
|
|
||||||
run: |
|
|
||||||
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
|
||||||
docker buildx ls
|
|
||||||
docker buildx build --push \
|
|
||||||
--tag benbusby/whoogle-search:${GITHUB_REF#refs/*/v}\
|
|
||||||
--platform linux/amd64,linux/arm/v7,linux/arm64 .
|
|
||||||
docker buildx build --push \
|
|
||||||
--tag ghcr.io/benbusby/whoogle-search:${GITHUB_REF#refs/*/v}\
|
|
||||||
--platform linux/amd64,linux/arm/v7,linux/arm64 .
|
|
28
.github/workflows/docker_main.yml
vendored
|
@ -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
|
|
26
.github/workflows/docker_tests.yml
vendored
|
@ -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
|
|
67
.github/workflows/pypi.yml
vendored
|
@ -1,67 +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@v4
|
|
||||||
- name: Set up Python 3.9
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: 3.9
|
|
||||||
- name: Install pypa/build
|
|
||||||
run: >-
|
|
||||||
python -m
|
|
||||||
pip install
|
|
||||||
build
|
|
||||||
setuptools
|
|
||||||
--user
|
|
||||||
- name: Set dev timestamp
|
|
||||||
run: echo "DEV_BUILD=$(date +%s)" >> $GITHUB_ENV
|
|
||||||
- name: Build binary wheel and source tarball
|
|
||||||
run: >-
|
|
||||||
python -m
|
|
||||||
build
|
|
||||||
--sdist
|
|
||||||
--wheel
|
|
||||||
--outdir dist/
|
|
||||||
.
|
|
||||||
- name: Publish distribution to TestPyPI
|
|
||||||
uses: pypa/gh-action-pypi-publish@master
|
|
||||||
with:
|
|
||||||
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
|
|
||||||
repository_url: https://test.pypi.org/legacy/
|
|
||||||
publish:
|
|
||||||
name: Build and publish to PyPI
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Set up Python 3.9
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: 3.9
|
|
||||||
- name: Install pypa/build
|
|
||||||
run: >-
|
|
||||||
python -m
|
|
||||||
pip install
|
|
||||||
build
|
|
||||||
--user
|
|
||||||
- name: Build binary wheel and source tarball
|
|
||||||
run: >-
|
|
||||||
python -m
|
|
||||||
build
|
|
||||||
--sdist
|
|
||||||
--wheel
|
|
||||||
--outdir dist/
|
|
||||||
.
|
|
||||||
- name: Publish distribution to PyPI
|
|
||||||
if: startsWith(github.ref, 'refs/tags')
|
|
||||||
uses: pypa/gh-action-pypi-publish@master
|
|
||||||
with:
|
|
||||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
19
.github/workflows/scan.yml
vendored
|
@ -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
|
|
17
.github/workflows/tests.yml
vendored
|
@ -1,17 +0,0 @@
|
||||||
name: tests
|
|
||||||
|
|
||||||
on: [push, pull_request]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: '3.x'
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pip install --upgrade pip && pip install -r requirements.txt
|
|
||||||
- name: Run tests
|
|
||||||
run: ./run test
|
|
27
.gitignore
vendored
|
@ -1,27 +0,0 @@
|
||||||
venv/
|
|
||||||
.venv/
|
|
||||||
.idea/
|
|
||||||
__pycache__/
|
|
||||||
*.pyc
|
|
||||||
*.pem
|
|
||||||
*.conf
|
|
||||||
*.key
|
|
||||||
config.json
|
|
||||||
test/static
|
|
||||||
flask_session/
|
|
||||||
app/static/config
|
|
||||||
app/static/custom_config
|
|
||||||
app/static/bangs/*
|
|
||||||
!app/static/bangs/00-whoogle.json
|
|
||||||
|
|
||||||
# pip stuff
|
|
||||||
/build/
|
|
||||||
dist/
|
|
||||||
*.egg-info/
|
|
||||||
|
|
||||||
# env
|
|
||||||
whoogle.env
|
|
||||||
|
|
||||||
# vim
|
|
||||||
*~
|
|
||||||
*.swp
|
|
1
.replit
|
@ -1 +0,0 @@
|
||||||
entrypoint = "misc/replit.py"
|
|
103
Dockerfile
|
@ -1,103 +0,0 @@
|
||||||
FROM python:3.12.6-alpine3.20 AS builder
|
|
||||||
|
|
||||||
RUN apk --update add \
|
|
||||||
build-base \
|
|
||||||
libxml2-dev \
|
|
||||||
libxslt-dev \
|
|
||||||
openssl-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.12.6-alpine3.20
|
|
||||||
|
|
||||||
RUN apk add --update --no-cache tor curl openrc libstdc++
|
|
||||||
# git go //for obfs4proxy
|
|
||||||
# libcurl4-openssl-dev
|
|
||||||
|
|
||||||
RUN apk -U upgrade
|
|
||||||
|
|
||||||
# uncomment to build obfs4proxy
|
|
||||||
# RUN git clone https://gitlab.com/yawning/obfs4.git
|
|
||||||
# WORKDIR /obfs4
|
|
||||||
# RUN go build -o obfs4proxy/obfs4proxy ./obfs4proxy
|
|
||||||
# RUN cp ./obfs4proxy/obfs4proxy /usr/bin/obfs4proxy
|
|
||||||
|
|
||||||
ARG DOCKER_USER=whoogle
|
|
||||||
ARG DOCKER_USERID=927
|
|
||||||
ARG config_dir=/config
|
|
||||||
RUN mkdir -p $config_dir
|
|
||||||
RUN chmod a+w $config_dir
|
|
||||||
VOLUME $config_dir
|
|
||||||
|
|
||||||
ARG url_prefix=''
|
|
||||||
ARG 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 reddit_alt='farside.link/libreddit'
|
|
||||||
ARG medium_alt='farside.link/scribe'
|
|
||||||
ARG translate_alt='farside.link/lingva'
|
|
||||||
ARG imgur_alt='farside.link/rimgo'
|
|
||||||
ARG wikipedia_alt='farside.link/wikiless'
|
|
||||||
ARG imdb_alt='farside.link/libremdb'
|
|
||||||
ARG quora_alt='farside.link/quetre'
|
|
||||||
ARG so_alt='farside.link/anonymousoverflow'
|
|
||||||
|
|
||||||
ENV CONFIG_VOLUME=$config_dir \
|
|
||||||
WHOOGLE_URL_PREFIX=$url_prefix \
|
|
||||||
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_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 \
|
|
||||||
WHOOGLE_ALT_SO=$so_alt
|
|
||||||
|
|
||||||
WORKDIR /whoogle
|
|
||||||
|
|
||||||
COPY --from=builder /install /usr/local
|
|
||||||
COPY misc/tor/torrc /etc/tor/torrc
|
|
||||||
COPY misc/tor/start-tor.sh misc/tor/start-tor.sh
|
|
||||||
COPY app/ app/
|
|
||||||
COPY run whoogle.env* ./
|
|
||||||
|
|
||||||
# Create user/group to run as
|
|
||||||
RUN adduser -D -g $DOCKER_USERID -u $DOCKER_USERID $DOCKER_USER
|
|
||||||
|
|
||||||
# Fix ownership / permissions
|
|
||||||
RUN chown -R ${DOCKER_USER}:${DOCKER_USER} /whoogle /var/lib/tor
|
|
||||||
|
|
||||||
# Allow writing symlinks to build dir
|
|
||||||
RUN chown $DOCKER_USERID:$DOCKER_USERID app/static/build
|
|
||||||
|
|
||||||
USER $DOCKER_USER:$DOCKER_USER
|
|
||||||
|
|
||||||
EXPOSE $EXPOSE_PORT
|
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=5s \
|
|
||||||
CMD curl -f http://localhost:${EXPOSE_PORT}/healthz || exit 1
|
|
||||||
|
|
||||||
CMD misc/tor/start-tor.sh & ./run
|
|
21
LICENSE
|
@ -1,21 +0,0 @@
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2020 Ben Busby
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
|
@ -1,6 +0,0 @@
|
||||||
graft app/static
|
|
||||||
graft app/templates
|
|
||||||
graft app/misc
|
|
||||||
include requirements.txt
|
|
||||||
recursive-include test
|
|
||||||
global-exclude *.pyc
|
|
733
README.md
|
@ -1,4 +1,731 @@
|
||||||
![RP's Search](docs/banner.png)
|
![Whoogle Search](docs/banner.png)
|
||||||
|
|
||||||
2024.11.06
|
[![Latest Release](https://img.shields.io/github/v/release/benbusby/whoogle-search)](https://github.com/benbusby/shoogle/releases)
|
||||||
- Frissítve a legújabb 0.9.1-es verzióra
|
[![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)
|
||||||
|
[![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
|
||||||
|
1. [Features](#features)
|
||||||
|
3. [Install/Deploy Options](#install)
|
||||||
|
1. [Heroku Quick Deploy](#heroku-quick-deploy)
|
||||||
|
1. [Render.com](#render)
|
||||||
|
1. [Repl.it](#replit)
|
||||||
|
1. [Fly.io](#flyio)
|
||||||
|
1. [Koyeb](#koyeb)
|
||||||
|
1. [pipx](#pipx)
|
||||||
|
1. [pip](#pip)
|
||||||
|
1. [Manual](#manual)
|
||||||
|
1. [Docker](#manual-docker)
|
||||||
|
1. [Arch/AUR](#arch-linux--arch-based-distributions)
|
||||||
|
1. [Helm/Kubernetes](#helm-chart-for-kubernetes)
|
||||||
|
4. [Environment Variables and Configuration](#environment-variables)
|
||||||
|
5. [Usage](#usage)
|
||||||
|
6. [Extra Steps](#extra-steps)
|
||||||
|
1. [Set Primary Search Engine](#set-whoogle-as-your-primary-search-engine)
|
||||||
|
2. [Custom Redirecting](#custom-redirecting)
|
||||||
|
2. [Custom Bangs](#custom-bangs)
|
||||||
|
3. [Prevent Downtime (Heroku Only)](#prevent-downtime-heroku-only)
|
||||||
|
4. [Manual HTTPS Enforcement](#https-enforcement)
|
||||||
|
5. [Using with Firefox Containers](#using-with-firefox-containers)
|
||||||
|
6. [Reverse Proxying](#reverse-proxying)
|
||||||
|
1. [Nginx](#nginx)
|
||||||
|
7. [Contributing](#contributing)
|
||||||
|
8. [FAQ](#faq)
|
||||||
|
9. [Public Instances](#public-instances)
|
||||||
|
10. [Screenshots](#screenshots)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- No ads or sponsored content
|
||||||
|
- 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
|
||||||
|
- Tor and HTTP/SOCKS proxy support
|
||||||
|
- 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))
|
||||||
|
- Randomly generated User Agent
|
||||||
|
- Easy to install/deploy
|
||||||
|
- DDG-style bang (i.e. `!<tag> <query>`) searches
|
||||||
|
- User-defined [custom bangs](#custom-bangs)
|
||||||
|
- Optional location-based searching (i.e. results near \<city\>)
|
||||||
|
- Optional NoJS mode to view search results in a separate window with JavaScript blocked
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
## Install
|
||||||
|
There are a few different ways to begin using the app, depending on your preferences:
|
||||||
|
|
||||||
|
___
|
||||||
|
|
||||||
|
### [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)
|
||||||
|
|
||||||
|
Provides:
|
||||||
|
- Easy Deployment of App
|
||||||
|
- A HTTPS url (https://\<your app name\>.herokuapp.com)
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Requires a **PAID** Heroku Account.
|
||||||
|
- Sometimes has issues with auto-redirecting to `https`. Make sure to navigate to the `https` version of your app before adding as a default search engine.
|
||||||
|
|
||||||
|
___
|
||||||
|
|
||||||
|
### [Render](https://render.com)
|
||||||
|
|
||||||
|
Create an account on [render.com](https://render.com) and import the Whoogle repo with the following settings:
|
||||||
|
|
||||||
|
- Runtime: `Python 3`
|
||||||
|
- Build Command: `pip install -r requirements.txt`
|
||||||
|
- Run Command: `./run`
|
||||||
|
|
||||||
|
___
|
||||||
|
|
||||||
|
### [Repl.it](https://repl.it)
|
||||||
|
[![Run on Repl.it](https://repl.it/badge/github/benbusby/whoogle-search)](https://repl.it/github/benbusby/whoogle-search)
|
||||||
|
|
||||||
|
*Note: Requires a (free) Replit account*
|
||||||
|
|
||||||
|
Provides:
|
||||||
|
- Free deployment of app
|
||||||
|
- Free HTTPS url (https://\<app name\>.\<username\>\.repl\.co)
|
||||||
|
- Supports custom domains
|
||||||
|
- Downtime after periods of inactivity ([solution](https://repl.it/talk/learn/How-to-use-and-setup-UptimeRobot/9003)\)
|
||||||
|
|
||||||
|
___
|
||||||
|
|
||||||
|
### [Fly.io](https://fly.io)
|
||||||
|
|
||||||
|
You will need a [Fly.io](https://fly.io) account to deploy Whoogle.
|
||||||
|
|
||||||
|
#### Install the CLI: https://fly.io/docs/hands-on/installing/
|
||||||
|
|
||||||
|
#### Deploy the app
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flyctl auth login
|
||||||
|
flyctl launch --image benbusby/whoogle-search:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
The first deploy won't succeed because the default `internal_port` is wrong.
|
||||||
|
To fix this, open the generated `fly.toml` file, set `services.internal_port` to `5000` and run `flyctl launch` again.
|
||||||
|
|
||||||
|
Your app is now available at `https://<app-name>.fly.dev`.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Requires a [**PAID**](https://fly.io/docs/about/pricing/#free-allowances) Fly.io Account.
|
||||||
|
|
||||||
|
___
|
||||||
|
|
||||||
|
### [Koyeb](https://www.koyeb.com)
|
||||||
|
|
||||||
|
Use one of the following guides to install Whoogle on Koyeb:
|
||||||
|
|
||||||
|
1. Using GitHub: https://www.koyeb.com/docs/quickstart/deploy-with-git
|
||||||
|
2. Using Docker: https://www.koyeb.com/docs/quickstart/deploy-a-docker-application
|
||||||
|
|
||||||
|
___
|
||||||
|
|
||||||
|
### [pipx](https://github.com/pipxproject/pipx#install-pipx)
|
||||||
|
Persistent install:
|
||||||
|
|
||||||
|
`pipx install https://github.com/benbusby/whoogle-search/archive/refs/heads/main.zip`
|
||||||
|
|
||||||
|
Sandboxed temporary instance:
|
||||||
|
|
||||||
|
`pipx run --spec git+https://github.com/benbusby/whoogle-search.git whoogle-search`
|
||||||
|
|
||||||
|
___
|
||||||
|
|
||||||
|
### pip
|
||||||
|
`pip install whoogle-search`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ whoogle-search --help
|
||||||
|
usage: whoogle-search [-h] [--port <port number>] [--host <ip address>] [--debug] [--https-only] [--userpass <username:password>]
|
||||||
|
[--proxyauth <username:password>] [--proxytype <socks4|socks5|http>] [--proxyloc <location:port>]
|
||||||
|
|
||||||
|
Whoogle Search console runner
|
||||||
|
|
||||||
|
optional arguments:
|
||||||
|
-h, --help Show this help message and exit
|
||||||
|
--port <port number> Specifies a port to run on (default 5000)
|
||||||
|
--host <ip address> Specifies the host address to use (default 127.0.0.1)
|
||||||
|
--debug Activates debug mode for the server (default False)
|
||||||
|
--https-only Enforces HTTPS redirects for all requests
|
||||||
|
--userpass <username:password>
|
||||||
|
Sets a username/password basic auth combo (default None)
|
||||||
|
--proxyauth <username:password>
|
||||||
|
Sets a username/password for a HTTP/SOCKS proxy (default None)
|
||||||
|
--proxytype <socks4|socks5|http>
|
||||||
|
Sets a proxy type for all connections (default None)
|
||||||
|
--proxyloc <location:port>
|
||||||
|
Sets a proxy location for all connections (default None)
|
||||||
|
```
|
||||||
|
See the [available environment variables](#environment-variables) for additional configuration.
|
||||||
|
|
||||||
|
___
|
||||||
|
|
||||||
|
### Manual
|
||||||
|
|
||||||
|
*Note: `Content-Security-Policy` headers can be sent by Whoogle if you set `WHOOGLE_CSP`.*
|
||||||
|
|
||||||
|
#### Dependencies
|
||||||
|
- [Python3](https://www.python.org/downloads/)
|
||||||
|
- `libcurl4-openssl-dev` and `libssl-dev`
|
||||||
|
- macOS: `brew install openssl curl-openssl`
|
||||||
|
- Ubuntu: `sudo apt-get install -y libcurl4-openssl-dev libssl-dev`
|
||||||
|
- Arch: `pacman -S curl openssl`
|
||||||
|
|
||||||
|
#### Install
|
||||||
|
|
||||||
|
Clone the repo and run the following commands to start the app in a local-only environment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/benbusby/whoogle-search.git
|
||||||
|
cd whoogle-search
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
./run
|
||||||
|
```
|
||||||
|
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:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=Whoogle
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
# Basic auth configuration, uncomment to enable
|
||||||
|
#Environment=WHOOGLE_USER=<username>
|
||||||
|
#Environment=WHOOGLE_PASS=<password>
|
||||||
|
# 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_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_RD=farside.link/libreddit
|
||||||
|
#Environment=WHOOGLE_ALT_MD=farside.link/scribe
|
||||||
|
#Environment=WHOOGLE_ALT_TL=farside.link/lingva
|
||||||
|
#Environment=WHOOGLE_ALT_IMG=farside.link/rimgo
|
||||||
|
#Environment=WHOOGLE_ALT_WIKI=farside.link/wikiless
|
||||||
|
#Environment=WHOOGLE_ALT_IMDB=farside.link/libremdb
|
||||||
|
#Environment=WHOOGLE_ALT_QUORA=farside.link/quetre
|
||||||
|
#Environment=WHOOGLE_ALT_SO=farside.link/anonymousoverflow
|
||||||
|
# Load values from dotenv only
|
||||||
|
#Environment=WHOOGLE_DOTENV=1
|
||||||
|
# specify dotenv location if not in default location
|
||||||
|
#Environment=WHOOGLE_DOTENV_PATH=<path/to>/whoogle.env
|
||||||
|
Type=simple
|
||||||
|
User=<username>
|
||||||
|
# If installed as a package, add:
|
||||||
|
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
|
||||||
|
WorkingDirectory=<whoogle_repo_dir>
|
||||||
|
ExecReload=/bin/kill -HUP $MAINPID
|
||||||
|
Restart=always
|
||||||
|
RestartSec=3
|
||||||
|
SyslogIdentifier=whoogle
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
Then,
|
||||||
|
```
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
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`
|
||||||
|
|
||||||
|
___
|
||||||
|
|
||||||
|
### 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).
|
||||||
|
2. Clone and deploy the docker app using a method below:
|
||||||
|
|
||||||
|
#### Docker CLI
|
||||||
|
|
||||||
|
Through Docker Hub:
|
||||||
|
```bash
|
||||||
|
docker pull benbusby/whoogle-search
|
||||||
|
docker run --publish 5000:5000 --detach --name whoogle-search benbusby/whoogle-search:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
or with docker-compose:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/benbusby/whoogle-search.git
|
||||||
|
cd whoogle-search
|
||||||
|
docker-compose up
|
||||||
|
```
|
||||||
|
|
||||||
|
or by building yourself:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/benbusby/whoogle-search.git
|
||||||
|
cd whoogle-search
|
||||||
|
docker build --tag whoogle-search:1.0 .
|
||||||
|
docker run --publish 5000:5000 --detach --name whoogle-search whoogle-search:1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Optionally, you can also enable some of the following environment variables to further customize your instance:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --publish 5000:5000 --detach --name whoogle-search \
|
||||||
|
-e WHOOGLE_USER=username \
|
||||||
|
-e WHOOGLE_PASS=password \
|
||||||
|
-e WHOOGLE_PROXY_USER=username \
|
||||||
|
-e WHOOGLE_PROXY_PASS=password \
|
||||||
|
-e WHOOGLE_PROXY_TYPE=socks5 \
|
||||||
|
-e WHOOGLE_PROXY_LOC=ip \
|
||||||
|
whoogle-search:1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
And kill with: `docker rm --force whoogle-search`
|
||||||
|
|
||||||
|
#### Using [Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli)
|
||||||
|
```bash
|
||||||
|
heroku login
|
||||||
|
heroku container:login
|
||||||
|
git clone https://github.com/benbusby/whoogle-search.git
|
||||||
|
cd whoogle-search
|
||||||
|
heroku create
|
||||||
|
heroku container:push web
|
||||||
|
heroku container:release web
|
||||||
|
heroku open
|
||||||
|
```
|
||||||
|
|
||||||
|
This series of commands can take a while, but once you run it once, you shouldn't have to run it again. The final command, `heroku open` will launch a tab in your web browser, where you can test out Whoogle and even [set it as your primary search engine](https://github.com/benbusby/whoogle#set-whoogle-as-your-primary-search-engine).
|
||||||
|
You may also edit environment variables from your app’s Settings tab in the Heroku Dashboard.
|
||||||
|
|
||||||
|
___
|
||||||
|
|
||||||
|
### Arch Linux & Arch-based Distributions
|
||||||
|
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.
|
||||||
|
|
||||||
|
Depending on your preferences, you can also deploy the app yourself on your own infrastructure. This route would require a few extra steps:
|
||||||
|
- A server (I personally recommend [Digital Ocean](https://www.digitalocean.com/pricing/) or [Linode](https://www.linode.com/pricing/), their cheapest tiers will work fine)
|
||||||
|
- Your own URL (I suppose this is optional, but recommended)
|
||||||
|
- SSL certificates (free through [Let's Encrypt](https://letsencrypt.org/getting-started/))
|
||||||
|
- A bit more experience or willingness to work through issues
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
There are a few optional environment variables available for customizing a Whoogle instance. These can be set manually, or copied into `whoogle.env` and enabled for your preferred deployment method:
|
||||||
|
|
||||||
|
- Local runs: Set `WHOOGLE_DOTENV=1` before running
|
||||||
|
- 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_DOTENV_PATH | The path to `whoogle.env` if not in default location |
|
||||||
|
| WHOOGLE_USER | The username for basic auth. WHOOGLE_PASS must also be set if used. |
|
||||||
|
| WHOOGLE_PASS | The password for basic auth. WHOOGLE_USER must also be set if used. |
|
||||||
|
| WHOOGLE_PROXY_USER | The username of the proxy server. |
|
||||||
|
| WHOOGLE_PROXY_PASS | The password of the proxy server. |
|
||||||
|
| WHOOGLE_PROXY_TYPE | The type of the proxy server. Can be "socks5", "socks4", or "http". |
|
||||||
|
| WHOOGLE_PROXY_LOC | The location of the proxy server (host or ip). |
|
||||||
|
| WHOOGLE_USER_AGENT | The desktop user agent to use. Defaults to a randomly generated one. |
|
||||||
|
| WHOOGLE_USER_AGENT_MOBILE | The mobile user agent to use. Defaults to a randomly generated one. |
|
||||||
|
| WHOOGLE_USE_CLIENT_USER_AGENT | Enable to use your own user agent for all requests. Defaults to false. |
|
||||||
|
| WHOOGLE_REDIRECTS | Specify sites that should be redirected elsewhere. See [custom redirecting](#custom-redirecting). |
|
||||||
|
| EXPOSE_PORT | The port where Whoogle will be exposed. |
|
||||||
|
| HTTPS_ONLY | Enforce HTTPS. (See [here](https://github.com/benbusby/whoogle-search#https-enforcement)) |
|
||||||
|
| WHOOGLE_ALT_TW | The twitter.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
|
||||||
|
| WHOOGLE_ALT_YT | The youtube.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
|
||||||
|
| WHOOGLE_ALT_RD | The reddit.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
|
||||||
|
| WHOOGLE_ALT_TL | The Google Translate alternative to use. This is used for all "translate ____" searches. Set to "" to disable. |
|
||||||
|
| WHOOGLE_ALT_MD | The medium.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
|
||||||
|
| WHOOGLE_ALT_IMG | The imgur.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
|
||||||
|
| WHOOGLE_ALT_WIKI | The wikipedia.org alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
|
||||||
|
| WHOOGLE_ALT_IMDB | The imdb.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
|
||||||
|
| WHOOGLE_ALT_QUORA | The quora.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
|
||||||
|
| WHOOGLE_ALT_SO | The stackoverflow.com alternative to use when site alternatives are enabled in the config. Set to "" to disable. |
|
||||||
|
| WHOOGLE_AUTOCOMPLETE | Controls visibility of autocomplete/search suggestions. Default on -- use '0' to disable. |
|
||||||
|
| WHOOGLE_MINIMAL | Remove everything except basic result cards from all search queries. |
|
||||||
|
| WHOOGLE_CSP | Sets a default set of 'Content-Security-Policy' headers |
|
||||||
|
| WHOOGLE_RESULTS_PER_PAGE | Set the number of results per page |
|
||||||
|
| WHOOGLE_TOR_SERVICE | Enable/disable the Tor service on startup. Default on -- use '0' to disable. |
|
||||||
|
| WHOOGLE_TOR_USE_PASS | Use password authentication for tor control port. |
|
||||||
|
| WHOOGLE_TOR_CONF | The absolute path to the config file containing the password for the tor control port. Default: ./misc/tor/control.conf WHOOGLE_TOR_PASS must be 1 for this to work.|
|
||||||
|
| WHOOGLE_SHOW_FAVICONS | Show/hide favicons next to search result URLs. Default on. |
|
||||||
|
| WHOOGLE_UPDATE_CHECK | Enable/disable the automatic daily check for new versions of Whoogle. Default on. |
|
||||||
|
| WHOOGLE_FALLBACK_ENGINE_URL | Set a fallback Search Engine URL when there is internal server error or instance is rate-limited. Search query is appended to the end of the URL (eg. https://duckduckgo.com/?k1=-1&q=). |
|
||||||
|
|
||||||
|
### Config Environment Variables
|
||||||
|
These environment variables allow setting default config values, but can be overwritten manually by using the home page config menu. These allow a shortcut for destroying/rebuilding an instance to the same config state every time.
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
| ------------------------------------ | --------------------------------------------------------------- |
|
||||||
|
| WHOOGLE_CONFIG_DISABLE | Hide config from UI and disallow changes to config by client |
|
||||||
|
| WHOOGLE_CONFIG_COUNTRY | Filter results by hosting country |
|
||||||
|
| WHOOGLE_CONFIG_LANGUAGE | Set interface language |
|
||||||
|
| WHOOGLE_CONFIG_SEARCH_LANGUAGE | Set search result language |
|
||||||
|
| WHOOGLE_CONFIG_BLOCK | Block websites from search results (use comma-separated list) |
|
||||||
|
| WHOOGLE_CONFIG_BLOCK_TITLE | Block search result with a REGEX filter on title |
|
||||||
|
| WHOOGLE_CONFIG_BLOCK_URL | Block search result with a REGEX filter on URL |
|
||||||
|
| WHOOGLE_CONFIG_THEME | Set theme mode (light, dark, or system) |
|
||||||
|
| WHOOGLE_CONFIG_SAFE | Enable safe searches |
|
||||||
|
| WHOOGLE_CONFIG_ALTS | Use social media site alternatives (nitter, invidious, etc) |
|
||||||
|
| WHOOGLE_CONFIG_NEAR | Restrict results to only those near a particular city |
|
||||||
|
| WHOOGLE_CONFIG_TOR | Use Tor routing (if available) |
|
||||||
|
| WHOOGLE_CONFIG_NEW_TAB | Always open results in new tab |
|
||||||
|
| WHOOGLE_CONFIG_VIEW_IMAGE | Enable View Image option |
|
||||||
|
| WHOOGLE_CONFIG_GET_ONLY | Search using GET requests only |
|
||||||
|
| WHOOGLE_CONFIG_URL | The root url of the instance (`https://<your url>/`) |
|
||||||
|
| WHOOGLE_CONFIG_STYLE | The custom CSS to use for styling (should be single line) |
|
||||||
|
| WHOOGLE_CONFIG_PREFERENCES_ENCRYPTED | Encrypt preferences token, requires preferences key |
|
||||||
|
| WHOOGLE_CONFIG_PREFERENCES_KEY | Key to encrypt preferences in URL (REQUIRED to show url) |
|
||||||
|
| WHOOGLE_CONFIG_ANON_VIEW | Include the "anonymous view" option for each search result |
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
Same as most search engines, with the exception of filtering by time range.
|
||||||
|
|
||||||
|
To filter by a range of time, append ":past <time>" to the end of your search, where <time> can be `hour`, `day`, `month`, or `year`. Example: `coronavirus updates :past hour`
|
||||||
|
|
||||||
|
## Extra Steps
|
||||||
|
|
||||||
|
### Set Whoogle as your primary search engine
|
||||||
|
*Note: If you're using a reverse proxy to run Whoogle Search, make sure the "Root URL" config option on the home page is set to your URL before going through these steps.*
|
||||||
|
|
||||||
|
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.
|
||||||
|
- 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"
|
||||||
|
- URL: `http[s]://\<your whoogle url\>/search?q=%s`
|
||||||
|
- Firefox (Android)
|
||||||
|
- Version <79.0.0
|
||||||
|
- Navigate to your app's url
|
||||||
|
- Long-press on the search text field
|
||||||
|
- Click the "Add Search Engine" menu item
|
||||||
|
- Select a name and click ok
|
||||||
|
- Click the 3 dot menu in the top right
|
||||||
|
- Navigate to the settings menu and select the "Search" sub-menu
|
||||||
|
- Select Whoogle and press "Set as default"
|
||||||
|
- Version >=79.0.0
|
||||||
|
- Click the 3 dot menu in the top right
|
||||||
|
- Navigate to the settings menu and select the "Search" sub-menu
|
||||||
|
- Click "Add search engine"
|
||||||
|
- Select the 'Other' radio button
|
||||||
|
- Name: "Whoogle"
|
||||||
|
- Search string to use: `https://\<your whoogle url\>/search?q=%s`
|
||||||
|
- [Alfred](https://www.alfredapp.com/) (Mac OS X)
|
||||||
|
1. Go to `Alfred Preferences` > `Features` > `Web Search` and click `Add Custom Search`. Then configure these settings
|
||||||
|
- Search URL: `https://\<your whoogle url\>/search?q={query}`
|
||||||
|
- Title: `Whoogle for '{query}'` (or whatever you want)
|
||||||
|
- Keyword: `whoogle`
|
||||||
|
|
||||||
|
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.
|
||||||
|
- Manual
|
||||||
|
- Under search engines > manage search engines > add, manually enter your Whoogle instance details with a `<whoogle url>/search?q=%s` formatted search URL.
|
||||||
|
|
||||||
|
### Custom Redirecting
|
||||||
|
You can set custom site redirects using the `WHOOGLE_REDIRECTS` environment
|
||||||
|
variable. A lot of sites, such as Twitter, Reddit, etc, have built-in redirects
|
||||||
|
to [Farside links](https://sr.ht/~benbusby/farside), but you may want to define
|
||||||
|
your own.
|
||||||
|
|
||||||
|
To do this, you can use the following syntax:
|
||||||
|
|
||||||
|
```
|
||||||
|
WHOOGLE_REDIRECTS="<parent_domain>:<new_domain>"
|
||||||
|
```
|
||||||
|
|
||||||
|
For example, if you want to redirect from "badsite.com" to "goodsite.com":
|
||||||
|
|
||||||
|
```
|
||||||
|
WHOOGLE_REDIRECTS="badsite.com:goodsite.com"
|
||||||
|
```
|
||||||
|
|
||||||
|
This can be used for multiple sites as well, with comma separation:
|
||||||
|
|
||||||
|
```
|
||||||
|
WHOOGLE_REDIRECTS="badA.com:goodA.com,badB.com:goodB.com"
|
||||||
|
```
|
||||||
|
|
||||||
|
NOTE: Do not include "http(s)://" when defining your redirect.
|
||||||
|
|
||||||
|
### Custom Bangs
|
||||||
|
You can create your own custom bangs. By default, bangs are stored in
|
||||||
|
`app/static/bangs`. See [`00-whoogle.json`](https://github.com/benbusby/whoogle-search/blob/main/app/static/bangs/00-whoogle.json)
|
||||||
|
for an example. These are parsed in alphabetical order with later files
|
||||||
|
overriding bangs set in earlier files, with the exception that DDG bangs
|
||||||
|
(downloaded to `app/static/bangs/bangs.json`) are always parsed first. Thus,
|
||||||
|
any custom bangs will always override the DDG ones.
|
||||||
|
|
||||||
|
### Prevent Downtime (Heroku only)
|
||||||
|
Part of the deal with Heroku's free tier is that you're allocated 550 hours/month (meaning it can't stay active 24/7), and the app is temporarily shut down after 30 minutes of inactivity. Once it becomes inactive, any Whoogle searches will still work, but it'll take an extra 10-15 seconds for the app to come back online before displaying the result, which can be frustrating if you're in a hurry.
|
||||||
|
|
||||||
|
A good solution for this is to set up a simple cronjob on any device at your home that is consistently powered on and connected to the internet (in my case, a PiHole worked perfectly). All the device needs to do is fetch app content on a consistent basis to keep the app alive in whatever ~17 hour window you want it on (17 hrs * 31 days = 527, meaning you'd still have 23 leftover hours each month if you searched outside of your target window).
|
||||||
|
|
||||||
|
For instance, adding `*/20 7-23 * * * curl https://<your heroku app name>.herokuapp.com > /home/<username>/whoogle-refresh` will fetch the home page of the app every 20 minutes between 7am and midnight, allowing for downtime from midnight to 7am. And again, this wouldn't be a hard limit - you'd still have plenty of remaining hours of uptime each month in case you were searching after this window has closed.
|
||||||
|
|
||||||
|
Since the instance is destroyed and rebuilt after inactivity, config settings will be reset once the app enters downtime. If you have configuration settings active that you'd like to keep between periods of downtime (like dark mode for example), you could instead add `*/20 7-23 * * * curl -d "dark=1" -X POST https://<your heroku app name>.herokuapp.com/config > /home/<username>/whoogle-refresh` to keep these settings more or less permanent, and still keep the app from entering downtime when you're using it.
|
||||||
|
|
||||||
|
### HTTPS Enforcement
|
||||||
|
Only needed if your setup requires Flask to redirect to HTTPS on its own -- generally this is something that doesn't need to be handled by Whoogle Search.
|
||||||
|
|
||||||
|
Note: You should have your own domain name and [an https certificate](https://letsencrypt.org/getting-started/) in order for this to work properly.
|
||||||
|
|
||||||
|
- Heroku: Ensure that the `Root URL` configuration on the home page begins with `https://` and not `http://`
|
||||||
|
- Docker build: Add `--build-arg use_https=1` to your run command
|
||||||
|
- Docker image: Set the environment variable HTTPS_ONLY=1
|
||||||
|
- 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_set_header X-Forwarded-Host $http_host;
|
||||||
|
proxy_pass http://localhost:5000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can then add SSL support using LetsEncrypt by following a guide such as [this one](https://www.nginx.com/blog/using-free-ssltls-certificates-from-lets-encrypt-with-nginx/).
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Under the hood, Whoogle is a basic Flask app with the following structure:
|
||||||
|
|
||||||
|
- `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)?**
|
||||||
|
|
||||||
|
Whoogle is intended to only ever be deployed to private instances by individuals of any background, with as little effort as possible. Prior knowledge of/experience with the command line or deploying applications is not necessary to deploy Whoogle, which isn't the case with Searx. As a result, Whoogle is missing some features of Searx in order to be as easy to deploy as possible.
|
||||||
|
|
||||||
|
Whoogle also only uses Google search results, not Bing/Quant/etc, and uses the existing Google search UI to make the transition away from Google search as unnoticeable as possible.
|
||||||
|
|
||||||
|
I'm a huge fan of Searx though and encourage anyone to use that instead if they want access to other search engines/a different UI/more configuration.
|
||||||
|
|
||||||
|
**Why does the image results page look different?**
|
||||||
|
|
||||||
|
A lot of the app currently piggybacks on Google's existing support for fetching results pages with JavaScript disabled. To their credit, they've done an excellent job with styling pages, but it seems that the image results page - particularly on mobile - is a little rough. Moving forward, with enough interest, I'd like to transition to fetching the results and parsing them into a unique Whoogle-fied interface that I can style myself.
|
||||||
|
|
||||||
|
## 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://search.sethforprivacy.com](https://search.sethforprivacy.com) | 🇩🇪 DE | English | |
|
||||||
|
| [https://whoogle.dcs0.hu](https://whoogle.dcs0.hu) | 🇭🇺 HU | Multi-choice | |
|
||||||
|
| [https://gowogle.voring.me](https://gowogle.voring.me) | 🇺🇸 US | Multi-choice | |
|
||||||
|
| [https://whoogle.privacydev.net](https://whoogle.privacydev.net) | 🇫🇷 FR | English | |
|
||||||
|
| [https://wg.vern.cc](https://wg.vern.cc) | 🇺🇸 US | English | |
|
||||||
|
| [https://whoogle.hxvy0.gq](https://whoogle.hxvy0.gq) | 🇨🇦 CA | Turkish Only | ✅ |
|
||||||
|
| [https://whoogle.hostux.net](https://whoogle.hostux.net) | 🇫🇷 FR | Multi-choice | |
|
||||||
|
| [https://whoogle.lunar.icu](https://whoogle.lunar.icu) | 🇩🇪 DE | Multi-choice | ✅ |
|
||||||
|
| [https://wgl.frail.duckdns.org](https://wgl.frail.duckdns.org) | 🇧🇷 BR | Multi-choice | |
|
||||||
|
| [https://whoogle.no-logs.com](https://whoogle.no-logs.com/) | 🇸🇪 SE | Multi-choice | |
|
||||||
|
| [https://whoogle.ftw.lol](https://whoogle.ftw.lol) | 🇩🇪 DE | Multi-choice | |
|
||||||
|
| [https://whoogle-search--replitcomreside.repl.co](https://whoogle-search--replitcomreside.repl.co) | 🇺🇸 US | English | |
|
||||||
|
| [https://search.notrustverify.ch](https://search.notrustverify.ch) | 🇨🇭 CH | Multi-choice | |
|
||||||
|
| [https://whoogle.datura.network](https://whoogle.datura.network) | 🇩🇪 DE | Multi-choice | |
|
||||||
|
| [https://whoogle.yepserver.xyz](https://whoogle.yepserver.xyz) | 🇺🇦 UA | Multi-choice | |
|
||||||
|
| [https://search.nezumi.party](https://search.nezumi.party) | 🇮🇹 IT | Multi-choice | |
|
||||||
|
| [https://search.snine.nl](https://search.snine.nl) | 🇳🇱 NL | Mult-choice | ✅ |
|
||||||
|
|
||||||
|
|
||||||
|
* A checkmark in the "Cloudflare" category here refers to the use of the reverse proxy, [Cloudflare](https://cloudflare.com). The checkmark will not be listed for a site which uses Cloudflare DNS but rather the proxying service which grants Cloudflare the ability to monitor traffic to the website.
|
||||||
|
|
||||||
|
#### 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 |
|
||||||
|
| [http://whoogle.g4c3eya4clenolymqbpgwz3q3tawoxw56yhzk4vugqrl6dtu3ejvhjid.onion](http://whoogle.g4c3eya4clenolymqbpgwz3q3tawoxw56yhzk4vugqrl6dtu3ejvhjid.onion/) | 🇫🇷 FR | English |
|
||||||
|
| [http://whoogle.daturab6drmkhyeia4ch5gvfc2f3wgo6bhjrv3pz6n7kxmvoznlkq4yd.onion](http://whoogle.daturab6drmkhyeia4ch5gvfc2f3wgo6bhjrv3pz6n7kxmvoznlkq4yd.onion/) | 🇩🇪 DE | Multi-choice | |
|
||||||
|
|
||||||
|
#### I2P Instances
|
||||||
|
|
||||||
|
| Website | Country | Language |
|
||||||
|
|-|-|-|
|
||||||
|
| [http://verneks7rfjptpz5fpii7n7nrxilsidi2qxepeuuf66c3tsf4nhq.b32.i2p](http://verneks7rfjptpz5fpii7n7nrxilsidi2qxepeuuf66c3tsf4nhq.b32.i2p) | 🇺🇸 US | English |
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
#### Desktop
|
||||||
|
![Whoogle Desktop](docs/screenshot_desktop.png)
|
||||||
|
|
||||||
|
#### Mobile
|
||||||
|
![Whoogle Mobile](docs/screenshot_mobile.png)
|
||||||
|
|
194
app.json
|
@ -1,194 +0,0 @@
|
||||||
{
|
|
||||||
"name": "Whoogle Search",
|
|
||||||
"description": "A lightweight, privacy-oriented, containerized Google search proxy for desktop/mobile that removes Javascript, AMP links, tracking, and ads/sponsored content",
|
|
||||||
"repository": "https://github.com/benbusby/whoogle-search",
|
|
||||||
"logo": "https://raw.githubusercontent.com/benbusby/whoogle-search/master/app/static/img/favicon/ms-icon-150x150.png",
|
|
||||||
"keywords": [
|
|
||||||
"search",
|
|
||||||
"metasearch",
|
|
||||||
"flask",
|
|
||||||
"docker",
|
|
||||||
"heroku",
|
|
||||||
"adblock",
|
|
||||||
"degoogle",
|
|
||||||
"privacy"
|
|
||||||
],
|
|
||||||
"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": "",
|
|
||||||
"required": false
|
|
||||||
},
|
|
||||||
"WHOOGLE_PASS": {
|
|
||||||
"description": "The password for basic auth. WHOOGLE_USER must also be set if used. Leave empty to disable.",
|
|
||||||
"value": "",
|
|
||||||
"required": false
|
|
||||||
},
|
|
||||||
"WHOOGLE_PROXY_USER": {
|
|
||||||
"description": "The username of the proxy server. Leave empty to disable.",
|
|
||||||
"value": "",
|
|
||||||
"required": false
|
|
||||||
},
|
|
||||||
"WHOOGLE_PROXY_PASS": {
|
|
||||||
"description": "The password of the proxy server. Leave empty to disable.",
|
|
||||||
"value": "",
|
|
||||||
"required": false
|
|
||||||
},
|
|
||||||
"WHOOGLE_PROXY_TYPE": {
|
|
||||||
"description": "The type of the proxy server. For example \"socks5\". Leave empty to disable.",
|
|
||||||
"value": "",
|
|
||||||
"required": false
|
|
||||||
},
|
|
||||||
"WHOOGLE_PROXY_LOC": {
|
|
||||||
"description": "The location of the proxy server (host or ip). Leave empty to disable.",
|
|
||||||
"value": "",
|
|
||||||
"required": false
|
|
||||||
},
|
|
||||||
"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",
|
|
||||||
"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",
|
|
||||||
"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",
|
|
||||||
"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_ALT_SO": {
|
|
||||||
"description": "The site to use as a replacement for stackoverflow.com when site alternatives are enabled in the config.",
|
|
||||||
"value": "farside.link/anonymousoverflow",
|
|
||||||
"required": false
|
|
||||||
},
|
|
||||||
"WHOOGLE_MINIMAL": {
|
|
||||||
"description": "Remove everything except basic result cards from all search queries (set to 1 or leave blank)",
|
|
||||||
"value": "",
|
|
||||||
"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": "",
|
|
||||||
"required": false
|
|
||||||
},
|
|
||||||
"WHOOGLE_CONFIG_TIME_PERIOD" : {
|
|
||||||
"description": "[CONFIG] The time period to use for restricting search results",
|
|
||||||
"value": "",
|
|
||||||
"required": false
|
|
||||||
},
|
|
||||||
"WHOOGLE_CONFIG_LANGUAGE": {
|
|
||||||
"description": "[CONFIG] The language to use for the interface (use values from https://raw.githubusercontent.com/benbusby/whoogle-search/develop/app/static/settings/languages.json)",
|
|
||||||
"value": "",
|
|
||||||
"required": false
|
|
||||||
},
|
|
||||||
"WHOOGLE_CONFIG_SEARCH_LANGUAGE": {
|
|
||||||
"description": "[CONFIG] The language to use for search results (use values from https://raw.githubusercontent.com/benbusby/whoogle-search/develop/app/static/settings/languages.json)",
|
|
||||||
"value": "",
|
|
||||||
"required": false
|
|
||||||
},
|
|
||||||
"WHOOGLE_CONFIG_DISABLE": {
|
|
||||||
"description": "[CONFIG] Disable ability for client to change config (set to 1 or leave blank)",
|
|
||||||
"value": "",
|
|
||||||
"required": false
|
|
||||||
},
|
|
||||||
"WHOOGLE_CONFIG_BLOCK": {
|
|
||||||
"description": "[CONFIG] Block websites from search results (comma-separated list)",
|
|
||||||
"value": "",
|
|
||||||
"required": false
|
|
||||||
},
|
|
||||||
"WHOOGLE_CONFIG_THEME": {
|
|
||||||
"description": "[CONFIG] Set theme to 'dark', 'light', or 'system'",
|
|
||||||
"value": "system",
|
|
||||||
"required": false
|
|
||||||
},
|
|
||||||
"WHOOGLE_CONFIG_SAFE": {
|
|
||||||
"description": "[CONFIG] Use safe mode for searches (set to 1 or leave blank)",
|
|
||||||
"value": "",
|
|
||||||
"required": false
|
|
||||||
},
|
|
||||||
"WHOOGLE_CONFIG_ALTS": {
|
|
||||||
"description": "[CONFIG] Use social media alternatives (set to 1 or leave blank)",
|
|
||||||
"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": "",
|
|
||||||
"required": false
|
|
||||||
},
|
|
||||||
"WHOOGLE_CONFIG_NEW_TAB": {
|
|
||||||
"description": "[CONFIG] Always open results in new tab (set to 1 or leave blank)",
|
|
||||||
"value": "",
|
|
||||||
"required": false
|
|
||||||
},
|
|
||||||
"WHOOGLE_CONFIG_VIEW_IMAGE": {
|
|
||||||
"description": "[CONFIG] Enable View Image option (set to 1 or leave blank)",
|
|
||||||
"value": "",
|
|
||||||
"required": false
|
|
||||||
},
|
|
||||||
"WHOOGLE_CONFIG_GET_ONLY": {
|
|
||||||
"description": "[CONFIG] Search using GET requests only (set to 1 or leave blank)",
|
|
||||||
"value": "",
|
|
||||||
"required": false
|
|
||||||
},
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
BIN
app/.DS_Store
vendored
199
app/__init__.py
|
@ -1,199 +0,0 @@
|
||||||
from app.filter import clean_query
|
|
||||||
from app.request import send_tor_signal
|
|
||||||
from app.utils.session import generate_key
|
|
||||||
from app.utils.bangs import gen_bangs_json, load_all_bangs
|
|
||||||
from app.utils.misc import gen_file_hash, read_config_bool
|
|
||||||
from base64 import b64encode
|
|
||||||
from bs4 import MarkupResemblesLocatorWarning
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
from flask import Flask
|
|
||||||
import json
|
|
||||||
import logging.config
|
|
||||||
import os
|
|
||||||
from stem import Signal
|
|
||||||
import threading
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
|
||||||
|
|
||||||
from app.utils.misc import read_config_bool
|
|
||||||
from app.version import __version__
|
|
||||||
|
|
||||||
app = Flask(__name__, static_folder=os.path.dirname(
|
|
||||||
os.path.abspath(__file__)) + '/static')
|
|
||||||
|
|
||||||
app.wsgi_app = ProxyFix(app.wsgi_app)
|
|
||||||
|
|
||||||
# look for WHOOGLE_ENV, else look in parent directory
|
|
||||||
dot_env_path = os.getenv(
|
|
||||||
"WHOOGLE_DOTENV_PATH",
|
|
||||||
os.path.join(os.path.dirname(os.path.abspath(__file__)), "../whoogle.env"))
|
|
||||||
|
|
||||||
# Load .env file if enabled
|
|
||||||
if os.path.exists(dot_env_path):
|
|
||||||
load_dotenv(dot_env_path)
|
|
||||||
|
|
||||||
app.enc_key = generate_key()
|
|
||||||
|
|
||||||
if read_config_bool('HTTPS_ONLY'):
|
|
||||||
app.config['SESSION_COOKIE_NAME'] = '__Secure-session'
|
|
||||||
app.config['SESSION_COOKIE_SECURE'] = True
|
|
||||||
|
|
||||||
app.config['VERSION_NUMBER'] = __version__
|
|
||||||
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'))
|
|
||||||
app.config['COUNTRIES'] = json.load(open(
|
|
||||||
os.path.join(app.config['STATIC_FOLDER'], 'settings/countries.json'),
|
|
||||||
encoding='utf-8'))
|
|
||||||
app.config['TIME_PERIODS'] = json.load(open(
|
|
||||||
os.path.join(app.config['STATIC_FOLDER'], 'settings/time_periods.json'),
|
|
||||||
encoding='utf-8'))
|
|
||||||
app.config['TRANSLATIONS'] = json.load(open(
|
|
||||||
os.path.join(app.config['STATIC_FOLDER'], 'settings/translations.json'),
|
|
||||||
encoding='utf-8'))
|
|
||||||
app.config['THEMES'] = json.load(open(
|
|
||||||
os.path.join(app.config['STATIC_FOLDER'], 'settings/themes.json'),
|
|
||||||
encoding='utf-8'))
|
|
||||||
app.config['HEADER_TABS'] = json.load(open(
|
|
||||||
os.path.join(app.config['STATIC_FOLDER'], 'settings/header_tabs.json'),
|
|
||||||
encoding='utf-8'))
|
|
||||||
app.config['CONFIG_PATH'] = os.getenv(
|
|
||||||
'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['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')
|
|
||||||
|
|
||||||
# Ensure all necessary directories exist
|
|
||||||
if not os.path.exists(app.config['CONFIG_PATH']):
|
|
||||||
os.makedirs(app.config['CONFIG_PATH'])
|
|
||||||
|
|
||||||
if not os.path.exists(app.config['SESSION_FILE_DIR']):
|
|
||||||
os.makedirs(app.config['SESSION_FILE_DIR'])
|
|
||||||
|
|
||||||
if not os.path.exists(app.config['BANG_PATH']):
|
|
||||||
os.makedirs(app.config['BANG_PATH'])
|
|
||||||
|
|
||||||
if not os.path.exists(app.config['BUILD_FOLDER']):
|
|
||||||
os.makedirs(app.config['BUILD_FOLDER'])
|
|
||||||
|
|
||||||
# Session values
|
|
||||||
app_key_path = os.path.join(app.config['CONFIG_PATH'], 'whoogle.key')
|
|
||||||
if os.path.exists(app_key_path):
|
|
||||||
try:
|
|
||||||
app.config['SECRET_KEY'] = open(app_key_path, 'r').read()
|
|
||||||
except PermissionError:
|
|
||||||
app.config['SECRET_KEY'] = str(b64encode(os.urandom(32)))
|
|
||||||
else:
|
|
||||||
app.config['SECRET_KEY'] = str(b64encode(os.urandom(32)))
|
|
||||||
with open(app_key_path, 'w') as key_file:
|
|
||||||
key_file.write(app.config['SECRET_KEY'])
|
|
||||||
key_file.close()
|
|
||||||
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=365)
|
|
||||||
|
|
||||||
# NOTE: SESSION_COOKIE_SAMESITE must be set to 'lax' to allow the user's
|
|
||||||
# previous session to persist when accessing the instance from an external
|
|
||||||
# link. Setting this value to 'strict' causes Whoogle to revalidate a new
|
|
||||||
# session, and fail, resulting in cookies being disabled.
|
|
||||||
app.config['SESSION_COOKIE_SAMESITE'] = 'Strict'
|
|
||||||
|
|
||||||
# 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
|
|
||||||
generating_bangs = False
|
|
||||||
if not os.path.exists(app.config['BANG_FILE']):
|
|
||||||
generating_bangs = True
|
|
||||||
json.dump({}, open(app.config['BANG_FILE'], 'w'))
|
|
||||||
bangs_thread = threading.Thread(
|
|
||||||
target=gen_bangs_json,
|
|
||||||
args=(app.config['BANG_FILE'],))
|
|
||||||
bangs_thread.start()
|
|
||||||
|
|
||||||
# Build new mapping of static files for cache busting
|
|
||||||
cache_busting_dirs = ['css', 'js']
|
|
||||||
for cb_dir in cache_busting_dirs:
|
|
||||||
full_cb_dir = os.path.join(app.config['STATIC_FOLDER'], cb_dir)
|
|
||||||
for cb_file in os.listdir(full_cb_dir):
|
|
||||||
# Create hash from current file state
|
|
||||||
full_cb_path = os.path.join(full_cb_dir, cb_file)
|
|
||||||
cb_file_link = gen_file_hash(full_cb_dir, cb_file)
|
|
||||||
build_path = os.path.join(app.config['BUILD_FOLDER'], cb_file_link)
|
|
||||||
|
|
||||||
try:
|
|
||||||
os.symlink(full_cb_path, build_path)
|
|
||||||
except FileExistsError:
|
|
||||||
# Symlink hasn't changed, ignore
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Create mapping for relative path urls
|
|
||||||
map_path = build_path.replace(app.config['APP_ROOT'], '')
|
|
||||||
if map_path.startswith('/'):
|
|
||||||
map_path = map_path[1:]
|
|
||||||
app.config['CACHE_BUSTING_MAP'][cb_file] = map_path
|
|
||||||
|
|
||||||
# Templating functions
|
|
||||||
app.jinja_env.globals.update(clean_query=clean_query)
|
|
||||||
app.jinja_env.globals.update(
|
|
||||||
cb_url=lambda f: app.config['CACHE_BUSTING_MAP'][f.lower()])
|
|
||||||
|
|
||||||
# Attempt to acquire tor identity, to determine if Tor config is available
|
|
||||||
send_tor_signal(Signal.HEARTBEAT)
|
|
||||||
|
|
||||||
# Suppress spurious warnings from BeautifulSoup
|
|
||||||
warnings.simplefilter('ignore', MarkupResemblesLocatorWarning)
|
|
||||||
|
|
||||||
from app import routes # noqa
|
|
||||||
|
|
||||||
# The gen_bangs_json function takes care of loading bangs, so skip it here if
|
|
||||||
# it's already being loaded
|
|
||||||
if not generating_bangs:
|
|
||||||
load_all_bangs(app.config['BANG_FILE'])
|
|
||||||
|
|
||||||
# Disable logging from imported modules
|
|
||||||
logging.config.dictConfig({
|
|
||||||
'version': 1,
|
|
||||||
'disable_existing_loggers': True,
|
|
||||||
})
|
|
|
@ -1,3 +0,0 @@
|
||||||
from .routes import run_app
|
|
||||||
|
|
||||||
run_app()
|
|
785
app/filter.py
|
@ -1,785 +0,0 @@
|
||||||
import cssutils
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
from bs4.element import ResultSet, Tag
|
|
||||||
from cryptography.fernet import Fernet
|
|
||||||
from flask import render_template
|
|
||||||
import html
|
|
||||||
import urllib.parse as urlparse
|
|
||||||
from urllib.parse import parse_qs
|
|
||||||
import re
|
|
||||||
|
|
||||||
from app.models.g_classes import GClasses
|
|
||||||
from app.request import VALID_PARAMS, MAPS_URL
|
|
||||||
from app.utils.misc import get_abs_url, read_config_bool
|
|
||||||
from app.utils.results import (
|
|
||||||
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 = [
|
|
||||||
'google.com/aclk'
|
|
||||||
'*.googleapis.com'
|
|
||||||
'*.gstatic.com'
|
|
||||||
'*.google-analytics.com'
|
|
||||||
'adservice.google.com'
|
|
||||||
'support.google.com',
|
|
||||||
'accounts.google.com',
|
|
||||||
'policies.google.com',
|
|
||||||
'google.com/preferences',
|
|
||||||
'google.com/intl',
|
|
||||||
'advanced_search',
|
|
||||||
'tbm=shop',
|
|
||||||
'ageverification.google.co.kr'
|
|
||||||
]
|
|
||||||
|
|
||||||
unsupported_g_divs = [
|
|
||||||
'google.com/preferences?hl=',
|
|
||||||
'ageverification.google.co.kr'
|
|
||||||
'google.com/aclk?sa='
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
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, keep_blank_values=True)['q'][0] if ('&q=' in href or '?q=' in href) else ''
|
|
||||||
|
|
||||||
|
|
||||||
def build_map_url(href: str) -> str:
|
|
||||||
"""Tries to extract known args that explain the location in the url. If a
|
|
||||||
location is found, returns the default url with it. Otherwise, returns the
|
|
||||||
url unchanged.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
href: The full url to check.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: The parsed url, or the url unchanged.
|
|
||||||
"""
|
|
||||||
# parse the url
|
|
||||||
parsed_url = parse_qs(href)
|
|
||||||
# iterate through the known parameters and try build the url
|
|
||||||
for param in MAPS_ARGS:
|
|
||||||
if param in parsed_url:
|
|
||||||
return MAPS_URL + "?q=" + parsed_url[param][0]
|
|
||||||
|
|
||||||
# query could not be extracted returning unchanged url
|
|
||||||
return href
|
|
||||||
|
|
||||||
|
|
||||||
def clean_query(query: str) -> str:
|
|
||||||
"""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,
|
|
||||||
config: Config,
|
|
||||||
root_url='',
|
|
||||||
page_url='',
|
|
||||||
query='',
|
|
||||||
mobile=False) -> None:
|
|
||||||
self.soup = None
|
|
||||||
self.config = config
|
|
||||||
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)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def elements(self):
|
|
||||||
return self._elements
|
|
||||||
|
|
||||||
def encrypt_path(self, path, is_element=False) -> str:
|
|
||||||
# Encrypts path to avoid plaintext results in logs
|
|
||||||
if is_element:
|
|
||||||
# Element paths are encrypted separately from text, to allow key
|
|
||||||
# regeneration once all items have been served to the user
|
|
||||||
enc_path = Fernet(self.user_key).encrypt(path.encode()).decode()
|
|
||||||
self._elements += 1
|
|
||||||
return enc_path
|
|
||||||
|
|
||||||
return Fernet(self.user_key).encrypt(path.encode()).decode()
|
|
||||||
|
|
||||||
def clean(self, soup) -> BeautifulSoup:
|
|
||||||
self.soup = soup
|
|
||||||
self.main_divs = self.soup.find('div', {'id': 'main'})
|
|
||||||
self.remove_ads()
|
|
||||||
self.remove_block_titles()
|
|
||||||
self.remove_block_url()
|
|
||||||
self.collapse_sections()
|
|
||||||
self.update_css()
|
|
||||||
self.update_styling()
|
|
||||||
self.remove_block_tabs()
|
|
||||||
|
|
||||||
# self.main_divs is only populated for the main page of search results
|
|
||||||
# (i.e. not images/news/etc).
|
|
||||||
if self.main_divs:
|
|
||||||
for div in self.main_divs:
|
|
||||||
self.sanitize_div(div)
|
|
||||||
|
|
||||||
for img in [_ for _ in self.soup.find_all('img') if 'src' in _.attrs]:
|
|
||||||
self.update_element_src(img, 'image/png')
|
|
||||||
|
|
||||||
for audio in [_ for _ in self.soup.find_all('audio') if 'src' in _.attrs]:
|
|
||||||
self.update_element_src(audio, 'audio/mpeg')
|
|
||||||
audio['controls'] = ''
|
|
||||||
|
|
||||||
for link in self.soup.find_all('a', href=True):
|
|
||||||
self.update_link(link)
|
|
||||||
self.add_favicon(link)
|
|
||||||
|
|
||||||
if self.config.alts:
|
|
||||||
self.site_alt_swap()
|
|
||||||
|
|
||||||
input_form = self.soup.find('form')
|
|
||||||
if input_form is not None:
|
|
||||||
input_form['method'] = 'GET' if self.config.get_only else 'POST'
|
|
||||||
# Use a relative URI for submissions
|
|
||||||
input_form['action'] = 'search'
|
|
||||||
|
|
||||||
# Ensure no extra scripts passed through
|
|
||||||
for script in self.soup('script'):
|
|
||||||
script.decompose()
|
|
||||||
|
|
||||||
# Update default footer and header
|
|
||||||
footer = self.soup.find('footer')
|
|
||||||
if footer:
|
|
||||||
# Remove divs that have multiple links beyond just page navigation
|
|
||||||
[_.decompose() for _ in footer.find_all('div', recursive=False)
|
|
||||||
if len(_.find_all('a', href=True)) > 3]
|
|
||||||
for link in footer.find_all('a', href=True):
|
|
||||||
link['href'] = f'{link["href"]}&preferences={self.config.preferences}'
|
|
||||||
|
|
||||||
header = self.soup.find('header')
|
|
||||||
if header:
|
|
||||||
header.decompose()
|
|
||||||
self.remove_site_blocks(self.soup)
|
|
||||||
return self.soup
|
|
||||||
|
|
||||||
def sanitize_div(self, div) -> None:
|
|
||||||
"""Removes escaped script and iframe tags from results
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
None (The soup object is modified directly)
|
|
||||||
"""
|
|
||||||
if not div:
|
|
||||||
return
|
|
||||||
|
|
||||||
for d in div.find_all('div', recursive=True):
|
|
||||||
d_text = d.find(text=True, recursive=False)
|
|
||||||
|
|
||||||
# Ensure we're working with tags that contain text content
|
|
||||||
if not d_text or not d.string:
|
|
||||||
continue
|
|
||||||
|
|
||||||
d.string = html.unescape(d_text)
|
|
||||||
div_soup = BeautifulSoup(d.string, 'html.parser')
|
|
||||||
|
|
||||||
# Remove all valid script or iframe tags in the div
|
|
||||||
for script in div_soup.find_all('script'):
|
|
||||||
script.decompose()
|
|
||||||
|
|
||||||
for iframe in div_soup.find_all('iframe'):
|
|
||||||
iframe.decompose()
|
|
||||||
|
|
||||||
d.string = str(div_soup)
|
|
||||||
|
|
||||||
def add_favicon(self, link) -> None:
|
|
||||||
"""Adds icons for each returned result, using the result site's favicon
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
None (The soup object is modified directly)
|
|
||||||
"""
|
|
||||||
# Skip empty, parentless, or internal links
|
|
||||||
show_favicons = read_config_bool('WHOOGLE_SHOW_FAVICONS', True)
|
|
||||||
is_valid_link = link and link.parent and link['href'].startswith('http')
|
|
||||||
if not show_favicons or not is_valid_link:
|
|
||||||
return
|
|
||||||
|
|
||||||
parent = link.parent
|
|
||||||
is_result_div = False
|
|
||||||
|
|
||||||
# Check each parent to make sure that the div doesn't already have a
|
|
||||||
# favicon attached, and that the div is a result div
|
|
||||||
while parent:
|
|
||||||
p_cls = parent.attrs.get('class') or []
|
|
||||||
if 'has-favicon' in p_cls or GClasses.scroller_class in p_cls:
|
|
||||||
return
|
|
||||||
elif GClasses.result_class_a not in p_cls:
|
|
||||||
parent = parent.parent
|
|
||||||
else:
|
|
||||||
is_result_div = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if not is_result_div:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Construct the html for inserting the icon into the parent div
|
|
||||||
parsed = urlparse.urlparse(link['href'])
|
|
||||||
favicon = self.encrypt_path(
|
|
||||||
f'{parsed.scheme}://{parsed.netloc}/favicon.ico',
|
|
||||||
is_element=True)
|
|
||||||
src = f'{self.root_url}/{Endpoint.element}?url={favicon}' + \
|
|
||||||
'&type=image/x-icon'
|
|
||||||
html = f'<img class="site-favicon" src="{src}">'
|
|
||||||
|
|
||||||
favicon = BeautifulSoup(html, 'html.parser')
|
|
||||||
link.parent.insert(0, favicon)
|
|
||||||
|
|
||||||
# Update all parents to indicate that a favicon has been attached
|
|
||||||
parent = link.parent
|
|
||||||
while parent:
|
|
||||||
p_cls = parent.get('class') or []
|
|
||||||
p_cls.append('has-favicon')
|
|
||||||
parent['class'] = p_cls
|
|
||||||
parent = parent.parent
|
|
||||||
|
|
||||||
if GClasses.result_class_a in p_cls:
|
|
||||||
break
|
|
||||||
|
|
||||||
def remove_site_blocks(self, soup) -> None:
|
|
||||||
if not self.config.block or not soup.body:
|
|
||||||
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
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
None (The soup object is modified directly)
|
|
||||||
"""
|
|
||||||
if not self.main_divs:
|
|
||||||
return
|
|
||||||
|
|
||||||
for div in [_ for _ in self.main_divs.find_all('div', recursive=True)]:
|
|
||||||
div_ads = [_ for _ in div.find_all('span', recursive=True)
|
|
||||||
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.config.block_title)
|
|
||||||
for div in [_ for _ in self.main_divs.find_all('div', recursive=True)]:
|
|
||||||
block_divs = [_ for _ in div.find_all('h3', recursive=True)
|
|
||||||
if block_title.search(_.text) is not None]
|
|
||||||
_ = 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.config.block_url)
|
|
||||||
for div in [_ for _ in self.main_divs.find_all('div', recursive=True)]:
|
|
||||||
block_divs = [_ for _ in div.find_all('a', recursive=True)
|
|
||||||
if block_url.search(_.attrs['href']) is not None]
|
|
||||||
_ = div.decompose() if len(block_divs) else None
|
|
||||||
|
|
||||||
def remove_block_tabs(self) -> 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 self.soup.find_all(
|
|
||||||
'div',
|
|
||||||
attrs={'class': f'{GClasses.images_tbm_tab}'}
|
|
||||||
):
|
|
||||||
_ = div.decompose()
|
|
||||||
|
|
||||||
def collapse_sections(self) -> None:
|
|
||||||
"""Collapses long result sections ("people also asked", "related
|
|
||||||
searches", etc) into "details" elements
|
|
||||||
|
|
||||||
These sections are typically the only sections in the results page that
|
|
||||||
have more than ~5 child divs within a primary result div.
|
|
||||||
|
|
||||||
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
|
|
||||||
#töörölni kell People also ask,
|
|
||||||
search_terms = ["People also search for", "Related searches", "Kapcsolódó keresések", "Mások ezeket keresték még"]
|
|
||||||
details_list = []
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# Determine the class based on the label content
|
|
||||||
if any(term in label for term in search_terms):
|
|
||||||
details_class = 'search-recommendations'
|
|
||||||
details_attrs = {'class': details_class, 'open': 'true'}
|
|
||||||
else:
|
|
||||||
details_class = 'other-results'
|
|
||||||
details_attrs = {'class': details_class}
|
|
||||||
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
details = BeautifulSoup(features='html.parser').new_tag('details', attrs=details_attrs)
|
|
||||||
summary = BeautifulSoup(features='html.parser').new_tag('summary', attrs={'class': "summary_div"})
|
|
||||||
summary.string = label
|
|
||||||
|
|
||||||
if subtitle:
|
|
||||||
soup = BeautifulSoup(subtitle, 'html.parser')
|
|
||||||
summary.append(soup)
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
for details in details_list:
|
|
||||||
self.main_divs.append(details)
|
|
||||||
|
|
||||||
def update_element_src(self, element: Tag, mime: str, attr='src') -> None:
|
|
||||||
"""Encrypts the original src of an element and rewrites the element src
|
|
||||||
to use the "/element?src=" pass-through.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
None (The soup element is modified directly)
|
|
||||||
|
|
||||||
"""
|
|
||||||
src = element[attr].split(' ')[0]
|
|
||||||
|
|
||||||
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'),
|
|
||||||
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) -> 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 self.soup.find_all('style'):
|
|
||||||
style.string = clean_css(style.string, self.page_url)
|
|
||||||
|
|
||||||
# TODO: Convert remote stylesheets to style tags and proxy all
|
|
||||||
# remote requests
|
|
||||||
# for link in soup.find_all('link', attrs={'rel': 'stylesheet'}):
|
|
||||||
# print(link)
|
|
||||||
|
|
||||||
def update_styling(self) -> None:
|
|
||||||
# Update CSS classes for result divs
|
|
||||||
soup = GClasses.replace_css_classes(self.soup)
|
|
||||||
|
|
||||||
# Remove unnecessary button(s)
|
|
||||||
for button in self.soup.find_all('button'):
|
|
||||||
button.decompose()
|
|
||||||
|
|
||||||
# Remove svg logos
|
|
||||||
for svg in self.soup.find_all('svg'):
|
|
||||||
svg.decompose()
|
|
||||||
|
|
||||||
# Update logo
|
|
||||||
logo = self.soup.find('a', {'class': 'l'})
|
|
||||||
if logo and self.mobile:
|
|
||||||
logo['style'] = ('display:flex; justify-content:center; '
|
|
||||||
'align-items:center; color:#685e79; '
|
|
||||||
'font-size:18px; ')
|
|
||||||
|
|
||||||
# Fix search bar length on mobile
|
|
||||||
""" try:
|
|
||||||
search_bar = self.soup.find('header').find('form').find('div')
|
|
||||||
search_bar['style'] = 'width: 100%;'
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Fix body max width on images tab
|
|
||||||
style = self.soup.find('style')
|
|
||||||
div = self.soup.find('div', attrs={
|
|
||||||
'class': f'{GClasses.images_tbm_tab}'})
|
|
||||||
if style and div and not self.mobile:
|
|
||||||
css = style.string
|
|
||||||
css_html_tag = (
|
|
||||||
'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:900px;}',
|
|
||||||
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
|
|
||||||
|
|
||||||
Args:
|
|
||||||
link: A bs4 Tag element to inspect and update
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
None (the tag is updated directly)
|
|
||||||
|
|
||||||
"""
|
|
||||||
parsed_link = urlparse.urlparse(link['href'])
|
|
||||||
if '/url?q=' in link['href']:
|
|
||||||
link_netloc = extract_q(parsed_link.query, link['href'])
|
|
||||||
else:
|
|
||||||
link_netloc = parsed_link.netloc
|
|
||||||
|
|
||||||
# Remove any elements that direct to unsupported Google pages
|
|
||||||
if any(url in link_netloc for url in unsupported_g_pages):
|
|
||||||
# FIXME: The "Shopping" tab requires further filtering (see #136)
|
|
||||||
# Temporarily removing all links to that tab for now.
|
|
||||||
|
|
||||||
# Replaces the /url google unsupported link to the direct url
|
|
||||||
link['href'] = link_netloc
|
|
||||||
parent = link.parent
|
|
||||||
|
|
||||||
if any(divlink in link_netloc for divlink in unsupported_g_divs):
|
|
||||||
# Handle case where a search is performed in a different
|
|
||||||
# language than what is configured. This usually returns a
|
|
||||||
# div with the same classes as normal search results, but with
|
|
||||||
# a link to configure language preferences through Google.
|
|
||||||
# Since we want all language config done through Whoogle, we
|
|
||||||
# can safely decompose this element.
|
|
||||||
while parent:
|
|
||||||
p_cls = parent.attrs.get('class') or []
|
|
||||||
if f'{GClasses.result_class_a}' in p_cls:
|
|
||||||
parent.decompose()
|
|
||||||
break
|
|
||||||
parent = parent.parent
|
|
||||||
else:
|
|
||||||
# Remove cases where google links appear in the footer
|
|
||||||
while parent:
|
|
||||||
p_cls = parent.attrs.get('class') or []
|
|
||||||
if parent.name == 'footer' or f'{GClasses.footer}' in p_cls:
|
|
||||||
link.decompose()
|
|
||||||
parent = parent.parent
|
|
||||||
|
|
||||||
if link.decomposed:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Replace href with only the intended destination (no "utm" type tags)
|
|
||||||
href = link['href'].replace('https://www.google.com', '')
|
|
||||||
result_link = urlparse.urlparse(href)
|
|
||||||
q = extract_q(result_link.query, href)
|
|
||||||
|
|
||||||
if q.startswith('/') and q not in self.query 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
|
|
||||||
link.decompose()
|
|
||||||
return
|
|
||||||
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_params = parse_qs(urlparse.urlparse(href).query)
|
|
||||||
for param in VALID_PARAMS:
|
|
||||||
if param not in query_params:
|
|
||||||
continue
|
|
||||||
param_val = query_params[param][0]
|
|
||||||
new_search += '&' + param + '=' + param_val
|
|
||||||
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)
|
|
||||||
|
|
||||||
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"
|
|
||||||
|
|
||||||
def site_alt_swap(self) -> None:
|
|
||||||
"""Replaces link locations and page elements if "alts" config
|
|
||||||
is enabled
|
|
||||||
"""
|
|
||||||
for site, alt in SITE_ALTS.items():
|
|
||||||
if site != "medium.com" and alt != "":
|
|
||||||
# Ignore medium.com replacements since these are handled
|
|
||||||
# specifically in the link description replacement, and medium
|
|
||||||
# results are never given their own "card" result where this
|
|
||||||
# replacement would make sense.
|
|
||||||
# Also ignore if the alt is empty, since this is used to indicate
|
|
||||||
# that the alt is not enabled.
|
|
||||||
for div in self.soup.find_all('div', text=re.compile(site)):
|
|
||||||
# Use the number of words in the div string to determine if the
|
|
||||||
# string is a result description (shouldn't replace domains used
|
|
||||||
# in desc text).
|
|
||||||
if len(div.string.split(' ')) == 1:
|
|
||||||
div.string = div.string.replace(site, alt)
|
|
||||||
|
|
||||||
for link in self.soup.find_all('a', href=True):
|
|
||||||
# Search and replace all link descriptions
|
|
||||||
# with alternative location
|
|
||||||
link['href'] = get_site_alt(link['href'])
|
|
||||||
link_desc = link.find_all(
|
|
||||||
text=re.compile('|'.join(SITE_ALTS.keys())))
|
|
||||||
if len(link_desc) == 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Replace link description
|
|
||||||
link_desc = link_desc[0]
|
|
||||||
if site not in link_desc or not alt:
|
|
||||||
continue
|
|
||||||
|
|
||||||
new_desc = BeautifulSoup(features='html.parser').new_tag('div')
|
|
||||||
link_str = str(link_desc)
|
|
||||||
|
|
||||||
# Medium links should be handled differently, since 'medium.com'
|
|
||||||
# is a common substring of domain names, but shouldn't be
|
|
||||||
# replaced (i.e. 'philomedium.com' should stay as it is).
|
|
||||||
if 'medium.com' in link_str:
|
|
||||||
if link_str.startswith('medium.com') or '.medium.com' in link_str:
|
|
||||||
link_str = SITE_ALTS['medium.com'] + link_str[
|
|
||||||
link_str.find('medium.com') + len('medium.com'):]
|
|
||||||
new_desc.string = link_str
|
|
||||||
else:
|
|
||||||
new_desc.string = link_str.replace(site, alt)
|
|
||||||
|
|
||||||
link_desc.replace_with(new_desc)
|
|
||||||
|
|
||||||
|
|
||||||
def view_image(self, soup) -> BeautifulSoup:
|
|
||||||
"""Replaces the soup with a new one that handles mobile results and
|
|
||||||
adds the link of the image full res to the results.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
soup: A BeautifulSoup object containing the image mobile results.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
BeautifulSoup: The new BeautifulSoup object
|
|
||||||
"""
|
|
||||||
|
|
||||||
# get some tags that are unchanged between mobile and pc versions
|
|
||||||
cor_suggested = soup.find_all(class_="By0U9")
|
|
||||||
next_pages = soup.find(class_= "uZgmoc")
|
|
||||||
|
|
||||||
results = []
|
|
||||||
# find results div
|
|
||||||
results_div = soup.find('div', attrs={'class': "nQvrDb"})
|
|
||||||
# find all the results (if any)
|
|
||||||
results_all = []
|
|
||||||
if results_div:
|
|
||||||
results_all = results_div.find_all('div', attrs={'class': "lIMUZd"})
|
|
||||||
|
|
||||||
for item in results_all:
|
|
||||||
urls = item.find('a')['href'].split('&imgrefurl=')
|
|
||||||
|
|
||||||
# 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_tbn = urlparse.unquote(item.find('a').find('img')['src'])
|
|
||||||
|
|
||||||
results.append({
|
|
||||||
'domain': urlparse.urlparse(web_page).netloc,
|
|
||||||
'img_url': img_url,
|
|
||||||
'web_page': web_page,
|
|
||||||
'img_tbn': img_tbn,
|
|
||||||
})
|
|
||||||
|
|
||||||
# Create a new BeautifulSoup object with the template
|
|
||||||
soup = BeautifulSoup(render_template('imageresults.html',
|
|
||||||
length=len(results),
|
|
||||||
results=results,
|
|
||||||
view_label="Kép megtekintése"),
|
|
||||||
features='html.parser')
|
|
||||||
|
|
||||||
# replace correction suggested by google object if exists
|
|
||||||
if len(cor_suggested):
|
|
||||||
soup.find_all(class_= "By0U9"
|
|
||||||
)[0].replaceWith(cor_suggested[0])
|
|
||||||
|
|
||||||
# replace next page object at the bottom of the page
|
|
||||||
soup.find_all(class_= "uZgmoc")[0].replaceWith(next_pages)
|
|
||||||
|
|
||||||
return soup
|
|
BIN
app/models/.DS_Store
vendored
|
@ -1,267 +0,0 @@
|
||||||
from inspect import Attribute
|
|
||||||
from typing import Optional
|
|
||||||
from app.utils.misc import read_config_bool
|
|
||||||
from flask import current_app
|
|
||||||
import os
|
|
||||||
from base64 import urlsafe_b64encode, urlsafe_b64decode
|
|
||||||
from cryptography.fernet import Fernet
|
|
||||||
import hashlib
|
|
||||||
import brotli
|
|
||||||
import logging
|
|
||||||
import json
|
|
||||||
|
|
||||||
import cssutils
|
|
||||||
from cssutils.css.cssstylesheet import CSSStyleSheet
|
|
||||||
from cssutils.css.cssstylerule import CSSStyleRule
|
|
||||||
|
|
||||||
# removes warnings from cssutils
|
|
||||||
cssutils.log.setLevel(logging.CRITICAL)
|
|
||||||
|
|
||||||
|
|
||||||
def get_rule_for_selector(stylesheet: CSSStyleSheet,
|
|
||||||
selector: str) -> Optional[CSSStyleRule]:
|
|
||||||
"""Search for a rule that matches a given selector in a stylesheet.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
stylesheet (CSSStyleSheet) -- the stylesheet to search
|
|
||||||
selector (str) -- the selector to search for
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Optional[CSSStyleRule] -- the rule that matches the selector or None
|
|
||||||
"""
|
|
||||||
for rule in stylesheet.cssRules:
|
|
||||||
if hasattr(rule, "selectorText") and selector == rule.selectorText:
|
|
||||||
return rule
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
app_config = current_app.config
|
|
||||||
self.url = os.getenv('WHOOGLE_CONFIG_URL', '')
|
|
||||||
self.lang_search = os.getenv('WHOOGLE_CONFIG_SEARCH_LANGUAGE', '')
|
|
||||||
self.lang_interface = os.getenv('WHOOGLE_CONFIG_LANGUAGE', default='lang_hu')
|
|
||||||
self.style_modified = os.getenv(
|
|
||||||
'WHOOGLE_CONFIG_STYLE', '')
|
|
||||||
self.block = os.getenv('WHOOGLE_CONFIG_BLOCK', '')
|
|
||||||
self.block_title = os.getenv('WHOOGLE_CONFIG_BLOCK_TITLE', '')
|
|
||||||
self.block_url = os.getenv('WHOOGLE_CONFIG_BLOCK_URL', '')
|
|
||||||
self.country = os.getenv('WHOOGLE_CONFIG_COUNTRY', '')
|
|
||||||
self.tbs = os.getenv('WHOOGLE_CONFIG_TIME_PERIOD', '')
|
|
||||||
self.theme = os.getenv('WHOOGLE_CONFIG_THEME', 'dark')
|
|
||||||
self.safe = read_config_bool('WHOOGLE_CONFIG_SAFE', default=True)
|
|
||||||
self.dark = read_config_bool('WHOOGLE_CONFIG_DARK') # deprecated
|
|
||||||
self.alts = read_config_bool('WHOOGLE_CONFIG_ALTS')
|
|
||||||
self.nojs = read_config_bool('WHOOGLE_CONFIG_NOJS')
|
|
||||||
self.tor = read_config_bool('WHOOGLE_CONFIG_TOR')
|
|
||||||
self.near = os.getenv('WHOOGLE_CONFIG_NEAR', '')
|
|
||||||
self.new_tab = read_config_bool('WHOOGLE_CONFIG_NEW_TAB')
|
|
||||||
self.view_image = read_config_bool('WHOOGLE_CONFIG_VIEW_IMAGE', default=True)
|
|
||||||
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',
|
|
||||||
'tbs'
|
|
||||||
]
|
|
||||||
|
|
||||||
# Skip setting custom config if there isn't one
|
|
||||||
if kwargs:
|
|
||||||
mutable_attrs = self.get_mutable_attrs()
|
|
||||||
for attr in mutable_attrs:
|
|
||||||
if attr in kwargs.keys():
|
|
||||||
setattr(self, attr, kwargs[attr])
|
|
||||||
elif attr not in kwargs.keys() and mutable_attrs[attr] == bool:
|
|
||||||
setattr(self, attr, False)
|
|
||||||
|
|
||||||
def __getitem__(self, name):
|
|
||||||
return getattr(self, name)
|
|
||||||
|
|
||||||
def __setitem__(self, name, value):
|
|
||||||
return setattr(self, name, value)
|
|
||||||
|
|
||||||
def __delitem__(self, name):
|
|
||||||
return delattr(self, name)
|
|
||||||
|
|
||||||
def __contains__(self, name):
|
|
||||||
return hasattr(self, name)
|
|
||||||
|
|
||||||
def get_mutable_attrs(self):
|
|
||||||
return {name: type(attr) for name, attr in self.__dict__.items()
|
|
||||||
if not name.startswith("__")
|
|
||||||
and (type(attr) is bool or type(attr) is str)}
|
|
||||||
|
|
||||||
def get_attrs(self):
|
|
||||||
return {name: attr for name, attr in self.__dict__.items()
|
|
||||||
if not name.startswith("__")
|
|
||||||
and (type(attr) is bool or type(attr) is str)}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def style(self) -> str:
|
|
||||||
"""Returns the default style updated with specified modifications.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str -- the new style
|
|
||||||
"""
|
|
||||||
style_sheet = cssutils.parseString(
|
|
||||||
open(os.path.join(current_app.config['STATIC_FOLDER'],
|
|
||||||
'css/variables.css')).read()
|
|
||||||
)
|
|
||||||
|
|
||||||
modified_sheet = cssutils.parseString(self.style_modified)
|
|
||||||
for rule in modified_sheet:
|
|
||||||
rule_default = get_rule_for_selector(style_sheet,
|
|
||||||
rule.selectorText)
|
|
||||||
# if modified rule is in default stylesheet, update it
|
|
||||||
if rule_default is not None:
|
|
||||||
# TODO: update this in a smarter way to handle :root better
|
|
||||||
# for now if we change a varialbe in :root all other default
|
|
||||||
# variables need to be also present
|
|
||||||
rule_default.style = rule.style
|
|
||||||
# else add the new rule to the default stylesheet
|
|
||||||
else:
|
|
||||||
style_sheet.add(rule)
|
|
||||||
return str(style_sheet.cssText, 'utf-8')
|
|
||||||
|
|
||||||
@property
|
|
||||||
def preferences(self) -> str:
|
|
||||||
# if encryption key is not set will uncheck preferences encryption
|
|
||||||
if self.preferences_encrypted:
|
|
||||||
self.preferences_encrypted = bool(self.preferences_key)
|
|
||||||
|
|
||||||
|
|
||||||
# add a tag for visibility if preferences token startswith 'e' it means
|
|
||||||
# the token is encrypted, 'u' means the token is unencrypted and can be
|
|
||||||
# used by other whoogle instances
|
|
||||||
encrypted_flag = "e" if self.preferences_encrypted else 'u'
|
|
||||||
preferences_digest = self._encode_preferences()
|
|
||||||
return f"{encrypted_flag}{preferences_digest}"
|
|
||||||
|
|
||||||
def is_safe_key(self, key) -> bool:
|
|
||||||
"""Establishes a group of config options that are safe to set
|
|
||||||
in the url.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key (str) -- the key to check against
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool -- True/False depending on if the key is in the "safe"
|
|
||||||
array
|
|
||||||
"""
|
|
||||||
|
|
||||||
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
|
|
||||||
public instances.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
params -- the url arguments (can be any deemed safe by is_safe())
|
|
||||||
|
|
||||||
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
|
|
||||||
return self
|
|
||||||
|
|
||||||
def to_params(self, keys: list = []) -> str:
|
|
||||||
"""Generates a set of safe params for using in Whoogle URLs
|
|
||||||
|
|
||||||
Args:
|
|
||||||
keys (list) -- optional list of keys of URL parameters
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str -- a set of URL parameters
|
|
||||||
"""
|
|
||||||
if not len(keys):
|
|
||||||
keys = self.safe_keys
|
|
||||||
|
|
||||||
param_str = ''
|
|
||||||
for safe_key in keys:
|
|
||||||
if not self[safe_key]:
|
|
||||||
continue
|
|
||||||
param_str = param_str + f'&{safe_key}={self[safe_key]}'
|
|
||||||
|
|
||||||
return param_str
|
|
||||||
|
|
||||||
def _get_fernet_key(self, password: str) -> bytes:
|
|
||||||
hash_object = hashlib.md5(password.encode())
|
|
||||||
key = urlsafe_b64encode(hash_object.hexdigest().encode())
|
|
||||||
return key
|
|
||||||
|
|
||||||
def _encode_preferences(self) -> str:
|
|
||||||
preferences_json = json.dumps(self.get_attrs()).encode()
|
|
||||||
compressed_preferences = brotli.compress(preferences_json)
|
|
||||||
|
|
||||||
if self.preferences_encrypted and self.preferences_key:
|
|
||||||
key = self._get_fernet_key(self.preferences_key)
|
|
||||||
encrypted_preferences = Fernet(key).encrypt(compressed_preferences)
|
|
||||||
compressed_preferences = brotli.compress(encrypted_preferences)
|
|
||||||
|
|
||||||
return urlsafe_b64encode(compressed_preferences).decode()
|
|
||||||
|
|
||||||
def _decode_preferences(self, preferences: str) -> dict:
|
|
||||||
mode = preferences[0]
|
|
||||||
preferences = preferences[1:]
|
|
||||||
|
|
||||||
try:
|
|
||||||
decoded_data = brotli.decompress(urlsafe_b64decode(preferences.encode() + b'=='))
|
|
||||||
|
|
||||||
if mode == 'e' and self.preferences_key:
|
|
||||||
# preferences are encrypted
|
|
||||||
key = self._get_fernet_key(self.preferences_key)
|
|
||||||
decrypted_data = Fernet(key).decrypt(decoded_data)
|
|
||||||
decoded_data = brotli.decompress(decrypted_data)
|
|
||||||
|
|
||||||
config = json.loads(decoded_data)
|
|
||||||
except Exception:
|
|
||||||
config = {}
|
|
||||||
|
|
||||||
return config
|
|
|
@ -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}')
|
|
|
@ -1,48 +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'
|
|
||||||
scroller_class = 'idg8be'
|
|
||||||
line_tag = 'BsXmcf'
|
|
||||||
|
|
||||||
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
|
|
352
app/request.py
|
@ -1,352 +0,0 @@
|
||||||
from app.models.config import Config
|
|
||||||
from app.utils.misc import read_config_bool
|
|
||||||
from datetime import datetime
|
|
||||||
from defusedxml import ElementTree as ET
|
|
||||||
import random
|
|
||||||
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'
|
|
||||||
AUTOCOMPLETE_URL = ('https://suggestqueries.google.com/'
|
|
||||||
'complete/search?client=toolbar&')
|
|
||||||
|
|
||||||
MOBILE_UA = '{}/5.0 (Android 0; Mobile; rv:54.0) Gecko/54.0 {}/59.0'
|
|
||||||
DESKTOP_UA = '{}/5.0 (X11; {} x86_64; rv:75.0) Gecko/20100101 {}/75.0'
|
|
||||||
|
|
||||||
# Valid query params
|
|
||||||
VALID_PARAMS = ['tbs', 'tbm', 'start', 'near', 'source', 'nfpr']
|
|
||||||
|
|
||||||
|
|
||||||
class TorError(Exception):
|
|
||||||
"""Exception raised for errors in Tor requests.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
message: a message describing the error that occurred
|
|
||||||
disable: optionally disables Tor in the user config (note:
|
|
||||||
this should only happen if the connection has been dropped
|
|
||||||
altogether).
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, message, disable=False) -> None:
|
|
||||||
self.message = message
|
|
||||||
self.disable = disable
|
|
||||||
super().__init__(message)
|
|
||||||
|
|
||||||
|
|
||||||
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.signal(signal)
|
|
||||||
os.environ['TOR_AVAILABLE'] = '1'
|
|
||||||
return True
|
|
||||||
except (SocketError, AuthenticationFailure,
|
|
||||||
ConnectionRefusedError, ConnectionError):
|
|
||||||
# TODO: Handle Tor authentication (password and cookie)
|
|
||||||
os.environ['TOR_AVAILABLE'] = '0'
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def gen_user_agent(is_mobile) -> str:
|
|
||||||
user_agent = os.environ.get('WHOOGLE_USER_AGENT', '')
|
|
||||||
user_agent_mobile = os.environ.get('WHOOGLE_USER_AGENT_MOBILE', '')
|
|
||||||
if user_agent and not is_mobile:
|
|
||||||
return user_agent
|
|
||||||
|
|
||||||
if user_agent_mobile and is_mobile:
|
|
||||||
return user_agent_mobile
|
|
||||||
|
|
||||||
firefox = random.choice(['Choir', 'Squier', 'Higher', 'Wire']) + 'fox'
|
|
||||||
linux = random.choice(['Win', 'Sin', 'Gin', 'Fin', 'Kin']) + 'ux'
|
|
||||||
|
|
||||||
if is_mobile:
|
|
||||||
return MOBILE_UA.format("Mozilla", firefox)
|
|
||||||
|
|
||||||
return DESKTOP_UA.format("Mozilla", linux, firefox)
|
|
||||||
|
|
||||||
|
|
||||||
def gen_query(query, args, config) -> str:
|
|
||||||
param_dict = {key: '' for key in VALID_PARAMS}
|
|
||||||
|
|
||||||
# Use :past(hour/day/week/month/year) if available
|
|
||||||
# example search "new restaurants :past month"
|
|
||||||
lang = ''
|
|
||||||
if ':past' in query and 'tbs' not in args:
|
|
||||||
time_range = str.strip(query.split(':past', 1)[-1])
|
|
||||||
param_dict['tbs'] = '&tbs=' + ('qdr:' + str.lower(time_range[0]))
|
|
||||||
elif 'tbs' in args or 'tbs' in config:
|
|
||||||
result_tbs = args.get('tbs') if 'tbs' in args else config['tbs']
|
|
||||||
param_dict['tbs'] = '&tbs=' + result_tbs
|
|
||||||
|
|
||||||
# Occasionally the 'tbs' param provided by google also contains a
|
|
||||||
# field for 'lr', but formatted strangely. This is a rough solution
|
|
||||||
# for this.
|
|
||||||
#
|
|
||||||
# Example:
|
|
||||||
# &tbs=qdr:h,lr:lang_1pl
|
|
||||||
# -- the lr param needs to be extracted and remove the leading '1'
|
|
||||||
result_params = [_ for _ in result_tbs.split(',') if 'lr:' in _]
|
|
||||||
if len(result_params) > 0:
|
|
||||||
result_param = result_params[0]
|
|
||||||
lang = result_param[result_param.find('lr:') + 3:len(result_param)]
|
|
||||||
|
|
||||||
# Ensure search query is parsable
|
|
||||||
query = urlparse.quote(query)
|
|
||||||
|
|
||||||
# Pass along type of results (news, images, books, etc)
|
|
||||||
if 'tbm' in args:
|
|
||||||
param_dict['tbm'] = '&tbm=' + args.get('tbm')
|
|
||||||
|
|
||||||
# Get results page start value (10 per page, ie page 2 start val = 20)
|
|
||||||
if 'start' in args:
|
|
||||||
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)
|
|
||||||
|
|
||||||
# Set language for results (lr) if source isn't set, otherwise use the
|
|
||||||
# result language param provided in the results
|
|
||||||
if 'source' in args:
|
|
||||||
param_dict['source'] = '&source=' + args.get('source')
|
|
||||||
param_dict['lr'] = ('&lr=' + ''.join(
|
|
||||||
[_ for _ in lang if not _.isdigit()]
|
|
||||||
)) if lang else ''
|
|
||||||
else:
|
|
||||||
param_dict['lr'] = (
|
|
||||||
'&lr=' + config.lang_search
|
|
||||||
) if config.lang_search else ''
|
|
||||||
|
|
||||||
# 'nfpr' defines the exclusion of results from an auto-corrected query
|
|
||||||
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['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 val in param_dict.values():
|
|
||||||
if not val:
|
|
||||||
continue
|
|
||||||
query += val
|
|
||||||
|
|
||||||
return query
|
|
||||||
|
|
||||||
|
|
||||||
class Request:
|
|
||||||
"""Class used for handling all outbound requests, including search queries,
|
|
||||||
search suggestions, and loading of external content (images, audio, etc).
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
normal_ua: the user's current user agent
|
|
||||||
root_path: the root path of the whoogle instance
|
|
||||||
config: the user's current whoogle configuration
|
|
||||||
"""
|
|
||||||
|
|
||||||
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', 12)) + '&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 ''
|
|
||||||
)
|
|
||||||
|
|
||||||
self.country = config.country if config.country 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.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', '')
|
|
||||||
auth_str = ''
|
|
||||||
if proxy_user:
|
|
||||||
auth_str = f'{proxy_user}:{proxy_pass}@'
|
|
||||||
|
|
||||||
proxy_str = f'{proxy_type}://{auth_str}{proxy_path}'
|
|
||||||
self.proxies = {
|
|
||||||
'https': proxy_str,
|
|
||||||
'http': proxy_str
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
self.proxies = {
|
|
||||||
'http': 'socks5://127.0.0.1:9050',
|
|
||||||
'https': 'socks5://127.0.0.1:9050'
|
|
||||||
} if config.tor else {}
|
|
||||||
self.tor = config.tor
|
|
||||||
self.tor_valid = False
|
|
||||||
self.root_path = root_path
|
|
||||||
|
|
||||||
def __getitem__(self, name):
|
|
||||||
return getattr(self, name)
|
|
||||||
|
|
||||||
def autocomplete(self, query) -> list:
|
|
||||||
"""Sends a query to Google's search suggestion service
|
|
||||||
|
|
||||||
Args:
|
|
||||||
query: The in-progress query to send
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list: The list of matches for possible search suggestions
|
|
||||||
|
|
||||||
"""
|
|
||||||
ac_query = dict(q=query)
|
|
||||||
if self.language:
|
|
||||||
ac_query['lr'] = self.language
|
|
||||||
if self.country:
|
|
||||||
ac_query['gl'] = self.country
|
|
||||||
if self.lang_interface:
|
|
||||||
ac_query['hl'] = self.lang_interface
|
|
||||||
|
|
||||||
response = self.send(base_url=AUTOCOMPLETE_URL,
|
|
||||||
query=urlparse.urlencode(ac_query)).text
|
|
||||||
|
|
||||||
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 []
|
|
||||||
|
|
||||||
def send(self, base_url='', query='', attempt=0,
|
|
||||||
force_mobile=True, user_agent='') -> Response:
|
|
||||||
"""Sends an outbound request to a URL. Optionally sends the request
|
|
||||||
using Tor, if enabled by the user.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
base_url: The URL to use in the 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
|
|
||||||
|
|
||||||
"""
|
|
||||||
use_client_user_agent = int(os.environ.get('WHOOGLE_USE_CLIENT_USER_AGENT', '0'))
|
|
||||||
if user_agent and use_client_user_agent == 1:
|
|
||||||
modified_user_agent = user_agent
|
|
||||||
else:
|
|
||||||
if force_mobile and not self.mobile:
|
|
||||||
modified_user_agent = self.modified_user_agent_mobile
|
|
||||||
else:
|
|
||||||
modified_user_agent = self.modified_user_agent
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
'User-Agent': modified_user_agent
|
|
||||||
}
|
|
||||||
|
|
||||||
# Adding the Accept-Language to the Header if possible
|
|
||||||
if self.lang_interface:
|
|
||||||
headers.update({'Accept-Language':
|
|
||||||
self.lang_interface.replace('lang_', '')
|
|
||||||
+ ';q=1.0'})
|
|
||||||
|
|
||||||
# view is suppressed correctly
|
|
||||||
now = datetime.now()
|
|
||||||
cookies = {
|
|
||||||
'CONSENT': 'PENDING+987',
|
|
||||||
'SOCS': 'CAESHAgBEhIaAB',
|
|
||||||
}
|
|
||||||
|
|
||||||
# Validate Tor conn and request new identity if the last one failed
|
|
||||||
if self.tor and not send_tor_signal(
|
|
||||||
Signal.NEWNYM if attempt > 0 else Signal.HEARTBEAT):
|
|
||||||
raise TorError(
|
|
||||||
"Tor was previously enabled, but the connection has been "
|
|
||||||
"dropped. Please check your Tor configuration and try again.",
|
|
||||||
disable=True)
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
if not self.tor_valid:
|
|
||||||
raise TorError(
|
|
||||||
"Tor connection succeeded, but the connection could "
|
|
||||||
"not be validated by torproject.org",
|
|
||||||
disable=True)
|
|
||||||
except ConnectionError:
|
|
||||||
raise TorError(
|
|
||||||
"Error raised during Tor connection validation",
|
|
||||||
disable=True)
|
|
||||||
|
|
||||||
response = requests.get(
|
|
||||||
(base_url or self.search_url) + query,
|
|
||||||
proxies=self.proxies,
|
|
||||||
headers=headers,
|
|
||||||
cookies=cookies)
|
|
||||||
|
|
||||||
# Retry query with new identity if using Tor (max 10 attempts)
|
|
||||||
if 'form id="captcha-form"' in response.text and self.tor:
|
|
||||||
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 response
|
|
726
app/routes.py
|
@ -1,726 +0,0 @@
|
||||||
import argparse
|
|
||||||
import base64
|
|
||||||
import io
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import pickle
|
|
||||||
import re
|
|
||||||
import urllib.parse as urlparse
|
|
||||||
import uuid
|
|
||||||
import validators
|
|
||||||
import sys
|
|
||||||
import traceback
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from functools import wraps
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from werkzeug.exceptions import HTTPException
|
|
||||||
from urllib.parse import unquote
|
|
||||||
|
|
||||||
import waitress
|
|
||||||
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 suggest_bang, resolve_bang
|
|
||||||
from app.utils.misc import empty_gif, placeholder_img, get_proxy_host_url, \
|
|
||||||
fetch_favicon
|
|
||||||
from app.filter import Filter
|
|
||||||
from app.utils.misc import read_config_bool, get_client_ip, get_request_url, \
|
|
||||||
check_for_update, encrypt_string
|
|
||||||
from app.utils.widgets import *
|
|
||||||
from app.utils.results import bold_search_terms,\
|
|
||||||
add_currency_card, check_currency, get_tabs_content
|
|
||||||
from app.utils.search import Search, needs_https, has_captcha
|
|
||||||
from app.utils.session import valid_user_session
|
|
||||||
from bs4 import BeautifulSoup as bsoup
|
|
||||||
from flask import Flask, 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 werkzeug.datastructures import MultiDict
|
|
||||||
|
|
||||||
ac_var = 'WHOOGLE_AUTOCOMPLETE'
|
|
||||||
autocomplete_enabled = os.getenv(ac_var, '1')
|
|
||||||
|
|
||||||
def get_search_name(tbm):
|
|
||||||
for tab in app.config['HEADER_TABS'].values():
|
|
||||||
if tab['tbm'] == tbm:
|
|
||||||
return tab['name']
|
|
||||||
|
|
||||||
|
|
||||||
def auth_required(f):
|
|
||||||
@wraps(f)
|
|
||||||
def decorated(*args, **kwargs):
|
|
||||||
# do not ask password if cookies already present
|
|
||||||
if (
|
|
||||||
valid_user_session(session)
|
|
||||||
and 'cookies_disabled' not in request.args
|
|
||||||
and session['auth']
|
|
||||||
):
|
|
||||||
return f(*args, **kwargs)
|
|
||||||
|
|
||||||
auth = request.authorization
|
|
||||||
|
|
||||||
# Skip if username/password not set
|
|
||||||
whoogle_user = os.getenv('WHOOGLE_USER', '')
|
|
||||||
whoogle_pass = os.getenv('WHOOGLE_PASS', '')
|
|
||||||
if (not whoogle_user or not whoogle_pass) or (
|
|
||||||
auth
|
|
||||||
and whoogle_user == auth.username
|
|
||||||
and whoogle_pass == auth.password):
|
|
||||||
session['auth'] = True
|
|
||||||
return f(*args, **kwargs)
|
|
||||||
else:
|
|
||||||
return make_response('Not logged in', 401, {
|
|
||||||
'WWW-Authenticate': 'Basic realm="Login Required"'})
|
|
||||||
|
|
||||||
return decorated
|
|
||||||
|
|
||||||
|
|
||||||
def session_required(f):
|
|
||||||
@wraps(f)
|
|
||||||
def decorated(*args, **kwargs):
|
|
||||||
if not valid_user_session(session):
|
|
||||||
session.pop('_permanent', None)
|
|
||||||
|
|
||||||
# Note: This sets all requests to use the encryption key determined per
|
|
||||||
# instance on app init. This can be updated in the future to use a key
|
|
||||||
# that is unique for their session (session['key']) but this should use
|
|
||||||
# a config setting to enable the session based key. Otherwise there can
|
|
||||||
# be problems with searches performed by users with cookies blocked if
|
|
||||||
# a session based key is always used.
|
|
||||||
g.session_key = app.enc_key
|
|
||||||
|
|
||||||
# Clear out old sessions
|
|
||||||
invalid_sessions = []
|
|
||||||
for user_session in os.listdir(app.config['SESSION_FILE_DIR']):
|
|
||||||
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():
|
|
||||||
session.permanent = True
|
|
||||||
|
|
||||||
# Check for latest version if needed
|
|
||||||
now = datetime.now()
|
|
||||||
needs_update_check = now - timedelta(hours=24) > app.config['LAST_UPDATE_CHECK']
|
|
||||||
if read_config_bool('WHOOGLE_UPDATE_CHECK', True) and needs_update_check:
|
|
||||||
app.config['LAST_UPDATE_CHECK'] = now
|
|
||||||
app.config['HAS_UPDATE'] = check_for_update(
|
|
||||||
app.config['RELEASES_URL'],
|
|
||||||
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 {}
|
|
||||||
|
|
||||||
# Generate session values for user if unavailable
|
|
||||||
if not valid_user_session(session):
|
|
||||||
session['config'] = default_config
|
|
||||||
session['uuid'] = str(uuid.uuid4())
|
|
||||||
session['key'] = app.enc_key
|
|
||||||
session['auth'] = False
|
|
||||||
|
|
||||||
# Establish config values per user session
|
|
||||||
g.user_config = Config(**session['config'])
|
|
||||||
|
|
||||||
# Update user config if specified in search args
|
|
||||||
g.user_config = g.user_config.from_params(g.request_params)
|
|
||||||
|
|
||||||
if not g.user_config.url:
|
|
||||||
g.user_config.url = get_request_url(request.url_root)
|
|
||||||
|
|
||||||
g.user_request = Request(
|
|
||||||
request.headers.get('User-Agent'),
|
|
||||||
get_request_url(request.url_root),
|
|
||||||
config=g.user_config)
|
|
||||||
|
|
||||||
g.app_location = g.user_config.url
|
|
||||||
|
|
||||||
|
|
||||||
@app.after_request
|
|
||||||
def after_request_func(resp):
|
|
||||||
resp.headers['X-Content-Type-Options'] = 'nosniff'
|
|
||||||
resp.headers['X-Frame-Options'] = 'DENY'
|
|
||||||
resp.headers['Cache-Control'] = 'max-age=86400'
|
|
||||||
|
|
||||||
if os.getenv('WHOOGLE_CSP', False):
|
|
||||||
resp.headers['Content-Security-Policy'] = app.config['CSP']
|
|
||||||
if os.environ.get('HTTPS_ONLY', False):
|
|
||||||
resp.headers['Content-Security-Policy'] += \
|
|
||||||
'upgrade-insecure-requests'
|
|
||||||
|
|
||||||
return resp
|
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(404)
|
|
||||||
def unknown_page(e):
|
|
||||||
app.logger.warn(e)
|
|
||||||
return redirect(g.app_location)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route(f'/{Endpoint.healthz}', methods=['GET'])
|
|
||||||
def healthz():
|
|
||||||
return ''
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/', methods=['GET'])
|
|
||||||
@app.route(f'/{Endpoint.home}', methods=['GET'])
|
|
||||||
@auth_required
|
|
||||||
def index():
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
return render_template('index.html',
|
|
||||||
has_update=app.config['HAS_UPDATE'],
|
|
||||||
languages=app.config['LANGUAGES'],
|
|
||||||
countries=app.config['COUNTRIES'],
|
|
||||||
time_periods=app.config['TIME_PERIODS'],
|
|
||||||
themes=app.config['THEMES'],
|
|
||||||
autocomplete_enabled=autocomplete_enabled,
|
|
||||||
translation=app.config['TRANSLATIONS'][
|
|
||||||
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=g.user_config,
|
|
||||||
tor_available=int(os.environ.get('TOR_AVAILABLE')),
|
|
||||||
version_number=app.config['VERSION_NUMBER'])
|
|
||||||
|
|
||||||
|
|
||||||
@app.route(f'/{Endpoint.opensearch}', methods=['GET'])
|
|
||||||
def opensearch():
|
|
||||||
opensearch_url = g.app_location
|
|
||||||
if opensearch_url.endswith('/'):
|
|
||||||
opensearch_url = opensearch_url[:-1]
|
|
||||||
|
|
||||||
# Enforce https for opensearch template
|
|
||||||
if needs_https(opensearch_url):
|
|
||||||
opensearch_url = opensearch_url.replace('http://', 'https://', 1)
|
|
||||||
|
|
||||||
get_only = g.user_config.get_only or 'Chrome' in request.headers.get(
|
|
||||||
'User-Agent')
|
|
||||||
|
|
||||||
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'))
|
|
||||||
), 200, {'Content-Type': 'application/xml'}
|
|
||||||
|
|
||||||
|
|
||||||
@app.route(f'/{Endpoint.search_html}', methods=['GET'])
|
|
||||||
def search_html():
|
|
||||||
search_url = g.app_location
|
|
||||||
if search_url.endswith('/'):
|
|
||||||
search_url = search_url[:-1]
|
|
||||||
return render_template('search.html', url=search_url)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route(f'/{Endpoint.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
|
|
||||||
# mimetype in the format "b'q=<query>'" through the request.data field
|
|
||||||
q = str(request.data).replace('q=', '')
|
|
||||||
|
|
||||||
# Search bangs if the query begins with "!", but not "! " (feeling lucky)
|
|
||||||
if q.startswith('!') and len(q) > 1 and not q.startswith('! '):
|
|
||||||
return jsonify([q, suggest_bang(q)])
|
|
||||||
|
|
||||||
if not q and not request.data:
|
|
||||||
return jsonify({'?': []})
|
|
||||||
elif request.data:
|
|
||||||
q = urlparse.unquote_plus(
|
|
||||||
request.data.decode('utf-8').replace('q=', ''))
|
|
||||||
|
|
||||||
# Return a list of suggestions for the query
|
|
||||||
#
|
|
||||||
# Note: If Tor is enabled, this returns nothing, as the request is
|
|
||||||
# almost always rejected
|
|
||||||
return jsonify([
|
|
||||||
q,
|
|
||||||
g.user_request.autocomplete(q) if not g.user_config.tor else []
|
|
||||||
])
|
|
||||||
|
|
||||||
@app.route(f'/{Endpoint.search}', methods=['GET', 'POST'])
|
|
||||||
@session_required
|
|
||||||
@auth_required
|
|
||||||
def search():
|
|
||||||
if request.method == 'POST':
|
|
||||||
# Redirect as a GET request with an encrypted query
|
|
||||||
post_data = MultiDict(request.form)
|
|
||||||
post_data['q'] = encrypt_string(g.session_key, post_data['q'])
|
|
||||||
get_req_str = urlparse.urlencode(post_data)
|
|
||||||
return redirect(url_for('.search') + '?' + get_req_str)
|
|
||||||
|
|
||||||
search_util = Search(request, g.user_config, g.session_key)
|
|
||||||
query = search_util.new_search_query()
|
|
||||||
|
|
||||||
bang = resolve_bang(query)
|
|
||||||
if bang:
|
|
||||||
return redirect(bang)
|
|
||||||
|
|
||||||
# Redirect to home if invalid/blank search
|
|
||||||
if not query:
|
|
||||||
return redirect(url_for('.index'))
|
|
||||||
|
|
||||||
# Generate response and number of external elements from the page
|
|
||||||
try:
|
|
||||||
response = search_util.generate_response()
|
|
||||||
except TorError as e:
|
|
||||||
session['error_message'] = e.message + (
|
|
||||||
"\\n\\nTor config is now disabled!" if e.disable else "")
|
|
||||||
session['config']['tor'] = False if e.disable else session['config'][
|
|
||||||
'tor']
|
|
||||||
return redirect(url_for('.index'))
|
|
||||||
|
|
||||||
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_', '')
|
|
||||||
|
|
||||||
# removing st-card to only use whoogle time selector
|
|
||||||
soup = bsoup(response, "html.parser");
|
|
||||||
for x in soup.find_all(attrs={"id": "st-card"}):
|
|
||||||
x.replace_with("")
|
|
||||||
|
|
||||||
response = str(soup)
|
|
||||||
|
|
||||||
# Return 503 if temporarily blocked by captcha
|
|
||||||
if has_captcha(str(response)):
|
|
||||||
app.logger.error('503 (CAPTCHA)')
|
|
||||||
fallback_engine = os.environ.get('WHOOGLE_FALLBACK_ENGINE_URL', '')
|
|
||||||
if (fallback_engine):
|
|
||||||
return redirect(fallback_engine + query)
|
|
||||||
|
|
||||||
|
|
||||||
return render_template(
|
|
||||||
'error.html',
|
|
||||||
blocked=True,
|
|
||||||
error_message=translation['ratelimit'],
|
|
||||||
translation=translation,
|
|
||||||
farside='https://farside.link',
|
|
||||||
config=g.user_config,
|
|
||||||
query=urlparse.unquote(query),
|
|
||||||
params=g.user_config.to_params(keys=['preferences'])), 503
|
|
||||||
|
|
||||||
response = bold_search_terms(response, query)
|
|
||||||
|
|
||||||
# check for widgets and add if requested
|
|
||||||
if search_util.widget != '':
|
|
||||||
html_soup = bsoup(str(response), 'html.parser')
|
|
||||||
if search_util.widget == 'ip':
|
|
||||||
response = add_ip_card(html_soup, get_client_ip(request))
|
|
||||||
elif search_util.widget == 'calculator' and not 'nojs' in request.args:
|
|
||||||
response = add_calculator_card(html_soup)
|
|
||||||
|
|
||||||
# Update tabs content
|
|
||||||
tabs = get_tabs_content(app.config['HEADER_TABS'],
|
|
||||||
search_util.full_query,
|
|
||||||
search_util.search_type,
|
|
||||||
g.user_config.preferences,
|
|
||||||
translation)
|
|
||||||
|
|
||||||
# Feature to display currency_card
|
|
||||||
# Since this is determined by more than just the
|
|
||||||
# query is it not defined as a standard widget
|
|
||||||
conversion = check_currency(str(response))
|
|
||||||
if conversion:
|
|
||||||
html_soup = bsoup(str(response), 'html.parser')
|
|
||||||
response = add_currency_card(html_soup, conversion)
|
|
||||||
|
|
||||||
preferences = g.user_config.preferences
|
|
||||||
home_url = f"home?preferences={preferences}" if preferences else "home"
|
|
||||||
cleanresponse = str(response).replace("andlt;","<").replace("andgt;",">")
|
|
||||||
|
|
||||||
return render_template(
|
|
||||||
'display.html',
|
|
||||||
has_update=app.config['HAS_UPDATE'],
|
|
||||||
query=urlparse.unquote(query),
|
|
||||||
search_type=search_util.search_type,
|
|
||||||
search_name=get_search_name(search_util.search_type),
|
|
||||||
config=g.user_config,
|
|
||||||
autocomplete_enabled=autocomplete_enabled,
|
|
||||||
lingva_url=app.config['TRANSLATE_URL'],
|
|
||||||
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=cleanresponse,
|
|
||||||
version_number=app.config['VERSION_NUMBER'],
|
|
||||||
search_header=render_template(
|
|
||||||
'header.html',
|
|
||||||
home_url=home_url,
|
|
||||||
config=g.user_config,
|
|
||||||
translation=translation,
|
|
||||||
languages=app.config['LANGUAGES'],
|
|
||||||
countries=app.config['COUNTRIES'],
|
|
||||||
time_periods=app.config['TIME_PERIODS'],
|
|
||||||
logo=render_template('logo.html', dark=g.user_config.dark),
|
|
||||||
query=urlparse.unquote(query),
|
|
||||||
search_type=search_util.search_type,
|
|
||||||
mobile=g.user_request.mobile,
|
|
||||||
tabs=tabs)).replace(" ", "")
|
|
||||||
|
|
||||||
|
|
||||||
@app.route(f'/{Endpoint.config}', methods=['GET', 'POST', 'PUT'])
|
|
||||||
@session_required
|
|
||||||
@auth_required
|
|
||||||
def config():
|
|
||||||
config_disabled = (
|
|
||||||
app.config['CONFIG_DISABLE'] or
|
|
||||||
not valid_user_session(session))
|
|
||||||
|
|
||||||
name = ''
|
|
||||||
if 'name' in request.args:
|
|
||||||
name = os.path.normpath(request.args.get('name'))
|
|
||||||
if not re.match(r'^[A-Za-z0-9_.+-]+$', name):
|
|
||||||
return make_response('Invalid config name', 400)
|
|
||||||
|
|
||||||
if request.method == 'GET':
|
|
||||||
return json.dumps(g.user_config.__dict__)
|
|
||||||
elif request.method == 'PUT' and not config_disabled:
|
|
||||||
if name:
|
|
||||||
config_pkl = os.path.join(app.config['CONFIG_PATH'], name)
|
|
||||||
session['config'] = (pickle.load(open(config_pkl, 'rb'))
|
|
||||||
if os.path.exists(config_pkl)
|
|
||||||
else session['config'])
|
|
||||||
return json.dumps(session['config'])
|
|
||||||
else:
|
|
||||||
return json.dumps({})
|
|
||||||
elif not config_disabled:
|
|
||||||
config_data = request.form.to_dict()
|
|
||||||
if 'url' not in config_data or not config_data['url']:
|
|
||||||
config_data['url'] = g.user_config.url
|
|
||||||
|
|
||||||
# Save config by name to allow a user to easily load later
|
|
||||||
if 'name' in request.args:
|
|
||||||
pickle.dump(
|
|
||||||
config_data,
|
|
||||||
open(os.path.join(
|
|
||||||
app.config['CONFIG_PATH'],
|
|
||||||
name), 'wb'))
|
|
||||||
|
|
||||||
session['config'] = config_data
|
|
||||||
return redirect(config_data['url'])
|
|
||||||
else:
|
|
||||||
return redirect(url_for('.index'), code=403)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route(f'/{Endpoint.imgres}')
|
|
||||||
@session_required
|
|
||||||
@auth_required
|
|
||||||
def imgres():
|
|
||||||
return redirect(request.args.get('imgurl'))
|
|
||||||
|
|
||||||
|
|
||||||
@app.route(f'/{Endpoint.element}')
|
|
||||||
@session_required
|
|
||||||
@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
|
|
||||||
|
|
||||||
src_type = request.args.get('type')
|
|
||||||
|
|
||||||
# Ensure requested element is from a valid domain
|
|
||||||
domain = urlparse.urlparse(src_url).netloc
|
|
||||||
if not validators.domain(domain):
|
|
||||||
return send_file(io.BytesIO(empty_gif), mimetype='image/gif')
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = g.user_request.send(base_url=src_url)
|
|
||||||
|
|
||||||
# Display an empty gif if the requested element couldn't be retrieved
|
|
||||||
if response.status_code != 200 or len(response.content) == 0:
|
|
||||||
if 'favicon' in src_url:
|
|
||||||
favicon = fetch_favicon(src_url)
|
|
||||||
return send_file(io.BytesIO(favicon), mimetype='image/png')
|
|
||||||
else:
|
|
||||||
return send_file(io.BytesIO(empty_gif), mimetype='image/gif')
|
|
||||||
|
|
||||||
file_data = response.content
|
|
||||||
tmp_mem = io.BytesIO()
|
|
||||||
tmp_mem.write(file_data)
|
|
||||||
tmp_mem.seek(0)
|
|
||||||
|
|
||||||
return send_file(tmp_mem, mimetype=src_type)
|
|
||||||
except exceptions.RequestException:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return send_file(io.BytesIO(empty_gif), mimetype='image/gif')
|
|
||||||
|
|
||||||
|
|
||||||
@app.route(f'/{Endpoint.window}')
|
|
||||||
@session_required
|
|
||||||
@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)
|
|
||||||
|
|
||||||
# Ensure requested URL has a valid domain
|
|
||||||
if not validators.domain(target.netloc):
|
|
||||||
return render_template(
|
|
||||||
'error.html',
|
|
||||||
error_message='Invalid location'), 400
|
|
||||||
|
|
||||||
host_url = f'{target.scheme}://{target.netloc}'
|
|
||||||
|
|
||||||
get_body = g.user_request.send(base_url=target_url).text
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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()
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/robots.txt')
|
|
||||||
def robots():
|
|
||||||
response = make_response(
|
|
||||||
'''User-Agent: *
|
|
||||||
Disallow: /''', 200)
|
|
||||||
response.mimetype = 'text/plain'
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/')
|
|
||||||
def favicon():
|
|
||||||
return app.send_static_file('img/favicon.ico')
|
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(Exception)
|
|
||||||
def internal_error(e):
|
|
||||||
query = ''
|
|
||||||
if request.method == 'POST':
|
|
||||||
query = request.form.get('q')
|
|
||||||
else:
|
|
||||||
query = request.args.get('q')
|
|
||||||
|
|
||||||
# Attempt to parse the query
|
|
||||||
try:
|
|
||||||
search_util = Search(request, g.user_config, g.session_key)
|
|
||||||
query = search_util.new_search_query()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
print(traceback.format_exc(), file=sys.stderr)
|
|
||||||
|
|
||||||
fallback_engine = os.environ.get('WHOOGLE_FALLBACK_ENGINE_URL', '')
|
|
||||||
if (fallback_engine):
|
|
||||||
return redirect(fallback_engine + query)
|
|
||||||
|
|
||||||
localization_lang = g.user_config.get_localization_lang()
|
|
||||||
translation = app.config['TRANSLATIONS'][localization_lang]
|
|
||||||
|
|
||||||
# Itt kapja meg a hiba részleteit, és ezeket fogja megjeleníteni.
|
|
||||||
error_detail = str(e) # Konvertálja a hiba objektumot szöveges formátumba
|
|
||||||
|
|
||||||
return render_template(
|
|
||||||
'error.html',
|
|
||||||
error_message=f'Internal server error (500): {error_detail}', # Ide kerül a pontos hibaüzenet
|
|
||||||
translation=translation,
|
|
||||||
farside='https://farside.link',
|
|
||||||
config=g.user_config,
|
|
||||||
query=unquote(query),
|
|
||||||
params=g.user_config.to_params(keys=['preferences'])), 500
|
|
||||||
|
|
||||||
|
|
||||||
def run_app() -> None:
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description='RaveSearch console runner')
|
|
||||||
parser.add_argument(
|
|
||||||
'--port',
|
|
||||||
default=5000,
|
|
||||||
metavar='<port number>',
|
|
||||||
help='Specifies a port to run on (default 5000)')
|
|
||||||
parser.add_argument(
|
|
||||||
'--host',
|
|
||||||
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(
|
|
||||||
'--unix-socket-perms',
|
|
||||||
default='600',
|
|
||||||
metavar='<octal permissions>',
|
|
||||||
help='Octal permissions to use for the Unix domain socket (default 600)')
|
|
||||||
parser.add_argument(
|
|
||||||
'--debug',
|
|
||||||
default=False,
|
|
||||||
action='store_true',
|
|
||||||
help='Activates debug mode for the server (default False)')
|
|
||||||
parser.add_argument(
|
|
||||||
'--https-only',
|
|
||||||
default=False,
|
|
||||||
action='store_true',
|
|
||||||
help='Enforces HTTPS redirects for all requests')
|
|
||||||
parser.add_argument(
|
|
||||||
'--userpass',
|
|
||||||
default='',
|
|
||||||
metavar='<username:password>',
|
|
||||||
help='Sets a username/password basic auth combo (default None)')
|
|
||||||
parser.add_argument(
|
|
||||||
'--proxyauth',
|
|
||||||
default='',
|
|
||||||
metavar='<username:password>',
|
|
||||||
help='Sets a username/password for a HTTP/SOCKS proxy (default None)')
|
|
||||||
parser.add_argument(
|
|
||||||
'--proxytype',
|
|
||||||
default='',
|
|
||||||
metavar='<socks4|socks5|http>',
|
|
||||||
help='Sets a proxy type for all connections (default None)')
|
|
||||||
parser.add_argument(
|
|
||||||
'--proxyloc',
|
|
||||||
default='',
|
|
||||||
metavar='<location:port>',
|
|
||||||
help='Sets a proxy location for all connections (default None)')
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
if args.userpass:
|
|
||||||
user_pass = args.userpass.split(':')
|
|
||||||
os.environ['WHOOGLE_USER'] = user_pass[0]
|
|
||||||
os.environ['WHOOGLE_PASS'] = user_pass[1]
|
|
||||||
|
|
||||||
if args.proxytype and args.proxyloc:
|
|
||||||
if args.proxyauth:
|
|
||||||
proxy_user_pass = args.proxyauth.split(':')
|
|
||||||
os.environ['WHOOGLE_PROXY_USER'] = proxy_user_pass[0]
|
|
||||||
os.environ['WHOOGLE_PROXY_PASS'] = proxy_user_pass[1]
|
|
||||||
os.environ['WHOOGLE_PROXY_TYPE'] = args.proxytype
|
|
||||||
os.environ['WHOOGLE_PROXY_LOC'] = args.proxyloc
|
|
||||||
|
|
||||||
if args.https_only:
|
|
||||||
os.environ['HTTPS_ONLY'] = '1'
|
|
||||||
|
|
||||||
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, unix_socket_perms=args.unix_socket_perms)
|
|
||||||
else:
|
|
||||||
waitress.serve(
|
|
||||||
app,
|
|
||||||
listen="{}:{}".format(args.host, args.port),
|
|
||||||
url_prefix=os.environ.get('WHOOGLE_URL_PREFIX', ''))
|
|
BIN
app/static/.DS_Store
vendored
|
@ -1,14 +0,0 @@
|
||||||
{
|
|
||||||
"!i": {
|
|
||||||
"url": "search?q={}&tbm=isch",
|
|
||||||
"suggestion": "!i (Whoogle Images)"
|
|
||||||
},
|
|
||||||
"!v": {
|
|
||||||
"url": "search?q={}&tbm=vid",
|
|
||||||
"suggestion": "!v (Whoogle Videos)"
|
|
||||||
},
|
|
||||||
"!n": {
|
|
||||||
"url": "search?q={}&tbm=nws",
|
|
||||||
"suggestion": "!n (Whoogle News)"
|
|
||||||
}
|
|
||||||
}
|
|
2
app/static/build/.gitignore
vendored
|
@ -1,2 +0,0 @@
|
||||||
*
|
|
||||||
!.gitignore
|
|
|
@ -1,864 +0,0 @@
|
||||||
:root {
|
|
||||||
/* DARK THEME COLORS */
|
|
||||||
--primary: #ed0f59;
|
|
||||||
--primary-active: #bc134b;
|
|
||||||
--primary-grad2: #e24c41;
|
|
||||||
--grad2-active: #a53028;
|
|
||||||
--primary-grad3: #dc7541;
|
|
||||||
--grad3-active: #a34e24;
|
|
||||||
--body-bg: #0a0c0d;
|
|
||||||
--body-color: #e4e8ea;
|
|
||||||
--ad-bg: #282e31;
|
|
||||||
--gray-200: #1b1f21;
|
|
||||||
--gray-300: #23292c;
|
|
||||||
--gray-400: #282e31;
|
|
||||||
--gray-800: #9aa2a6;
|
|
||||||
--gray-900: #50585c;
|
|
||||||
--gray-950: #31373a;
|
|
||||||
--danger: #ff3333;
|
|
||||||
--danger-active: #e52e2e;
|
|
||||||
|
|
||||||
--visited: #8e7263;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/* :root {
|
|
||||||
/* DARK THEME COLORS */
|
|
||||||
/*--primary: #105d5e;
|
|
||||||
--primary-active: #137071;
|
|
||||||
--primary-grad2: #009a6e;
|
|
||||||
--grad2-active: #00af7e;
|
|
||||||
--primary-grad3: #b3eda9;
|
|
||||||
--grad3-active: #d0fdc8;
|
|
||||||
--primary-grad4: #ebfadb;
|
|
||||||
--ad-bg: #293e33;
|
|
||||||
--silver-bg: #767f7d;
|
|
||||||
--silver-light-bg: #c2cbc9;
|
|
||||||
--active: #e8e300;
|
|
||||||
--body-bg: #0a0c0d;
|
|
||||||
--body-color: #e4e8ea;
|
|
||||||
--body-color: #edf3f5;
|
|
||||||
|
|
||||||
--gray-200: #1b1f21;
|
|
||||||
--gray-300: #23292c;
|
|
||||||
--gray-400: #282e31;
|
|
||||||
--gray-800: #9aa2a6;
|
|
||||||
--gray-900: #50585c;
|
|
||||||
--gray-950: #31373a;
|
|
||||||
--danger: #ff3333;
|
|
||||||
--danger-active: #e52e2e;
|
|
||||||
} */
|
|
||||||
|
|
||||||
html {
|
|
||||||
background: var(--body-bg) !important;
|
|
||||||
color: var(--body-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background:var(--body-bg) !important;
|
|
||||||
color: var(--body-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
div {
|
|
||||||
color: var(--body-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* label {
|
|
||||||
color: var(--whoogle-dark-contrast-text) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
li a {
|
|
||||||
color: var(--whoogle-dark-result-url) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
color: var(--whoogle-dark-text) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.anon-view {
|
|
||||||
color: var(--bo) !important;
|
|
||||||
padding: 30px;
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* textarea {
|
|
||||||
background: rgb(29, 29, 29) !important;
|
|
||||||
color: var(--whoogle-dark-text) !important;
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* Látogatott találat címke */
|
|
||||||
a:visited h3 div {
|
|
||||||
color: var(--visited) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Találat címke */
|
|
||||||
a:link h3 div {
|
|
||||||
color: var(--primary-grad3) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:link div {
|
|
||||||
color: var(--body-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Találat címke hover */
|
|
||||||
a:link div:hover {
|
|
||||||
color: var(--grad3-active) !important;
|
|
||||||
text-decoration: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:link div:visited {
|
|
||||||
color: var(--visited) !important;
|
|
||||||
text-decoration: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
text-decoration: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Wikipedia cím */
|
|
||||||
.lU7jec h3 {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: bold;
|
|
||||||
line-height: 26px;
|
|
||||||
color: var(--primary-grad3) !important;
|
|
||||||
margin: 0 0 24px 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* wikipedia favicon */
|
|
||||||
.BNeawe.s3v9rd.AP7Wnd.has-favicon > .BNeawe.has-favicon > img.site-favicon {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Helyek favicon */
|
|
||||||
div > .other-results > .ZINbbc.xpd.Et0od.pkph0e.has-favicon > .has-favicon > img.site-favicon {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tAd8D.AP7Wnd {
|
|
||||||
color: var(--body-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tAd8D.AP7Wnd {
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
div span {
|
|
||||||
color: var(--primary-grad3) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
background-color: var(--gray-300) !important;
|
|
||||||
color: var(--body-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:focus-visible {
|
|
||||||
border: 1px solid var(--primary-active) !important;
|
|
||||||
outline: none;
|
|
||||||
border-radius: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
background: transparent !important;
|
|
||||||
color: var(--body-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------------------------------------- */
|
|
||||||
|
|
||||||
.autocomplete {
|
|
||||||
background-color: var(--gray-300) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.autocomplete:hover {
|
|
||||||
background-image: linear-gradient(90deg, var(--primary), var(--primary));
|
|
||||||
background-clip: padding-box, border-box;
|
|
||||||
background-origin: border-box;
|
|
||||||
border: 1px solid var(--primary-grad3);
|
|
||||||
border-radius: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.autocomplete:focus {
|
|
||||||
background-image: linear-gradient(90deg, var(--primary), var(--primary));
|
|
||||||
background-clip: padding-box, border-box;
|
|
||||||
background-origin: border-box;
|
|
||||||
border: 1px solid var(--primary-grad3);
|
|
||||||
border-radius: 100px;
|
|
||||||
}
|
|
||||||
/* Keresőgomb */
|
|
||||||
#search-submit {
|
|
||||||
color: var(--body-color) !important;
|
|
||||||
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-grad2) 60%, var(--primary-grad3) 100%) !important;
|
|
||||||
border: 1px solid var(--grad3-active) !important;
|
|
||||||
border-radius: 100px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#search-submit:hover {
|
|
||||||
color: var(--body-color) !important;
|
|
||||||
background: linear-gradient(200deg, var(--primary) 0%, var(--primary-grad2) 60%, var(--primary-grad3) 100%) !important;
|
|
||||||
border: 1px solid var(--grad3-active) !important;
|
|
||||||
border-radius: 100px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.kattide {
|
|
||||||
color: var(--body-color) !important;
|
|
||||||
background: linear-gradient(45deg, var(--primary) 0%, var(--primary-grad2) 80%, var(--primary-grad3) 100%);
|
|
||||||
padding-left: 0.8rem;
|
|
||||||
padding-right: 0.8rem;
|
|
||||||
-webkit-box-decoration-break: clone;
|
|
||||||
box-decoration-break: clone;
|
|
||||||
}
|
|
||||||
|
|
||||||
.kattide:hover {
|
|
||||||
color: white !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Keresőkategóriák megjelenítése */
|
|
||||||
.selected-header-button,
|
|
||||||
button.selected-header-button:active,
|
|
||||||
button.selected-header-button:focus-within {
|
|
||||||
color: var(--primary-grad3) !important;
|
|
||||||
border-bottom: 2px solid var(--primary-grad2) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-button:hover {
|
|
||||||
color: var(--primary) !important;
|
|
||||||
border-bottom: 2px solid var(--primary) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Keresési találat div keret */
|
|
||||||
#main > div:focus-within {
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 0 6px 1px var(--primary-grad3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ZINbbc {
|
|
||||||
background-color: #29292987 !important;
|
|
||||||
overflow: hidden;
|
|
||||||
margin-bottom: 25px !important;
|
|
||||||
padding: 15px !important;
|
|
||||||
border-radius: 8px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ezeket keresték még */
|
|
||||||
.gGQDvd {
|
|
||||||
padding: 10px 0 0 10px !important;
|
|
||||||
height: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.kjGX2 {
|
|
||||||
left: 40px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toMBf {
|
|
||||||
left: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 601px) {
|
|
||||||
.zBAuLc {
|
|
||||||
line-height: normal;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0 0 0 0 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.s3v9rd {
|
|
||||||
padding: 8px 0 0 0 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.j039Wc {
|
|
||||||
padding-top: 34px !important;
|
|
||||||
margin-bottom: -14px !important;
|
|
||||||
}
|
|
||||||
/* Keresés tartalom */
|
|
||||||
.s3v9rd {
|
|
||||||
font-size: 16px;
|
|
||||||
padding: 10px 0 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bRsWnc {
|
|
||||||
background-color: var(--whoogle-dark-result-bg) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.x54gtf {
|
|
||||||
background-color: var(--whoogle-dark-divider) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Q0HXG {
|
|
||||||
background-color: var(--whoogle-dark-divider) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.LKSyXe {
|
|
||||||
background-color: var(--whoogle-dark-divider) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tAd8D {
|
|
||||||
padding: 10px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sa1toc {
|
|
||||||
background: var(--whoogle-dark-page-bg) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qFvlD {
|
|
||||||
border: 1px solid !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xeDNfc {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.info-text {
|
|
||||||
color: var(--whoogle-dark-contrast-text) !important;
|
|
||||||
opacity: 75%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.collapsible {
|
|
||||||
color: var(--whoogle-dark-text) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.collapsible:after {
|
|
||||||
color: var(--whoogle-dark-text) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.active {
|
|
||||||
background: transparent !important;
|
|
||||||
border: 2px solid grey !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content,
|
|
||||||
.result-config {
|
|
||||||
background-color: transparent !important;
|
|
||||||
color: var(--whoogle-contrast-text) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.active:after {
|
|
||||||
color: var(--whoogle-dark-contrast-text) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link {
|
|
||||||
color: var(--whoogle-dark-contrast-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-color {
|
|
||||||
color: var(--whoogle-dark-result-url) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Keresési ajánlások */
|
|
||||||
.autocomplete-items {
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.autocomplete-items div {
|
|
||||||
color: var(--body-color);
|
|
||||||
background-color: rgb(51, 51, 51);
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.autocomplete-items div:first-child {
|
|
||||||
border-top-left-radius: 10px;
|
|
||||||
border-top-right-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.autocomplete-items div:last-child {
|
|
||||||
border-bottom-left-radius: 10px;
|
|
||||||
border-bottom-right-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.autocomplete-items div:hover {
|
|
||||||
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-grad2) 60%, var(--primary-grad3) 100%) !important;
|
|
||||||
color: var(--body-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.autocomplete-active {
|
|
||||||
background: linear-gradient(200deg, var(--primary) 0%, var(--primary-grad2) 60%, var(--primary-grad3) 100%) !important;
|
|
||||||
color: var(--body-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
margin-bottom: 30px;
|
|
||||||
color: var(--whoogle-dark-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-div {
|
|
||||||
margin: 0 0 30px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Keresés feletti mini url */
|
|
||||||
.UPmit.AP7Wnd {
|
|
||||||
color: #d0bc8e !important;
|
|
||||||
font-size: 0.8em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lcJF1d {
|
|
||||||
margin: 25px 18px 18px 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.egMi0 {
|
|
||||||
margin: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qN9Ked {
|
|
||||||
display: table-caption !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.kCrYT {
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.BsXmcf {
|
|
||||||
position: relative !important;
|
|
||||||
height: 0 !important;
|
|
||||||
background: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Q6Xouf {
|
|
||||||
max-width: 555px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Helyek cím */
|
|
||||||
.deIvCb {
|
|
||||||
font-weight: 600 !important;
|
|
||||||
font-size: 18px !important;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nGphre > .site-favicon {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Képtalálatok */
|
|
||||||
.results {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 16px;
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
.result-item {
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
background-color: #29292987;
|
|
||||||
width: calc(33.333% - 16px);
|
|
||||||
box-sizing: border-box;
|
|
||||||
text-align: center;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-item img {
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-item p {
|
|
||||||
margin: 10px 0 0;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.img-title {
|
|
||||||
color: var(--primary-grad3);
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.img-url {
|
|
||||||
color: var(--body-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.img-url:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
color: var(--grad2-active);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Stílusok a javaslatokhoz */
|
|
||||||
.suggestions {
|
|
||||||
margin: 20px 0;
|
|
||||||
padding: 10px;
|
|
||||||
background-color: #e9e9e9;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Stílusok a lapozáshoz */
|
|
||||||
.pagination {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination a {
|
|
||||||
margin: 0 5px;
|
|
||||||
padding: 10px 15px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
background-color: #fff;
|
|
||||||
text-decoration: none;
|
|
||||||
color: #007bff;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination a:hover {
|
|
||||||
background-color: #007bff;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.e3goi {
|
|
||||||
padding: 10px;
|
|
||||||
width: 100%;
|
|
||||||
height: 160px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.TxbwNb {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.RAyV4b {
|
|
||||||
width: 100%;
|
|
||||||
height: 130px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.t0fcAb {
|
|
||||||
scale: 1.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Tor4Ec {
|
|
||||||
text-align: center;
|
|
||||||
width: 100%;
|
|
||||||
height: 20px;
|
|
||||||
display: block;
|
|
||||||
padding: 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qXLe6d {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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 {
|
|
||||||
width: 350px;
|
|
||||||
display: flex;
|
|
||||||
border: none;
|
|
||||||
color: #70757a;
|
|
||||||
font-size: 14px;
|
|
||||||
text-align: center;
|
|
||||||
/* background-color: #29292987; */
|
|
||||||
border-radius: 8px;
|
|
||||||
margin: 35px auto;
|
|
||||||
padding: 10px;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.frGj1b {
|
|
||||||
margin: 10px;
|
|
||||||
font-size: 16px;
|
|
||||||
border: 1px solid;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 10px 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.BnJWBc {
|
|
||||||
text-align: center;
|
|
||||||
padding: 6px 0 13px 0;
|
|
||||||
height: 35px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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%;
|
|
||||||
height: 100%;
|
|
||||||
background-color: #29292987;
|
|
||||||
border-radius: 10px;
|
|
||||||
margin: 10px 0;
|
|
||||||
display: grid;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 10px 0;
|
|
||||||
}
|
|
||||||
.ezO2md {
|
|
||||||
border: thin solid #dadce0;
|
|
||||||
padding: 12px 16px 12px 16px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
.K35ahc {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.owohpf {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.fYyStc {
|
|
||||||
word-break: break-word;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 16px;
|
|
||||||
padding: 0 0 10px 0;
|
|
||||||
}
|
|
||||||
.img_button {
|
|
||||||
padding: 10px;
|
|
||||||
margin: 0 auto 10px;
|
|
||||||
border: 1px solid #84dbff;
|
|
||||||
border-radius: 6px;
|
|
||||||
color: #3c4043;
|
|
||||||
background-color: #2e97c3;
|
|
||||||
}
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
.NHQNef {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
.Cb8Z7c {
|
|
||||||
white-space: pre;
|
|
||||||
}
|
|
||||||
a.ZWRArf {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
a .CVA68e:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
html {
|
|
||||||
font-size: 1.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1000px) {
|
|
||||||
html {
|
|
||||||
font-size: 3rem;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,284 +0,0 @@
|
||||||
html {
|
|
||||||
font-family: "Ubuntu", sans-serif !important;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 20px;
|
|
||||||
color: #3c4043;
|
|
||||||
word-wrap: break-word;
|
|
||||||
width: 100%;
|
|
||||||
display: grid;
|
|
||||||
justify-content: center;
|
|
||||||
align-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-search {
|
|
||||||
max-width: 40rem;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
.mobile-logo {
|
|
||||||
width: 100% !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
margin: 5px 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-link,
|
|
||||||
.logo-letter {
|
|
||||||
text-decoration: none !important;
|
|
||||||
letter-spacing: -1px;
|
|
||||||
text-align: center;
|
|
||||||
border-radius: 2px 0 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-config {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-logo {
|
|
||||||
font: 22px/36px Futura, Arial, sans-serif;
|
|
||||||
padding-left: 5px;
|
|
||||||
/* display: flex; */
|
|
||||||
width: 10rem;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-div {
|
|
||||||
margin-top: 20px;
|
|
||||||
letter-spacing: -1px;
|
|
||||||
text-align: center;
|
|
||||||
font: 22pt Futura, Arial, sans-serif;
|
|
||||||
padding: 10px 0 5px 0;
|
|
||||||
height: 37px;
|
|
||||||
font-smoothing: antialiased;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-bar-desktop {
|
|
||||||
border-radius: 8px 8px 0 0;
|
|
||||||
height: 40px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-div {
|
|
||||||
margin-top: 30px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-form {
|
|
||||||
display: contents;
|
|
||||||
width: 100%;
|
|
||||||
margin: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input {
|
|
||||||
background: none;
|
|
||||||
margin: 2px 4px 2px 8px;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracking-link {
|
|
||||||
font-size: large;
|
|
||||||
text-align: center;
|
|
||||||
margin: 15px;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#mobile-header-logo {
|
|
||||||
height: 1.75em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-input-div {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-search-bar {
|
|
||||||
display: block;
|
|
||||||
font-size: 16px;
|
|
||||||
padding: 0 0 0 20px;
|
|
||||||
margin-right: -25px;
|
|
||||||
-webkit-box-flex: 1;
|
|
||||||
outline: none;
|
|
||||||
width: 100%;
|
|
||||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
|
||||||
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: var(--primary-grad2) !important;
|
|
||||||
text-decoration: none;
|
|
||||||
tap-highlight-color: rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
color: var(--grad2-active) !important;
|
|
||||||
text-decoration: none;
|
|
||||||
text-decoration-line: none;
|
|
||||||
tap-highlight-color: rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-tab-div {
|
|
||||||
overflow: auto;
|
|
||||||
margin: 30px 0 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.desktop-header {
|
|
||||||
height: 39px;
|
|
||||||
display: box;
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-tab {
|
|
||||||
box-pack: justify;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 37px;
|
|
||||||
justify-content: space-evenly;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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 {
|
|
||||||
color: #4285f4;
|
|
||||||
font-weight: bold;
|
|
||||||
background-color: #1d1d1d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-header {
|
|
||||||
height: 65px;
|
|
||||||
display: table;
|
|
||||||
margin: 0 auto;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-header a,
|
|
||||||
.mobile-header span {
|
|
||||||
color: #70757a;
|
|
||||||
text-decoration: none;
|
|
||||||
display: table-cell;
|
|
||||||
word-spacing: 30px;
|
|
||||||
height: auto;
|
|
||||||
/* padding: 8px 12px 8px 12px; */
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.mobile-header a,
|
|
||||||
.mobile-header span {
|
|
||||||
padding: 0 22px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.adv-search {
|
|
||||||
font-size: 30px;
|
|
||||||
margin: 0 0 8px 0;
|
|
||||||
vertical-align: sub;
|
|
||||||
}
|
|
||||||
|
|
||||||
.adv-search:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
#adv-search-toggle {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-collapsible {
|
|
||||||
max-height: 0px;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: max-height 0.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;
|
|
||||||
}
|
|
||||||
.header-tab-div-2 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 802px) {
|
|
||||||
.header-tab-div {
|
|
||||||
margin-bottom: 10px !important;
|
|
||||||
}
|
|
||||||
.header-tab-div-2 {
|
|
||||||
margin: 50px 20px 0 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.header-tab {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,48 +0,0 @@
|
||||||
#search-bar {
|
|
||||||
height: 48px !important;
|
|
||||||
background: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#search-reset {
|
|
||||||
all: unset;
|
|
||||||
margin-left: -40px;
|
|
||||||
text-align: center;
|
|
||||||
background-color: transparent !important;
|
|
||||||
cursor: pointer;
|
|
||||||
height: 45px;
|
|
||||||
width: 65px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 300px) {
|
|
||||||
#search-reset {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
|
@ -1,205 +0,0 @@
|
||||||
html {
|
|
||||||
background: var(--whoogle-page-bg) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background: var(--whoogle-page-bg) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
div {
|
|
||||||
color: var(--whoogle-text) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
color: var(--whoogle-contrast-text) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
li a {
|
|
||||||
color: var(--whoogle-result-url) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
background: var(--whoogle-page-bg) !important;
|
|
||||||
color: var(--whoogle-text) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bRsWnc {
|
|
||||||
background-color: var(--whoogle-result-bg) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.x54gtf {
|
|
||||||
background-color: var(--whoogle-divider) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Q0HXG {
|
|
||||||
background-color: var(--whoogle-divider) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.LKSyXe {
|
|
||||||
background-color: var(--whoogle-divider) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
a:visited h3 div {
|
|
||||||
color: var(--whoogle-result-visited) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:link h3 div {
|
|
||||||
color: var(--whoogle-result-title) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:link div {
|
|
||||||
color: var(--whoogle-result-url) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
div span {
|
|
||||||
color: var(--whoogle-secondary-text) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
background-color: var(--whoogle-page-bg) !important;
|
|
||||||
color: var(--whoogle-text) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#search-bar {
|
|
||||||
color: var(--whoogle-text) !important;
|
|
||||||
background-color: var(--whoogle-page-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-search {
|
|
||||||
border-color: var(--whoogle-element-bg) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-container {
|
|
||||||
background-color: var(--whoogle-page-bg) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#search-submit {
|
|
||||||
border: 1px solid var(--whoogle-element-bg) !important;
|
|
||||||
background: var(--whoogle-element-bg) !important;
|
|
||||||
color: var(--whoogle-contrast-text) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-text {
|
|
||||||
color: var(--whoogle-contrast-text) !important;
|
|
||||||
opacity: 75%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.collapsible {
|
|
||||||
color: var(--whoogle-text) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.collapsible:after {
|
|
||||||
color: var(--whoogle-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.active {
|
|
||||||
background-color: var(--whoogle-element-bg) !important;
|
|
||||||
color: var(--whoogle-contrast-text) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content, .result-config {
|
|
||||||
background-color: var(--whoogle-element-bg) !important;
|
|
||||||
color: var(--whoogle-contrast-text) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.active:after {
|
|
||||||
color: var(--whoogle-contrast-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.link {
|
|
||||||
color: var(--whoogle-element-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-color {
|
|
||||||
color: var(--whoogle-result-url) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.autocomplete-items {
|
|
||||||
border: 1px solid var(--whoogle-element-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.autocomplete-items div {
|
|
||||||
background-color: var(--whoogle-page-bg);
|
|
||||||
border-bottom: 1px solid var(--whoogle-element-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.autocomplete-items div:hover {
|
|
||||||
background-color: var(--whoogle-element-bg);
|
|
||||||
color: var(--whoogle-contrast-text) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.autocomplete-active {
|
|
||||||
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;
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
.cls-1 {
|
|
||||||
fill: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
height: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
height: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1000px) {
|
|
||||||
svg {
|
|
||||||
margin-top: 0.3em;
|
|
||||||
height: 70%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.rplogo {
|
|
||||||
max-width: 40rem;
|
|
||||||
display: block;
|
|
||||||
margin: 30px auto;
|
|
||||||
position: relative;
|
|
||||||
border-radius: inherit;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
|
@ -1,380 +0,0 @@
|
||||||
@font-face {
|
|
||||||
font-family: "Ubuntu";
|
|
||||||
src: url("fonts/ubuntu.woff2") format("woff");
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: "Ubuntu", sans-serif !important;
|
|
||||||
max-width: 900px !important;
|
|
||||||
min-width: 0 !important;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin: 25px auto !important;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 400;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
body,
|
|
||||||
main {
|
|
||||||
margin: 0;
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
clear: both;
|
|
||||||
min-height: 4rem;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
display: block;
|
|
||||||
overflow: hidden;
|
|
||||||
margin-top: 45px;
|
|
||||||
text-wrap: balance;
|
|
||||||
}
|
|
||||||
.logo-container {
|
|
||||||
background: no-repeat;
|
|
||||||
min-height: 4rem;
|
|
||||||
background-position: center;
|
|
||||||
background-size: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-search {
|
|
||||||
background: transparent !important;
|
|
||||||
}
|
|
||||||
::placeholder {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: inherit;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-column {
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: repeat(2, 1fr);
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
gap: 1.5rem;
|
|
||||||
grid-auto-flow: dense;
|
|
||||||
margin-top: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 500px){
|
|
||||||
.social-column {
|
|
||||||
grid-template-columns: repeat(1, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.rpkick {
|
|
||||||
grid-row-end: span 2;
|
|
||||||
grid-column-end: span 1;
|
|
||||||
height: 50px;
|
|
||||||
width: 200px;
|
|
||||||
padding: 0 10px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.04);
|
|
||||||
border-radius: 10px;
|
|
||||||
transition: background-color 0.3s ease-out 0.3ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rpkick:hover {
|
|
||||||
background-color: rgba(83, 250, 22, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rptwitch {
|
|
||||||
grid-row-end: span 2;
|
|
||||||
grid-column-end: span 1;
|
|
||||||
height: 50px;
|
|
||||||
width: 200px;
|
|
||||||
padding: 0 10px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.04);
|
|
||||||
border-radius: 10px;
|
|
||||||
transition: background-color 0.3s ease-out 0.3ms;
|
|
||||||
}
|
|
||||||
.rptwitch:hover {
|
|
||||||
background-color: rgba(101, 66, 166, 0.14);
|
|
||||||
}
|
|
||||||
.rpdiscord {
|
|
||||||
grid-row-end: span 2;
|
|
||||||
grid-column-end: span 1;
|
|
||||||
height: 50px;
|
|
||||||
width: 200px;
|
|
||||||
padding: 0 10px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
||||||
border-radius: 10px;
|
|
||||||
transition: background-color 0.3s ease-out 0.3ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rpdiscord:hover {
|
|
||||||
background-color: rgba(98, 102, 243, 0.08);
|
|
||||||
}
|
|
||||||
.rpgit {
|
|
||||||
grid-row-end: span 2;
|
|
||||||
grid-column-end: span 1;
|
|
||||||
height: 50px;
|
|
||||||
width: 200px;
|
|
||||||
padding: 0 10px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.04);
|
|
||||||
border-radius: 10px;
|
|
||||||
transition: background-color 0.3s ease-out 0.3ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rpgit:hover {
|
|
||||||
background-color: rgba(204, 204, 204, 0.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rpyoutube {
|
|
||||||
grid-row-end: span 2;
|
|
||||||
grid-column-end: span 1;
|
|
||||||
height: 50px;
|
|
||||||
width: 200px;
|
|
||||||
padding: 0 10px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.04);
|
|
||||||
border-radius: 10px;
|
|
||||||
transition: background-color 0.3s ease-out 0.3ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rpyoutube:hover {
|
|
||||||
background-color: rgba(129, 0, 0, 0.054);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rpweb {
|
|
||||||
grid-row-end: span 2;
|
|
||||||
grid-column-end: span 1;
|
|
||||||
height: 50px;
|
|
||||||
width: 200px;
|
|
||||||
padding: 0 10px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.04);
|
|
||||||
border-radius: 10px;
|
|
||||||
transition: background-color 0.3s ease-out 0.3ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rpweb:hover {
|
|
||||||
background-color: rgba(251, 255, 33, 0.049);
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-container {
|
|
||||||
margin-top: 26vh;
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
flex: 1;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#search-form,
|
|
||||||
#search-fields {
|
|
||||||
max-width: 650px;
|
|
||||||
margin: 0 auto;
|
|
||||||
background: inherit;
|
|
||||||
border: inherit;
|
|
||||||
padding: 0;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-items {
|
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
#search-bar {
|
|
||||||
width: 95%;
|
|
||||||
margin: 0 auto;
|
|
||||||
height: 3rem;
|
|
||||||
display: inline-flex;
|
|
||||||
flex-direction: row;
|
|
||||||
white-space: nowrap;
|
|
||||||
font-size: 24px;
|
|
||||||
border: none !important;
|
|
||||||
border-radius: 100px;
|
|
||||||
padding: 0 0 0 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#search-submit {
|
|
||||||
display: inline-flex;
|
|
||||||
flex-direction: row;
|
|
||||||
white-space: nowrap;
|
|
||||||
margin: 0.5rem auto;
|
|
||||||
cursor: pointer;
|
|
||||||
width: 100%;
|
|
||||||
height: 3rem;
|
|
||||||
font-size: 18px;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-options {
|
|
||||||
max-height: 370px;
|
|
||||||
overflow-y: scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-buttons {
|
|
||||||
max-height: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-div {
|
|
||||||
margin-top: 10px;
|
|
||||||
padding: 15px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
button::-moz-focus-inner {
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.collapsible {
|
|
||||||
background-color: rgba(0, 0, 0, 0);
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 10px 35px 10px 35px;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 686px;
|
|
||||||
border: none;
|
|
||||||
text-align: left;
|
|
||||||
outline: none;
|
|
||||||
font-size: 15px;
|
|
||||||
border-radius: 10px 10px 0 0;
|
|
||||||
margin: 1.5rem 0 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.collapsible:after {
|
|
||||||
content: "\002B";
|
|
||||||
font-weight: bold;
|
|
||||||
float: right;
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.active:after {
|
|
||||||
content: "\2212";
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
max-width: 646px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 18px;
|
|
||||||
max-height: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: max-height 0.2s ease-out;
|
|
||||||
border-radius: 0 0 10px 10px;
|
|
||||||
border-left: 2px solid grey;
|
|
||||||
border-right: 2px solid grey;
|
|
||||||
}
|
|
||||||
|
|
||||||
.open {
|
|
||||||
padding-top: 20px;
|
|
||||||
padding-bottom: 60px;
|
|
||||||
border-bottom: 2px solid grey;
|
|
||||||
background-color: #1d1d1d !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-text {
|
|
||||||
font-style: italic;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#config-style {
|
|
||||||
resize: none;
|
|
||||||
width: 100%;
|
|
||||||
height: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.whoogle-logo {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.whoogle-svg {
|
|
||||||
width: 80%;
|
|
||||||
height: initial;
|
|
||||||
display: block;
|
|
||||||
margin: auto;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.autocomplete {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0 auto;
|
|
||||||
color: #ffffff !important;
|
|
||||||
border: 1px solid #ffffff29;
|
|
||||||
border-radius: 100px;
|
|
||||||
box-shadow: 0 2px 16px 0 rgba(5, 5, 6, 15%), 0 4px 8px 0 rgba(5, 5, 6, 35%);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/* .autocomplete:hover {
|
|
||||||
border: 1px solid #2e97c3;
|
|
||||||
box-shadow: 0 1px 4px 0 #2e97c3, 0 1px 2px 0 #2e97c3;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
.autocomplete-items {
|
|
||||||
width: 650px;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 99;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: #1d1d1d;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: opacity 0s ease 0s, visibility 0s ease 0s, all 0.5s ease 0s;
|
|
||||||
opacity: 1;
|
|
||||||
visibility: visible;
|
|
||||||
animation-delay: 0.1s;
|
|
||||||
text-align: left;
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.autocomplete-items div {
|
|
||||||
padding: 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
details summary {
|
|
||||||
padding: 10px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mobile styles */
|
|
||||||
|
|
||||||
|
|
||||||
.BNeawe.UPmit.AP7Wnd.lRVwie {
|
|
||||||
font-variant: all-small-caps;
|
|
||||||
text-wrap: balance;
|
|
||||||
}
|
|
||||||
|
|
||||||
.w1C3Le,
|
|
||||||
.BmP5tf,
|
|
||||||
.G5NbBd,
|
|
||||||
.CS4w5b {
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lcJF1d {
|
|
||||||
margin-left: 30px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.egMi0 {
|
|
||||||
margin: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-bottom {
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lRVwie {
|
|
||||||
text-wrap: balance !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -1,185 +0,0 @@
|
||||||
body {
|
|
||||||
max-width: 900px !important;
|
|
||||||
min-width: 0 !important;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin: 25px auto !important;
|
|
||||||
color: white;
|
|
||||||
padding: 0 8px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vvjwJb {
|
|
||||||
font-size: 20px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.autocomplete {
|
|
||||||
position: relative;
|
|
||||||
display: -webkit-box;
|
|
||||||
width: 100%;
|
|
||||||
margin: 0 auto;
|
|
||||||
color: #ffffff !important;
|
|
||||||
border: 1px solid #ffffff29;
|
|
||||||
border-radius: 100px;
|
|
||||||
box-shadow: 0 2px 16px 0 rgba(5, 5, 6, 15%), 0 4px 8px 0 rgba(5, 5, 6, 35%);
|
|
||||||
background: #1d1d1d !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.autocomplete-items {
|
|
||||||
position: absolute;
|
|
||||||
border-bottom: none;
|
|
||||||
border-top: none;
|
|
||||||
z-index: 99;
|
|
||||||
|
|
||||||
/*position the autocomplete items to be the same width as the container:*/
|
|
||||||
top: 100%;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.autocomplete-items div {
|
|
||||||
padding: 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
details summary {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-favicon {
|
|
||||||
float: left;
|
|
||||||
width: 18px;
|
|
||||||
padding: 0 5px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.site-favicon {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 601px) {
|
|
||||||
.has-favicon .sCuL3 {
|
|
||||||
padding: 0 0 0 30px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#flex_text_audio_icon_chunk {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rp-credit {
|
|
||||||
padding: 10px 0 0 0;
|
|
||||||
margin: 0 auto;
|
|
||||||
display: table;
|
|
||||||
}
|
|
||||||
|
|
||||||
audio {
|
|
||||||
display: block;
|
|
||||||
margin-right: auto;
|
|
||||||
padding-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 801px) {
|
|
||||||
body {
|
|
||||||
max-width: 900px !important;
|
|
||||||
min-width: 0 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 801px) {
|
|
||||||
details summary {
|
|
||||||
margin-bottom: 10px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-button {
|
|
||||||
background-color: inherit;
|
|
||||||
color: #fff;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 10px 35px;
|
|
||||||
display: table-cell;
|
|
||||||
align-items: center;
|
|
||||||
text-transform: uppercase;
|
|
||||||
border: none;
|
|
||||||
border-bottom-width: medium;
|
|
||||||
border-bottom-style: none;
|
|
||||||
border-bottom-color: currentcolor;
|
|
||||||
border-bottom: 2px solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.sCuL3 {
|
|
||||||
top: -3px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.l97dzf {
|
|
||||||
font-weight: 600 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tAd8D.AP7Wnd {
|
|
||||||
padding: 20px 0 20px 40px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skVgpb {
|
|
||||||
display: ruby !important;
|
|
||||||
padding: 10px 0 0 40px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.VGHMXd {
|
|
||||||
display: contents !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vbShOe {
|
|
||||||
padding: 10px 0 0 10px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* .BNeawe.uEec3.AP7Wnd {
|
|
||||||
display: none !important;
|
|
||||||
} */
|
|
||||||
|
|
||||||
.RWuggc.kCrYT > div > .BNeawe.tAd8D.AP7Wnd {
|
|
||||||
padding: 20px 0 20px 15px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.BVG0Nb {
|
|
||||||
border: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Xdlr0d {
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
* > span > .BNeawe.uEec3.AP7Wnd {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.AVsepf {
|
|
||||||
padding: 0 0 10px 10px !important;
|
|
||||||
}
|
|
|
@ -1,81 +0,0 @@
|
||||||
/* Colors */
|
|
||||||
:root {
|
|
||||||
/* LIGHT THEME COLORS */
|
|
||||||
--whoogle-logo: #685e79;
|
|
||||||
--whoogle-page-bg: #ffffff;
|
|
||||||
--whoogle-element-bg: #4285f4;
|
|
||||||
--whoogle-text: #000000;
|
|
||||||
--whoogle-contrast-text: #ffffff;
|
|
||||||
--whoogle-secondary-text: #70757a;
|
|
||||||
--whoogle-result-bg: #ffffff;
|
|
||||||
--whoogle-result-title: #1967d2;
|
|
||||||
--whoogle-result-url: #0d652d;
|
|
||||||
--whoogle-result-visited: #4b11a8;
|
|
||||||
|
|
||||||
/* DARK THEME COLORS */
|
|
||||||
/* --primary: #ff1a66;
|
|
||||||
--primary-active: #e5175c;
|
|
||||||
--primary-grad2: #ff5448;
|
|
||||||
--grad2-active: #e54b41;
|
|
||||||
--primary-grad3: #ff8448;
|
|
||||||
--grad3-active: #e57741; */
|
|
||||||
--body-bg: #0a0c0d;
|
|
||||||
--body-color: #e4e8ea;
|
|
||||||
--ad-bg: #282e31;
|
|
||||||
--gray-200: #1b1f21;
|
|
||||||
--gray-300: #23292c;
|
|
||||||
--gray-400: #282e31;
|
|
||||||
--gray-800: #9aa2a6;
|
|
||||||
--gray-900: #50585c;
|
|
||||||
--gray-950: #31373a;
|
|
||||||
--danger: #ff3333;
|
|
||||||
--danger-active: #e52e2e;
|
|
||||||
|
|
||||||
|
|
||||||
--aa: #D1D1D1;
|
|
||||||
--ab: #DBDBDB;
|
|
||||||
--bb: #85C7F2;
|
|
||||||
--ba: #636363;
|
|
||||||
--bc: #4C4C4C;
|
|
||||||
|
|
||||||
--qw: #F3E8EE;
|
|
||||||
--primary-grad2: #BACDB0;
|
|
||||||
--primary: #729B79;
|
|
||||||
--primary-grad3: #475B63;
|
|
||||||
--ew: #2E2C2F;
|
|
||||||
|
|
||||||
--re: #2D3142;
|
|
||||||
--er: #BFC0C0;
|
|
||||||
--rr: #FFFFFF;
|
|
||||||
--ee: #EF8354;
|
|
||||||
--rw: #4F5D75;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#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;
|
|
||||||
}
|
|
BIN
app/static/img/.DS_Store
vendored
Before Width: | Height: | Size: 15 KiB |
BIN
app/static/img/favicon/.DS_Store
vendored
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 18 KiB |
|
@ -1,9 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<browserconfig>
|
|
||||||
<msapplication>
|
|
||||||
<tile>
|
|
||||||
<square150x150logo src="/mstile-150x150.png"/>
|
|
||||||
<TileColor>#da532c</TileColor>
|
|
||||||
</tile>
|
|
||||||
</msapplication>
|
|
||||||
</browserconfig>
|
|
Before Width: | Height: | Size: 833 B |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 15 KiB |
|
@ -1,44 +0,0 @@
|
||||||
{
|
|
||||||
"name": "RaveSearch",
|
|
||||||
"short_name": "RaveSearch",
|
|
||||||
"display": "fullscreen",
|
|
||||||
"scope": "/",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "favicon-32x32.png",
|
|
||||||
"sizes": "36x36",
|
|
||||||
"type": "image\/png",
|
|
||||||
"density": "0.75"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "favicon-32x32.png",
|
|
||||||
"sizes": "48x48",
|
|
||||||
"type": "image\/png",
|
|
||||||
"density": "1.0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "favicon-32x32.png",
|
|
||||||
"sizes": "72x72",
|
|
||||||
"type": "image\/png",
|
|
||||||
"density": "1.5"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "android-chrome-192x192.png",
|
|
||||||
"sizes": "96x96",
|
|
||||||
"type": "image\/png",
|
|
||||||
"density": "2.0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "android-chrome-192x192.png",
|
|
||||||
"sizes": "144x144",
|
|
||||||
"type": "image\/png",
|
|
||||||
"density": "3.0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "android-chrome-192x192.png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image\/png",
|
|
||||||
"density": "4.0"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
Before Width: | Height: | Size: 10 KiB |
|
@ -1,523 +0,0 @@
|
||||||
<?xml version="1.0" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
|
||||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
|
||||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="352.000000pt" height="352.000000pt" viewBox="0 0 352.000000 352.000000"
|
|
||||||
preserveAspectRatio="xMidYMid meet">
|
|
||||||
<metadata>
|
|
||||||
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
|
||||||
</metadata>
|
|
||||||
<g transform="translate(0.000000,352.000000) scale(0.100000,-0.100000)"
|
|
||||||
fill="#000000" stroke="none">
|
|
||||||
<path d="M1300 3362 c0 -104 30 -223 80 -322 25 -48 110 -180 117 -180 3 0 2
|
|
||||||
15 -1 33 -5 24 -4 27 3 12 5 -11 10 -30 10 -41 1 -12 25 -57 54 -101 92 -139
|
|
||||||
155 -295 160 -400 2 -40 1 -73 -3 -73 -3 0 -14 23 -24 51 -10 28 -25 53 -32
|
|
||||||
56 -7 3 -27 29 -44 59 -16 30 -34 54 -39 54 -5 0 -30 34 -56 75 -25 41 -51 77
|
|
||||||
-56 81 -6 3 2 -12 17 -35 15 -23 25 -43 23 -46 -3 -2 -41 29 -85 70 -85 78
|
|
||||||
-153 173 -259 365 -60 109 -82 140 -97 140 -17 0 -7 -37 22 -83 38 -61 38 -67
|
|
||||||
-1 -22 -17 19 -34 33 -39 30 -18 -11 -32 -94 -22 -124 10 -27 9 -30 -8 -28
|
|
||||||
-28 4 -49 26 -81 83 -16 28 -57 75 -92 105 -34 29 -49 39 -34 22 17 -19 25
|
|
||||||
-36 21 -46 -4 -12 25 -48 107 -130 119 -118 213 -226 205 -235 -10 -10 -181
|
|
||||||
135 -210 178 -17 24 -32 38 -34 32 -2 -7 -40 7 -102 38 -60 30 -106 47 -117
|
|
||||||
44 -15 -5 -15 -4 -4 4 12 9 9 12 -15 18 -16 4 -31 10 -34 14 -6 9 -110 62
|
|
||||||
-110 56 0 -3 90 -77 200 -166 110 -88 200 -163 200 -165 0 -2 -17 -2 -37 1
|
|
||||||
-27 3 -14 -4 42 -24 101 -36 140 -56 192 -99 50 -40 152 -168 113 -141 -161
|
|
||||||
112 -187 120 -43 14 100 -73 124 -96 103 -96 -22 0 -170 81 -265 144 -54 36
|
|
||||||
-100 65 -102 63 -1 -2 11 -16 27 -32 17 -16 30 -34 30 -41 0 -7 14 -23 32 -35
|
|
||||||
30 -22 31 -23 10 -32 -31 -14 -141 16 -232 64 -77 40 -220 151 -220 170 0 7
|
|
||||||
17 9 53 4 67 -9 59 -1 -20 19 -34 9 -112 36 -174 61 -61 25 -113 44 -115 43
|
|
||||||
-5 -6 67 -113 75 -113 5 0 13 -4 18 -9 5 -5 3 -6 -4 -2 -17 10 -16 0 3 -29 15
|
|
||||||
-24 15 -25 -2 -26 -34 -1 16 -17 55 -18 19 -1 68 -17 108 -37 71 -35 90 -50
|
|
||||||
80 -60 -6 -6 -122 32 -162 54 -16 9 -34 17 -40 17 -5 1 -32 10 -60 20 -44 17
|
|
||||||
-47 20 -25 25 20 5 16 8 -23 14 -27 4 -51 5 -54 3 -2 -3 22 -33 54 -67 l59
|
|
||||||
-63 88 -2 c86 -3 207 -32 251 -62 11 -7 27 -13 37 -13 10 0 18 -4 18 -8 0 -5
|
|
||||||
-26 -10 -57 -11 -32 -1 -74 -6 -93 -11 -19 -5 -55 -11 -80 -14 -92 -12 -166
|
|
||||||
-24 -209 -36 -24 -6 -62 -13 -85 -15 -38 -3 -66 -10 -91 -23 -26 -14 17 25 47
|
|
||||||
42 28 17 31 17 19 3 -7 -9 -9 -17 -3 -17 5 0 15 7 22 15 11 13 93 55 108 55 3
|
|
||||||
0 -3 -10 -14 -21 -16 -18 -12 -17 20 5 21 14 47 26 57 26 39 0 25 17 -24 30
|
|
||||||
-29 7 -57 17 -64 22 -8 6 -18 5 -30 -2 -17 -11 -17 -11 0 -6 11 3 24 3 30 -1
|
|
||||||
7 -5 28 -14 47 -20 34 -12 33 -12 -23 -8 -32 2 -65 9 -72 15 -8 7 -19 9 -25 5
|
|
||||||
-20 -12 -9 -23 33 -30 38 -8 39 -9 13 -13 -38 -5 -78 6 -70 19 3 6 -9 3 -27
|
|
||||||
-7 -36 -18 -155 -132 -147 -140 3 -2 81 10 174 27 155 29 180 31 303 26 74 -4
|
|
||||||
138 -10 142 -14 5 -5 -70 -29 -167 -53 l-175 -45 53 -9 53 -9 -55 -13 c-30 -7
|
|
||||||
-66 -13 -80 -14 -42 -1 -1 -20 43 -20 73 0 27 -18 -65 -25 -137 -11 -146 -20
|
|
||||||
-37 -34 97 -13 125 -19 118 -25 -2 -2 -45 -32 -95 -67 l-90 -64 113 -1 c71 -1
|
|
||||||
118 -5 126 -13 8 -8 7 -11 -6 -11 -25 0 -206 -98 -267 -145 -50 -39 -89 -75
|
|
||||||
-79 -75 21 1 179 39 165 39 -31 1 -13 10 38 20 26 5 49 5 52 0 3 -5 13 -5 24
|
|
||||||
1 24 13 33 13 25 0 -13 -21 49 -8 128 27 76 34 130 66 82 49 -40 -15 -142 -34
|
|
||||||
-205 -39 l-65 -6 144 71 c188 93 305 117 297 62 -1 -9 -9 -20 -17 -25 -10 -6
|
|
||||||
-6 -9 15 -9 23 0 27 -3 23 -20 -4 -15 0 -20 14 -20 24 0 24 -9 2 -25 -15 -11
|
|
||||||
-10 -12 30 -8 38 5 53 1 81 -18 26 -18 32 -27 24 -36 -8 -10 -4 -13 20 -13 40
|
|
||||||
0 61 -25 33 -40 -12 -6 -21 -16 -21 -22 0 -9 3 -9 12 0 15 15 70 16 85 1 15
|
|
||||||
-15 -12 -59 -36 -59 -9 0 -23 -5 -31 -11 -11 -7 -6 -9 20 -4 19 4 45 18 58 32
|
|
||||||
15 16 37 27 59 30 20 2 43 7 52 12 20 10 43 3 35 -11 -3 -5 13 4 37 22 32 23
|
|
||||||
51 30 70 26 18 -3 32 1 44 14 9 10 30 23 46 29 l29 10 -16 -25 c-9 -13 -15
|
|
||||||
-27 -12 -29 2 -3 17 16 33 40 28 46 42 58 31 29 -5 -14 -4 -15 9 -4 18 15 21
|
|
||||||
-1 5 -31 -8 -13 -7 -19 0 -19 6 0 7 -6 1 -17 -4 -10 -25 -67 -45 -128 l-36
|
|
||||||
-110 5 70 5 69 -39 -66 c-68 -117 -115 -168 -105 -115 3 16 0 14 -10 -8 -8
|
|
||||||
-16 -45 -60 -83 -96 -37 -37 -78 -80 -91 -95 l-23 -29 22 34 c13 20 18 36 12
|
|
||||||
40 -6 3 -8 29 -6 56 2 28 4 57 5 65 0 8 11 26 23 40 12 14 30 48 39 77 10 28
|
|
||||||
31 71 48 95 l31 43 -47 -45 c-25 -24 -46 -40 -46 -36 0 4 -27 -17 -61 -48 -57
|
|
||||||
-53 -172 -136 -187 -136 -4 0 -23 -13 -42 -28 -45 -37 -80 -56 -80 -43 0 5 5
|
|
||||||
13 11 17 6 4 25 36 41 73 17 36 37 74 46 84 8 9 12 17 8 17 -4 0 2 17 13 38
|
|
||||||
23 43 52 112 46 112 -2 0 -34 -39 -72 -86 -43 -54 -95 -106 -142 -140 -73 -54
|
|
||||||
-91 -62 -91 -45 0 6 -23 0 -51 -11 -34 -15 -46 -24 -37 -29 10 -7 9 -9 -4 -9
|
|
||||||
-10 0 -32 -14 -50 -31 -20 -18 -25 -26 -13 -21 11 5 40 13 65 16 l45 7 -35
|
|
||||||
-30 c-40 -35 -31 -40 18 -11 l34 21 -7 -22 c-6 -17 6 -11 51 28 33 28 69 56
|
|
||||||
82 62 43 22 14 -40 -42 -91 -33 -31 -45 -47 -34 -48 9 0 29 10 45 23 51 41 82
|
|
||||||
57 113 57 34 0 35 -2 18 -33 -6 -12 21 8 61 46 70 65 91 79 91 56 0 -6 16 1
|
|
||||||
35 15 37 28 37 28 49 -25 12 -55 -75 -174 -189 -258 -27 -21 -83 -57 -122 -80
|
|
||||||
-77 -45 -90 -57 -47 -41 14 6 27 10 29 10 2 0 -9 -30 -25 -67 -24 -54 -26 -62
|
|
||||||
-9 -40 19 25 58 40 44 17 -3 -6 -2 -10 3 -10 13 0 -33 -92 -57 -114 -12 -11
|
|
||||||
-21 -25 -21 -32 0 -7 -5 -16 -10 -19 -7 -4 -9 2 -4 19 12 50 -14 25 -43 -41
|
|
||||||
-31 -69 -76 -203 -70 -209 4 -4 110 122 150 177 16 21 33 39 39 39 6 0 3 -10
|
|
||||||
-8 -21 -11 -13 3 -6 32 14 l51 37 6 -98 c8 -119 35 -220 89 -330 45 -89 68
|
|
||||||
-118 68 -86 1 24 26 229 39 314 14 86 14 110 1 90 -7 -10 -10 21 -9 97 1 103
|
|
||||||
2 113 24 133 l24 23 -5 -54 c-3 -30 -7 -74 -9 -99 -4 -41 -3 -42 4 -10 13 51
|
|
||||||
61 183 72 194 15 16 10 -78 -6 -117 -24 -58 -17 -56 18 5 18 31 41 66 50 76 9
|
|
||||||
10 17 23 17 28 0 5 24 35 53 68 l54 59 7 -34 c8 -42 0 -133 -21 -221 -17 -78
|
|
||||||
-12 -109 10 -58 l15 35 10 -25 c29 -73 108 -228 166 -323 36 -60 66 -111 66
|
|
||||||
-113 0 -3 -6 -2 -12 2 -8 4 -10 3 -5 -2 5 -5 14 -9 20 -9 7 0 26 -25 42 -56
|
|
||||||
41 -76 129 -218 135 -218 6 0 94 142 135 218 17 31 35 56 42 56 6 0 15 4 20 9
|
|
||||||
5 5 3 6 -4 2 -7 -4 -13 -5 -13 -2 0 2 30 53 66 113 58 95 137 250 166 323 l10
|
|
||||||
25 15 -35 c22 -51 27 -20 10 58 -21 88 -29 179 -21 221 l7 34 54 -59 c29 -33
|
|
||||||
53 -63 53 -68 0 -5 8 -18 17 -28 9 -10 32 -45 50 -76 35 -61 42 -63 18 -5 -16
|
|
||||||
39 -21 133 -6 117 11 -11 59 -143 72 -194 7 -32 8 -31 4 10 -2 25 -6 69 -9 99
|
|
||||||
l-5 54 24 -23 c22 -20 23 -30 24 -133 1 -76 -2 -107 -9 -97 -13 20 -13 -4 1
|
|
||||||
-90 13 -85 38 -290 39 -314 0 -32 23 -3 68 86 54 110 81 211 89 330 l6 98 51
|
|
||||||
-37 c29 -20 43 -27 32 -14 -11 11 -14 21 -8 21 6 0 23 -18 39 -39 40 -55 146
|
|
||||||
-181 150 -177 6 6 -39 140 -70 209 -29 66 -55 91 -43 41 5 -17 3 -23 -4 -19
|
|
||||||
-5 3 -10 12 -10 19 0 7 -9 21 -21 32 -24 22 -70 114 -57 114 5 0 6 5 3 10 -14
|
|
||||||
23 25 8 44 -18 17 -21 15 -13 -9 41 -16 37 -27 67 -25 67 2 0 15 -4 29 -10 43
|
|
||||||
-16 30 -4 -46 41 -40 23 -95 59 -123 80 -114 84 -201 203 -189 258 12 53 12
|
|
||||||
53 49 25 19 -14 35 -21 35 -15 0 23 21 9 91 -56 40 -38 67 -58 61 -46 -17 31
|
|
||||||
-16 33 18 33 31 0 62 -16 113 -57 16 -13 36 -23 45 -23 11 1 -1 17 -34 48 -56
|
|
||||||
51 -85 113 -42 91 13 -6 49 -34 82 -62 45 -39 57 -45 51 -28 l-7 22 34 -21
|
|
||||||
c49 -29 58 -24 18 11 l-35 30 45 -7 c25 -3 54 -11 65 -16 12 -5 7 3 -13 21
|
|
||||||
-18 17 -40 31 -50 31 -13 0 -14 2 -4 9 9 5 -3 14 -37 29 -28 11 -51 17 -51 11
|
|
||||||
0 -17 -18 -9 -91 45 -47 34 -99 86 -142 140 -38 47 -70 86 -72 86 -6 0 23 -69
|
|
||||||
46 -113 11 -20 17 -37 13 -37 -4 0 0 -8 8 -17 9 -10 29 -48 46 -84 16 -37 35
|
|
||||||
-69 41 -73 6 -4 11 -12 11 -17 0 -13 -35 6 -80 43 -19 15 -38 28 -42 28 -15 0
|
|
||||||
-130 83 -187 136 -34 31 -61 52 -61 48 0 -4 -21 12 -46 36 l-47 45 31 -43 c17
|
|
||||||
-24 38 -67 48 -95 9 -29 27 -63 39 -77 12 -14 23 -32 23 -40 1 -8 3 -37 5 -65
|
|
||||||
2 -27 0 -53 -6 -56 -6 -4 -1 -20 12 -40 l22 -34 -23 29 c-13 15 -54 58 -91 95
|
|
||||||
-38 36 -75 80 -83 96 -10 22 -13 24 -10 8 10 -53 -37 -2 -105 115 l-39 66 5
|
|
||||||
-69 5 -70 -36 110 c-20 61 -41 118 -45 128 -6 11 -5 17 1 17 7 0 8 6 0 19 -16
|
|
||||||
30 -13 46 5 31 13 -11 14 -10 9 4 -11 29 3 17 31 -29 16 -24 31 -43 33 -40 3
|
|
||||||
2 -3 16 -12 29 l-16 25 29 -10 c16 -6 37 -19 46 -29 12 -13 26 -17 44 -14 19
|
|
||||||
4 38 -3 70 -26 24 -18 40 -27 37 -22 -8 14 15 21 35 11 9 -5 32 -10 52 -12 22
|
|
||||||
-3 44 -14 59 -30 13 -14 39 -28 58 -32 26 -5 31 -3 20 4 -8 6 -22 11 -31 11
|
|
||||||
-24 0 -51 44 -36 59 15 15 70 14 85 -1 9 -9 12 -9 12 0 0 6 -9 16 -21 22 -28
|
|
||||||
15 -7 40 33 40 24 0 28 3 20 13 -8 9 -2 18 24 36 28 19 43 23 81 18 40 -4 45
|
|
||||||
-3 30 8 -22 16 -22 25 2 25 14 0 18 5 14 20 -4 17 0 20 23 20 21 0 25 3 15 9
|
|
||||||
-8 5 -16 16 -17 25 -8 55 109 31 297 -62 l144 -71 -65 6 c-63 5 -165 24 -205
|
|
||||||
39 -48 17 6 -15 82 -49 79 -35 141 -48 128 -27 -8 13 1 13 25 0 11 -6 21 -6
|
|
||||||
24 -1 3 5 26 5 52 0 51 -10 69 -19 38 -20 -14 0 144 -38 165 -39 10 0 -29 36
|
|
||||||
-79 75 -61 47 -242 145 -267 145 -13 0 -14 3 -6 11 8 8 55 12 126 13 l113 1
|
|
||||||
-90 64 c-50 35 -93 65 -95 67 -7 6 21 12 118 25 109 14 100 23 -37 34 -92 7
|
|
||||||
-138 25 -65 25 44 0 85 19 43 20 -14 1 -50 7 -80 14 l-55 13 53 9 53 9 -175
|
|
||||||
45 c-97 24 -172 48 -167 53 4 4 68 10 142 14 123 5 148 3 303 -26 93 -17 171
|
|
||||||
-29 174 -27 8 8 -111 122 -147 140 -18 10 -30 13 -27 7 8 -13 -32 -24 -70 -19
|
|
||||||
-26 4 -25 5 14 13 41 7 52 18 32 30 -6 4 -17 2 -25 -5 -7 -6 -40 -13 -72 -15
|
|
||||||
-56 -4 -57 -4 -23 8 19 6 40 15 47 20 6 4 19 4 30 1 17 -5 17 -5 0 6 -12 7
|
|
||||||
-22 8 -30 2 -7 -5 -35 -15 -64 -22 -49 -13 -63 -30 -24 -30 10 0 36 -12 57
|
|
||||||
-26 32 -22 36 -23 20 -5 -11 11 -17 21 -14 21 15 0 97 -42 108 -55 7 -8 17
|
|
||||||
-15 22 -15 6 0 4 8 -3 17 -12 14 -9 14 19 -3 41 -24 64 -48 36 -38 -10 4 -23
|
|
||||||
9 -28 12 -5 3 -28 6 -51 8 -23 1 -62 8 -86 14 -43 12 -117 24 -209 36 -25 3
|
|
||||||
-61 9 -80 14 -19 5 -61 10 -92 11 -32 1 -58 6 -58 11 0 4 8 8 18 8 10 0 26 6
|
|
||||||
37 13 44 30 165 59 251 62 l88 2 59 63 c32 34 56 64 54 67 -3 2 -27 1 -54 -3
|
|
||||||
-39 -6 -43 -9 -23 -14 22 -5 19 -8 -25 -25 -28 -10 -54 -19 -60 -20 -5 0 -24
|
|
||||||
-8 -40 -17 -40 -22 -156 -60 -162 -54 -10 10 9 25 80 60 40 20 89 36 108 37
|
|
||||||
39 1 89 17 55 18 -17 1 -17 2 -2 26 19 29 20 39 4 29 -8 -4 -10 -3 -5 2 5 5
|
|
||||||
13 9 18 9 8 0 80 107 75 113 -2 1 -54 -18 -115 -43 -62 -25 -140 -52 -174 -61
|
|
||||||
-79 -20 -87 -28 -19 -19 35 5 52 3 52 -4 0 -19 -143 -130 -220 -170 -91 -48
|
|
||||||
-201 -78 -232 -64 -21 9 -20 10 10 32 18 12 32 28 32 35 0 7 13 25 30 41 16
|
|
||||||
16 28 30 27 32 -2 2 -48 -27 -102 -63 -95 -63 -243 -144 -265 -144 -21 0 3 23
|
|
||||||
103 96 144 106 118 98 -43 -14 -39 -27 63 101 113 141 52 43 91 63 192 99 56
|
|
||||||
20 69 27 43 24 -21 -3 -38 -3 -38 -1 0 2 90 77 200 165 110 89 200 163 200
|
|
||||||
166 0 6 -104 -47 -110 -56 -3 -4 -18 -10 -34 -14 -24 -6 -27 -9 -15 -18 11 -8
|
|
||||||
11 -9 -4 -4 -11 3 -57 -14 -117 -44 -62 -31 -100 -45 -102 -38 -2 6 -17 -8
|
|
||||||
-34 -32 -29 -43 -200 -188 -210 -178 -8 9 86 117 205 235 82 82 111 118 107
|
|
||||||
130 -4 10 4 27 21 46 15 17 0 7 -34 -22 -35 -30 -76 -77 -92 -105 -32 -57 -53
|
|
||||||
-79 -81 -83 -17 -2 -18 1 -8 28 10 30 -4 113 -22 124 -5 3 -22 -11 -39 -30
|
|
||||||
-39 -45 -39 -39 -1 22 29 46 39 83 22 83 -15 0 -37 -31 -97 -140 -106 -192
|
|
||||||
-174 -287 -259 -365 -44 -41 -82 -72 -85 -70 -2 3 8 23 23 46 15 23 23 38 17
|
|
||||||
35 -5 -4 -31 -40 -56 -81 -26 -41 -51 -75 -56 -75 -5 0 -23 -24 -39 -54 -17
|
|
||||||
-30 -37 -56 -44 -59 -7 -3 -22 -28 -32 -56 -10 -28 -21 -51 -24 -51 -4 0 -5
|
|
||||||
33 -3 73 5 105 68 261 161 400 28 44 52 89 53 101 0 11 5 30 10 41 7 15 8 12
|
|
||||||
3 -12 -3 -18 -4 -33 -1 -33 6 0 92 131 116 178 54 106 84 232 79 332 l-3 65
|
|
||||||
-24 -65 c-13 -36 -42 -100 -63 -142 -21 -43 -38 -83 -38 -90 0 -7 -10 -26 -21
|
|
||||||
-43 -12 -16 -18 -22 -15 -11 7 20 -32 116 -47 116 -5 0 -6 -8 -3 -17 6 -14 3
|
|
||||||
-14 -15 3 -13 11 -19 13 -13 4 4 -8 7 -23 5 -34 -1 -11 2 -16 8 -12 6 4 11 2
|
|
||||||
11 -3 0 -6 -4 -11 -10 -11 -5 0 -10 -9 -10 -20 0 -11 -6 -20 -12 -20 -10 0
|
|
||||||
-10 -2 0 -9 10 -6 10 -11 -3 -21 -13 -11 -18 -11 -24 -1 -5 7 -10 12 -12 10
|
|
||||||
-2 -1 -15 2 -28 6 l-24 7 24 13 c13 7 27 11 30 9 4 -2 5 4 2 15 -3 13 -11 17
|
|
||||||
-24 14 -10 -3 -27 1 -37 8 -15 12 -16 12 -5 -2 10 -12 10 -18 0 -28 -10 -11
|
|
||||||
-8 -11 11 -1 13 6 26 9 29 7 2 -3 -5 -10 -16 -17 -21 -11 -21 -11 9 -41 30
|
|
||||||
-30 40 -69 19 -69 -6 0 -9 9 -6 20 3 11 1 18 -4 14 -5 -3 -9 -12 -9 -19 0 -9
|
|
||||||
-6 -12 -17 -8 -15 5 -15 3 2 -11 18 -16 18 -17 2 -12 -10 3 -21 6 -23 6 -2 0
|
|
||||||
-4 9 -4 20 0 11 -4 20 -10 20 -5 0 -10 -6 -10 -13 0 -15 45 -57 61 -57 6 0 7
|
|
||||||
-5 2 -12 -8 -13 -10 -19 -16 -50 -1 -9 -14 -26 -27 -37 l-25 -20 23 -1 c16 0
|
|
||||||
21 -4 15 -12 -4 -7 -6 -21 -5 -30 2 -13 -8 -20 -42 -29 l-46 -11 0 -184 c0
|
|
||||||
-116 -4 -184 -10 -184 -6 0 -10 68 -10 183 l0 182 -31 17 c-17 10 -28 22 -24
|
|
||||||
27 3 5 1 12 -5 16 -5 3 -10 11 -10 16 0 6 5 7 10 4 14 -9 58 19 93 59 20 22
|
|
||||||
30 27 34 18 4 -10 11 -5 24 15 10 15 19 33 19 38 -1 6 -5 2 -11 -9 -9 -15 -13
|
|
||||||
-16 -20 -5 -7 12 -9 11 -9 -1 0 -8 -18 -26 -40 -39 -21 -14 -37 -27 -35 -31 2
|
|
||||||
-3 -4 -6 -13 -6 -9 -1 -24 -3 -32 -5 -8 -2 -15 3 -15 11 -2 26 5 30 28 18 20
|
|
||||||
-10 21 -10 8 6 -13 15 -12 16 15 6 25 -9 26 -8 12 3 -15 12 -15 14 0 25 14 10
|
|
||||||
13 11 -5 2 -30 -14 -48 -13 -37 3 11 14 11 37 1 54 -4 7 1 14 14 18 14 5 18
|
|
||||||
10 11 17 -6 6 -16 7 -24 2 -20 -13 -40 -14 -57 -3 -12 8 -10 8 8 3 22 -6 23
|
|
||||||
-4 19 24 -4 26 -2 29 11 22 13 -6 16 -3 16 16 0 13 -3 24 -7 24 -5 0 -8 10 -7
|
|
||||||
23 0 21 1 21 9 2 6 -14 9 -16 10 -5 2 8 2 18 1 23 -2 13 17 8 34 -9 9 -9 14
|
|
||||||
-22 12 -29 -3 -6 1 -12 9 -12 7 0 11 4 8 8 -5 9 26 49 39 49 4 0 5 -4 2 -10
|
|
||||||
-3 -5 -2 -10 3 -10 6 0 12 7 16 15 3 8 13 15 21 15 9 0 13 5 10 10 -3 6 -15
|
|
||||||
10 -26 10 -12 0 -17 -4 -13 -11 5 -8 1 -8 -14 0 -25 13 -43 15 -23 2 12 -8 12
|
|
||||||
-12 -2 -26 -15 -15 -19 -15 -39 -1 -12 9 -17 16 -10 16 6 0 12 5 12 11 0 8 -4
|
|
||||||
8 -13 1 -8 -7 -29 -9 -50 -5 -25 4 -37 2 -37 -6 0 -8 8 -10 22 -5 20 6 21 5 9
|
|
||||||
-10 -20 -24 -40 -20 -36 9 1 14 -2 28 -7 31 -5 4 8 17 29 30 35 21 36 23 13
|
|
||||||
24 -15 0 -40 -14 -63 -35 -21 -19 -41 -35 -45 -35 -4 0 2 7 12 15 11 8 16 15
|
|
||||||
11 15 -4 0 0 10 11 21 19 22 19 22 -1 10 -11 -7 -31 -28 -45 -46 -19 -26 -27
|
|
||||||
-30 -33 -19 -4 8 -5 19 -2 24 3 6 4 10 1 10 -13 0 -56 -105 -50 -124 4 -15 -1
|
|
||||||
-12 -15 9 -12 17 -21 36 -21 43 0 7 -17 47 -38 90 -21 42 -49 105 -62 140 -12
|
|
||||||
34 -24 62 -26 62 -2 0 -4 -30 -4 -68z m320 -182 c0 -5 -2 -10 -4 -10 -3 0 -8
|
|
||||||
5 -11 10 -3 6 -1 10 4 10 6 0 11 -4 11 -10z m-750 -112 c0 -6 -7 -5 -15 2 -8
|
|
||||||
7 -15 17 -15 22 0 6 7 5 15 -2 8 -7 15 -17 15 -22z m1780 7 c-7 -9 -15 -13
|
|
||||||
-18 -10 -3 2 1 11 8 20 7 9 15 13 18 10 3 -2 -1 -11 -8 -20z m-1906 -90 c11
|
|
||||||
-8 16 -15 10 -15 -5 0 -18 7 -28 15 -11 8 -16 15 -10 15 5 0 18 -7 28 -15z
|
|
||||||
m922 0 c-11 -8 -25 -15 -30 -15 -6 0 -2 7 8 15 11 8 25 15 30 15 6 0 2 -7 -8
|
|
||||||
-15z m1108 0 c-10 -8 -23 -15 -28 -15 -6 0 -1 7 10 15 10 8 23 15 28 15 6 0 1
|
|
||||||
-7 -10 -15z m-1219 -136 c47 -92 56 -131 16 -65 -21 34 -54 116 -46 116 3 0
|
|
||||||
16 -23 30 -51z m410 10 c-15 -40 -53 -109 -61 -109 -5 0 12 42 41 99 31 62 39
|
|
||||||
66 20 10z m-736 -44 c0 -5 -13 6 -29 25 -17 19 -30 39 -30 45 0 5 14 -6 30
|
|
||||||
-25 16 -19 29 -39 29 -45z m1076 30 c-15 -20 -29 -34 -32 -31 -3 2 7 21 22 41
|
|
||||||
15 19 30 33 33 31 3 -3 -8 -22 -23 -41z m-1074 -105 c57 -55 108 -100 113
|
|
||||||
-100 11 0 -10 26 -78 98 -28 28 -46 52 -41 52 12 0 121 -103 158 -148 15 -19
|
|
||||||
27 -28 27 -22 0 18 56 -20 120 -82 68 -67 139 -197 142 -263 1 -22 6 -46 11
|
|
||||||
-53 5 -7 8 -15 5 -17 -2 -3 -17 21 -34 51 -16 31 -34 56 -39 56 -18 -1 -36 21
|
|
||||||
-62 71 -37 73 -117 191 -121 178 -2 -6 22 -51 53 -102 65 -104 69 -142 7 -53
|
|
||||||
-43 59 -199 200 -211 189 -3 -4 19 -29 49 -57 139 -128 161 -150 176 -181 15
|
|
||||||
-28 9 -25 -44 26 -33 31 -64 57 -69 57 -5 0 -31 21 -59 48 -28 26 -74 67 -103
|
|
||||||
90 -36 30 -50 49 -46 61 3 12 -10 25 -50 49 -31 18 -53 37 -50 42 3 5 29 0 58
|
|
||||||
-10 28 -9 65 -21 81 -24 l28 -7 -25 23 c-49 46 -120 128 -109 128 5 0 57 -45
|
|
||||||
113 -100z m1114 48 c-26 -29 -59 -63 -72 -76 l-25 -23 28 7 c16 3 53 15 81 24
|
|
||||||
29 10 55 15 58 10 3 -5 -19 -24 -50 -42 -40 -24 -53 -37 -50 -49 4 -12 -10
|
|
||||||
-31 -46 -61 -29 -23 -75 -64 -103 -90 -28 -27 -54 -48 -59 -48 -5 0 -36 -26
|
|
||||||
-69 -57 -53 -51 -59 -54 -44 -26 15 31 37 53 176 181 30 28 52 53 49 57 -12
|
|
||||||
11 -168 -130 -211 -189 -62 -89 -58 -51 7 53 31 51 55 96 53 102 -4 13 -84
|
|
||||||
-105 -122 -179 -25 -49 -41 -67 -60 -67 -5 0 -23 -26 -40 -57 -16 -32 -32 -56
|
|
||||||
-34 -53 -3 2 0 10 5 17 5 7 10 31 11 53 3 66 74 196 142 263 64 62 120 100
|
|
||||||
120 82 0 -6 12 3 27 22 37 45 146 148 158 148 5 0 -13 -24 -41 -52 -68 -72
|
|
||||||
-89 -98 -78 -98 5 0 56 45 113 100 110 106 155 135 76 48z m-1807 -81 c34 -33
|
|
||||||
46 -63 13 -33 -11 10 -24 15 -29 12 -13 -8 -112 72 -112 90 0 19 82 -25 128
|
|
||||||
-69z m2552 69 c0 -18 -99 -98 -112 -90 -5 3 -18 -2 -29 -12 -33 -30 -21 0 14
|
|
||||||
33 25 24 109 80 125 83 1 0 2 -6 2 -14z m-1386 -32 c17 -6 22 -24 8 -24 -5 0
|
|
||||||
-17 7 -28 15 -19 15 -8 20 20 9z m112 -9 c-23 -17 -36 -19 -36 -6 0 11 18 19
|
|
||||||
40 20 11 0 10 -3 -4 -14z m-1408 -99 c60 -28 86 -55 40 -41 -31 10 -105 53
|
|
||||||
-114 66 -4 7 -3 10 2 8 5 -2 37 -17 72 -33z m2752 19 c-16 -20 -119 -69 -127
|
|
||||||
-61 -9 8 -6 10 62 44 69 34 82 38 65 17z m-2895 -195 c4 -7 -5 -9 -26 -5 -44
|
|
||||||
8 -50 15 -11 15 17 0 34 -5 37 -10z m3034 6 c-10 -11 -70 -16 -64 -6 3 5 20
|
|
||||||
10 37 10 17 0 29 -2 27 -4z m-2205 -15 c3 -5 18 -12 33 -16 36 -8 93 -52 93
|
|
||||||
-70 0 -7 12 -27 26 -44 14 -17 23 -31 20 -31 -19 0 -105 52 -127 77 -39 43
|
|
||||||
-72 93 -60 93 5 0 12 -4 15 -9z m124 -20 c32 -16 75 -48 95 -70 20 -23 37 -38
|
|
||||||
37 -34 0 3 -7 15 -17 25 -9 10 -14 21 -10 24 13 13 131 -71 122 -87 -4 -5 1
|
|
||||||
-6 9 -3 24 9 66 -74 66 -132 l-1 -49 -14 32 c-8 18 -25 38 -39 44 -14 7 -44
|
|
||||||
38 -68 71 -24 32 -44 54 -46 48 -2 -6 -22 9 -44 32 -40 42 -40 42 4 -12 24
|
|
||||||
-30 48 -61 53 -69 14 -24 -82 54 -111 90 -15 19 -46 53 -68 77 -22 23 -37 42
|
|
||||||
-33 42 4 0 34 -13 65 -29z m1096 -13 c-22 -24 -53 -58 -68 -77 -29 -36 -125
|
|
||||||
-114 -111 -90 5 8 29 39 53 69 44 54 44 54 4 12 -22 -23 -42 -38 -44 -32 -2 6
|
|
||||||
-22 -16 -46 -48 -24 -33 -54 -64 -68 -71 -14 -6 -31 -26 -39 -44 l-14 -32 -1
|
|
||||||
49 c0 58 42 141 66 132 8 -3 13 -2 9 3 -9 16 109 100 122 87 4 -3 -1 -14 -10
|
|
||||||
-24 -10 -10 -17 -22 -17 -25 0 -4 17 11 37 34 32 36 131 97 158 99 5 0 -9 -19
|
|
||||||
-31 -42z m95 10 c-39 -58 -73 -89 -121 -114 -61 -31 -65 -30 -34 7 14 17 26
|
|
||||||
37 26 44 0 18 57 62 93 70 15 4 30 11 33 16 3 5 10 9 15 9 6 0 0 -15 -12 -32z
|
|
||||||
m-1702 6 c-3 -3 -12 -4 -19 -1 -8 3 -5 6 6 6 11 1 17 -2 13 -5z m2110 0 c-3
|
|
||||||
-3 -12 -4 -19 -1 -8 3 -5 6 6 6 11 1 17 -2 13 -5z m-1255 -131 c30 -36 30 -47
|
|
||||||
0 -19 -18 17 -32 31 -32 33 0 9 19 1 32 -14z m408 14 c0 -2 -14 -16 -32 -33
|
|
||||||
-30 -28 -30 -17 0 19 13 15 32 23 32 14z m-1263 -29 c-2 -7 3 -18 12 -25 14
|
|
||||||
-11 14 -13 -4 -27 -11 -8 -26 -16 -32 -16 -27 0 -11 -18 22 -24 47 -9 52 -22
|
|
||||||
14 -41 l-31 -17 32 -19 c20 -13 30 -25 26 -34 -3 -8 0 -15 7 -15 19 -1 -35
|
|
||||||
-38 -62 -43 -52 -10 -210 51 -234 90 -4 7 -17 13 -27 13 -26 0 -26 17 0 25 11
|
|
||||||
3 25 15 30 25 6 10 28 29 50 41 22 13 40 27 40 31 0 5 19 17 43 28 49 22 121
|
|
||||||
27 114 8z m924 -10 c11 -21 11 -22 -4 -9 -10 7 -17 17 -17 22 0 15 9 10 21
|
|
||||||
-13z m245 -6 c-18 -16 -18 -16 -6 6 6 13 14 21 18 18 3 -4 -2 -14 -12 -24z
|
|
||||||
m1028 8 c25 -10 46 -23 46 -28 0 -4 18 -18 40 -31 22 -12 44 -31 50 -41 5 -10
|
|
||||||
19 -22 30 -25 26 -8 26 -25 0 -25 -10 0 -23 -6 -27 -13 -24 -39 -182 -100
|
|
||||||
-234 -90 -27 5 -81 42 -62 43 7 0 10 7 7 15 -4 9 6 21 26 34 l32 19 -31 17
|
|
||||||
c-38 19 -33 32 14 41 33 6 49 24 23 24 -7 0 -22 8 -33 16 -18 14 -18 16 -4 27
|
|
||||||
9 7 14 18 12 25 -7 19 59 15 111 -8z m-1681 -67 c3 -5 -6 -16 -18 -26 -27 -19
|
|
||||||
-45 -6 -25 18 13 16 36 20 43 8z m1097 -8 c20 -24 2 -37 -25 -18 -12 10 -21
|
|
||||||
21 -18 26 7 12 30 8 43 -8z m-842 -60 c-3 -3 -9 2 -12 12 -6 14 -5 15 5 6 7
|
|
||||||
-7 10 -15 7 -18z m555 10 c-3 -9 -8 -14 -10 -11 -3 3 -2 9 2 15 9 16 15 13 8
|
|
||||||
-4z m-713 -9 c0 -3 -4 -8 -10 -11 -5 -3 -10 -1 -10 4 0 6 5 11 10 11 6 0 10
|
|
||||||
-2 10 -4z m880 -6 c0 -5 -2 -10 -4 -10 -3 0 -8 5 -11 10 -3 6 -1 10 4 10 6 0
|
|
||||||
11 -4 11 -10z m-470 -29 c0 -4 9 -7 20 -7 11 0 20 3 20 7 0 4 10 9 21 12 17 4
|
|
||||||
19 3 10 -6 -20 -20 -11 -30 11 -11 18 16 21 17 15 2 -3 -9 -3 -19 1 -21 4 -2
|
|
||||||
1 -2 -5 -1 -18 5 -16 -7 4 -21 14 -10 15 -16 5 -28 -18 -21 -55 -30 -48 -11 5
|
|
||||||
14 1 16 -34 16 -35 0 -39 -2 -34 -16 7 -19 -30 -10 -48 11 -10 12 -9 18 5 28
|
|
||||||
20 14 22 26 5 21 -7 -1 -10 -1 -6 1 4 2 4 12 1 21 -6 15 -3 14 15 -2 22 -19
|
|
||||||
31 -9 11 11 -9 9 -7 10 10 6 11 -3 21 -8 21 -12z m-112 -47 c12 -8 12 -19 5
|
|
||||||
-57 -9 -42 -8 -49 15 -82 16 -23 28 -31 33 -24 4 6 12 8 18 5 25 -16 9 12 -24
|
|
||||||
41 -19 17 -35 37 -35 45 1 7 14 -2 30 -21 16 -19 31 -33 33 -30 2 2 11 16 20
|
|
||||||
32 10 15 26 27 37 27 11 0 27 -12 37 -27 9 -16 18 -30 20 -32 2 -3 17 11 33
|
|
||||||
30 16 19 29 28 30 21 0 -8 -16 -28 -35 -45 -33 -29 -49 -57 -24 -41 6 3 14 1
|
|
||||||
18 -5 5 -7 17 1 33 24 23 33 24 40 15 82 -7 38 -7 49 5 57 8 6 17 18 20 26 6
|
|
||||||
17 38 -51 38 -81 0 -11 4 -19 9 -19 5 0 15 -38 21 -83 11 -72 11 -90 -3 -128
|
|
||||||
l-15 -44 -2 55 c-1 52 -1 54 -17 35 -15 -19 -15 -19 -10 10 2 17 2 38 -2 47
|
|
||||||
-4 12 -14 4 -38 -37 -31 -53 -31 -54 -7 -45 30 12 31 -2 5 -52 -11 -19 -18
|
|
||||||
-38 -16 -40 2 -2 -14 -24 -36 -48 -44 -48 -59 -72 -59 -92 1 -7 7 2 15 20 8
|
|
||||||
17 19 32 25 32 7 0 7 -7 -2 -22 -7 -13 -19 -52 -28 -88 -10 -40 -22 -65 -30
|
|
||||||
-65 -8 0 -20 25 -30 65 -9 36 -21 75 -28 88 -9 15 -9 22 -2 22 6 0 17 -15 25
|
|
||||||
-32 8 -18 14 -27 15 -20 0 20 -15 44 -59 92 -22 24 -38 46 -36 48 2 2 -5 21
|
|
||||||
-16 40 -26 50 -25 64 5 52 24 -9 24 -8 -7 45 -24 41 -34 49 -38 37 -4 -9 -4
|
|
||||||
-30 -2 -47 5 -29 5 -29 -10 -10 -16 19 -16 17 -17 -35 l-2 -55 -15 44 c-14 38
|
|
||||||
-14 56 -3 128 6 45 16 83 21 83 5 0 9 8 9 19 0 30 32 98 38 81 3 -8 12 -20 20
|
|
||||||
-26z m-1196 16 c65 -19 58 -33 -13 -25 -35 3 -73 9 -84 12 -18 6 -18 7 5 14
|
|
||||||
36 10 51 10 92 -1z m2748 1 c23 -7 23 -8 5 -14 -44 -13 -148 -19 -141 -8 15
|
|
||||||
23 90 35 136 22z m-1960 -27 c0 -8 -4 -12 -10 -9 -5 3 -10 10 -10 16 0 5 5 9
|
|
||||||
10 9 6 0 10 -7 10 -16z m1100 7 c0 -6 -4 -13 -10 -16 -5 -3 -10 1 -10 9 0 9 5
|
|
||||||
16 10 16 6 0 10 -4 10 -9z m-619 -58 c12 -15 12 -16 -6 -9 -16 6 -18 5 -7 -5
|
|
||||||
17 -18 15 -34 -2 -20 -19 16 -27 51 -11 51 7 0 19 -8 26 -17z m152 -3 c-3 -10
|
|
||||||
-11 -25 -19 -31 -17 -14 -19 2 -1 20 10 10 8 11 -8 5 -18 -7 -18 -6 -6 9 19
|
|
||||||
24 40 22 34 -3z m-1363 -40 c-36 -17 -72 -34 -80 -40 -29 -20 -121 -60 -138
|
|
||||||
-60 -15 1 -14 2 3 15 11 8 25 18 30 23 18 15 142 80 161 85 10 2 35 5 54 5 33
|
|
||||||
1 31 -1 -30 -28z m2646 -16 c38 -21 74 -42 79 -46 6 -5 19 -15 30 -23 17 -13
|
|
||||||
18 -14 3 -15 -17 0 -109 40 -138 60 -8 6 -44 24 -80 41 l-65 30 51 -5 c33 -3
|
|
||||||
75 -18 120 -42z m-1661 -54 c-10 -11 -20 -18 -22 -16 -6 5 30 44 36 39 2 -1
|
|
||||||
-4 -12 -14 -23z m595 -17 c0 -3 -9 2 -20 12 -11 10 -20 22 -20 27 0 4 9 -1 20
|
|
||||||
-12 11 -11 20 -23 20 -27z m-926 -14 c-6 -11 -24 -2 -24 11 0 5 7 7 15 4 8 -4
|
|
||||||
12 -10 9 -15z m1251 2 c-3 -6 -11 -8 -17 -5 -6 4 -5 9 3 15 16 10 23 4 14 -10z
|
|
||||||
m-1013 -18 c-6 -9 -18 -18 -28 -19 -15 -2 -14 1 6 16 31 24 33 24 22 3z m798
|
|
||||||
-18 c0 -11 -34 5 -42 19 -9 17 -9 17 16 1 14 -10 26 -19 26 -20z m-990 15 c0
|
|
||||||
-5 -9 -10 -21 -10 -11 0 -17 5 -14 10 3 6 13 10 21 10 8 0 14 -4 14 -10z
|
|
||||||
m1175 0 c3 -5 -3 -10 -14 -10 -12 0 -21 5 -21 10 0 6 6 10 14 10 8 0 18 -4 21
|
|
||||||
-10z m-1139 -15 c-26 -19 -42 -19 -26 0 7 8 20 15 29 15 13 -1 13 -3 -3 -15z
|
|
||||||
m1094 0 c16 -19 0 -19 -26 0 -16 12 -16 14 -3 15 9 0 22 -7 29 -15z m-1029 -9
|
|
||||||
c-7 -8 -21 -14 -31 -13 -26 2 -5 27 22 27 17 0 18 -3 9 -14z m977 1 c8 -10 6
|
|
||||||
-13 -8 -14 -10 -1 -24 5 -31 13 -9 11 -8 14 9 14 11 0 24 -6 30 -13z m-617
|
|
||||||
-169 c11 -22 18 -42 15 -45 -2 -3 -14 12 -25 34 -11 21 -20 31 -21 23 -1 -22
|
|
||||||
-30 19 -30 43 0 10 -4 15 -10 12 -11 -7 -14 8 -3 18 9 9 50 -37 74 -85z m-20
|
|
||||||
75 c13 -16 12 -17 -3 -4 -10 7 -18 15 -18 17 0 8 8 3 21 -13z m279 13 c0 -2
|
|
||||||
-8 -10 -17 -17 -16 -13 -17 -12 -4 4 13 16 21 21 21 13z m40 -17 c0 -6 -4 -7
|
|
||||||
-10 -4 -5 3 -10 -2 -10 -12 0 -24 -29 -65 -30 -43 -1 8 -10 -2 -21 -23 -11
|
|
||||||
-22 -23 -37 -25 -34 -8 7 31 79 58 110 22 24 38 27 38 6z m-374 -116 c-3 -10
|
|
||||||
-7 -36 -10 -58 l-6 -40 15 34 c9 18 18 31 20 29 9 -9 -43 -187 -72 -248 -39
|
|
||||||
-80 -41 -62 -13 105 11 66 20 135 20 154 0 25 4 32 15 27 8 -3 15 -1 15 4 0 6
|
|
||||||
5 10 11 10 6 0 8 -8 5 -17z m384 7 c0 -5 7 -7 15 -4 11 5 15 -2 15 -27 0 -19
|
|
||||||
9 -88 20 -154 28 -167 26 -185 -13 -105 -29 61 -81 239 -72 248 2 2 11 -11 20
|
|
||||||
-29 l15 -34 -6 40 c-3 22 -7 48 -10 58 -3 9 -1 17 5 17 6 0 11 -4 11 -10z
|
|
||||||
m-450 -52 c0 -73 -28 -180 -66 -255 -37 -74 -173 -238 -159 -192 7 24 -24 -15
|
|
||||||
-69 -84 -9 -16 -19 -27 -22 -25 -6 7 52 178 83 243 16 33 41 79 56 102 15 24
|
|
||||||
27 47 27 53 0 16 42 101 47 96 3 -3 0 -21 -7 -41 -7 -20 -13 -51 -14 -68 -2
|
|
||||||
-18 -6 -45 -10 -62 -10 -46 24 30 64 140 18 50 36 100 41 113 14 36 29 25 29
|
|
||||||
-20z m563 -73 c46 -128 82 -208 71 -160 -4 17 -8 44 -10 62 -1 17 -7 48 -14
|
|
||||||
68 -7 20 -10 38 -7 41 5 5 47 -80 47 -96 0 -6 12 -29 27 -53 15 -23 40 -69 56
|
|
||||||
-102 31 -65 89 -236 83 -243 -3 -2 -13 9 -22 25 -45 69 -76 108 -69 84 14 -46
|
|
||||||
-122 118 -159 192 -38 75 -66 182 -66 255 0 69 19 47 63 -73z m-362 -60 c16
|
|
||||||
-33 32 -75 35 -92 3 -18 10 -33 14 -33 4 0 11 15 14 33 6 34 55 141 77 169 9
|
|
||||||
11 19 15 27 10 10 -7 7 -26 -11 -90 -27 -92 -21 -100 14 -16 18 47 24 53 30
|
|
||||||
37 10 -28 -21 -134 -68 -228 -22 -44 -48 -104 -57 -132 -10 -29 -21 -53 -26
|
|
||||||
-53 -5 0 -16 24 -26 53 -9 28 -35 88 -57 132 -47 94 -78 200 -68 228 6 16 12
|
|
||||||
10 30 -37 35 -84 41 -76 14 16 -18 64 -21 83 -11 90 16 10 32 -10 69 -87z
|
|
||||||
m-412 -60 c-19 -36 -37 -65 -41 -65 -10 0 -10 1 3 41 13 38 58 103 66 95 3 -3
|
|
||||||
-10 -35 -28 -71z m944 12 c22 -47 29 -77 20 -77 -8 0 -73 119 -73 134 0 16 38
|
|
||||||
-25 53 -57z m-649 -106 c9 -52 15 -96 12 -98 -9 -9 -36 81 -42 144 -10 100 11
|
|
||||||
69 30 -46z m362 43 c-6 -60 -33 -150 -42 -141 -5 5 25 188 33 200 12 20 15 1
|
|
||||||
9 -59z m-755 -119 c-25 -40 -65 -96 -89 -125 -24 -29 -50 -63 -58 -74 -12 -17
|
|
||||||
-16 -18 -23 -7 -7 11 -9 11 -14 0 -8 -18 2 57 13 96 4 17 14 56 21 89 12 57
|
|
||||||
33 81 49 56 10 -16 8 -28 -9 -59 -8 -14 -9 -21 -2 -17 6 4 18 21 27 38 8 16
|
|
||||||
18 27 21 25 3 -3 -2 -20 -10 -36 -9 -17 -12 -31 -7 -31 4 0 13 14 20 31 7 17
|
|
||||||
31 59 53 92 l41 62 7 -33 c6 -29 1 -44 -40 -107z m1162 18 c17 -35 35 -63 39
|
|
||||||
-63 3 0 0 14 -9 31 -8 16 -13 33 -10 36 3 2 13 -9 21 -25 9 -17 21 -34 27 -38
|
|
||||||
7 -4 6 3 -2 17 -17 31 -19 43 -9 59 16 25 37 1 49 -56 7 -33 17 -72 21 -89 11
|
|
||||||
-39 21 -114 13 -96 -5 11 -7 11 -14 0 -7 -11 -11 -10 -23 7 -8 11 -34 45 -58
|
|
||||||
74 -24 29 -64 85 -89 125 -38 59 -45 78 -41 105 l5 34 23 -30 c13 -16 39 -57
|
|
||||||
57 -91z m-1324 -22 c-42 -94 -119 -232 -144 -257 -7 -8 -18 -14 -23 -14 -6 0
|
|
||||||
-16 -14 -24 -30 -7 -17 -21 -33 -30 -36 -9 -3 -18 -15 -20 -26 -4 -25 -23 -35
|
|
||||||
-31 -15 -3 8 6 27 19 42 27 32 32 60 9 51 -9 -3 -15 0 -15 10 0 10 6 14 15 10
|
|
||||||
11 -4 20 4 29 27 44 104 60 130 117 190 55 58 63 56 20 -6 -15 -23 -41 -67
|
|
||||||
-55 -97 -15 -30 -32 -63 -38 -73 -5 -11 -8 -21 -5 -23 7 -7 74 99 129 204 61
|
|
||||||
116 78 145 84 140 2 -3 -15 -46 -37 -97z m1479 -24 c61 -117 132 -230 139
|
|
||||||
-223 3 2 0 12 -5 23 -6 10 -23 43 -38 73 -14 30 -40 74 -55 97 -43 62 -35 64
|
|
||||||
20 6 57 -60 73 -86 117 -190 9 -23 18 -31 29 -27 9 4 15 0 15 -10 0 -10 -6
|
|
||||||
-13 -15 -10 -23 9 -18 -19 9 -51 13 -15 22 -34 19 -42 -8 -20 -27 -10 -31 15
|
|
||||||
-2 11 -11 23 -20 26 -9 3 -23 19 -30 36 -8 16 -18 30 -24 30 -31 0 -122 160
|
|
||||||
-206 359 -3 9 -1 12 6 8 6 -4 37 -58 70 -120z m-960 26 c23 -77 72 -276 68
|
|
||||||
-280 -6 -6 -55 86 -72 136 -19 57 -31 181 -17 181 5 0 15 -17 21 -37z m429
|
|
||||||
-30 c-3 -38 -13 -89 -21 -114 -17 -50 -66 -142 -72 -136 -4 4 45 203 68 280
|
|
||||||
18 60 32 45 25 -30z m-287 -63 c29 -66 56 -120 60 -120 4 0 31 54 60 120 29
|
|
||||||
66 56 120 60 120 9 0 -1 -47 -24 -109 -26 -71 -17 -81 14 -16 16 34 29 52 29
|
|
||||||
41 1 -20 1 -20 16 0 8 10 15 16 15 12 0 -15 -61 -171 -93 -235 -19 -37 -43
|
|
||||||
-105 -53 -150 -10 -46 -21 -83 -24 -83 -3 0 -14 37 -24 83 -10 45 -34 113 -53
|
|
||||||
150 -32 64 -93 220 -93 235 0 4 7 -2 15 -12 15 -20 15 -20 16 0 0 11 13 -7 29
|
|
||||||
-41 31 -65 40 -55 14 16 -23 62 -33 109 -24 109 4 0 31 -54 60 -120z m-190
|
|
||||||
-25 c16 -35 30 -69 30 -75 0 -6 6 -23 14 -38 7 -15 16 -45 18 -67 3 -22 10
|
|
||||||
-51 17 -64 23 -42 3 -34 -29 12 -41 57 -83 159 -69 167 9 6 4 39 -16 108 -14
|
|
||||||
44 5 21 35 -43z m532 28 c-18 -49 -24 -86 -13 -92 14 -9 -28 -110 -69 -168
|
|
||||||
-32 -46 -52 -54 -29 -12 7 13 14 42 17 64 2 22 11 52 18 67 8 15 14 32 14 38
|
|
||||||
0 18 61 142 67 136 3 -3 1 -18 -5 -33z m-572 -129 c17 -43 42 -99 55 -124 13
|
|
||||||
-25 29 -59 34 -75 l11 -30 -25 30 c-13 16 -32 45 -42 64 -10 20 -30 57 -45 83
|
|
||||||
-30 52 -61 156 -51 171 7 12 23 -18 63 -119z m634 53 c-8 -31 -27 -79 -42
|
|
||||||
-105 -15 -26 -35 -63 -45 -83 -10 -19 -29 -48 -42 -64 l-25 -30 11 30 c5 16
|
|
||||||
21 50 34 75 13 25 37 79 54 120 43 110 54 131 62 123 5 -5 2 -34 -7 -66z
|
|
||||||
m-1314 -2 c-13 -14 -28 -25 -33 -25 -6 0 0 11 13 25 13 14 28 25 33 25 6 0 0
|
|
||||||
-11 -13 -25z m1960 0 c13 -14 19 -25 13 -25 -5 0 -20 11 -33 25 -13 14 -19 25
|
|
||||||
-13 25 5 0 20 -11 33 -25z m-1051 -220 c0 -12 -12 5 -30 40 -16 33 -29 69 -28
|
|
||||||
80 0 12 12 -5 30 -40 16 -33 29 -69 28 -80z m154 43 c-16 -37 -29 -56 -31 -46
|
|
||||||
-2 10 9 46 25 80 16 37 29 56 31 46 2 -10 -9 -46 -25 -80z m-903 -125 c-1 -27
|
|
||||||
-9 -66 -20 -88 -11 -22 -19 -47 -19 -55 0 -8 10 5 21 29 21 42 22 43 30 20 5
|
|
||||||
-13 7 -42 3 -64 -3 -22 -8 -65 -11 -95 l-6 -54 -15 39 c-8 22 -18 56 -20 75
|
|
||||||
-5 29 -7 32 -13 15 -6 -16 -9 -12 -15 20 -3 22 -11 49 -16 61 -16 33 -7 89 14
|
|
||||||
88 9 -1 17 3 17 9 0 12 31 45 43 46 4 1 7 -20 7 -46z m753 -16 c20 -34 39 -75
|
|
||||||
42 -90 9 -35 21 -35 30 0 7 29 75 156 80 151 2 -2 -16 -59 -39 -128 -27 -79
|
|
||||||
-47 -125 -56 -125 -9 0 -28 41 -51 110 -20 61 -40 118 -44 128 -15 38 5 14 38
|
|
||||||
-46z m897 46 c11 -10 20 -23 20 -29 0 -7 8 -11 18 -10 20 1 29 -55 13 -88 -5
|
|
||||||
-12 -13 -39 -16 -61 -6 -32 -9 -36 -15 -20 -6 17 -8 14 -13 -15 -2 -19 -12
|
|
||||||
-53 -20 -75 l-15 -39 -6 54 c-3 30 -8 73 -11 95 -4 22 -2 51 3 64 8 23 9 22
|
|
||||||
30 -20 11 -24 21 -37 21 -29 0 8 -8 33 -19 55 -17 35 -28 135 -15 135 3 0 14
|
|
||||||
-8 25 -17z m-972 -38 c-3 -3 -13 7 -23 22 l-17 28 23 -22 c12 -12 20 -25 17
|
|
||||||
-28z m286 20 c-9 -14 -19 -23 -22 -20 -5 4 26 45 35 45 2 0 -4 -11 -13 -25z
|
|
||||||
m-295 -72 l33 -48 -36 39 c-31 34 -44 56 -33 56 2 0 18 -21 36 -47z m303 9
|
|
||||||
c-12 -16 -29 -35 -39 -43 -9 -8 -2 7 17 33 18 27 35 46 38 44 2 -3 -5 -18 -16
|
|
||||||
-34z m-380 -89 c30 -31 108 -176 108 -201 0 -6 -85 105 -105 137 -5 9 -20 33
|
|
||||||
-32 52 -39 62 -25 67 29 12z m488 31 c0 -4 -10 -23 -22 -43 -13 -19 -27 -43
|
|
||||||
-33 -52 -20 -32 -105 -143 -105 -137 0 25 78 170 108 201 34 35 52 45 52 31z
|
|
||||||
m-1047 -263 c-16 -20 -32 124 -17 156 5 12 10 -11 14 -67 4 -47 5 -87 3 -89z
|
|
||||||
m1575 80 c-3 -62 -11 -93 -21 -80 -2 2 -1 42 1 87 3 56 8 80 14 74 6 -6 9 -41
|
|
||||||
6 -81z"/>
|
|
||||||
<path d="M1591 2353 c7 -12 15 -20 18 -17 3 2 -3 12 -13 22 -17 16 -18 16 -5
|
|
||||||
-5z"/>
|
|
||||||
<path d="M1900 2355 c-7 -9 -11 -17 -9 -20 3 -2 10 5 17 15 14 24 10 26 -8 5z"/>
|
|
||||||
<path d="M1376 2320 c14 -27 65 -100 70 -100 8 0 -25 55 -51 85 -15 17 -23 23
|
|
||||||
-19 15z"/>
|
|
||||||
<path d="M1460 2225 c0 -5 5 -17 10 -25 5 -8 10 -10 10 -5 0 6 -5 17 -10 25
|
|
||||||
-5 8 -10 11 -10 5z"/>
|
|
||||||
<path d="M2098 2298 c-28 -37 -51 -78 -44 -78 4 0 45 58 70 98 15 24 -2 11
|
|
||||||
-26 -20z"/>
|
|
||||||
<path d="M2026 2215 c-9 -26 -7 -32 5 -12 6 10 9 21 6 23 -2 3 -7 -2 -11 -11z"/>
|
|
||||||
<path d="M1600 1926 c0 -35 32 -100 41 -84 6 9 15 6 35 -12 l28 -25 -18 29
|
|
||||||
c-10 16 -34 49 -52 73 -31 41 -34 43 -34 19z"/>
|
|
||||||
<path d="M1865 1906 c-16 -22 -39 -54 -49 -71 l-20 -30 28 25 c20 18 29 21 35
|
|
||||||
12 10 -17 44 57 39 85 -2 17 -8 14 -33 -21z"/>
|
|
||||||
<path d="M1660 1720 c11 -22 23 -40 25 -40 3 0 -4 18 -15 40 -11 22 -23 40
|
|
||||||
-25 40 -3 0 4 -18 15 -40z"/>
|
|
||||||
<path d="M1830 1720 c-11 -22 -18 -40 -15 -40 2 0 14 18 25 40 11 22 18 40 15
|
|
||||||
40 -2 0 -14 -18 -25 -40z"/>
|
|
||||||
<path d="M1260 1150 c-6 -11 -8 -20 -6 -20 3 0 10 9 16 20 6 11 8 20 6 20 -3
|
|
||||||
0 -10 -9 -16 -20z"/>
|
|
||||||
<path d="M2230 1150 c6 -11 13 -20 16 -20 2 0 0 9 -6 20 -6 11 -13 20 -16 20
|
|
||||||
-2 0 0 -9 6 -20z"/>
|
|
||||||
<path d="M1640 1505 c0 -5 5 -17 10 -25 5 -8 10 -10 10 -5 0 6 -5 17 -10 25
|
|
||||||
-5 8 -10 11 -10 5z"/>
|
|
||||||
<path d="M1846 1495 c-9 -26 -7 -32 5 -12 6 10 9 21 6 23 -2 3 -7 -2 -11 -11z"/>
|
|
||||||
<path d="M1056 1088 c-9 -12 -15 -32 -15 -43 1 -17 2 -17 6 -2 2 9 9 17 14 17
|
|
||||||
5 0 9 9 9 19 0 11 6 22 13 24 9 4 9 6 0 6 -6 1 -19 -9 -27 -21z"/>
|
|
||||||
<path d="M2418 1103 c6 -2 12 -13 12 -24 0 -10 4 -19 9 -19 5 0 12 -8 14 -17
|
|
||||||
4 -15 5 -15 6 2 1 25 -25 65 -42 64 -9 0 -9 -2 1 -6z"/>
|
|
||||||
<path d="M932 570 c0 -19 2 -27 5 -17 2 9 2 25 0 35 -3 9 -5 1 -5 -18z"/>
|
|
||||||
<path d="M2562 570 c0 -19 2 -27 5 -17 2 9 2 25 0 35 -3 9 -5 1 -5 -18z"/>
|
|
||||||
<path d="M810 3416 c0 -31 191 -431 196 -411 1 6 4 38 6 73 3 49 1 62 -9 58
|
|
||||||
-8 -3 -15 5 -19 19 -3 14 -10 25 -15 25 -5 0 -9 7 -9 16 0 9 -9 28 -21 42 -11
|
|
||||||
15 -45 63 -75 106 -30 44 -54 76 -54 72z"/>
|
|
||||||
<path d="M2622 3324 c-35 -52 -68 -97 -73 -100 -5 -3 -9 -14 -9 -25 0 -10 -4
|
|
||||||
-19 -9 -19 -5 0 -12 -11 -15 -25 -4 -14 -11 -22 -19 -19 -10 4 -12 -9 -9 -58
|
|
||||||
2 -35 5 -67 6 -73 3 -13 116 210 161 319 20 48 35 89 33 91 -2 2 -32 -39 -66
|
|
||||||
-91z"/>
|
|
||||||
<path d="M1095 3393 c4 -10 19 -68 34 -128 30 -119 18 -99 180 -313 35 -46 86
|
|
||||||
-124 113 -174 27 -50 68 -116 92 -147 69 -90 117 -162 131 -194 7 -16 15 -25
|
|
||||||
18 -19 7 11 -47 96 -135 212 -31 41 -70 102 -86 135 -31 62 -77 193 -88 252
|
|
||||||
-3 18 -12 37 -20 44 -8 6 -14 17 -14 24 0 6 -20 43 -45 81 -55 85 -57 74 -6
|
|
||||||
-28 36 -72 57 -128 47 -128 -11 0 -98 170 -112 219 -9 31 -37 84 -62 119 -47
|
|
||||||
63 -60 76 -47 45z"/>
|
|
||||||
<path d="M2358 3348 c-25 -35 -53 -88 -62 -119 -14 -49 -101 -219 -112 -219
|
|
||||||
-10 0 11 56 47 128 51 102 49 113 -6 28 -25 -38 -45 -75 -45 -81 0 -7 -6 -18
|
|
||||||
-14 -24 -8 -7 -17 -26 -20 -44 -11 -59 -57 -190 -88 -252 -16 -33 -55 -94 -86
|
|
||||||
-135 -88 -116 -142 -201 -135 -212 3 -6 11 3 18 19 14 32 62 104 131 194 24
|
|
||||||
31 65 97 92 147 27 50 78 128 113 174 162 214 150 194 180 313 15 60 30 118
|
|
||||||
34 128 13 31 0 18 -47 -45z"/>
|
|
||||||
<path d="M1810 3250 c0 -13 5 -18 15 -14 12 5 13 10 4 20 -15 19 -19 18 -19
|
|
||||||
-6z"/>
|
|
||||||
<path d="M1784 3231 c5 -8 2 -11 -9 -9 -10 2 -20 -2 -22 -9 -3 -7 3 -9 17 -6
|
|
||||||
26 7 34 19 19 29 -8 5 -9 3 -5 -5z"/>
|
|
||||||
<path d="M1880 3210 c0 -13 5 -18 15 -14 12 5 13 10 4 20 -15 19 -19 18 -19
|
|
||||||
-6z"/>
|
|
||||||
<path d="M1174 3082 c8 -16 22 -35 32 -43 10 -8 5 4 -11 29 -32 48 -42 55 -21
|
|
||||||
14z"/>
|
|
||||||
<path d="M1807 3097 c-3 -8 3 -14 15 -14 16 -2 17 1 8 13 -15 17 -17 17 -23 1z"/>
|
|
||||||
<path d="M2305 3068 c-16 -25 -21 -37 -11 -29 16 14 51 71 43 71 -3 0 -17 -19
|
|
||||||
-32 -42z"/>
|
|
||||||
<path d="M1750 3066 c0 -8 -3 -22 -7 -32 -4 -11 -3 -15 4 -11 15 9 23 46 12
|
|
||||||
53 -5 3 -9 -1 -9 -10z"/>
|
|
||||||
<path d="M1750 2959 c0 -5 9 -9 20 -9 11 0 20 2 20 4 0 2 -9 6 -20 9 -11 3
|
|
||||||
-20 1 -20 -4z"/>
|
|
||||||
<path d="M1707 2799 c7 -7 15 -10 18 -7 3 3 -2 9 -12 12 -14 6 -15 5 -6 -5z"/>
|
|
||||||
<path d="M820 2720 c-13 -8 -12 -10 3 -10 9 0 17 5 17 10 0 12 -1 12 -20 0z"/>
|
|
||||||
<path d="M2660 2720 c0 -6 7 -10 15 -10 8 0 15 2 15 4 0 2 -7 6 -15 10 -8 3
|
|
||||||
-15 1 -15 -4z"/>
|
|
||||||
<path d="M471 2512 c8 -13 20 -17 37 -14 24 5 24 5 -8 18 -41 18 -42 18 -29
|
|
||||||
-4z"/>
|
|
||||||
<path d="M540 2519 c0 -5 9 -9 20 -9 11 0 20 2 20 4 0 2 -9 6 -20 9 -11 3 -20
|
|
||||||
1 -20 -4z"/>
|
|
||||||
<path d="M2935 2520 c-19 -8 -19 -9 3 -9 12 -1 22 4 22 9 0 6 -1 10 -2 9 -2 0
|
|
||||||
-12 -4 -23 -9z"/>
|
|
||||||
<path d="M2996 2516 l-29 -13 25 -5 c17 -3 29 1 37 14 13 21 7 21 -33 4z"/>
|
|
||||||
<path d="M580 1854 c-30 -7 -68 -17 -85 -22 -27 -9 -25 -10 25 -10 39 -1 49 2
|
|
||||||
35 8 -18 8 -18 9 5 10 28 2 114 28 90 28 -8 0 -40 -6 -70 -14z"/>
|
|
||||||
<path d="M2875 1855 c22 -8 51 -14 65 -15 23 -1 23 -2 5 -10 -14 -6 -4 -9 35
|
|
||||||
-8 50 0 52 1 25 9 -34 12 -157 40 -165 38 -3 -1 13 -7 35 -14z"/>
|
|
||||||
<path d="M700 1840 c-8 -5 -40 -10 -70 -11 -40 -1 -48 -3 -30 -8 22 -6 13 -12
|
|
||||||
-60 -40 -47 -18 -116 -52 -155 -76 -67 -41 -230 -173 -223 -180 4 -5 102 18
|
|
||||||
108 24 3 4 41 7 85 9 l80 2 -54 -32 c-72 -44 -202 -146 -198 -157 1 -4 -9 -18
|
|
||||||
-23 -29 -16 -14 -20 -21 -11 -22 8 0 23 7 33 17 14 13 18 13 18 2 1 -11 3 -11
|
|
||||||
13 1 25 32 134 81 151 68 2 -2 13 6 25 16 12 11 21 16 21 12 0 -5 17 2 38 13
|
|
||||||
l37 21 -35 -30 -35 -31 30 7 c38 9 146 98 186 154 24 33 27 41 13 36 -33 -13
|
|
||||||
-158 -46 -173 -46 -7 0 7 18 33 41 25 23 71 74 102 114 30 41 71 87 92 104 34
|
|
||||||
28 36 43 2 21z m-90 -95 c0 -3 -15 -11 -32 -20 -18 -8 -40 -19 -48 -25 -26
|
|
||||||
-17 -148 -69 -177 -75 l-28 -6 22 19 c13 11 48 28 78 37 57 18 75 32 28 21
|
|
||||||
-24 -5 -25 -5 -9 5 48 29 166 61 166 44z m-72 -84 c-6 -17 -90 -81 -112 -85
|
|
||||||
-48 -10 -139 -16 -133 -9 15 14 97 41 97 32 0 -6 16 2 35 17 19 14 35 22 35
|
|
||||||
16 0 -6 12 0 26 13 26 24 59 34 52 16z m-83 -171 c-3 -5 -14 -10 -24 -10 -10
|
|
||||||
0 -24 -7 -31 -15 -7 -8 -18 -15 -25 -15 -7 0 -18 -7 -25 -15 -7 -8 -23 -15
|
|
||||||
-35 -15 -13 0 -25 -3 -27 -7 -4 -11 -76 -43 -91 -42 -7 0 10 11 36 24 27 12
|
|
||||||
43 25 35 27 -7 3 2 8 20 12 17 4 32 11 32 16 0 4 8 11 18 14 9 3 28 13 42 21
|
|
||||||
28 17 85 20 75 5z"/>
|
|
||||||
<path d="M2802 1820 c21 -17 62 -64 92 -105 31 -40 77 -91 102 -114 26 -23 40
|
|
||||||
-41 33 -41 -15 0 -140 33 -173 46 -14 5 -11 -3 13 -36 40 -56 148 -145 186
|
|
||||||
-154 l30 -7 -35 31 -35 30 38 -21 c20 -11 37 -18 37 -13 0 4 8 0 18 -9 9 -8
|
|
||||||
20 -16 25 -16 42 1 130 -40 154 -71 10 -12 12 -12 13 -1 0 11 4 11 18 -2 10
|
|
||||||
-10 25 -17 33 -17 9 1 5 8 -11 22 -14 11 -24 25 -23 29 4 11 -126 113 -198
|
|
||||||
157 l-54 32 80 -2 c44 -2 82 -5 85 -9 6 -6 104 -29 108 -24 7 7 -156 139 -223
|
|
||||||
180 -38 24 -108 58 -155 76 -72 28 -81 34 -60 39 18 5 9 8 -35 12 -33 2 -69 8
|
|
||||||
-80 12 -11 4 -3 -7 17 -24z m220 -105 c29 -13 41 -22 26 -19 -47 10 -31 -2 27
|
|
||||||
-21 30 -9 65 -26 78 -37 l22 -19 -28 6 c-29 6 -151 58 -177 75 -8 6 -30 17
|
|
||||||
-47 25 -18 9 -33 18 -33 21 0 12 80 -6 132 -31z m-8 -70 c14 -13 26 -19 26
|
|
||||||
-13 0 6 16 -2 35 -16 19 -15 35 -23 35 -17 0 9 82 -18 97 -32 6 -7 -85 -1
|
|
||||||
-133 9 -22 4 -106 68 -112 85 -7 18 26 8 52 -16z m106 -160 c14 -8 33 -18 43
|
|
||||||
-21 9 -3 17 -10 17 -14 0 -5 15 -12 33 -16 17 -4 26 -9 19 -12 -8 -2 8 -15 35
|
|
||||||
-27 26 -13 43 -24 36 -24 -15 -1 -87 31 -91 42 -2 4 -14 7 -27 7 -12 0 -28 7
|
|
||||||
-35 15 -7 8 -18 15 -25 15 -7 0 -18 7 -25 15 -7 8 -21 15 -31 15 -10 0 -21 5
|
|
||||||
-24 10 -10 15 47 12 75 -5z"/>
|
|
||||||
<path d="M1246 1578 c-43 -51 -83 -104 -90 -118 -16 -29 -37 -92 -33 -96 5 -6
|
|
||||||
105 131 126 173 11 23 29 48 38 55 15 11 48 78 39 78 -2 0 -38 -42 -80 -92z"/>
|
|
||||||
<path d="M2183 1640 c6 -19 20 -41 30 -48 9 -7 27 -32 38 -55 21 -42 121 -179
|
|
||||||
126 -173 3 3 -16 61 -30 93 -9 19 -120 158 -163 203 -12 12 -12 9 -1 -20z"/>
|
|
||||||
<path d="M980 1637 c-50 -19 -70 -31 -70 -39 0 -5 9 -4 19 2 11 5 32 15 47 20
|
|
||||||
25 10 26 9 13 -7 -22 -28 -42 -42 -61 -43 -25 0 -33 -20 -8 -20 26 0 93 61 89
|
|
||||||
82 -3 12 -9 13 -29 5z"/>
|
|
||||||
<path d="M2490 1631 c0 -24 64 -81 91 -81 24 0 15 20 -9 20 -19 1 -39 15 -61
|
|
||||||
43 -13 16 -12 17 13 7 15 -5 36 -15 47 -20 10 -6 19 -7 19 -2 0 9 -19 19 -72
|
|
||||||
41 -25 9 -28 8 -28 -8z"/>
|
|
||||||
<path d="M1286 1573 c-6 -14 -5 -15 5 -6 7 7 10 15 7 18 -3 3 -9 -2 -12 -12z"/>
|
|
||||||
<path d="M2200 1581 c0 -6 4 -13 10 -16 6 -3 7 1 4 9 -7 18 -14 21 -14 7z"/>
|
|
||||||
<path d="M930 1535 c-7 -9 -11 -18 -8 -20 3 -3 11 1 18 10 7 9 11 18 8 20 -3
|
|
||||||
3 -11 -1 -18 -10z"/>
|
|
||||||
<path d="M2550 1542 c0 -5 7 -15 15 -22 8 -7 15 -8 15 -2 0 5 -7 15 -15 22 -8
|
|
||||||
7 -15 8 -15 2z"/>
|
|
||||||
<path d="M555 1290 l-40 -19 42 0 c41 -1 64 13 47 29 -5 4 -27 -1 -49 -10z"/>
|
|
||||||
<path d="M2894 1298 c-11 -18 12 -29 52 -26 l39 3 -42 17 c-27 10 -45 13 -49
|
|
||||||
6z"/>
|
|
||||||
<path d="M1431 538 c17 -34 99 -148 106 -148 3 0 -2 10 -10 23 -9 12 -35 50
|
|
||||||
-59 85 -43 64 -58 79 -37 40z"/>
|
|
||||||
<path d="M2032 498 c-24 -35 -50 -73 -59 -85 -8 -13 -13 -23 -10 -23 7 0 89
|
|
||||||
114 106 148 21 39 6 24 -37 -40z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 36 KiB |
|
@ -1,19 +0,0 @@
|
||||||
{
|
|
||||||
"name": "",
|
|
||||||
"short_name": "",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "/android-chrome-192x192.png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/android-chrome-256x256.png",
|
|
||||||
"sizes": "256x256",
|
|
||||||
"type": "image/png"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"theme_color": "#ffffff",
|
|
||||||
"background_color": "#ffffff",
|
|
||||||
"display": "standalone"
|
|
||||||
}
|
|
BIN
app/static/img/rp/.DS_Store
vendored
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 66 KiB |
|
@ -1,127 +0,0 @@
|
||||||
let searchInput;
|
|
||||||
let currentFocus;
|
|
||||||
let originalSearch;
|
|
||||||
let autocompleteResults;
|
|
||||||
|
|
||||||
const handleUserInput = () => {
|
|
||||||
let xhrRequest = new XMLHttpRequest();
|
|
||||||
xhrRequest.open("POST", "autocomplete");
|
|
||||||
xhrRequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
|
|
||||||
xhrRequest.onload = function () {
|
|
||||||
if (xhrRequest.readyState === 4 && xhrRequest.status !== 200) {
|
|
||||||
// Do nothing if failed to fetch autocomplete results
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fill autocomplete with fetched results
|
|
||||||
autocompleteResults = JSON.parse(xhrRequest.responseText)[1];
|
|
||||||
updateAutocompleteList();
|
|
||||||
};
|
|
||||||
|
|
||||||
xhrRequest.send('q=' + searchInput.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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;
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeActive(suggestion);
|
|
||||||
suggestion[currentFocus].classList.add("autocomplete-active");
|
|
||||||
|
|
||||||
// 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("autocomplete-list");
|
|
||||||
if (suggestion) suggestion = suggestion.getElementsByTagName("div");
|
|
||||||
if (e.keyCode === 40) { // down
|
|
||||||
e.preventDefault();
|
|
||||||
currentFocus++;
|
|
||||||
addActive(suggestion);
|
|
||||||
} else if (e.keyCode === 38) { //up
|
|
||||||
e.preventDefault();
|
|
||||||
currentFocus--;
|
|
||||||
addActive(suggestion);
|
|
||||||
} else if (e.keyCode === 13) { // enter
|
|
||||||
e.preventDefault();
|
|
||||||
if (currentFocus > -1) {
|
|
||||||
if (suggestion) suggestion[currentFocus].click();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
originalSearch = searchInput.value;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateAutocompleteList = () => {
|
|
||||||
let autocompleteItem, i;
|
|
||||||
let val = originalSearch;
|
|
||||||
|
|
||||||
let autocompleteList = document.getElementById("autocomplete-list");
|
|
||||||
autocompleteList.innerHTML = "";
|
|
||||||
|
|
||||||
if (!val || !autocompleteResults) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentFocus = -1;
|
|
||||||
|
|
||||||
for (i = 0; i < autocompleteResults.length; i++) {
|
|
||||||
if (autocompleteResults[i].substr(0, val.length).toUpperCase() === val.toUpperCase()) {
|
|
||||||
autocompleteItem = document.createElement("div");
|
|
||||||
autocompleteItem.setAttribute("class", "autocomplete-item");
|
|
||||||
autocompleteItem.innerHTML = "<strong>" + autocompleteResults[i].substr(0, val.length) + "</strong>";
|
|
||||||
autocompleteItem.innerHTML += autocompleteResults[i].substr(val.length);
|
|
||||||
autocompleteItem.innerHTML += "<input type=\"hidden\" value=\"" + autocompleteResults[i] + "\">";
|
|
||||||
autocompleteItem.addEventListener("click", function () {
|
|
||||||
searchInput.value = this.getElementsByTagName("input")[0].value;
|
|
||||||
autocompleteList.innerHTML = "";
|
|
||||||
document.getElementById("search-form").submit();
|
|
||||||
});
|
|
||||||
autocompleteList.appendChild(autocompleteItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
|
||||||
let autocompleteList = document.createElement("div");
|
|
||||||
autocompleteList.setAttribute("id", "autocomplete-list");
|
|
||||||
autocompleteList.setAttribute("class", "autocomplete-items");
|
|
||||||
|
|
||||||
searchInput = document.getElementById("search-bar");
|
|
||||||
searchInput.parentNode.appendChild(autocompleteList);
|
|
||||||
|
|
||||||
searchInput.addEventListener("keydown", (event) => autocompleteInput(event));
|
|
||||||
|
|
||||||
document.addEventListener("click", function (e) {
|
|
||||||
autocompleteList.innerHTML = "";
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,88 +0,0 @@
|
||||||
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();
|
|
||||||
searchBar.select();
|
|
||||||
|
|
||||||
searchBar.addEventListener("keyup", function(event) {
|
|
||||||
if (event.keyCode === 13) {
|
|
||||||
event.preventDefault();
|
|
||||||
searchBtn.click();
|
|
||||||
} else if (searchBar.value !== searchValue && !arrowKeys.includes(event.keyCode)) {
|
|
||||||
searchValue = searchBar.value;
|
|
||||||
handleUserInput();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const setupConfigLayout = () => {
|
|
||||||
// Setup whoogle config
|
|
||||||
const collapsible = document.getElementById("config-collapsible");
|
|
||||||
collapsible.addEventListener("click", function() {
|
|
||||||
this.classList.toggle("active");
|
|
||||||
let content = this.nextElementSibling;
|
|
||||||
if (content.style.maxHeight) {
|
|
||||||
content.style.maxHeight = null;
|
|
||||||
} else {
|
|
||||||
content.style.maxHeight = "400px";
|
|
||||||
}
|
|
||||||
|
|
||||||
content.classList.toggle("open");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadConfig = event => {
|
|
||||||
event.preventDefault();
|
|
||||||
let config = prompt("Enter name of config:");
|
|
||||||
if (!config) {
|
|
||||||
alert("Must specify a name for the config to load");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let xhrPUT = new XMLHttpRequest();
|
|
||||||
xhrPUT.open("PUT", "config?name=" + config + ".conf");
|
|
||||||
xhrPUT.onload = function() {
|
|
||||||
if (xhrPUT.readyState === 4 && xhrPUT.status !== 200) {
|
|
||||||
alert("Error loading Whoogle config");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
location.reload(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
xhrPUT.send();
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveConfig = event => {
|
|
||||||
event.preventDefault();
|
|
||||||
let config = prompt("Enter name for this config:");
|
|
||||||
if (!config) {
|
|
||||||
alert("Must specify a name for the config to save");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let configForm = document.getElementById("config-form");
|
|
||||||
configForm.action = 'config?name=' + config + ".conf";
|
|
||||||
configForm.submit();
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
|
||||||
setTimeout(function() {
|
|
||||||
document.getElementById("main");
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
setupSearchLayout();
|
|
||||||
setupConfigLayout();
|
|
||||||
|
|
||||||
document.getElementById("config-load").addEventListener("click", loadConfig);
|
|
||||||
document.getElementById("config-save").addEventListener("click", saveConfig);
|
|
||||||
|
|
||||||
// Focusing on the search input field requires a delay for elements to finish
|
|
||||||
// loading (seemingly only on FF)
|
|
||||||
setTimeout(function() { document.getElementById("search-bar").focus(); }, 250);
|
|
||||||
});
|
|
|
@ -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));
|
|
||||||
}
|
|
|
@ -1,67 +0,0 @@
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
const advSearchToggle = document.getElementById("adv-search-toggle");
|
|
||||||
const advSearchDiv = document.getElementById("adv-search-div");
|
|
||||||
const searchBar = document.getElementById("search-bar");
|
|
||||||
const countrySelect = document.getElementById("result-country");
|
|
||||||
const timePeriodSelect = document.getElementById("result-time-period");
|
|
||||||
const arrowKeys = [37, 38, 39, 40];
|
|
||||||
let searchValue = searchBar.value;
|
|
||||||
|
|
||||||
countrySelect.onchange = () => {
|
|
||||||
let str = window.location.href;
|
|
||||||
n = str.lastIndexOf("/search");
|
|
||||||
if (n > 0) {
|
|
||||||
str = str.substring(0, n) + `/search?q=${searchBar.value}`;
|
|
||||||
str = tackOnParams(str);
|
|
||||||
window.location.href = str;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
timePeriodSelect.onchange = () => {
|
|
||||||
let str = window.location.href;
|
|
||||||
n = str.lastIndexOf("/search");
|
|
||||||
if (n > 0) {
|
|
||||||
str = str.substring(0, n) + `/search?q=${searchBar.value}`;
|
|
||||||
str = tackOnParams(str);
|
|
||||||
window.location.href = str;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function tackOnParams(str) {
|
|
||||||
if (timePeriodSelect.value != "") {
|
|
||||||
str = str + `&tbs=${timePeriodSelect.value}`;
|
|
||||||
}
|
|
||||||
if (countrySelect.value != "") {
|
|
||||||
str = str + `&country=${countrySelect.value}`;
|
|
||||||
}
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleAdvancedSearch = on => {
|
|
||||||
if (on) {
|
|
||||||
advSearchDiv.style.maxHeight = "70px";
|
|
||||||
} else {
|
|
||||||
advSearchDiv.style.maxHeight = "0px";
|
|
||||||
}
|
|
||||||
localStorage.advSearchToggled = on;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
toggleAdvancedSearch(JSON.parse(localStorage.advSearchToggled));
|
|
||||||
} catch (error) {
|
|
||||||
console.warn("Did not recover advanced search toggle state");
|
|
||||||
}
|
|
||||||
|
|
||||||
advSearchToggle.onclick = () => {
|
|
||||||
toggleAdvancedSearch(advSearchToggle.checked);
|
|
||||||
}
|
|
||||||
|
|
||||||
searchBar.addEventListener("keyup", function(event) {
|
|
||||||
if (event.keyCode === 13) {
|
|
||||||
document.getElementById("search-form").submit();
|
|
||||||
} else if (searchBar.value !== searchValue && !arrowKeys.includes(event.keyCode)) {
|
|
||||||
searchValue = searchBar.value;
|
|
||||||
handleUserInput();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,62 +0,0 @@
|
||||||
(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;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.target.tagName === 'INPUT') return true;
|
|
||||||
if (typeof keymap[e.key] === 'function') {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
keymap[`${shift && e.key == 'Tab' ? 'Shift' : ''}${e.key}`]();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener('keyup', (e) => {
|
|
||||||
if (e.key === 'Shift') {
|
|
||||||
shift = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function goUp () {
|
|
||||||
if (activeIdx > 0) focusResult(activeIdx - 1);
|
|
||||||
else focusSearch();
|
|
||||||
}
|
|
||||||
|
|
||||||
function goDown () {
|
|
||||||
if (activeIdx < results.length - 1) focusResult(activeIdx + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function focusResult (idx) {
|
|
||||||
activeIdx = idx;
|
|
||||||
results[activeIdx].scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
|
|
||||||
results[activeIdx].focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
function focusSearch () {
|
|
||||||
if (window.usingCalculator) {
|
|
||||||
// if this function exists, it means the calculator widget has been displayed
|
|
||||||
if (usingCalculator()) return;
|
|
||||||
}
|
|
||||||
activeIdx = -1;
|
|
||||||
searchBar.focus();
|
|
||||||
}
|
|
||||||
}());
|
|
|
@ -1,76 +0,0 @@
|
||||||
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, '');
|
|
||||||
|
|
||||||
// Note: regex functions for checking for tracking queries were derived
|
|
||||||
// from here -- https://stackoverflow.com/questions/619977
|
|
||||||
const matchTracking = {
|
|
||||||
"ups": {
|
|
||||||
"link": `https://www.ups.com/track?tracknum=${query}`,
|
|
||||||
"expr": [
|
|
||||||
/\b(1Z ?[0-9A-Z]{3} ?[0-9A-Z]{3} ?[0-9A-Z]{2} ?[0-9A-Z]{4} ?[0-9A-Z]{3} ?[0-9A-Z]|[\dT]\d\d\d ?\d\d\d\d ?\d\d\d)\b/
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"usps": {
|
|
||||||
"link": `https://tools.usps.com/go/TrackConfirmAction_input?origTrackNum=${query}`,
|
|
||||||
"expr": [
|
|
||||||
/(\b\d{30}\b)|(\b91\d+\b)|(\b\d{20}\b)/,
|
|
||||||
/^E\D{1}\d{9}\D{2}$|^9\d{15,21}$/,
|
|
||||||
/^91[0-9]+$/,
|
|
||||||
/^[A-Za-z]{2}[0-9]+US$/
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"fedex": {
|
|
||||||
"link": `https://www.fedex.com/apps/fedextrack/?tracknumbers=${query}`,
|
|
||||||
"expr": [
|
|
||||||
/(\b96\d{20}\b)|(\b\d{15}\b)|(\b\d{12}\b)/,
|
|
||||||
/\b((98\d\d\d\d\d?\d\d\d\d|98\d\d) ?\d\d\d\d ?\d\d\d\d( ?\d\d\d)?)\b/,
|
|
||||||
/^[0-9]{15}$/
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Creates a link to a UPS/USPS/FedEx tracking page
|
|
||||||
const createTrackingLink = href => {
|
|
||||||
let link = document.createElement("a");
|
|
||||||
link.className = "tracking-link";
|
|
||||||
link.innerHTML = "View Tracking Info";
|
|
||||||
link.href = href;
|
|
||||||
mainDiv.prepend(link);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Compares the query against a set of regex patterns
|
|
||||||
// for tracking numbers
|
|
||||||
const compareQuery = provider => {
|
|
||||||
provider.expr.some(regex => {
|
|
||||||
if (query.match(regex)) {
|
|
||||||
createTrackingLink(provider.link);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const key of Object.keys(matchTracking)) {
|
|
||||||
compareQuery(matchTracking[key]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
|
||||||
checkForTracking();
|
|
||||||
|
|
||||||
// Clear input if reset button tapped
|
|
||||||
const searchBar = 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();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,978 +0,0 @@
|
||||||
[
|
|
||||||
{
|
|
||||||
"name": "-------",
|
|
||||||
"value": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Hungary",
|
|
||||||
"value": "HU"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"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": "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"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
]
|
|
|
@ -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://www.openstreetmap.org/search?query={map_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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,210 +0,0 @@
|
||||||
[
|
|
||||||
{
|
|
||||||
"name": "English",
|
|
||||||
"value": "lang_en"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Hungarian (Magyar)",
|
|
||||||
"value": "lang_hu"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Afrikaans (Afrikaans)",
|
|
||||||
"value": "lang_af"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Arabic (عربى)",
|
|
||||||
"value": "lang_ar"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Armenian (հայերեն)",
|
|
||||||
"value": "lang_hy"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Azerbaijani (Azərbaycanca)",
|
|
||||||
"value": "lang_az"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Belarusian (Беларуская)",
|
|
||||||
"value": "lang_be"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Bulgarian (български)",
|
|
||||||
"value": "lang_bg"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Catalan (Català)",
|
|
||||||
"value": "lang_ca"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"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": "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": "Welsh (Cymraeg)",
|
|
||||||
"value": "lang_cy"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Xhosa (isiXhosa)",
|
|
||||||
"value": "lang_xh"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Zulu (isiZulu)",
|
|
||||||
"value": "lang_zu"
|
|
||||||
}
|
|
||||||
]
|
|
|
@ -1,5 +0,0 @@
|
||||||
[
|
|
||||||
"light",
|
|
||||||
"dark",
|
|
||||||
"system"
|
|
||||||
]
|
|
|
@ -1,8 +0,0 @@
|
||||||
[
|
|
||||||
{"name": "Any time", "value": ""},
|
|
||||||
{"name": "Past hour", "value": "qdr:h"},
|
|
||||||
{"name": "Past 24 hours", "value": "qdr:d"},
|
|
||||||
{"name": "Past week", "value": "qdr:w"},
|
|
||||||
{"name": "Past month", "value": "qdr:m"},
|
|
||||||
{"name": "Past year", "value": "qdr:y"}
|
|
||||||
]
|
|
|
@ -1,260 +0,0 @@
|
||||||
<!--
|
|
||||||
Calculator widget.
|
|
||||||
This file should contain all required
|
|
||||||
CSS, HTML, and JS for it.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<style>
|
|
||||||
#calc-text {
|
|
||||||
background: var(--whoogle-dark-page-bg);
|
|
||||||
padding: 8px;
|
|
||||||
border-radius: 8px;
|
|
||||||
text-align: right;
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 16px;
|
|
||||||
color: var(--whoogle-dark-text);
|
|
||||||
}
|
|
||||||
#prev-equation {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
.error-border {
|
|
||||||
border: 1px solid red;
|
|
||||||
}
|
|
||||||
|
|
||||||
#calc-btns {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(6, 1fr);
|
|
||||||
grid-template-rows: repeat(5, 1fr);
|
|
||||||
gap: 5px;
|
|
||||||
}
|
|
||||||
#calc-btns button {
|
|
||||||
background: #313141;
|
|
||||||
color: var(--whoogle-dark-text);
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
#calc-btns button:hover {
|
|
||||||
background: #414151;
|
|
||||||
}
|
|
||||||
#calc-btns .common {
|
|
||||||
background: #51516a;
|
|
||||||
}
|
|
||||||
#calc-btns .common:hover {
|
|
||||||
background: #61617a;
|
|
||||||
}
|
|
||||||
#calc-btn-0 { grid-row: 5; grid-column: 3; }
|
|
||||||
#calc-btn-1 { grid-row: 4; grid-column: 3; }
|
|
||||||
#calc-btn-2 { grid-row: 4; grid-column: 4; }
|
|
||||||
#calc-btn-3 { grid-row: 4; grid-column: 5; }
|
|
||||||
#calc-btn-4 { grid-row: 3; grid-column: 3; }
|
|
||||||
#calc-btn-5 { grid-row: 3; grid-column: 4; }
|
|
||||||
#calc-btn-6 { grid-row: 3; grid-column: 5; }
|
|
||||||
#calc-btn-7 { grid-row: 2; grid-column: 3; }
|
|
||||||
#calc-btn-8 { grid-row: 2; grid-column: 4; }
|
|
||||||
#calc-btn-9 { grid-row: 2; grid-column: 5; }
|
|
||||||
#calc-btn-EQ { grid-row: 5; grid-column: 5; }
|
|
||||||
#calc-btn-PT { grid-row: 5; grid-column: 4; }
|
|
||||||
#calc-btn-BCK { grid-row: 5; grid-column: 6; }
|
|
||||||
#calc-btn-ADD { grid-row: 4; grid-column: 6; }
|
|
||||||
#calc-btn-SUB { grid-row: 3; grid-column: 6; }
|
|
||||||
#calc-btn-MLT { grid-row: 2; grid-column: 6; }
|
|
||||||
#calc-btn-DIV { grid-row: 1; grid-column: 6; }
|
|
||||||
#calc-btn-CLR { grid-row: 1; grid-column: 5; }
|
|
||||||
#calc-btn-PRC{ grid-row: 1; grid-column: 4; }
|
|
||||||
#calc-btn-RP { grid-row: 1; grid-column: 3; }
|
|
||||||
#calc-btn-LP { grid-row: 1; grid-column: 2; }
|
|
||||||
#calc-btn-ABS { grid-row: 1; grid-column: 1; }
|
|
||||||
#calc-btn-SIN { grid-row: 2; grid-column: 2; }
|
|
||||||
#calc-btn-COS { grid-row: 3; grid-column: 2; }
|
|
||||||
#calc-btn-TAN { grid-row: 4; grid-column: 2; }
|
|
||||||
#calc-btn-SQR { grid-row: 5; grid-column: 2; }
|
|
||||||
#calc-btn-EXP { grid-row: 2; grid-column: 1; }
|
|
||||||
#calc-btn-E { grid-row: 3; grid-column: 1; }
|
|
||||||
#calc-btn-PI { grid-row: 4; grid-column: 1; }
|
|
||||||
#calc-btn-LOG { grid-row: 5; grid-column: 1; }
|
|
||||||
</style>
|
|
||||||
<p id="prev-equation"></p>
|
|
||||||
<div id="calculator-widget">
|
|
||||||
<p id="calc-text">0</p>
|
|
||||||
<div id="calc-btns">
|
|
||||||
<button id="calc-btn-0" class="common">0</button>
|
|
||||||
<button id="calc-btn-1" class="common">1</button>
|
|
||||||
<button id="calc-btn-2" class="common">2</button>
|
|
||||||
<button id="calc-btn-3" class="common">3</button>
|
|
||||||
<button id="calc-btn-4" class="common">4</button>
|
|
||||||
<button id="calc-btn-5" class="common">5</button>
|
|
||||||
<button id="calc-btn-6" class="common">6</button>
|
|
||||||
<button id="calc-btn-7" class="common">7</button>
|
|
||||||
<button id="calc-btn-8" class="common">8</button>
|
|
||||||
<button id="calc-btn-9" class="common">9</button>
|
|
||||||
<button id="calc-btn-EQ" class="common">=</button>
|
|
||||||
<button id="calc-btn-PT" class="common">.</button>
|
|
||||||
<button id="calc-btn-BCK">⬅</button>
|
|
||||||
<button id="calc-btn-ADD">+</button>
|
|
||||||
<button id="calc-btn-SUB">-</button>
|
|
||||||
<button id="calc-btn-MLT">x</button>
|
|
||||||
<button id="calc-btn-DIV">/</button>
|
|
||||||
<button id="calc-btn-CLR">C</button>
|
|
||||||
<button id="calc-btn-PRC">%</button>
|
|
||||||
<button id="calc-btn-RP">)</button>
|
|
||||||
<button id="calc-btn-LP">(</button>
|
|
||||||
<button id="calc-btn-ABS">|x|</button>
|
|
||||||
<button id="calc-btn-SIN">sin</button>
|
|
||||||
<button id="calc-btn-COS">cos</button>
|
|
||||||
<button id="calc-btn-TAN">tan</button>
|
|
||||||
<button id="calc-btn-SQR">√</button>
|
|
||||||
<button id="calc-btn-EXP">^</button>
|
|
||||||
<button id="calc-btn-E">ℇ</button>
|
|
||||||
<button id="calc-btn-PI">π</button>
|
|
||||||
<button id="calc-btn-LOG">log</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
// JS does not have this by default.
|
|
||||||
// from https://www.freecodecamp.org/news/how-to-factorialize-a-number-in-javascript-9263c89a4b38/
|
|
||||||
function factorial(num) {
|
|
||||||
if (num < 0)
|
|
||||||
return -1;
|
|
||||||
else if (num === 0)
|
|
||||||
return 1;
|
|
||||||
else {
|
|
||||||
return (num * factorial(num - 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// returns true if the user is currently focused on the calculator widget
|
|
||||||
function usingCalculator() {
|
|
||||||
let activeElement = document.activeElement;
|
|
||||||
while (true) {
|
|
||||||
if (!activeElement) return false;
|
|
||||||
if (activeElement.id === "calculator-wrapper") return true;
|
|
||||||
activeElement = activeElement.parentElement;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const $ = q => document.querySelectorAll(q);
|
|
||||||
// key bindings for commonly used buttons
|
|
||||||
const keybindings = {
|
|
||||||
"0": "0",
|
|
||||||
"1": "1",
|
|
||||||
"2": "2",
|
|
||||||
"3": "3",
|
|
||||||
"4": "4",
|
|
||||||
"5": "5",
|
|
||||||
"6": "6",
|
|
||||||
"7": "7",
|
|
||||||
"8": "8",
|
|
||||||
"9": "9",
|
|
||||||
"Enter": "EQ",
|
|
||||||
".": "PT",
|
|
||||||
"+": "ADD",
|
|
||||||
"-": "SUB",
|
|
||||||
"*": "MLT",
|
|
||||||
"/": "DIV",
|
|
||||||
"%": "PRC",
|
|
||||||
"c": "CLR",
|
|
||||||
"(": "LP",
|
|
||||||
")": "RP",
|
|
||||||
"Backspace": "BCK",
|
|
||||||
}
|
|
||||||
window.addEventListener("keydown", event => {
|
|
||||||
if (!usingCalculator()) return;
|
|
||||||
if (event.key === "Enter" && document.activeElement.id !== "search-bar")
|
|
||||||
event.preventDefault();
|
|
||||||
if (keybindings[event.key])
|
|
||||||
document.getElementById("calc-btn-" + keybindings[event.key]).click();
|
|
||||||
})
|
|
||||||
// calculates the string
|
|
||||||
const calc = () => {
|
|
||||||
var mathtext = document.getElementById("calc-text");
|
|
||||||
var statement = mathtext.innerHTML
|
|
||||||
// remove empty ()
|
|
||||||
.replace("()", "")
|
|
||||||
// special constants
|
|
||||||
.replace("π", "(Math.PI)")
|
|
||||||
.replace("ℇ", "(Math.E)")
|
|
||||||
// turns 3(1+2) into 3*(1+2) (for example)
|
|
||||||
.replace(/(?<=[0-9\)])(?<=[^+\-x*\/%^])\(/, "x(")
|
|
||||||
// same except reversed
|
|
||||||
.replace(/\)(?=[0-9\(])(?=[^+\-x*\/%^])/, ")x")
|
|
||||||
// replace human friendly x with JS *
|
|
||||||
.replace("x", "*")
|
|
||||||
// trig & misc functions
|
|
||||||
.replace("sin", "Math.sin")
|
|
||||||
.replace("cos", "Math.cos")
|
|
||||||
.replace("tan", "Math.tan")
|
|
||||||
.replace("√", "Math.sqrt")
|
|
||||||
.replace("^", "**")
|
|
||||||
.replace("abs", "Math.abs")
|
|
||||||
.replace("log", "Math.log")
|
|
||||||
;
|
|
||||||
// add any missing )s to the end
|
|
||||||
while(true) if (
|
|
||||||
(statement.match(/\(/g) || []).length >
|
|
||||||
(statement.match(/\)/g) || []).length
|
|
||||||
) statement += ")"; else break;
|
|
||||||
// evaluate the expression.
|
|
||||||
console.log("calculating [" + statement + "]");
|
|
||||||
try {
|
|
||||||
var result = eval(statement);
|
|
||||||
document.getElementById("prev-equation").innerHTML = mathtext.innerHTML + " = ";
|
|
||||||
mathtext.innerHTML = result;
|
|
||||||
mathtext.classList.remove("error-border");
|
|
||||||
} catch (e) {
|
|
||||||
mathtext.classList.add("error-border");
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const updateCalc = (e) => {
|
|
||||||
// character(s) recieved from button
|
|
||||||
var c = event.target.innerHTML;
|
|
||||||
var mathtext = document.getElementById("calc-text");
|
|
||||||
if (mathtext.innerHTML === "0") mathtext.innerHTML = "";
|
|
||||||
// special cases
|
|
||||||
switch (c) {
|
|
||||||
case "C":
|
|
||||||
// Clear
|
|
||||||
mathtext.innerHTML = "0";
|
|
||||||
break;
|
|
||||||
case "⬅":
|
|
||||||
// Delete
|
|
||||||
mathtext.innerHTML = mathtext.innerHTML.slice(0, -1);
|
|
||||||
if (mathtext.innerHTML.length === 0) {
|
|
||||||
mathtext.innerHTML = "0";
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "=":
|
|
||||||
calc()
|
|
||||||
break;
|
|
||||||
case "sin":
|
|
||||||
case "cos":
|
|
||||||
case "tan":
|
|
||||||
case "log":
|
|
||||||
case "√":
|
|
||||||
mathtext.innerHTML += `${c}(`;
|
|
||||||
break;
|
|
||||||
case "|x|":
|
|
||||||
mathtext.innerHTML += "abs("
|
|
||||||
break;
|
|
||||||
case "+":
|
|
||||||
case "-":
|
|
||||||
case "x":
|
|
||||||
case "/":
|
|
||||||
case "%":
|
|
||||||
case "^":
|
|
||||||
if (mathtext.innerHTML.length === 0) mathtext.innerHTML = "0";
|
|
||||||
// prevent typing 2 operators in a row
|
|
||||||
if (mathtext.innerHTML.match(/[+\-x\/%^] $/))
|
|
||||||
mathtext.innerHTML = mathtext.innerHTML.slice(0, -3);
|
|
||||||
mathtext.innerHTML += ` ${c} `;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
mathtext.innerHTML += c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (let i of $("#calc-btns button")) {
|
|
||||||
i.addEventListener('click', event => {
|
|
||||||
updateCalc(event);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -1,40 +0,0 @@
|
||||||
<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 %}
|
|
||||||
<link rel="search" href="opensearch.xml" type="application/opensearchdescription+xml" title="RaveSearch">
|
|
||||||
{% else %}
|
|
||||||
<link rel="search" href="opensearch.xml?tbm={{ search_type }}" type="application/opensearchdescription+xml" title="RaveSearch ({{ 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') }}">
|
|
||||||
|
|
||||||
<style>
|
|
||||||
@import "{{ cb_url('dark-theme.css') }}";
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<title>{{ clean_query(query) }} - RaveSearch</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
{{ search_header|safe }}
|
|
||||||
{% if is_translation %}
|
|
||||||
<iframe
|
|
||||||
id="lingva-iframe"
|
|
||||||
src="{{ lingva_url }}/auto/{{ translate_to }}/{{ translate_str }}">
|
|
||||||
</iframe>
|
|
||||||
{% endif %}
|
|
||||||
{{ response|safe }}
|
|
||||||
</body>
|
|
||||||
{% include 'footer.html' %}
|
|
||||||
{% if autocomplete_enabled == '1' %}
|
|
||||||
<script src="{{ cb_url('autocomplete.js') }}"></script>
|
|
||||||
{% endif %}
|
|
||||||
<script src="{{ cb_url('utils.js') }}"></script>
|
|
||||||
<script src="{{ cb_url('keyboard.js') }}"></script>
|
|
||||||
<script src="{{ cb_url('currency.js') }}"></script>
|
|
||||||
</html>
|
|
|
@ -1,106 +0,0 @@
|
||||||
{% 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>
|
|
||||||
{% if query and translation %}
|
|
||||||
<p>
|
|
||||||
<h4><a class="link" href="https://farside.link">{{ translation['continue-search'] }}</a></h4>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a href="https://github.com/benbusby/whoogle-search">Whoogle</a>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a class="link-color" href="{{farside}}/whoogle/search?q={{query}}{{params}}">
|
|
||||||
{{farside}}/whoogle/search?q={{query}}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://github.com/searxng/searxng">SearXNG</a>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a class="link-color" href="{{farside}}/searxng/search?q={{query}}">
|
|
||||||
{{farside}}/searxng/search?q={{query}}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<hr>
|
|
||||||
<h4>Other options:</h4>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a href="https://kagi.com">Kagi</a>
|
|
||||||
<ul>
|
|
||||||
<li>Requires account</li>
|
|
||||||
<li>
|
|
||||||
<a class="link-color" href="https://kagi.com/search?q={{query}}">
|
|
||||||
kagi.com/search?q={{query}}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://duckduckgo.com">DuckDuckGo</a>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a class="link-color" href="https://duckduckgo.com/search?q={{query}}">
|
|
||||||
duckduckgo.com/search?q={{query}}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://search.brave.com">Brave Search</a>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a class="link-color" href="https://search.brave.com/search?q={{query}}">
|
|
||||||
search.brave.com/search?q={{query}}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://ecosia.com">Ecosia</a>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a class="link-color" href="https://ecosia.com/search?q={{query}}">
|
|
||||||
ecosia.com/search?q={{query}}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://google.com">Google</a>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a class="link-color" href="https://google.com/search?q={{query}}">
|
|
||||||
google.com/search?q={{query}}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<hr>
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
<a class="link" href="home">Return Home</a>
|
|
||||||
</div>
|
|
|
@ -1,18 +0,0 @@
|
||||||
|
|
||||||
</div>
|
|
||||||
<footer>
|
|
||||||
<p class="footer">
|
|
||||||
A RaveSearch a Whoogle v{{ version_number }} alapjait használja ||
|
|
||||||
<a class="link" href="https://github.com/benbusby/whoogle-search"
|
|
||||||
>{{ translation['github-link'] }}</a
|
|
||||||
></p>
|
|
||||||
<!-- {% if has_update %} ||
|
|
||||||
<span class="update_available">Update Available 🟢</span>
|
|
||||||
{% endif %} -->
|
|
||||||
<span>
|
|
||||||
<p class="rp-credit" style="color: white">
|
|
||||||
Szeretnél többet megtudni a keresőről? <a class="kattide" href="https://rp1.hu/leirasok/ravepriest1-kereso/">RavePriest1</a>
|
|
||||||
</p>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
</footer>
|
|
|
@ -1,89 +0,0 @@
|
||||||
|
|
||||||
<div class="header-div">
|
|
||||||
<div id="search_header">
|
|
||||||
<a id="search_logo" href="{{ home_url }}" tabindex="0">
|
|
||||||
{{ logo|safe }}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form class="search-form header" id="search-form" method="{{ 'get' if config.get_only else 'post' }}" role="search" action="search">
|
|
||||||
<div class="header-search">
|
|
||||||
<div class="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"
|
|
||||||
name="q"
|
|
||||||
type="text"
|
|
||||||
value="{{ clean_query(query) }}"
|
|
||||||
dir="auto"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<input id="search-reset" type="reset" value="x">
|
|
||||||
<input name="tbm" value="{{ search_type }}" style="display: none">
|
|
||||||
<input name="country" value="{{ config.country }}" style="display: none;">
|
|
||||||
<input type="submit" id="search-submit" style="display: none;">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="header-tab-div">
|
|
||||||
<div class="mobile-header header-tab">
|
|
||||||
{% for tab_id, tab_content in tabs.items() %}
|
|
||||||
{% if tab_content['selected'] %}
|
|
||||||
<a><button class="header-button selected-header-button">{{ tab_content['name'] }}</button></a>
|
|
||||||
{% else %}
|
|
||||||
<a href="{{ tab_content['href'] }}"><button class="header-button">{{ tab_content['name'] }}</button></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 class="result-collapsible" id="adv-search-div">
|
|
||||||
<div class="result-config">
|
|
||||||
<label for="config-country">{{ translation['config-country'] }}: </label>
|
|
||||||
<select name="country" id="result-country">
|
|
||||||
{% for country in countries %}
|
|
||||||
<option value="{{ country.value }}"
|
|
||||||
{% if (
|
|
||||||
config.country != '' and config.country in country.value
|
|
||||||
) or (
|
|
||||||
config.country == '' and country.value == '')
|
|
||||||
%}
|
|
||||||
selected
|
|
||||||
{% endif %}>
|
|
||||||
{{ country.name }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
<br /><br />
|
|
||||||
<label for="config-time-period">{{ translation['config-time-period'] }}: </label>
|
|
||||||
<select name="tbs" id="result-time-period">
|
|
||||||
{% for time_period in time_periods %}
|
|
||||||
<option value="{{ time_period.value }}"
|
|
||||||
{% if (
|
|
||||||
config.tbs != '' and config.tbs in time_period.value
|
|
||||||
) or (
|
|
||||||
config.tbs == '' and time_period.value == '')
|
|
||||||
%}
|
|
||||||
selected
|
|
||||||
{% endif %}>
|
|
||||||
{{ translation[time_period.value] }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script type="text/javascript" src="{{ cb_url('header.js') }}"></script>
|
|
|
@ -1,210 +0,0 @@
|
||||||
{% 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">
|
|
||||||
{{ 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">
|
|
||||||
<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">
|
|
||||||
{{ 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">
|
|
||||||
<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 name="tbm" value="{{ search_type }}" style="display: none">
|
|
||||||
<input name="country" value="{{ config.country }}" style="display: none;">
|
|
||||||
<input name="tbs" value="{{ config.tbs }}" style="display: none;">
|
|
||||||
<input type="submit" style="display: none;">
|
|
||||||
<div class="sc"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</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>
|
|
||||||
</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>
|
|
||||||
<br />
|
|
||||||
<label for="config-time-period">{{ translation['config-time-period'] }}: </label>
|
|
||||||
<select name="tbs" id="result-time-period">
|
|
||||||
{% for time_period in time_periods %}
|
|
||||||
<option value="{{ time_period.value }}"
|
|
||||||
{% if (
|
|
||||||
config.tbs != '' and config.tbs in time_period.value
|
|
||||||
) or (
|
|
||||||
config.tbs == '' and time_period.value == '')
|
|
||||||
%}
|
|
||||||
selected
|
|
||||||
{% endif %}>
|
|
||||||
{{ translation[time_period.value] }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script type="text/javascript" src="{{ cb_url('header.js') }}"></script>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div id="search_view">
|
|
||||||
<div class="search_box">
|
|
||||||
<input id="q" name="q" type="text" placeholder="{{ _('Search for...') }}" tabindex="1" autocomplete="off" autocapitalize="none" spellcheck="false" autocorrect="off" dir="auto" value="{{ q or '' }}">
|
|
||||||
<button id="clear_search" type="reset" aria-label="{{ _('clear') }}" class="hide_if_nojs"><span>{{ icon_big('close') }}</span><span class="show_if_nojs">{{ _('clear') }}</span></button>
|
|
||||||
<button id="send_search" type="submit" {%- if search_on_category_select -%}name="category_{{ selected_categories[0]|replace(' ', '_') }}"{%- endif -%} aria-label="{{ _('search') }}"><span class="hide_if_nojs">{{ icon_big('search-outline') }}</span><span class="show_if_nojs">{{ _('search') }}</span></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% set display_tooltip = true %}
|
|
||||||
{% include 'simple/categories.html' %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="search_filters">
|
|
||||||
{% include 'simple/filters/languages.html' %}
|
|
||||||
{% include 'simple/filters/time_range.html' %}
|
|
||||||
{% include 'simple/filters/safesearch.html' %}
|
|
||||||
</div>
|
|
||||||
<input type="hidden" name="theme" value="{{ theme }}" >
|
|
||||||
{% if timeout_limit %}<input type="hidden" name="timeout_limit" value="{{ timeout_limit|e }}" >{% endif %}
|
|
||||||
</form>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div id="categories" class="search_categories">{{- '' -}}
|
|
||||||
<div id="categories_container">
|
|
||||||
{%- if not search_on_category_select or not display_tooltip -%}
|
|
||||||
{%- for category in categories_as_tabs -%}
|
|
||||||
<div class="category category_checkbox">{{- '' -}}
|
|
||||||
<input type="checkbox" id="checkbox_{{ category|replace(' ', '_') }}" name="category_{{ category }}"{% if category in selected_categories %} checked="checked"{% endif %}/>
|
|
||||||
<label for="checkbox_{{ category|replace(' ', '_') }}" class="tooltips">
|
|
||||||
{{- icon_big(category_icons[category]) if category in category_icons else icon_big('globe-outline') -}}
|
|
||||||
<div class="category_name">{{- _(category) -}}</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{%- endfor -%}
|
|
||||||
{%- if display_tooltip %}<div class="help">{{ _('Click on the magnifier to perform search') }}</div>{% endif -%}
|
|
||||||
{%- else -%}
|
|
||||||
{%- for category in categories_as_tabs -%}{{- '\n' -}}
|
|
||||||
<button type="submit" name="category_{{ category }}" class="category category_button {% if category in selected_categories %}selected{% endif %}">
|
|
||||||
{{- icon_big(category_icons[category]) if category in category_icons else icon_big('globe-outline') -}}
|
|
||||||
<div class="category_name">{{- _(category) -}}</div>{{- '' -}}
|
|
||||||
</button>{{- '' -}}
|
|
||||||
{%- endfor -%}
|
|
||||||
{{- '\n' -}}
|
|
||||||
{%- endif -%}
|
|
||||||
</div>{{- '' -}}
|
|
||||||
</div>
|
|
|
@ -1,451 +0,0 @@
|
||||||
<div>
|
|
||||||
<!-- <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: 900px;
|
|
||||||
}
|
|
||||||
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 {
|
|
||||||
width: 350px;
|
|
||||||
display: flex;
|
|
||||||
border: none;
|
|
||||||
color: #70757a;
|
|
||||||
font-size: 14px;
|
|
||||||
text-align: center;
|
|
||||||
/* background-color: #29292987; */
|
|
||||||
border-radius: 8px;
|
|
||||||
margin: 35px auto;
|
|
||||||
padding: 10px;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.frGj1b {
|
|
||||||
margin: 10px;
|
|
||||||
font-size: 16px;
|
|
||||||
border: 1px solid;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 10px 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.BnJWBc {
|
|
||||||
text-align: center;
|
|
||||||
padding: 6px 0 13px 0;
|
|
||||||
height: 35px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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%;
|
|
||||||
height: 100%;
|
|
||||||
background-color: #29292987;
|
|
||||||
border-radius: 10px;
|
|
||||||
margin: 10px 0;
|
|
||||||
display: grid;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 10px 0;
|
|
||||||
}
|
|
||||||
.ezO2md {
|
|
||||||
border: thin solid #dadce0;
|
|
||||||
padding: 12px 16px 12px 16px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-family: Roboto, Helvetica, Arial, sans-serif;
|
|
||||||
}
|
|
||||||
.K35ahc {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.owohpf {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.fYyStc {
|
|
||||||
word-break: break-word;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 16px;
|
|
||||||
padding: 0 0 10px 0;
|
|
||||||
}
|
|
||||||
.img_button {
|
|
||||||
padding: 10px;
|
|
||||||
margin: 0 auto 10px;
|
|
||||||
border: 1px solid #84dbff;
|
|
||||||
border-radius: 6px;
|
|
||||||
color: #3c4043;
|
|
||||||
background-color: #2e97c3;
|
|
||||||
}
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
.NHQNef {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
.Cb8Z7c {
|
|
||||||
white-space: pre;
|
|
||||||
}
|
|
||||||
a.ZWRArf {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
a .CVA68e:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.results {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 16px;
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
.result-item {
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
background-color: #29292987;
|
|
||||||
width: calc(33.333% - 16px);
|
|
||||||
box-sizing: border-box;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-item img {
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-item p {
|
|
||||||
margin: 10px 0 0;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.img-title {
|
|
||||||
color: var(--primary-grad3);
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.img-url {
|
|
||||||
color: var(--body-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.img-url:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
color: var(--grad2-active);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Stílusok a javaslatokhoz */
|
|
||||||
.suggestions {
|
|
||||||
margin: 20px 0;
|
|
||||||
padding: 10px;
|
|
||||||
background-color: #e9e9e9;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Stílusok a lapozáshoz */
|
|
||||||
.pagination {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination a {
|
|
||||||
margin: 0 5px;
|
|
||||||
padding: 10px 15px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
background-color: #fff;
|
|
||||||
text-decoration: none;
|
|
||||||
color: #007bff;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination a:hover {
|
|
||||||
background-color: #007bff;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.e3goi {
|
|
||||||
padding: 10px;
|
|
||||||
width: 100%;
|
|
||||||
height: 160px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.TxbwNb {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.RAyV4b {
|
|
||||||
width: 100%;
|
|
||||||
height: 130px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.t0fcAb {
|
|
||||||
scale: 1.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Tor4Ec {
|
|
||||||
text-align: center;
|
|
||||||
width: 100%;
|
|
||||||
height: 20px;
|
|
||||||
display: block;
|
|
||||||
padding: 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qXLe6d {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
</style> -->
|
|
||||||
|
|
||||||
<div class="results">
|
|
||||||
{% for result in results %}
|
|
||||||
<div class="result-item">
|
|
||||||
<a href="{{ result.web_page }}">
|
|
||||||
<img src="{{ result.img_tbn }}" alt="Image thumbnail">
|
|
||||||
</a>
|
|
||||||
<a href="{{ result.web_page }}"><p class="img-title">{{ result.domain }}</p></a>
|
|
||||||
<a href="{{ result.img_url }}"><p class="img-url">{{ result.img_url }}</p></a>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="uZgmoc">
|
|
||||||
<!-- next page object -->
|
|
||||||
</div>
|
|
||||||
<br />
|
|
||||||
|
|
||||||
|
|
||||||
<div class="lIMUZd">
|
|
||||||
<div class="By0U9">
|
|
||||||
<!-- correction suggested -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
|
@ -1,303 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
|
|
||||||
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="static/img/favicon/apple-touch-icon.png">
|
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="static/img/favicon/favicon-32x32.png">
|
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="static/img/favicon/favicon-16x16.png">
|
|
||||||
<link rel="manifest" href="static/img/favicon/site.webmanifest">
|
|
||||||
<link rel="mask-icon" href="static/img/favicon/safari-pinned-tab.svg" color="#00566f">
|
|
||||||
<meta name="msapplication-TileColor" content="#da532c">
|
|
||||||
<meta name="theme-color" content="#ffffff">
|
|
||||||
|
|
||||||
{% 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="RaveSearch">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<link rel="stylesheet" href="{{ cb_url('logo.css') }}">
|
|
||||||
|
|
||||||
<style>
|
|
||||||
@import "{{ cb_url('dark-theme.css') }}";
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="{{ cb_url('main.css') }}">
|
|
||||||
|
|
||||||
<title>RaveSearch</title>
|
|
||||||
</head>
|
|
||||||
<body id="main_index">
|
|
||||||
<main 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
|
|
||||||
placeholder="{{ translation['input-text'] }}"
|
|
||||||
type="text"
|
|
||||||
name="q"
|
|
||||||
id="search-bar"
|
|
||||||
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 %}
|
|
||||||
<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">
|
|
||||||
<label for="config-time-period">{{ translation['config-time-period'] }}</label>
|
|
||||||
<select name="tbs" id="config-time-period">
|
|
||||||
{% for time_period in time_periods %}
|
|
||||||
<option value="{{ time_period.value }}"
|
|
||||||
{% if (
|
|
||||||
config.tbs != '' and config.tbs in time_period.value
|
|
||||||
) or (
|
|
||||||
config.tbs == '' and time_period.value == '')
|
|
||||||
%}
|
|
||||||
selected
|
|
||||||
{% endif %}>
|
|
||||||
{{ translation[time_period.value] }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div> -->
|
|
||||||
<div class="config-div config-div-lang">
|
|
||||||
<label for="config-lang-interface">{{ translation['config-lang'] }}: </label>
|
|
||||||
<select name="lang_interface" id="config-lang-interface">
|
|
||||||
{% 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-autocomplete">
|
|
||||||
<label for="config-autocomplete">autocomplete</label>
|
|
||||||
<input type="checkbox" name="autocomplete" id="config-autocomplete" {{ 'checked' if config.autocomplete else '' }}>
|
|
||||||
</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>
|
|
||||||
<!-- 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 checked 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_modified"
|
|
||||||
id="config-style"
|
|
||||||
autocapitalize="off"
|
|
||||||
autocomplete="off"
|
|
||||||
spellcheck="false"
|
|
||||||
autocorrect="off"
|
|
||||||
value="">{{ config.style_modified.replace('\t', '') }}</textarea>
|
|
||||||
</div>
|
|
||||||
<div class="config-div config-div-pref-url">
|
|
||||||
<label for="config-pref-encryption">{{ translation['config-pref-encryption'] }}: </label>
|
|
||||||
<input type="checkbox" name="preferences_encrypted"
|
|
||||||
id="config-pref-encryption" {{ 'checked' if config.preferences_encrypted and config.preferences_key else '' }}>
|
|
||||||
<div><span class="info-text"> — {{ translation['config-pref-help'] }}</span></div>
|
|
||||||
<label for="config-pref-url">{{ translation['config-pref-url'] }}: </label>
|
|
||||||
<input type="text" name="pref-url" id="config-pref-url" value="{{ config.url }}?preferences={{ config.preferences }}">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="config-div config-buttons">
|
|
||||||
<input type="submit" id="config-load" value="{{ translation['load'] }}">
|
|
||||||
<input type="submit" id="config-submit" value="{{ translation['apply'] }}">
|
|
||||||
<input type="submit" id="config-save" value="{{ translation['save-as'] }}">
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
|
|
||||||
<div class="social-container">
|
|
||||||
<div class="social-column">
|
|
||||||
<div class="rpkick">
|
|
||||||
<a href="https://kick.com/ravepriest1">
|
|
||||||
<img
|
|
||||||
src="static/img/rp/rpkicksearch.png"
|
|
||||||
alt="rpkick"
|
|
||||||
title="RP Kick"
|
|
||||||
width="200px"
|
|
||||||
height="50px"
|
|
||||||
/></a>
|
|
||||||
</div>
|
|
||||||
<div class="rpdiscord">
|
|
||||||
<a href="https://discord.com/invite/MwUydP4gc5">
|
|
||||||
<img
|
|
||||||
src="static/img/rp/rpdiscordsearch.png"
|
|
||||||
alt="rpdiscord"
|
|
||||||
title="RP Discord"
|
|
||||||
width="200px"
|
|
||||||
height="50px"
|
|
||||||
/></a>
|
|
||||||
</div>
|
|
||||||
<div class="rptwitch">
|
|
||||||
<a href="https://www.twitch.tv/ravepriest1">
|
|
||||||
<img
|
|
||||||
src="static/img/rp/rptwitchsearch.png"
|
|
||||||
alt="rptwitch"
|
|
||||||
title="RP Twitch"
|
|
||||||
width="200px"
|
|
||||||
height="50px"
|
|
||||||
/></a>
|
|
||||||
</div>
|
|
||||||
<div class="rpgit">
|
|
||||||
<a href="https://git.rp1.hu/explore/repos">
|
|
||||||
<img
|
|
||||||
src="static/img/rp/rpgiteasearch.png"
|
|
||||||
alt="rpgit"
|
|
||||||
title="RP Gitea"
|
|
||||||
width="200px"
|
|
||||||
height="50px"
|
|
||||||
/></a>
|
|
||||||
</div>
|
|
||||||
<div class="rpyoutube">
|
|
||||||
<a href="https://www.youtube.com/@RPslair">
|
|
||||||
<img
|
|
||||||
src="static/img/rp/rpyoutubesearch.png"
|
|
||||||
alt="rpyoutube"
|
|
||||||
title="RP YouTube"
|
|
||||||
width="200px"
|
|
||||||
height="50px"
|
|
||||||
/></a>
|
|
||||||
</div>
|
|
||||||
<div class="rpweb">
|
|
||||||
<a href="https://rp1.hu">
|
|
||||||
<img
|
|
||||||
src="static/img/rp/rpwebsearch.png"
|
|
||||||
alt="rpweb"
|
|
||||||
title="RP Website"
|
|
||||||
width="200px"
|
|
||||||
height="50px"
|
|
||||||
/></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
{% include 'footer.html' %}
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
|
@ -1,8 +0,0 @@
|
||||||
<div class="rplogo">
|
|
||||||
<img
|
|
||||||
src="static/img/rplogo.webp"
|
|
||||||
alt="rplogo"
|
|
||||||
title="RP Kereső"
|
|
||||||
style="object-fit: contain; height: auto; width: 100%;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
|
@ -1,15 +0,0 @@
|
||||||
<form id="search-form" action="search" method="post">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="q"
|
|
||||||
style="width: 90%"
|
|
||||||
autofocus="autofocus"
|
|
||||||
autocapitalize="none"
|
|
||||||
spellcheck="false"
|
|
||||||
autocorrect="off"
|
|
||||||
placeholder="RaveSearch"
|
|
||||||
autocomplete="off"
|
|
||||||
dir="auto"
|
|
||||||
/>
|
|
||||||
<input type="submit" style="width: 9%" id="search-submit" value="Search" />
|
|
||||||
</form>
|
|
BIN
app/utils/.DS_Store
vendored
|
@ -1,146 +0,0 @@
|
||||||
import json
|
|
||||||
import requests
|
|
||||||
import urllib.parse as urlparse
|
|
||||||
import os
|
|
||||||
import glob
|
|
||||||
|
|
||||||
bangs_dict = {}
|
|
||||||
DDG_BANGS = 'https://duckduckgo.com/bang.js'
|
|
||||||
|
|
||||||
|
|
||||||
def load_all_bangs(ddg_bangs_file: str, ddg_bangs: dict = {}):
|
|
||||||
"""Loads all the bang files in alphabetical order
|
|
||||||
Args:
|
|
||||||
ddg_bangs_file: The str path to the new DDG bangs json file
|
|
||||||
ddg_bangs: The dict of ddg bangs. If this is empty, it will load the
|
|
||||||
bangs from the file
|
|
||||||
Returns:
|
|
||||||
None
|
|
||||||
"""
|
|
||||||
global bangs_dict
|
|
||||||
ddg_bangs_file = os.path.normpath(ddg_bangs_file)
|
|
||||||
|
|
||||||
if (bangs_dict and not ddg_bangs) or os.path.getsize(ddg_bangs_file) <= 4:
|
|
||||||
return
|
|
||||||
|
|
||||||
bangs = {}
|
|
||||||
bangs_dir = os.path.dirname(ddg_bangs_file)
|
|
||||||
bang_files = glob.glob(os.path.join(bangs_dir, '*.json'))
|
|
||||||
|
|
||||||
# Normalize the paths
|
|
||||||
bang_files = [os.path.normpath(f) for f in bang_files]
|
|
||||||
|
|
||||||
# Move the ddg bangs file to the beginning
|
|
||||||
bang_files = sorted([f for f in bang_files if f != ddg_bangs_file])
|
|
||||||
|
|
||||||
if ddg_bangs:
|
|
||||||
bangs |= ddg_bangs
|
|
||||||
else:
|
|
||||||
bang_files.insert(0, ddg_bangs_file)
|
|
||||||
|
|
||||||
for i, bang_file in enumerate(bang_files):
|
|
||||||
try:
|
|
||||||
bangs |= json.load(open(bang_file))
|
|
||||||
except json.decoder.JSONDecodeError:
|
|
||||||
# Ignore decoding error only for the ddg bangs file, since this can
|
|
||||||
# occur if file is still being written
|
|
||||||
if i != 0:
|
|
||||||
raise
|
|
||||||
|
|
||||||
bangs_dict = dict(sorted(bangs.items()))
|
|
||||||
|
|
||||||
|
|
||||||
def gen_bangs_json(bangs_file: str) -> None:
|
|
||||||
"""Generates a json file from the DDG bangs list
|
|
||||||
|
|
||||||
Args:
|
|
||||||
bangs_file: The str path to the new DDG bangs json file
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
None
|
|
||||||
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Request full list from DDG
|
|
||||||
r = requests.get(DDG_BANGS)
|
|
||||||
r.raise_for_status()
|
|
||||||
except requests.exceptions.HTTPError as err:
|
|
||||||
raise SystemExit(err)
|
|
||||||
|
|
||||||
# Convert to json
|
|
||||||
data = json.loads(r.text)
|
|
||||||
|
|
||||||
# Set up a json object (with better formatting) for all available bangs
|
|
||||||
bangs_data = {}
|
|
||||||
|
|
||||||
for row in data:
|
|
||||||
bang_command = '!' + row['t']
|
|
||||||
bangs_data[bang_command] = {
|
|
||||||
'url': row['u'].replace('{{{s}}}', '{}'),
|
|
||||||
'suggestion': bang_command + ' (' + row['s'] + ')'
|
|
||||||
}
|
|
||||||
|
|
||||||
json.dump(bangs_data, open(bangs_file, 'w'))
|
|
||||||
print('* Finished creating ddg bangs json')
|
|
||||||
load_all_bangs(bangs_file, bangs_data)
|
|
||||||
|
|
||||||
|
|
||||||
def suggest_bang(query: str) -> list[str]:
|
|
||||||
"""Suggests bangs for a user's query
|
|
||||||
Args:
|
|
||||||
query: The search query
|
|
||||||
Returns:
|
|
||||||
list[str]: A list of bang suggestions
|
|
||||||
"""
|
|
||||||
global bangs_dict
|
|
||||||
return [bangs_dict[_]['suggestion'] for _ in bangs_dict if _.startswith(query)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_bang(query: str) -> str:
|
|
||||||
"""Transform's a user's query to a bang search, if an operator is found
|
|
||||||
|
|
||||||
Args:
|
|
||||||
query: The search query
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: A formatted redirect for a bang search, or an empty str if there
|
|
||||||
wasn't a match or didn't contain a bang operator
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
global bangs_dict
|
|
||||||
|
|
||||||
#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 ''
|
|
|
@ -1,137 +0,0 @@
|
||||||
import base64
|
|
||||||
import hashlib
|
|
||||||
import contextlib
|
|
||||||
import io
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
|
|
||||||
from requests import exceptions, get
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
from bs4 import BeautifulSoup as bsoup
|
|
||||||
from cryptography.fernet import Fernet
|
|
||||||
from flask import Request
|
|
||||||
|
|
||||||
ddg_favicon_site = 'http://icons.duckduckgo.com/ip2'
|
|
||||||
|
|
||||||
empty_gif = base64.b64decode(
|
|
||||||
'R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==')
|
|
||||||
|
|
||||||
placeholder_img = base64.b64decode(
|
|
||||||
'iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAYAAADE6YVjAAABF0lEQVRIS8XWPw9EMBQA8Eok' \
|
|
||||||
'JBKrMFqMBt//GzAYLTZ/VomExPDu6uLiaPteqVynBn0/75W2Vp7nEIYhe6p1XcespmmAd7Is' \
|
|
||||||
'M+4URcGiKPogvMMvmIS2eN9MOMKbKWgf54SYgI4vKkTuQKJKSJErkKzUSkQHUs0lilAg7GMh' \
|
|
||||||
'ISoIA/hYMiKCKIA2soeowCWEMkfHtUmrXLcyGYYBfN9HF8djiaglWzNZlgVs21YisoAUaEXG' \
|
|
||||||
'cQTP86QIFgi7vyLzPIPjOEIEC7ANQv/4aZrAdd0TUtc1i+MYnSsMWjPp+x6CIPgJVlUVS5KE' \
|
|
||||||
'DKig/+wnVzM4pnzaGeHd+ENlWbI0TbVLJBtw2uMfP63wc9d2kDCWxi5Q27bsBerSJ9afJbeL' \
|
|
||||||
'AAAAAElFTkSuQmCC'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_favicon(url: str) -> bytes:
|
|
||||||
"""Fetches a favicon using DuckDuckGo's favicon retriever
|
|
||||||
|
|
||||||
Args:
|
|
||||||
url: The url to fetch the favicon from
|
|
||||||
Returns:
|
|
||||||
bytes - the favicon bytes, or a placeholder image if one
|
|
||||||
was not returned
|
|
||||||
"""
|
|
||||||
response = get(f'{ddg_favicon_site}/{urlparse(url).netloc}.ico')
|
|
||||||
|
|
||||||
if response.status_code == 200 and len(response.content) > 0:
|
|
||||||
tmp_mem = io.BytesIO()
|
|
||||||
tmp_mem.write(response.content)
|
|
||||||
tmp_mem.seek(0)
|
|
||||||
|
|
||||||
return tmp_mem.read()
|
|
||||||
return placeholder_img
|
|
||||||
|
|
||||||
|
|
||||||
def gen_file_hash(path: str, static_file: str) -> str:
|
|
||||||
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 f'{filename_split[0]}.{file_hash}{filename_split[-1]}'
|
|
||||||
|
|
||||||
|
|
||||||
def read_config_bool(var: str, default: bool=False) -> bool:
|
|
||||||
val = os.getenv(var, '1' if default else '0')
|
|
||||||
# user can specify one of the following values as 'true' inputs (all
|
|
||||||
# variants with upper case letters will also work):
|
|
||||||
# ('true', 't', '1', 'yes', 'y')
|
|
||||||
return val.lower() in ('true', 't', '1', 'yes', 'y')
|
|
||||||
|
|
||||||
|
|
||||||
def get_client_ip(r: Request) -> str:
|
|
||||||
if r.environ.get('HTTP_X_FORWARDED_FOR') is None:
|
|
||||||
return r.environ['REMOTE_ADDR']
|
|
||||||
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')
|
|
||||||
|
|
||||||
full_path = r.full_path if not root else ''
|
|
||||||
if full_path.startswith('/'):
|
|
||||||
full_path = f'/{full_path}'
|
|
||||||
|
|
||||||
if http_host:
|
|
||||||
prefix = os.environ.get('WHOOGLE_URL_PREFIX', '')
|
|
||||||
if prefix:
|
|
||||||
prefix = f'/{re.sub("[^0-9a-zA-Z]+", "", prefix)}'
|
|
||||||
return f'{scheme}://{http_host}{prefix}{full_path}'
|
|
||||||
|
|
||||||
return default
|
|
||||||
|
|
||||||
|
|
||||||
def check_for_update(version_url: str, current: str) -> int:
|
|
||||||
# Check for the latest version of Whoogle
|
|
||||||
has_update = ''
|
|
||||||
with contextlib.suppress(exceptions.ConnectionError, AttributeError):
|
|
||||||
update = bsoup(get(version_url).text, 'html.parser')
|
|
||||||
latest = update.select_one('[class="Link--primary"]').string[1:]
|
|
||||||
current = int(''.join(filter(str.isdigit, current)))
|
|
||||||
latest = int(''.join(filter(str.isdigit, latest)))
|
|
||||||
has_update = '' if current >= latest else latest
|
|
||||||
|
|
||||||
return has_update
|
|
||||||
|
|
||||||
|
|
||||||
def get_abs_url(url, page_url):
|
|
||||||
# Creates a valid absolute URL using a partial or relative URL
|
|
||||||
urls = {
|
|
||||||
"//": f"https:{url}",
|
|
||||||
"/": f"{urlparse(page_url).netloc}{url}",
|
|
||||||
"./": f"{page_url}{url[2:]}"
|
|
||||||
}
|
|
||||||
for start in urls:
|
|
||||||
if url.startswith(start):
|
|
||||||
return urls[start]
|
|
||||||
|
|
||||||
return url
|
|
||||||
|
|
||||||
|
|
||||||
def list_to_dict(lst: list) -> dict:
|
|
||||||
if len(lst) < 2:
|
|
||||||
return {}
|
|
||||||
return {lst[i].replace(' ', ''): lst[i+1].replace(' ', '')
|
|
||||||
for i in range(0, len(lst), 2)}
|
|
||||||
|
|
||||||
|
|
||||||
def encrypt_string(key: bytes, string: str) -> str:
|
|
||||||
cipher_suite = Fernet(key)
|
|
||||||
return cipher_suite.encrypt(string.encode()).decode()
|
|
||||||
|
|
||||||
|
|
||||||
def decrypt_string(key: bytes, string: str) -> str:
|
|
||||||
cipher_suite = Fernet(g.session_key)
|
|
||||||
return cipher_suite.decrypt(string.encode()).decode()
|
|
|
@ -1,466 +0,0 @@
|
||||||
from app.models.config import Config
|
|
||||||
from app.models.endpoint import Endpoint
|
|
||||||
from app.utils.misc import list_to_dict
|
|
||||||
from bs4 import BeautifulSoup, NavigableString
|
|
||||||
import copy
|
|
||||||
from flask import current_app
|
|
||||||
import html
|
|
||||||
import os
|
|
||||||
import urllib.parse as urlparse
|
|
||||||
from urllib.parse import parse_qs
|
|
||||||
import re
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
SKIP_ARGS = ['ref_src', 'utm']
|
|
||||||
SKIP_PREFIX = ['//www.', '//mobile.', '//m.']
|
|
||||||
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', 'Szponzorált',
|
|
||||||
'Anúncio', 'Quảng cáo','โฆษณา', 'sponsored', 'patrocinado', 'gesponsert', 'Sponzorováno', '스폰서', 'Gesponsord'
|
|
||||||
]
|
|
||||||
|
|
||||||
SITE_ALTS = {
|
|
||||||
'twitter.com': os.getenv('WHOOGLE_ALT_TW', 'farside.link/nitter'),
|
|
||||||
'youtube.com': os.getenv('WHOOGLE_ALT_YT', 'farside.link/invidious'),
|
|
||||||
'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'),
|
|
||||||
'stackoverflow.com': os.getenv('WHOOGLE_ALT_SO', 'farside.link/anonymousoverflow')
|
|
||||||
}
|
|
||||||
|
|
||||||
# Include custom site redirects from WHOOGLE_REDIRECTS
|
|
||||||
SITE_ALTS.update(list_to_dict(re.split(',|:', os.getenv('WHOOGLE_REDIRECTS', ''))))
|
|
||||||
|
|
||||||
|
|
||||||
def contains_cjko(s: str) -> bool:
|
|
||||||
"""This function check whether or not a string contains Chinese, Japanese,
|
|
||||||
or Korean characters. It employs regex and uses the u escape sequence to
|
|
||||||
match any character in a set of Unicode ranges.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
s (str): string to be checked
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if the input s contains the characters and False otherwise
|
|
||||||
"""
|
|
||||||
unicode_ranges = ('\u4e00-\u9fff' # Chinese characters
|
|
||||||
'\u3040-\u309f' # Japanese hiragana
|
|
||||||
'\u30a0-\u30ff' # Japanese katakana
|
|
||||||
'\u4e00-\u9faf' # Japanese kanji
|
|
||||||
'\uac00-\ud7af' # Korean hangul syllables
|
|
||||||
'\u1100-\u11ff' # Korean hangul jamo
|
|
||||||
)
|
|
||||||
return bool(re.search(fr'[{unicode_ranges}]', s))
|
|
||||||
|
|
||||||
|
|
||||||
def bold_search_terms(response: str, query: str) -> BeautifulSoup:
|
|
||||||
"""Wraps all search terms in bold tags (<b>). If any terms are wrapped
|
|
||||||
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
|
|
||||||
|
|
||||||
# Ensure target word is escaped for regex
|
|
||||||
target_word = re.escape(target_word)
|
|
||||||
|
|
||||||
# Check if the word contains Chinese, Japanese, or Korean characters
|
|
||||||
if contains_cjko(target_word):
|
|
||||||
reg_pattern = fr'((?![{{}}<>-]){target_word}(?![{{}}<>-]))'
|
|
||||||
else:
|
|
||||||
reg_pattern = fr'\b((?![{{}}<>-]){target_word}(?![{{}}<>-]))\b'
|
|
||||||
|
|
||||||
if re.match(r'.*[@_!#$%^&*()<>?/\|}{~:].*', target_word) or (
|
|
||||||
element.parent and element.parent.name == 'style'):
|
|
||||||
return
|
|
||||||
|
|
||||||
element.replace_with(BeautifulSoup(
|
|
||||||
re.sub(reg_pattern,
|
|
||||||
r'<b>\1</b>',
|
|
||||||
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'[@_!#$%^&*()<>?/\|}{~:]+', '', 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
|
|
||||||
|
|
||||||
Args:
|
|
||||||
element: The HTML element to inspect
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
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)
|
|
||||||
or 'ⓘ' in element)
|
|
||||||
|
|
||||||
|
|
||||||
def get_first_link(soup: BeautifulSoup) -> str:
|
|
||||||
"""Retrieves the first result link from the query response
|
|
||||||
|
|
||||||
Args:
|
|
||||||
soup: The BeautifulSoup response body
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: A str link to the first result
|
|
||||||
|
|
||||||
"""
|
|
||||||
first_link = ''
|
|
||||||
orig_details = []
|
|
||||||
|
|
||||||
# Temporarily remove details so we don't grab those links
|
|
||||||
for details in soup.find_all('details'):
|
|
||||||
temp_details = soup.new_tag('removed_details')
|
|
||||||
orig_details.append(details.replace_with(temp_details))
|
|
||||||
|
|
||||||
# Replace hrefs with only the intended destination (no "utm" type tags)
|
|
||||||
for a in soup.find_all('a', href=True):
|
|
||||||
# Return the first search result URL
|
|
||||||
if a['href'].startswith('http://') or a['href'].startswith('https://'):
|
|
||||||
first_link = a['href']
|
|
||||||
break
|
|
||||||
|
|
||||||
# Add the details back
|
|
||||||
for orig_detail, details in zip(orig_details, soup.find_all('removed_details')):
|
|
||||||
details.replace_with(orig_detail)
|
|
||||||
|
|
||||||
return first_link
|
|
||||||
|
|
||||||
|
|
||||||
def get_site_alt(link: str, site_alts: dict = SITE_ALTS) -> str:
|
|
||||||
"""Returns an alternative to a particular site, if one is configured
|
|
||||||
|
|
||||||
Args:
|
|
||||||
link: A string result URL to check against the site_alts map
|
|
||||||
site_alts: A map of site alternatives to replace with. defaults to SITE_ALTS
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: An updated (or ignored) result link
|
|
||||||
|
|
||||||
"""
|
|
||||||
# Need to replace full hostname with alternative to encapsulate
|
|
||||||
# subdomains as well
|
|
||||||
parsed_link = urlparse.urlparse(link)
|
|
||||||
|
|
||||||
# Extract subdomain separately from the domain+tld. The subdomain
|
|
||||||
# is used for wikiless translations.
|
|
||||||
split_host = parsed_link.netloc.split('.')
|
|
||||||
subdomain = split_host[0] if len(split_host) > 2 else ''
|
|
||||||
hostname = '.'.join(split_host[-2:])
|
|
||||||
|
|
||||||
# The full scheme + hostname is used when comparing against the list of
|
|
||||||
# available alternative services, due to how Medium links are constructed.
|
|
||||||
# (i.e. for medium.com: "https://something.medium.com" should match,
|
|
||||||
# "https://medium.com/..." should match, but "philomedium.com" should not)
|
|
||||||
hostcomp = f'{parsed_link.scheme}://{hostname}'
|
|
||||||
|
|
||||||
for site_key in site_alts.keys():
|
|
||||||
site_alt = f'{parsed_link.scheme}://{site_key}'
|
|
||||||
if not hostname or site_alt not in hostcomp or not site_alts[site_key]:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Wikipedia -> Wikiless replacements require the subdomain (if it's
|
|
||||||
# a 2-char language code) to be passed as a URL param to Wikiless
|
|
||||||
# in order to preserve the language setting.
|
|
||||||
params = ''
|
|
||||||
if 'wikipedia' in hostname and len(subdomain) == 2:
|
|
||||||
hostname = f'{subdomain}.{hostname}'
|
|
||||||
params = f'?lang={subdomain}'
|
|
||||||
elif 'medium' in hostname and len(subdomain) > 0:
|
|
||||||
hostname = f'{subdomain}.{hostname}'
|
|
||||||
|
|
||||||
parsed_alt = urlparse.urlparse(site_alts[site_key])
|
|
||||||
link = link.replace(hostname, site_alts[site_key]) + params
|
|
||||||
# If a scheme is specified in the alternative, this results in a
|
|
||||||
# replaced link that looks like "https://http://altservice.tld".
|
|
||||||
# In this case, we can remove the original scheme from the result
|
|
||||||
# and use the one specified for the alt.
|
|
||||||
if parsed_alt.scheme:
|
|
||||||
link = '//'.join(link.split('//')[1:])
|
|
||||||
|
|
||||||
for prefix in SKIP_PREFIX:
|
|
||||||
if parsed_alt.scheme:
|
|
||||||
# If a scheme is specified, remove everything before the
|
|
||||||
# first occurence of it
|
|
||||||
link = f'{parsed_alt.scheme}{link.split(parsed_alt.scheme, 1)[-1]}'
|
|
||||||
else:
|
|
||||||
# Otherwise, replace the first occurrence of the prefix
|
|
||||||
link = link.replace(prefix, '//', 1)
|
|
||||||
break
|
|
||||||
|
|
||||||
return link
|
|
||||||
|
|
||||||
|
|
||||||
def filter_link_args(link: str) -> str:
|
|
||||||
"""Filters out unnecessary URL args from a result link
|
|
||||||
|
|
||||||
Args:
|
|
||||||
link: The string result link to check for extraneous URL params
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: An updated (or ignored) result link
|
|
||||||
|
|
||||||
"""
|
|
||||||
parsed_link = urlparse.urlparse(link)
|
|
||||||
link_args = parse_qs(parsed_link.query)
|
|
||||||
safe_args = {}
|
|
||||||
|
|
||||||
if len(link_args) == 0 and len(parsed_link) > 0:
|
|
||||||
return link
|
|
||||||
|
|
||||||
for arg in link_args.keys():
|
|
||||||
if arg in SKIP_ARGS:
|
|
||||||
continue
|
|
||||||
|
|
||||||
safe_args[arg] = link_args[arg]
|
|
||||||
|
|
||||||
# Remove original link query and replace with filtered args
|
|
||||||
link = link.replace(parsed_link.query, '')
|
|
||||||
if len(safe_args) > 0:
|
|
||||||
link = link + urlparse.urlencode(safe_args, doseq=True)
|
|
||||||
else:
|
|
||||||
link = link.replace('?', '')
|
|
||||||
|
|
||||||
return link
|
|
||||||
|
|
||||||
|
|
||||||
def append_nojs(result: BeautifulSoup) -> None:
|
|
||||||
"""Appends a no-Javascript alternative for a search result
|
|
||||||
|
|
||||||
Args:
|
|
||||||
result: The search result to append a no-JS link to
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
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'
|
|
||||||
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 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
|
|
||||||
"""
|
|
||||||
map_query = full_query
|
|
||||||
if '-site:' in full_query:
|
|
||||||
block_idx = full_query.index('-site:')
|
|
||||||
map_query = map_query[:block_idx]
|
|
||||||
tabs = copy.deepcopy(tabs)
|
|
||||||
for tab_id, tab_content in tabs.items():
|
|
||||||
# update name to desired language
|
|
||||||
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,
|
|
||||||
map_query=map_query)
|
|
||||||
|
|
||||||
# update if selected tab (default all tab is selected)
|
|
||||||
if tab_content['tbm'] == search_type:
|
|
||||||
tabs['all']['selected'] = False
|
|
||||||
tab_content['selected'] = True
|
|
||||||
return tabs
|
|
|
@ -1,194 +0,0 @@
|
||||||
import os
|
|
||||||
import re
|
|
||||||
from typing import Any
|
|
||||||
from app.filter import Filter
|
|
||||||
from app.request import gen_query
|
|
||||||
from app.utils.misc import get_proxy_host_url
|
|
||||||
from app.utils.results import get_first_link
|
|
||||||
from bs4 import BeautifulSoup as bsoup
|
|
||||||
from cryptography.fernet import Fernet, InvalidToken
|
|
||||||
from flask import g
|
|
||||||
|
|
||||||
TOR_BANNER = '<hr><h1 style="text-align: center">You are using Tor</h1><hr>'
|
|
||||||
CAPTCHA = 'div class="g-recaptcha"'
|
|
||||||
|
|
||||||
|
|
||||||
def needs_https(url: str) -> bool:
|
|
||||||
"""Checks if the current instance needs to be upgraded to HTTPS
|
|
||||||
|
|
||||||
Note that all Heroku instances are available by default over HTTPS, but
|
|
||||||
do not automatically set up a redirect when visited over HTTP.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
url: The instance url
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True/False representing the need to upgrade
|
|
||||||
|
|
||||||
"""
|
|
||||||
https_only = bool(os.getenv('HTTPS_ONLY', 0))
|
|
||||||
is_heroku = url.endswith('.herokuapp.com')
|
|
||||||
is_http = url.startswith('http://')
|
|
||||||
|
|
||||||
return (is_heroku and is_http) or (https_only and is_http)
|
|
||||||
|
|
||||||
|
|
||||||
def has_captcha(results: str) -> bool:
|
|
||||||
"""Checks to see if the search results are blocked by a captcha
|
|
||||||
|
|
||||||
Args:
|
|
||||||
results: The search page html as a string
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True/False indicating if a captcha element was found
|
|
||||||
|
|
||||||
"""
|
|
||||||
return CAPTCHA in results
|
|
||||||
|
|
||||||
|
|
||||||
class Search:
|
|
||||||
"""Search query preprocessor - used before submitting the query or
|
|
||||||
redirecting to another site
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
request: the incoming flask request
|
|
||||||
config: the current user config settings
|
|
||||||
session_key: the flask user fernet key
|
|
||||||
"""
|
|
||||||
def __init__(self, request, config, session_key, 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.query = ''
|
|
||||||
self.widget = ''
|
|
||||||
self.view_image = True
|
|
||||||
self.cookies_disabled = cookies_disabled
|
|
||||||
self.search_type = self.request_params.get(
|
|
||||||
'tbm') if 'tbm' in self.request_params else ''
|
|
||||||
|
|
||||||
def __getitem__(self, name) -> Any:
|
|
||||||
return getattr(self, name)
|
|
||||||
|
|
||||||
def __setitem__(self, name, value) -> None:
|
|
||||||
return setattr(self, name, value)
|
|
||||||
|
|
||||||
def __delitem__(self, name) -> None:
|
|
||||||
return delattr(self, name)
|
|
||||||
|
|
||||||
def __contains__(self, name) -> bool:
|
|
||||||
return hasattr(self, name)
|
|
||||||
|
|
||||||
def new_search_query(self) -> str:
|
|
||||||
"""Parses a plaintext query into a valid string for submission
|
|
||||||
|
|
||||||
Also decrypts the query string, if encrypted (in the case of
|
|
||||||
paginated results).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: A valid query string
|
|
||||||
|
|
||||||
"""
|
|
||||||
q = self.request_params.get('q')
|
|
||||||
|
|
||||||
if q is None or len(q) == 0:
|
|
||||||
return ''
|
|
||||||
else:
|
|
||||||
# Attempt to decrypt if this is an internal link
|
|
||||||
try:
|
|
||||||
q = Fernet(self.session_key).decrypt(q.encode()).decode()
|
|
||||||
except InvalidToken:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Strip '!' for "feeling lucky" queries
|
|
||||||
if match := re.search(r"(^|\s)!($|\s)", q):
|
|
||||||
self.feeling_lucky = True
|
|
||||||
start, end = match.span()
|
|
||||||
self.query = " ".join([seg for seg in [q[:start], q[end:]] if seg])
|
|
||||||
else:
|
|
||||||
self.feeling_lucky = False
|
|
||||||
self.query = q
|
|
||||||
|
|
||||||
# Check for possible widgets
|
|
||||||
self.widget = "ip" if re.search("([^a-z0-9]|^)my *[^a-z0-9] *(ip|internet protocol)" +
|
|
||||||
"($|( *[^a-z0-9] *(((addres|address|adres|" +
|
|
||||||
"adress)|a)? *$)))", self.query.lower()) else self.widget
|
|
||||||
self.widget = 'calculator' if re.search(
|
|
||||||
r"\bcalculator\b|\bcalc\b|\bcalclator\b|\bmath\b",
|
|
||||||
self.query.lower()) else self.widget
|
|
||||||
return self.query
|
|
||||||
|
|
||||||
def generate_response(self) -> str:
|
|
||||||
"""Generates a response for the user's query
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: A string response to the search query, in the form of a URL
|
|
||||||
or string representation of HTML content.
|
|
||||||
|
|
||||||
"""
|
|
||||||
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,
|
|
||||||
mobile=mobile,
|
|
||||||
config=self.config,
|
|
||||||
query=self.query)
|
|
||||||
full_query = gen_query(self.query,
|
|
||||||
self.request_params,
|
|
||||||
self.config)
|
|
||||||
self.full_query = full_query
|
|
||||||
|
|
||||||
# force mobile search when view image is true and
|
|
||||||
# the request is not already made by a mobile
|
|
||||||
view_image = ('tbm=isch' in full_query
|
|
||||||
# and self.config.view_image
|
|
||||||
#and not g.user_request.mobile
|
|
||||||
)
|
|
||||||
|
|
||||||
get_body = g.user_request.send(query=full_query,
|
|
||||||
force_mobile=view_image,
|
|
||||||
user_agent=self.user_agent)
|
|
||||||
|
|
||||||
|
|
||||||
# Produce cleanable html soup from response
|
|
||||||
get_body_safed = get_body.text.replace("<","andlt;").replace(">","andgt;")
|
|
||||||
html_soup = bsoup(get_body_safed, 'html.parser')
|
|
||||||
|
|
||||||
# Replace current soup if view_image is active
|
|
||||||
if view_image:
|
|
||||||
html_soup = content_filter.view_image(html_soup)
|
|
||||||
|
|
||||||
# Indicate whether or not a Tor connection is active
|
|
||||||
if g.user_request.tor_valid:
|
|
||||||
html_soup.insert(0, bsoup(TOR_BANNER, 'html.parser'))
|
|
||||||
|
|
||||||
formatted_results = content_filter.clean(html_soup)
|
|
||||||
if self.feeling_lucky:
|
|
||||||
if lucky_link := get_first_link(formatted_results):
|
|
||||||
return lucky_link
|
|
||||||
|
|
||||||
# Fall through to regular search if unable to find link
|
|
||||||
self.feeling_lucky = False
|
|
||||||
|
|
||||||
# Append user config to all search links, if available
|
|
||||||
param_str = ''.join('&{}={}'.format(k, v)
|
|
||||||
for k, v in
|
|
||||||
self.request_params.to_dict(flat=True).items()
|
|
||||||
if self.config.is_safe_key(k))
|
|
||||||
for link in formatted_results.find_all('a', href=True):
|
|
||||||
link['rel'] = "nofollow noopener noreferrer"
|
|
||||||
if 'search?' not in link['href'] or link['href'].index(
|
|
||||||
'search?') > 1:
|
|
||||||
continue
|
|
||||||
link['href'] += param_str
|
|
||||||
|
|
||||||
return str(formatted_results)
|
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
from cryptography.fernet import Fernet
|
|
||||||
from flask import current_app as app
|
|
||||||
|
|
||||||
REQUIRED_SESSION_VALUES = ['uuid', 'config', 'key', 'auth']
|
|
||||||
|
|
||||||
|
|
||||||
def generate_key() -> bytes:
|
|
||||||
"""Generates a key for encrypting searches and element URLs
|
|
||||||
|
|
||||||
Args:
|
|
||||||
cookies_disabled: Flag for whether or not cookies are disabled by the
|
|
||||||
user. If so, the user can only use the default key
|
|
||||||
generated on app init for queries.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: A unique Fernet key
|
|
||||||
|
|
||||||
"""
|
|
||||||
# Generate/regenerate unique key per user
|
|
||||||
return Fernet.generate_key()
|
|
||||||
|
|
||||||
|
|
||||||
def valid_user_session(session: dict) -> bool:
|
|
||||||
"""Validates the current user session
|
|
||||||
|
|
||||||
Args:
|
|
||||||
session: The current Flask user session
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True/False indicating that all required session values are
|
|
||||||
available
|
|
||||||
|
|
||||||
"""
|
|
||||||
# Generate secret key for user if unavailable
|
|
||||||
for value in REQUIRED_SESSION_VALUES:
|
|
||||||
if value not in session:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
|
@ -1,71 +0,0 @@
|
||||||
from pathlib import Path
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
|
|
||||||
|
|
||||||
# root
|
|
||||||
BASE_DIR = Path(__file__).parent.parent.parent
|
|
||||||
|
|
||||||
def add_ip_card(html_soup: BeautifulSoup, ip: str) -> BeautifulSoup:
|
|
||||||
"""Adds the client's IP address to the search results
|
|
||||||
if query contains keywords
|
|
||||||
|
|
||||||
Args:
|
|
||||||
html_soup: The parsed search result containing the keywords
|
|
||||||
ip: ip address of the client
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
BeautifulSoup
|
|
||||||
|
|
||||||
"""
|
|
||||||
main_div = html_soup.select_one('#main')
|
|
||||||
if main_div:
|
|
||||||
# HTML IP card tag
|
|
||||||
ip_tag = html_soup.new_tag('div')
|
|
||||||
ip_tag['class'] = 'ZINbbc xpd O9g5cc uUPGi'
|
|
||||||
|
|
||||||
# For IP Address html tag
|
|
||||||
ip_address = html_soup.new_tag('div')
|
|
||||||
ip_address['class'] = 'kCrYT ip-address-div'
|
|
||||||
ip_address.string = ip
|
|
||||||
|
|
||||||
# Text below the IP address
|
|
||||||
ip_text = html_soup.new_tag('div')
|
|
||||||
ip_text.string = 'Your public IP address'
|
|
||||||
ip_text['class'] = 'kCrYT ip-text-div'
|
|
||||||
|
|
||||||
# Adding all the above html tags to the IP card
|
|
||||||
ip_tag.append(ip_address)
|
|
||||||
ip_tag.append(ip_text)
|
|
||||||
|
|
||||||
# Insert the element at the top of the result list
|
|
||||||
main_div.insert_before(ip_tag)
|
|
||||||
return html_soup
|
|
||||||
|
|
||||||
def add_calculator_card(html_soup: BeautifulSoup) -> BeautifulSoup:
|
|
||||||
"""Adds the a calculator widget to the search results
|
|
||||||
if query contains keywords
|
|
||||||
|
|
||||||
Args:
|
|
||||||
html_soup: The parsed search result containing the keywords
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
BeautifulSoup
|
|
||||||
"""
|
|
||||||
main_div = html_soup.select_one('#main')
|
|
||||||
if main_div:
|
|
||||||
# absolute path
|
|
||||||
widget_file = open(BASE_DIR / 'app/static/widgets/calculator.html', encoding="utf8")
|
|
||||||
widget_tag = html_soup.new_tag('div')
|
|
||||||
widget_tag['class'] = 'ZINbbc xpd O9g5cc uUPGi'
|
|
||||||
widget_tag['id'] = 'calculator-wrapper'
|
|
||||||
calculator_text = html_soup.new_tag('div')
|
|
||||||
calculator_text['class'] = 'kCrYT ip-address-div'
|
|
||||||
calculator_text.string = 'Calculator'
|
|
||||||
calculator_widget = html_soup.new_tag('div')
|
|
||||||
calculator_widget.append(BeautifulSoup(widget_file, 'html.parser'))
|
|
||||||
calculator_widget['class'] = 'kCrYT ip-text-div'
|
|
||||||
widget_tag.append(calculator_text)
|
|
||||||
widget_tag.append(calculator_widget)
|
|
||||||
main_div.insert_before(widget_tag)
|
|
||||||
widget_file.close()
|
|
||||||
return html_soup
|
|
|
@ -1,7 +0,0 @@
|
||||||
import os
|
|
||||||
|
|
||||||
optional_dev_tag = ''
|
|
||||||
if os.getenv('DEV_BUILD'):
|
|
||||||
optional_dev_tag = '.dev' + os.getenv('DEV_BUILD')
|
|
||||||
|
|
||||||
__version__ = '0.9.1' + optional_dev_tag
|
|
|
@ -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/
|
|
|
@ -1,23 +0,0 @@
|
||||||
apiVersion: v2
|
|
||||||
name: whoogle
|
|
||||||
description: A self hosted search engine on Kubernetes
|
|
||||||
type: application
|
|
||||||
version: 0.1.0
|
|
||||||
appVersion: 0.9.1
|
|
||||||
|
|
||||||
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
|
|
|
@ -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 }}
|
|