diff --git a/.github/workflows/buildx.yml b/.github/workflows/buildx.yml index 35861ee..8d201ce 100644 --- a/.github/workflows/buildx.yml +++ b/.github/workflows/buildx.yml @@ -2,7 +2,7 @@ name: buildx on: workflow_run: - workflows: ["tests"] + workflows: ["docker_tests"] branches: [main] types: - completed diff --git a/.github/workflows/docker_tests.yml b/.github/workflows/docker_tests.yml new file mode 100644 index 0000000..e3a3a2f --- /dev/null +++ b/.github/workflows/docker_tests.yml @@ -0,0 +1,21 @@ +name: docker_tests + +on: + workflow_run: + workflows: ["tests"] + branches: [main] + types: + - completed + +jobs: + on-success: + runs-on: ubuntu-latest + steps: + - name: checkout code + uses: actions/checkout@v2 + - name: build and test + run: | + docker build --tag whoogle-search:test . + docker run --publish 5000:5000 --detach --name whoogle-search whoogle-search:test + sleep 15 + docker exec whoogle-search curl -f http://localhost:5000/healthz || exit 1 diff --git a/.replit b/.replit index a02e438..c68c7ba 100644 --- a/.replit +++ b/.replit @@ -1,3 +1,3 @@ language = "bash" -run = "pip install -r requirements.txt && ./run" -onBoot = "pip install -r requirements.txt && ./run" +run = "killall -q python3 > /dev/null 2>&1; pip install -r requirements.txt && ./run" +onBoot = "killall -q python3 > /dev/null 2>&1; pip install -r requirements.txt && ./run" diff --git a/Dockerfile b/Dockerfile index cec9f54..c9f2f20 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,64 +1,64 @@ -FROM python:3.8-slim as builder +FROM python:3.8-alpine as builder -RUN apt-get update && apt-get install -y \ - build-essential \ +RUN apk --update add \ + build-base \ libxml2-dev \ libxslt-dev \ - libssl-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.8-slim +FROM python:3.8-alpine -RUN apt-get update && apt-get install -y \ - libcurl4-openssl-dev \ - tor \ - curl \ - && rm -rf /var/lib/apt/lists/* +RUN apk add --update --no-cache tor curl bash openrc +# libcurl4-openssl-dev +ARG DOCKER_USER=whoogle +ARG DOCKER_USERID=927 ARG config_dir=/config -RUN mkdir -p $config_dir +RUN mkdir -p -m 777 $config_dir VOLUME $config_dir -ENV CONFIG_VOLUME=$config_dir ARG username='' -ENV WHOOGLE_USER=$username ARG password='' -ENV WHOOGLE_PASS=$password - ARG proxyuser='' -ENV WHOOGLE_PROXY_USER=$proxyuser ARG proxypass='' -ENV WHOOGLE_PROXY_PASS=$proxypass ARG proxytype='' -ENV WHOOGLE_PROXY_TYPE=$proxytype ARG proxyloc='' -ENV WHOOGLE_PROXY_LOC=$proxyloc - ARG whoogle_dotenv='' -ENV WHOOGLE_DOTENV=$whoogle_dotenv - ARG use_https='' -ENV HTTPS_ONLY=$use_https - ARG whoogle_port=5000 -ENV EXPOSE_PORT=$whoogle_port - -ARG twitter_alt='nitter.net' -ENV WHOOGLE_ALT_TW=$twitter_alt -ARG youtube_alt='invidious.snopyta.org' -ENV WHOOGLE_ALT_YT=$youtube_alt -ARG instagram_alt='bibliogram.art/u' -ENV WHOOGLE_ALT_IG=$instagram_alt -ARG reddit_alt='libredd.it' -ENV WHOOGLE_ALT_RD=$reddit_alt +ARG twitter_alt='farside.link/nitter' +ARG youtube_alt='farside.link/invidious' +ARG instagram_alt='farside.link/bibliogram' +ARG reddit_alt='farside.link/libreddit' +ARG medium_alt='farside.link/scribe' ARG translate_alt='lingva.ml' -ENV WHOOGLE_ALT_TL=$translate_alt -ARG medium_alt='scribe.rip' -ENV WHOOGLE_ALT_MD=$medium_alt +ARG imgur_alt='imgin.voidnet.tech' +ARG wikipedia_alt='wikiless.org' + +ENV CONFIG_VOLUME=$config_dir \ + WHOOGLE_USER=$username \ + WHOOGLE_PASS=$password \ + WHOOGLE_PROXY_USER=$proxyuser \ + WHOOGLE_PROXY_PASS=$proxypass \ + WHOOGLE_PROXY_TYPE=$proxytype \ + WHOOGLE_PROXY_LOC=$proxyloc \ + WHOOGLE_DOTENV=$whoogle_dotenv \ + HTTPS_ONLY=$use_https \ + EXPOSE_PORT=$whoogle_port \ + WHOOGLE_ALT_TW=$twitter_alt \ + WHOOGLE_ALT_YT=$youtube_alt \ + WHOOGLE_ALT_IG=$instagram_alt \ + WHOOGLE_ALT_RD=$reddit_alt \ + WHOOGLE_ALT_MD=$medium_alt \ + WHOOGLE_ALT_TL=$translate_alt \ + WHOOGLE_ALT_IMG=$imgur_alt \ + WHOOGLE_ALT_WIKI=$wikipedia_alt WORKDIR /whoogle @@ -72,6 +72,13 @@ COPY run . # Allow writing symlinks to build dir RUN chown 102:102 app/static/build +# Create user/group to run as +RUN adduser -D -g $DOCKER_USERID -u $DOCKER_USERID $DOCKER_USER +# Fix ownership / permissions +RUN chown -R ${DOCKER_USER}:${DOCKER_USER} /whoogle /var/lib/tor + +USER $DOCKER_USER:$DOCKER_USER + EXPOSE $EXPOSE_PORT HEALTHCHECK --interval=30s --timeout=5s \ diff --git a/README.md b/README.md index 577ef73..5acef07 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Contents 6. [Manual](#f-manual) 7. [Docker](#g-manual-docker) 8. [Arch/AUR](#arch-linux--arch-based-distributions) + 9. [Helm/Kubernetes](#helm-chart-for-kubernetes) 4. [Environment Variables and Configuration](#environment-variables) 5. [Usage](#usage) 6. [Extra Steps](#extra-steps) @@ -84,7 +85,7 @@ Provides: - Free HTTPS url (https://\.herokuapp.com) - Downtime after periods of inactivity \([solution](https://github.com/benbusby/whoogle-search#prevent-downtime-heroku-only)\) -Notes: +Notes: - Requires a (free) Heroku account - Sometimes has issues with auto-redirecting to `https`. Make sure to navigate to the `https` version of your app before adding as a default search engine. @@ -163,7 +164,7 @@ See the [available environment variables](#environment-variables) for additional ### F) Manual -*Note: `Content-Security-Policy` headers are already sent by Whoogle -- you don't/shouldn't need to apply a CSP header yourself* +*Note: `Content-Security-Policy` headers can be sent by Whoogle if you set `WHOOGLE_CSP`.* Clone the repo and run the following commands to start the app in a local-only environment: @@ -178,7 +179,7 @@ pip install -r requirements.txt See the [available environment variables](#environment-variables) for additional configuration. #### systemd Configuration -After building the virtual environment, you can add the following to `/lib/systemd/system/whoogle.service` to set up a Whoogle Search systemd service: +After building the virtual environment, you can add something like the following to `/lib/systemd/system/whoogle.service` to set up a Whoogle Search systemd service: ```ini [Unit] @@ -195,19 +196,27 @@ Description=Whoogle #Environment=WHOOGLE_PROXY_LOC= # Site alternative configurations, uncomment to enable # Note: If not set, the feature will still be available -# with default values. -#Environment=WHOOGLE_ALT_TW=nitter.net -#Environment=WHOOGLE_ALT_YT=invidious.snopyta.org -#Environment=WHOOGLE_ALT_IG=bibliogram.art/u -#Environment=WHOOGLE_ALT_RD=libredd.it +# with default values. +#Environment=WHOOGLE_ALT_TW=farside.link/nitter +#Environment=WHOOGLE_ALT_YT=farside.link/invidious +#Environment=WHOOGLE_ALT_IG=farside.link/bibliogram/u +#Environment=WHOOGLE_ALT_RD=farside.link/libreddit +#Environment=WHOOGLE_ALT_MD=farside.link/scribe #Environment=WHOOGLE_ALT_TL=lingva.ml -#Environment=WHOOGLE_ALT_MD=scribe.rip +#Environment=WHOOGLE_ALT_IMG=imgin.voidnet.tech +#Environment=WHOOGLE_ALT_WIKI=wikiless.org # Load values from dotenv only #Environment=WHOOGLE_DOTENV=1 Type=simple User= -WorkingDirectory= -ExecStart=/venv/bin/python3 -um app --host 0.0.0.0 --port 5000 +# If installed as a package, add: +ExecStart=/python3 /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=/run +# For example: +# ExecStart=/var/www/whoogle-search/run ExecReload=/bin/kill -HUP $MAINPID Restart=always RestartSec=3 @@ -287,6 +296,13 @@ You may also edit environment variables from your app’s Settings tab in the He #### Arch Linux & Arch-based Distributions There is an [AUR package available](https://aur.archlinux.org/packages/whoogle-git/), as well as a pre-built and daily updated package available at [Chaotic-AUR](https://chaotic.cx). +#### Helm chart for Kubernetes +To use the Kubernetes Helm Chart: +1. Ensure you have [Helm](https://helm.sh/docs/intro/install/) `>=3.0.0` installed +2. Clone this repository +3. Update [charts/whoogle/values.yaml](./charts/whoogle/values.yaml) as desired +4. Run `helm install whoogle ./charts/whoogle` + #### Using your own server, or alternative container deployment There are other methods for deploying docker containers that are well outlined in [this article](https://rollout.io/blog/the-shortlist-of-docker-hosting/), but there are too many to describe set up for each here. Generally it should be about the same amount of effort as the Heroku deployment. @@ -320,8 +336,12 @@ There are a few optional environment variables available for customizing a Whoog | WHOOGLE_ALT_RD | The reddit.com alternative to use when site alternatives are enabled in the config. | | WHOOGLE_ALT_TL | The Google Translate alternative to use. This is used for all "translate ____" searches. | | WHOOGLE_ALT_MD | The medium.com alternative to use when site alternatives are enabled in the config. | +| WHOOGLE_ALT_IMG | The imgur.com alternative to use when site alternatives are enabled in the config. | +| WHOOGLE_ALT_WIKI | The wikipedia.com alternative to use when site alternatives are enabled in the config. | | WHOOGLE_AUTOCOMPLETE | Controls visibility of autocomplete/search suggestions. Default on -- use '0' to disable | | WHOOGLE_MINIMAL | Remove everything except basic result cards from all search queries. | +| WHOOGLE_CSP | Sets a default set of 'Content-Security-Policy' headers | +| WHOOGLE_RESULTS_PER_PAGE | Set the number of results per page | ### 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. @@ -413,7 +433,7 @@ Note: You should have your own domain name and [an https certificate](https://le - 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: @@ -448,7 +468,7 @@ Under the hood, Whoogle is a basic Flask app with the following structure: - 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. @@ -467,7 +487,7 @@ def contains(x: list, y: int) -> bool: """ return y in x -``` +``` #### Translating @@ -488,25 +508,33 @@ A lot of the app currently piggybacks on Google's existing support for fetching ## Public Instances -*Note: Use public instances at your own discretion. Maintainers of Whoogle do not personally validate the integrity of these instances, and popular public instances are more likely to be rate-limited or blocked.* - +*Note: Use public instances at your own discretion. The maintainers of Whoogle are only responsible for https://whoogle.fossho.st, and do not personally validate the integrity of any other instances. Popular public instances are more likely to be rate-limited or blocked.* + | Website | Country | Language | Cloudflare | |-|-|-|-| +| [https://whoogle.fossho.st](https://whoogle.fossho.st) | 🇺🇸 US | Multi-choice | | +| [https://search.albony.xyz](https://search.albony.xyz/) | 🇮🇳 IN | Multi-choice | | | [https://whoogle.sdf.org](https://whoogle.sdf.org) | 🇺🇸 US | Multi-choice | -| [https://whoogle.kavin.rocks](https://whoogle.kavin.rocks) | 🇮🇳 IN | Unknown | ✅ | | [https://search.garudalinux.org](https://search.garudalinux.org) | 🇩🇪 DE | Multi-choice | | | [https://whooglesearch.net](https://whooglesearch.net) | 🇩🇪 DE | Spanish | | -| [https://search.flawcra.cc](https://search.flawcra.cc) |🇩🇪 DE | Unknown | ✅ | | [https://search.exonip.de](https://search.exonip.de) | 🇳🇱 NL | Multi-choice | | -| [https://s.alefvanoon.xyz](https://s.alefvanoon.xyz) | 🇺🇸 US | English | ✅ | -| [https://search.flux.industries](https://search.flux.industries) | 🇩🇪 DE | German | ✅ | -| [http://whoogledq5f5wly5p4i2ohnvjwlihnlg4oajjum2oeddfwqdwupbuhqd.onion](http://whoogledq5f5wly5p4i2ohnvjwlihnlg4oajjum2oeddfwqdwupbuhqd.onion) | 🇮🇳 IN | Unknown | | +| [https://s.alefvanoon.xyz](https://s.alefvanoon.xyz) | 🇺🇸 US | Multi-choice | ✅ | +| [https://www.whooglesearch.ml](https://www.whooglesearch.ml) | 🇺🇸 US | English | | +| [https://search.sethforprivacy.com](https://search.sethforprivacy.com) | 🇩🇪 DE | English | | +| [https://whoogle.dcs0.hu](https://whoogle.dcs0.hu) | 🇭🇺 HU | Multi-choice | ✅ | -* A checkmark in the "Cloudflare" category here refers to the use of the reverse proxy, [Cloudflare](https://cloudflare). The checkmark will not be listed for a site which uses Cloudflare DNS but rather the proxying service which grants Cloudflare the ability to monitor traffic to the website. +* A checkmark in the "Cloudflare" category here refers to the use of the reverse proxy, [Cloudflare](https://cloudflare.com). The checkmark will not be listed for a site which uses Cloudflare DNS but rather the proxying service which grants Cloudflare the ability to monitor traffic to the website. + +#### Onion Instances + +| Website | Country | Language | +|-|-|-| +| [http://whoglqjdkgt2an4tdepberwqz3hk7tjo4kqgdnuj77rt7nshw2xqhqad.onion](http://whoglqjdkgt2an4tdepberwqz3hk7tjo4kqgdnuj77rt7nshw2xqhqad.onion) | 🇺🇸 US | Multi-choice +| [http://nuifgsnbb2mcyza74o7illtqmuaqbwu4flam3cdmsrnudwcmkqur37qd.onion](http://nuifgsnbb2mcyza74o7illtqmuaqbwu4flam3cdmsrnudwcmkqur37qd.onion) | 🇩🇪 DE | English ## Screenshots #### Desktop -![Whoogle Desktop](docs/screenshot_desktop.jpg) +![Whoogle Desktop](docs/screenshot_desktop.png) #### Mobile -![Whoogle Mobile](docs/screenshot_mobile.jpg) +![Whoogle Mobile](docs/screenshot_mobile.png) diff --git a/app.json b/app.json index c3d2dc3..a3b391e 100644 --- a/app.json +++ b/app.json @@ -47,22 +47,27 @@ }, "WHOOGLE_ALT_TW": { "description": "The site to use as a replacement for twitter.com when site alternatives are enabled in the config.", - "value": "nitter.net", + "value": "farside.link/nitter", "required": false }, "WHOOGLE_ALT_YT": { "description": "The site to use as a replacement for youtube.com when site alternatives are enabled in the config.", - "value": "invidious.snopyta.org", + "value": "farside.link/invidious", "required": false }, "WHOOGLE_ALT_IG": { "description": "The site to use as a replacement for instagram.com when site alternatives are enabled in the config.", - "value": "bibliogram.art/u", + "value": "farside.link/bibliogram/u", "required": false }, "WHOOGLE_ALT_RD": { "description": "The site to use as a replacement for reddit.com when site alternatives are enabled in the config.", - "value": "libredd.it", + "value": "farside.link/libreddit", + "required": false + }, + "WHOOGLE_ALT_MD": { + "description": "The site to use as a replacement for medium.com when site alternatives are enabled in the config.", + "value": "farside.link/scribe", "required": false }, "WHOOGLE_ALT_TL": { @@ -70,11 +75,16 @@ "value": "lingva.ml", "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": "scribe.rip", - "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": "imgin.voidnet.tech", + "required": false + }, + "WHOOGLE_ALT_WIKI": { + "description": "The site to use as a replacement for wikipedia.com when site alternatives are enabled in the config.", + "value": "wikiless.org", + "required": false + }, "WHOOGLE_MINIMAL": { "description": "Remove everything except basic result cards from all search queries (set to 1 or leave blank)", "value": "", diff --git a/app/__init__.py b/app/__init__.py index c9540ff..d0dc7cd 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -15,16 +15,21 @@ app = Flask(__name__, static_folder=os.path.dirname( os.path.abspath(__file__)) + '/static') # Load .env file if enabled -if os.getenv("WHOOGLE_DOTENV", ''): +if os.getenv('WHOOGLE_DOTENV', ''): dotenv_path = '../whoogle.env' load_dotenv(os.path.join(os.path.dirname(os.path.abspath(__file__)), dotenv_path)) app.default_key = generate_user_key() -app.no_cookie_ips = [] app.config['SECRET_KEY'] = os.urandom(32) app.config['SESSION_TYPE'] = 'filesystem' -app.config['VERSION_NUMBER'] = '0.6.0' +app.config['SESSION_COOKIE_SAMESITE'] = 'strict' + +if os.getenv('HTTPS_ONLY'): + app.config['SESSION_COOKIE_NAME'] = '__Secure-session' + app.config['SESSION_COOKIE_SECURE'] = True + +app.config['VERSION_NUMBER'] = '0.7.0' app.config['APP_ROOT'] = os.getenv( 'APP_ROOT', os.path.dirname(os.path.abspath(__file__))) @@ -38,9 +43,11 @@ app.config['LANGUAGES'] = json.load(open( os.path.join(app.config['STATIC_FOLDER'], 'settings/languages.json'), encoding='utf-8')) app.config['COUNTRIES'] = json.load(open( - os.path.join(app.config['STATIC_FOLDER'], 'settings/countries.json'))) + os.path.join(app.config['STATIC_FOLDER'], 'settings/countries.json'), + encoding='utf-8')) app.config['TRANSLATIONS'] = json.load(open( - os.path.join(app.config['STATIC_FOLDER'], 'settings/translations.json'))) + os.path.join(app.config['STATIC_FOLDER'], 'settings/translations.json'), + encoding='utf-8')) app.config['THEMES'] = json.load(open( os.path.join(app.config['STATIC_FOLDER'], 'settings/themes.json'))) app.config['HEADER_TABS'] = json.load(open( @@ -61,6 +68,8 @@ app.config['BANG_PATH'] = os.getenv( app.config['BANG_FILE'] = os.path.join( app.config['BANG_PATH'], 'bangs.json') +app.config['RELEASES_URL'] = 'https://github.com/' \ + 'benbusby/whoogle-search/releases' # The alternative to Google Translate is treated a bit differently than other # social media site alternatives, in that it is used for any translation diff --git a/app/filter.py b/app/filter.py index a864503..0df2386 100644 --- a/app/filter.py +++ b/app/filter.py @@ -1,3 +1,5 @@ +from app.models.config import Config +from app.models.endpoint import Endpoint from app.request import VALID_PARAMS, MAPS_URL from app.utils.misc import read_config_bool from app.utils.results import * @@ -44,18 +46,8 @@ class Filter: # type result (such as "people also asked", "related searches", etc) RESULT_CHILD_LIMIT = 7 - def __init__(self, user_key: str, mobile=False, config=None) -> None: - if config is None: - config = {} - self.near = config['near'] if 'near' in config else '' - self.dark = config['dark'] if 'dark' in config else False - self.nojs = config['nojs'] if 'nojs' in config else False - self.new_tab = config['new_tab'] if 'new_tab' in config else False - self.alt_redirect = config['alts'] if 'alts' in config else False - self.block_title = ( - config['block_title'] if 'block_title' in config else '') - self.block_url = ( - config['block_url'] if 'block_url' in config else '') + def __init__(self, user_key: str, config: Config, mobile=False) -> None: + self.config = config self.mobile = mobile self.user_key = user_key self.main_divs = ResultSet('') @@ -68,16 +60,6 @@ class Filter: def elements(self): return self._elements - def reskin(self, page: str) -> str: - # Aesthetic only re-skinning - if self.dark: - page = page.replace( - 'fff', '000').replace( - '202124', 'ddd').replace( - '1967D2', '3b85ea') - - return page - def encrypt_path(self, path, is_element=False) -> str: # Encrypts path to avoid plaintext results in logs if is_element: @@ -109,7 +91,7 @@ class Filter: input_form = soup.find('form') if input_form is not None: - input_form['method'] = 'POST' + input_form['method'] = 'GET' if self.config.get_only else 'POST' # Ensure no extra scripts passed through for script in soup('script'): @@ -143,9 +125,7 @@ class Filter: _ = div.decompose() if len(div_ads) else None def remove_block_titles(self) -> None: - if not self.main_divs: - return - if self.block_title == '': + if not self.main_divs or not self.config.block_title: return block_title = re.compile(self.block_title) for div in [_ for _ in self.main_divs.find_all('div', recursive=True)]: @@ -154,9 +134,7 @@ class Filter: _ = div.decompose() if len(block_divs) else None def remove_block_url(self) -> None: - if not self.main_divs: - return - if self.block_url == '': + if not self.main_divs or not self.config.block_url: return block_url = re.compile(self.block_url) for div in [_ for _ in self.main_divs.find_all('div', recursive=True)]: @@ -211,10 +189,17 @@ class Filter: # 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: - label = elem.text + content = list(elem.strings) + label = content[0] + if len(content) > 1: + subtitle = ' (' + \ + ''.join(content[1:]) + ')' elem.decompose() break @@ -229,6 +214,11 @@ class Filter: details = BeautifulSoup(features='html.parser').new_tag('details') summary = BeautifulSoup(features='html.parser').new_tag('summary') summary.string = label + + if subtitle: + soup = BeautifulSoup(subtitle, 'html.parser') + summary.append(soup) + details.append(summary) if parent and not minimal_mode: @@ -254,14 +244,14 @@ class Filter: if src.startswith(LOGO_URL): # Re-brand with Whoogle logo element.replace_with(BeautifulSoup( - render_template('logo.html', dark=self.dark), + render_template('logo.html'), features='html.parser')) return elif src.startswith(GOOG_IMG) or GOOG_STATIC in src: element['src'] = BLANK_B64 return - element['src'] = 'element?url=' + self.encrypt_path( + element['src'] = f'{Endpoint.element}?url=' + self.encrypt_path( src, is_element=True) + '&type=' + urlparse.quote(mime) @@ -353,10 +343,10 @@ class Filter: link['href'] = filter_link_args(q) # Add no-js option - if self.nojs: + if self.config.nojs: append_nojs(link) - if self.new_tab: + if self.config.new_tab: link['target'] = '_blank' else: if href.startswith(MAPS_URL): @@ -366,7 +356,7 @@ class Filter: link['href'] = href # Replace link location if "alts" config is enabled - if self.alt_redirect: + if self.config.alts: # Search and replace all link descriptions # with alternative location link['href'] = get_site_alt(link['href']) @@ -409,7 +399,12 @@ class Filter: for item in results_all: urls = item.find('a')['href'].split('&imgrefurl=') - img_url = urlparse.unquote(urls[0].replace('/imgres?imgurl=', '')) + # Skip urls that are not two-element lists + if len(urls) != 2: + continue + + img_url = urlparse.unquote(urls[0].replace( + f'/{Endpoint.imgres}?imgurl=', '')) try: # Try to strip out only the necessary part of the web page link diff --git a/app/models/config.py b/app/models/config.py index ef4204f..2bb96ea 100644 --- a/app/models/config.py +++ b/app/models/config.py @@ -17,8 +17,8 @@ class Config: 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.ctry = os.getenv('WHOOGLE_CONFIG_COUNTRY', '') - self.theme = os.getenv('WHOOGLE_CONFIG_THEME', '') + self.country = os.getenv('WHOOGLE_CONFIG_COUNTRY', '') + self.theme = os.getenv('WHOOGLE_CONFIG_THEME', 'system') self.safe = read_config_bool('WHOOGLE_CONFIG_SAFE') self.dark = read_config_bool('WHOOGLE_CONFIG_DARK') # deprecated self.alts = read_config_bool('WHOOGLE_CONFIG_ALTS') @@ -33,9 +33,13 @@ class Config: self.safe_keys = [ 'lang_search', 'lang_interface', - 'ctry', - 'dark', - 'theme' + 'country', + 'theme', + 'alts', + 'new_tab', + 'view_image', + 'block', + 'safe' ] # Skip setting custom config if there isn't one @@ -105,5 +109,26 @@ class Config: for param_key in params.keys(): if not self.is_safe_key(param_key): continue - self[param_key] = params.get(param_key) + param_val = params.get(param_key) + + if param_val == 'off': + param_val = False + elif param_val.isdigit(): + param_val = int(param_val) + + self[param_key] = param_val return self + + def to_params(self) -> str: + """Generates a set of safe params for using in Whoogle URLs + + Returns: + str -- a set of URL parameters + """ + param_str = '' + for safe_key in self.safe_keys: + if not self[safe_key]: + continue + param_str = param_str + f'&{safe_key}={self[safe_key]}' + + return param_str diff --git a/app/models/endpoint.py b/app/models/endpoint.py new file mode 100644 index 0000000..eeddc64 --- /dev/null +++ b/app/models/endpoint.py @@ -0,0 +1,23 @@ +from enum import Enum + + +class Endpoint(Enum): + autocomplete = 'autocomplete' + home = 'home' + healthz = 'healthz' + session = 'session' + config = 'config' + opensearch = 'opensearch.xml' + search = 'search' + search_html = 'search.html' + url = 'url' + imgres = 'imgres' + element = 'element' + window = 'window' + + def __str__(self): + return self.value + + def in_path(self, path: str) -> bool: + return path.startswith(self.value) or \ + path.startswith(f'/{self.value}') diff --git a/app/request.py b/app/request.py index 71f4e9b..17ac034 100644 --- a/app/request.py +++ b/app/request.py @@ -59,7 +59,7 @@ def gen_user_agent(is_mobile) -> str: return DESKTOP_UA.format("Mozilla", linux, firefox) -def gen_query(query, args, config, near_city=None) -> str: +def gen_query(query, args, config) -> str: param_dict = {key: '' for key in VALID_PARAMS} # Use :past(hour/day/week/month/year) if available @@ -96,8 +96,8 @@ def gen_query(query, args, config, near_city=None) -> str: param_dict['start'] = '&start=' + args.get('start') # Search for results near a particular city, if available - if near_city: - param_dict['near'] = '&near=' + urlparse.quote(near_city) + 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 @@ -107,19 +107,25 @@ def gen_query(query, args, config, near_city=None) -> str: [_ for _ in lang if not _.isdigit()] )) if lang else '' else: - param_dict['lr'] = '&lr=' + ( - config.lang_search if config.lang_search 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') - param_dict['cr'] = ('&cr=' + config.ctry) if config.ctry else '' - param_dict['hl'] = '&hl=' + ( - config.lang_interface.replace('lang_', '') - if config.lang_interface else '' - ) + # '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 @@ -213,16 +219,23 @@ class Request: list: The list of matches for possible search suggestions """ - ac_query = dict(hl=self.language, q=query) + ac_query = dict(q=query) + if self.language: + ac_query['hl'] = self.language + response = self.send(base_url=AUTOCOMPLETE_URL, query=urlparse.urlencode(ac_query)).text if not response: return [] - root = ET.fromstring(response) - return [_.attrib['data'] for _ in - root.findall('.//suggestion/[@data]')] + 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=False) -> Response: @@ -274,14 +287,19 @@ class Request: # Make sure that the tor connection is valid, if enabled if self.tor: - tor_check = requests.get('https://check.torproject.org/', - proxies=self.proxies, headers=headers) - self.tor_valid = 'Congratulations' in tor_check.text + 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: + 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( - "Tor connection succeeded, but the connection could not " - "be validated by torproject.org", + "Error raised during Tor connection validation", disable=True) response = requests.get( diff --git a/app/routes.py b/app/routes.py index d8e7a6a..86b9d94 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,30 +1,45 @@ import argparse import base64 -import html import io import json import pickle import urllib.parse as urlparse import uuid +from datetime import timedelta from functools import wraps 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 resolve_bang -from app.utils.misc import read_config_bool, get_client_ip -from app.utils.results import add_ip_card, bold_search_terms, get_tabs_content +from app.utils.misc import read_config_bool, get_client_ip, get_request_url +from app.utils.results import add_ip_card, bold_search_terms,\ + add_currency_card, check_currency, get_tabs_content from app.utils.search import * from app.utils.session import generate_user_key, valid_user_session from bs4 import BeautifulSoup as bsoup from flask import jsonify, make_response, request, redirect, render_template, \ send_file, session, url_for -from requests import exceptions +from requests import exceptions, get +from requests.models import PreparedRequest # Load DDG bang json files only on init bang_json = json.load(open(app.config['BANG_FILE'])) +# Check the newest version of WHOOGLE +update = bsoup(get(app.config['RELEASES_URL']).text, 'html.parser') +newest_version = update.select_one('[class="Link--primary"]').string[1:] +current_version = int(''.join(filter(str.isdigit, + app.config['VERSION_NUMBER']))) +newest_version = int(''.join(filter(str.isdigit, newest_version))) +newest_version = '' if current_version >= newest_version \ + else newest_version + +ac_var = 'WHOOGLE_AUTOCOMPLETE' +autocomplete_enabled = os.getenv(ac_var, '1') + def auth_required(f): @wraps(f) @@ -46,40 +61,91 @@ def auth_required(f): return decorated +def session_required(f): + @wraps(f) + def decorated(*args, **kwargs): + if (valid_user_session(session) and + 'cookies_disabled' not in request.args): + g.session_key = session['key'] + else: + session.pop('_permanent', None) + g.session_key = app.default_key + + # Clear out old sessions + invalid_sessions = [] + for user_session in os.listdir(app.config['SESSION_FILE_DIR']): + session_path = os.path.join( + app.config['SESSION_FILE_DIR'], + user_session) + try: + with open(session_path, 'rb') as session_file: + _ = pickle.load(session_file) + data = pickle.load(session_file) + if isinstance(data, dict) and 'valid' in data: + continue + invalid_sessions.append(session_path) + except (EOFError, FileNotFoundError): + pass + + for invalid_session in invalid_sessions: + try: + os.remove(invalid_session) + except FileNotFoundError: + # Don't throw error if the invalid session has been removed + pass + + return f(*args, **kwargs) + + return decorated + + @app.before_request def before_request_func(): g.request_params = ( request.args if request.method == 'GET' else request.form ) - g.cookies_disabled = False + + # Skip pre-request actions if verifying session + if '/session' in request.path and not valid_user_session(session): + return + + default_config = json.load(open(app.config['DEFAULT_CONFIG'])) \ + if os.path.exists(app.config['DEFAULT_CONFIG']) else {} # Generate session values for user if unavailable - if not valid_user_session(session): - session['config'] = json.load(open(app.config['DEFAULT_CONFIG'])) \ - if os.path.exists(app.config['DEFAULT_CONFIG']) else {} + if (not valid_user_session(session) and + 'cookies_disabled' not in request.args): + session['config'] = default_config session['uuid'] = str(uuid.uuid4()) - session['key'] = generate_user_key(True) + session['key'] = generate_user_key() - # Flag cookies as possibly disabled in order to prevent against - # unnecessary session directory expansion - g.cookies_disabled = True - - # Handle https upgrade - if needs_https(request.url): - return redirect( - request.url.replace('http://', 'https://', 1), - code=308) - - g.user_config = Config(**session['config']) + # Skip checking for session on any searches that don't + # require a valid session + if (not Endpoint.autocomplete.in_path(request.path) and + not Endpoint.healthz.in_path(request.path) and + not Endpoint.opensearch.in_path(request.path)): + return redirect(url_for( + 'session_check', + session_id=session['uuid'], + follow=get_request_url(request.url)), code=307) + else: + g.user_config = Config(**session['config']) + elif 'cookies_disabled' not in request.args: + # Set session as permanent + session.permanent = True + app.permanent_session_lifetime = timedelta(days=365) + g.user_config = Config(**session['config']) + else: + # User has cookies disabled, fall back to immutable default config + session.pop('_permanent', None) + g.user_config = Config(**default_config) if not g.user_config.url: - g.user_config.url = request.url_root.replace( - 'http://', - 'https://') if os.getenv('HTTPS_ONLY', False) else request.url_root + g.user_config.url = get_request_url(request.url_root) g.user_request = Request( request.headers.get('User-Agent'), - request.url_root, + get_request_url(request.url_root), config=g.user_config) g.app_location = g.user_config.url @@ -87,22 +153,14 @@ def before_request_func(): @app.after_request def after_request_func(resp): - # Check if address consistently has cookies blocked, - # in which case start removing session files after creation. - # - # Note: This is primarily done to prevent overpopulation of session - # directories, since browsers that block cookies will still trigger - # Flask's session creation routine with every request. - if g.cookies_disabled and request.remote_addr not in app.no_cookie_ips: - app.no_cookie_ips.append(request.remote_addr) - elif g.cookies_disabled and request.remote_addr in app.no_cookie_ips: - session_list = list(session.keys()) - for key in session_list: - session.pop(key) + resp.headers['X-Content-Type-Options'] = 'nosniff' + resp.headers['X-Frame-Options'] = 'DENY' - resp.headers['Content-Security-Policy'] = app.config['CSP'] - if os.environ.get('HTTPS_ONLY', False): - resp.headers['Content-Security-Policy'] += 'upgrade-insecure-requests' + 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 @@ -113,22 +171,28 @@ def unknown_page(e): return redirect(g.app_location) -@app.route('/healthz', methods=['GET']) +@app.route(f'/{Endpoint.healthz}', methods=['GET']) def healthz(): return '' -@app.route('/home', methods=['GET']) -def home(): - return redirect(url_for('.index')) +@app.route(f'/{Endpoint.session}/', methods=['GET', 'PUT', 'POST']) +def session_check(session_id): + if 'uuid' in session and session['uuid'] == session_id: + session['valid'] = True + return redirect(request.args.get('follow'), code=307) + else: + follow_url = request.args.get('follow') + req = PreparedRequest() + req.prepare_url(follow_url, {'cookies_disabled': 1}) + session.pop('_permanent', None) + return redirect(req.url, code=307) @app.route('/', methods=['GET']) +@app.route(f'/{Endpoint.home}', methods=['GET']) @auth_required def index(): - # Reset keys - session['key'] = generate_user_key(g.cookies_disabled) - # Redirect if an error was raised if 'error_message' in session and session['error_message']: error_message = session['error_message'] @@ -136,22 +200,27 @@ def index(): return render_template('error.html', error_message=error_message) return render_template('index.html', + newest_version=newest_version, languages=app.config['LANGUAGES'], countries=app.config['COUNTRIES'], themes=app.config['THEMES'], + autocomplete_enabled=autocomplete_enabled, translation=app.config['TRANSLATIONS'][ g.user_config.get_localization_lang() ], logo=render_template( 'logo.html', dark=g.user_config.dark), - config_disabled=app.config['CONFIG_DISABLE'], + config_disabled=( + app.config['CONFIG_DISABLE'] or + not valid_user_session(session) or + 'cookies_disabled' in request.args), config=g.user_config, tor_available=int(os.environ.get('TOR_AVAILABLE')), version_number=app.config['VERSION_NUMBER']) -@app.route('/opensearch.xml', methods=['GET']) +@app.route(f'/{Endpoint.opensearch}', methods=['GET']) def opensearch(): opensearch_url = g.app_location if opensearch_url.endswith('/'): @@ -171,7 +240,7 @@ def opensearch(): ), 200, {'Content-Disposition': 'attachment; filename="opensearch.xml"'} -@app.route('/search.html', methods=['GET']) +@app.route(f'/{Endpoint.search_html}', methods=['GET']) def search_html(): search_url = g.app_location if search_url.endswith('/'): @@ -179,9 +248,8 @@ def search_html(): return render_template('search.html', url=search_url) -@app.route('/autocomplete', methods=['GET', 'POST']) +@app.route(f'/{Endpoint.autocomplete}', methods=['GET', 'POST']) def autocomplete(): - ac_var = 'WHOOGLE_AUTOCOMPLETE' if os.getenv(ac_var) and not read_config_bool(ac_var): return jsonify({}) @@ -212,14 +280,14 @@ def autocomplete(): ]) -@app.route('/search', methods=['GET', 'POST']) +@app.route(f'/{Endpoint.search}', methods=['GET', 'POST']) +@session_required @auth_required def search(): # Update user config if specified in search args g.user_config = g.user_config.from_params(g.request_params) - search_util = Search(request, g.user_config, session, - cookies_disabled=g.cookies_disabled) + search_util = Search(request, g.user_config, g.session_key) query = search_util.new_search_query() bang = resolve_bang(query=query, bangs_dict=bang_json) @@ -228,7 +296,7 @@ def search(): # Redirect to home if invalid/blank search if not query: - return redirect('/') + return redirect(url_for('.index')) # Generate response and number of external elements from the page try: @@ -250,7 +318,16 @@ def search(): translate_to = localization_lang.replace('lang_', '') # Return 503 if temporarily blocked by captcha - resp_code = 503 if has_captcha(str(response)) else 200 + if has_captcha(str(response)): + return render_template( + 'error.html', + blocked=True, + error_message=translation['ratelimit'], + translation=translation, + farside='https://farside.link', + config=g.user_config, + query=urlparse.unquote(query), + params=g.user_config.to_params()), 503 response = bold_search_terms(response, query) # Feature to display IP address @@ -264,11 +341,19 @@ def search(): search_util.search_type, translation) + # Feature to display currency_card + conversion = check_currency(str(response)) + if conversion: + html_soup = bsoup(str(response), 'html.parser') + response = add_currency_card(html_soup, conversion) + return render_template( 'display.html', + newest_version=newest_version, query=urlparse.unquote(query), search_type=search_util.search_type, config=g.user_config, + autocomplete_enabled=autocomplete_enabled, lingva_url=app.config['TRANSLATE_URL'], translation=translation, translate_to=translate_to, @@ -292,10 +377,13 @@ def search(): tabs=tabs)), resp_code -@app.route('/config', methods=['GET', 'POST', 'PUT']) +@app.route(f'/{Endpoint.config}', methods=['GET', 'POST', 'PUT']) +@session_required @auth_required def config(): - config_disabled = app.config['CONFIG_DISABLE'] + config_disabled = ( + app.config['CONFIG_DISABLE'] or + not valid_user_session(session)) if request.method == 'GET': return json.dumps(g.user_config.__dict__) elif request.method == 'PUT' and not config_disabled: @@ -322,18 +410,14 @@ def config(): app.config['CONFIG_PATH'], request.args.get('name')), 'wb')) - # Overwrite default config if user has cookies disabled - if g.cookies_disabled: - open(app.config['DEFAULT_CONFIG'], 'w').write( - json.dumps(config_data, indent=4)) - session['config'] = config_data return redirect(config_data['url']) else: return redirect(url_for('.index'), code=403) -@app.route('/url', methods=['GET']) +@app.route(f'/{Endpoint.url}', methods=['GET']) +@session_required @auth_required def url(): if 'url' in request.args: @@ -348,16 +432,18 @@ def url(): error_message='Unable to resolve query: ' + q) -@app.route('/imgres') +@app.route(f'/{Endpoint.imgres}') +@session_required @auth_required def imgres(): return redirect(request.args.get('imgurl')) -@app.route('/element') +@app.route(f'/{Endpoint.element}') +@session_required @auth_required def element(): - cipher_suite = Fernet(session['key']) + cipher_suite = Fernet(g.session_key) src_url = cipher_suite.decrypt(request.args.get('url').encode()).decode() src_type = request.args.get('type') @@ -376,7 +462,7 @@ def element(): return send_file(io.BytesIO(empty_gif), mimetype='image/gif') -@app.route('/window') +@app.route(f'/{Endpoint.window}') @auth_required def window(): get_body = g.user_request.send(base_url=request.args.get('location')).text @@ -457,7 +543,8 @@ def run_app() -> None: os.environ['WHOOGLE_PROXY_TYPE'] = args.proxytype os.environ['WHOOGLE_PROXY_LOC'] = args.proxyloc - os.environ['HTTPS_ONLY'] = '1' if args.https_only else '' + if args.https_only: + os.environ['HTTPS_ONLY'] = '1' if args.debug: app.run(host=args.host, port=args.port, debug=args.debug) diff --git a/app/static/css/dark-theme.css b/app/static/css/dark-theme.css index 2d17c00..38df90a 100644 --- a/app/static/css/dark-theme.css +++ b/app/static/css/dark-theme.css @@ -138,10 +138,14 @@ select { color: var(--whoogle-dark-contrast-text) !important; } -#gh-link { +.link { color: var(--whoogle-dark-contrast-text); } +.link-color { + color: var(--whoogle-dark-result-url) !important; +} + .autocomplete-items { border: 1px solid var(--whoogle-dark-element-bg); } @@ -187,6 +191,10 @@ path { color: var(--whoogle-dark-text) !important; } -.ip-text-div{ +.ip-text-div, .update_available, .cb_label, .cb { color: var(--whoogle-dark-secondary-text) !important; } + +.cb:focus { + color: var(--whoogle-dark-contrast-text) !important; +} diff --git a/app/static/css/error.css b/app/static/css/error.css new file mode 100644 index 0000000..faea591 --- /dev/null +++ b/app/static/css/error.css @@ -0,0 +1,9 @@ +html { + font-size: 1.3rem; +} + +@media (max-width: 1000px) { + html { + font-size: 3rem; + } +} diff --git a/app/static/css/input.css b/app/static/css/input.css index 96cc108..2da2aa3 100644 --- a/app/static/css/input.css +++ b/app/static/css/input.css @@ -12,3 +12,30 @@ height: 40px; width: 50px; } +.ZINbbc.xpd.O9g5cc.uUPGi input::-webkit-outer-spin-button, +input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +.cb { + width: 40%; + overflow: hidden; + text-align: left; + line-height: 28px; + background: transparent; + border-radius: 6px; + border: 1px solid #5f6368; + font-size: 14px !important; + height: 36px; + padding: 0 0 0 12px; + margin: 10px 10px 10px 0; +} + +.conversion_box { + margin-top: 15px; +} + +.ZINbbc.xpd.O9g5cc.uUPGi input:focus-visible { + outline: 0; +} diff --git a/app/static/css/light-theme.css b/app/static/css/light-theme.css index fbe3ad6..2562555 100644 --- a/app/static/css/light-theme.css +++ b/app/static/css/light-theme.css @@ -125,10 +125,14 @@ input { color: var(--whoogle-contrast-text); } -#gh-link { +.link { color: var(--whoogle-element-bg); } +.link-color { + color: var(--whoogle-result-url) !important; +} + .autocomplete-items { border: 1px solid var(--whoogle-element-bg); } @@ -175,6 +179,10 @@ path { border-bottom: 0px; } -.ip-text-div{ +.ip-text-div, .update_available, .cb_label, .cb { color: var(--whoogle-secondary-text) !important; } + +.cb:focus { + color: var(--whoogle-text) !important; +} diff --git a/app/static/css/logo.css b/app/static/css/logo.css index 6aebfa4..0dfe8bb 100644 --- a/app/static/css/logo.css +++ b/app/static/css/logo.css @@ -12,6 +12,7 @@ a { @media (max-width: 1000px) { svg { - margin-top: .7em; + margin-top: .3em; + height: 70%; } } diff --git a/app/static/css/main.css b/app/static/css/main.css index c937ee2..e84133a 100644 --- a/app/static/css/main.css +++ b/app/static/css/main.css @@ -61,6 +61,15 @@ body { -webkit-appearance: none; } +.config-options { + max-height: 370px; + overflow-y: scroll; +} + +.config-buttons { + max-height: 30px; +} + .config-div { padding: 5px; } @@ -102,7 +111,6 @@ button::-moz-focus-inner { } .open { - overflow-y: scroll; padding-bottom: 20px; } @@ -136,6 +144,7 @@ footer { .whoogle-svg { width: 80%; + height: initial; display: block; margin: auto; padding-bottom: 10px; @@ -168,3 +177,10 @@ details summary { padding: 10px; font-weight: bold; } + +/* Mobile styles */ +@media (max-width: 1000px) { + select { + width: 100%; + } +} diff --git a/app/static/css/search.css b/app/static/css/search.css index 19fb385..1e76664 100644 --- a/app/static/css/search.css +++ b/app/static/css/search.css @@ -26,6 +26,10 @@ details summary { font-weight: bold; } +details summary span { + font-weight: normal; +} + #lingva-iframe { width: 100%; height: 650px; diff --git a/app/static/js/currency.js b/app/static/js/currency.js new file mode 100644 index 0000000..b669596 --- /dev/null +++ b/app/static/js/currency.js @@ -0,0 +1,9 @@ +const convert = (n1, n2, conversionFactor) => { + // id's for currency input boxes + let id1 = "cb" + n1; + let id2 = "cb" + n2; + // getting the value of the input box that just got filled + let inputBox = document.getElementById(id1).value; + // updating the other input box after conversion + document.getElementById(id2).value = ((inputBox * conversionFactor).toFixed(2)); +} diff --git a/app/static/settings/countries.json b/app/static/settings/countries.json index 061b524..43da3b6 100644 --- a/app/static/settings/countries.json +++ b/app/static/settings/countries.json @@ -1,248 +1,247 @@ [ {"name": "-------", "value": ""}, - {"name": "Afghanistan", "value": "countryAF"}, - {"name": "Albania", "value": "countryAL"}, - {"name": "Algeria", "value": "countryDZ"}, - {"name": "American Samoa", "value": "countryAS"}, - {"name": "Andorra", "value": "countryAD"}, - {"name": "Angola", "value": "countryAO"}, - {"name": "Anguilla", "value": "countryAI"}, - {"name": "Antarctica", "value": "countryAQ"}, - {"name": "Antigua and Barbuda", "value": "countryAG"}, - {"name": "Argentina", "value": "countryAR"}, - {"name": "Armenia", "value": "countryAM"}, - {"name": "Aruba", "value": "countryAW"}, - {"name": "Australia", "value": "countryAU"}, - {"name": "Austria", "value": "countryAT"}, - {"name": "Azerbaijan", "value": "countryAZ"}, - {"name": "Bahamas", "value": "countryBS"}, - {"name": "Bahrain", "value": "countryBH"}, - {"name": "Bangladesh", "value": "countryBD"}, - {"name": "Barbados", "value": "countryBB"}, - {"name": "Belarus", "value": "countryBY"}, - {"name": "Belgium", "value": "countryBE"}, - {"name": "Belize", "value": "countryBZ"}, - {"name": "Benin", "value": "countryBJ"}, - {"name": "Bermuda", "value": "countryBM"}, - {"name": "Bhutan", "value": "countryBT"}, - {"name": "Bolivia", "value": "countryBO"}, - {"name": "Bosnia and Herzegovina", "value": "countryBA"}, - {"name": "Botswana", "value": "countryBW"}, - {"name": "Bouvet Island", "value": "countryBV"}, - {"name": "Brazil", "value": "countryBR"}, - {"name": "British Indian Ocean Territory", "value": "countryIO"}, - {"name": "Brunei Darussalam", "value": "countryBN"}, - {"name": "Bulgaria", "value": "countryBG"}, - {"name": "Burkina Faso", "value": "countryBF"}, - {"name": "Burundi", "value": "countryBI"}, - {"name": "Cambodia", "value": "countryKH"}, - {"name": "Cameroon", "value": "countryCM"}, - {"name": "Canada", "value": "countryCA"}, - {"name": "Cape Verde", "value": "countryCV"}, - {"name": "Cayman Islands", "value": "countryKY"}, - {"name": "Central African Republic", "value": "countryCF"}, - {"name": "Chad", "value": "countryTD"}, - {"name": "Chile", "value": "countryCL"}, - {"name": "China", "value": "countryCN"}, - {"name": "Christmas Island", "value": "countryCX"}, - {"name": "Cocos (Keeling) Islands", "value": "countryCC"}, - {"name": "Colombia", "value": "countryCO"}, - {"name": "Comoros", "value": "countryKM"}, - {"name": "Congo", "value": "countryCG"}, - {"name": "Congo, Democratic Republic of the", "value": "countryCD"}, - {"name": "Cook Islands", "value": "countryCK"}, - {"name": "Costa Rica", "value": "countryCR"}, - {"name": "Cote D\"ivoire", "value": "countryCI"}, - {"name": "Croatia (Hrvatska)", "value": "countryHR"}, - {"name": "Cuba", "value": "countryCU"}, - {"name": "Cyprus", "value": "countryCY"}, - {"name": "Czech Republic", "value": "countryCZ"}, - {"name": "Denmark", "value": "countryDK"}, - {"name": "Djibouti", "value": "countryDJ"}, - {"name": "Dominica", "value": "countryDM"}, - {"name": "Dominican Republic", "value": "countryDO"}, - {"name": "East Timor", "value": "countryTP"}, - {"name": "Ecuador", "value": "countryEC"}, - {"name": "Egypt", "value": "countryEG"}, - {"name": "El Salvador", "value": "countrySV"}, - {"name": "Equatorial Guinea", "value": "countryGQ"}, - {"name": "Eritrea", "value": "countryER"}, - {"name": "Estonia", "value": "countryEE"}, - {"name": "Ethiopia", "value": "countryET"}, - {"name": "European Union", "value": "countryEU"}, - {"name": "Falkland Islands (Malvinas)", "value": "countryFK"}, - {"name": "Faroe Islands", "value": "countryFO"}, - {"name": "Fiji", "value": "countryFJ"}, - {"name": "Finland", "value": "countryFI"}, - {"name": "France", "value": "countryFR"}, - {"name": "France, Metropolitan", "value": "countryFX"}, - {"name": "French Guiana", "value": "countryGF"}, - {"name": "French Polynesia", "value": "countryPF"}, - {"name": "French Southern Territories", "value": "countryTF"}, - {"name": "Gabon", "value": "countryGA"}, - {"name": "Gambia", "value": "countryGM"}, - {"name": "Georgia", "value": "countryGE"}, - {"name": "Germany", "value": "countryDE"}, - {"name": "Ghana", "value": "countryGH"}, - {"name": "Gibraltar", "value": "countryGI"}, - {"name": "Greece", "value": "countryGR"}, - {"name": "Greenland", "value": "countryGL"}, - {"name": "Grenada", "value": "countryGD"}, - {"name": "Guadeloupe", "value": "countryGP"}, - {"name": "Guam", "value": "countryGU"}, - {"name": "Guatemala", "value": "countryGT"}, - {"name": "Guinea", "value": "countryGN"}, - {"name": "Guinea-Bissau", "value": "countryGW"}, - {"name": "Guyana", "value": "countryGY"}, - {"name": "Haiti", "value": "countryHT"}, - {"name": "Heard Island and Mcdonald Islands", "value": "countryHM"}, - {"name": "Holy See (Vatican City State)", "value": "countryVA"}, - {"name": "Honduras", "value": "countryHN"}, - {"name": "Hong Kong", "value": "countryHK"}, - {"name": "Hungary", "value": "countryHU"}, - {"name": "Iceland", "value": "countryIS"}, - {"name": "India", "value": "countryIN"}, - {"name": "Indonesia", "value": "countryID"}, - {"name": "Iran, Islamic Republic of", "value": "countryIR"}, - {"name": "Iraq", "value": "countryIQ"}, - {"name": "Ireland", "value": "countryIE"}, - {"name": "Israel", "value": "countryIL"}, - {"name": "Italy", "value": "countryIT"}, - {"name": "Jamaica", "value": "countryJM"}, - {"name": "Japan", "value": "countryJP"}, - {"name": "Jordan", "value": "countryJO"}, - {"name": "Kazakhstan", "value": "countryKZ"}, - {"name": "Kenya", "value": "countryKE"}, - {"name": "Kiribati", "value": "countryKI"}, - {"name": "Korea, Democratic People\"s Republic of", - "value": "countryKP"}, - {"name": "Korea, Republic of", "value": "countryKR"}, - {"name": "Kuwait", "value": "countryKW"}, - {"name": "Kyrgyzstan", "value": "countryKG"}, - {"name": "Lao People\"s Democratic Republic", "value": "countryLA"}, - {"name": "Latvia", "value": "countryLV"}, - {"name": "Lebanon", "value": "countryLB"}, - {"name": "Lesotho", "value": "countryLS"}, - {"name": "Liberia", "value": "countryLR"}, - {"name": "Libyan Arab Jamahiriya", "value": "countryLY"}, - {"name": "Liechtenstein", "value": "countryLI"}, - {"name": "Lithuania", "value": "countryLT"}, - {"name": "Luxembourg", "value": "countryLU"}, - {"name": "Macao", "value": "countryMO"}, + {"name": "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": "countryMK"}, - {"name": "Madagascar", "value": "countryMG"}, - {"name": "Malawi", "value": "countryMW"}, - {"name": "Malaysia", "value": "countryMY"}, - {"name": "Maldives", "value": "countryMV"}, - {"name": "Mali", "value": "countryML"}, - {"name": "Malta", "value": "countryMT"}, - {"name": "Marshall Islands", "value": "countryMH"}, - {"name": "Martinique", "value": "countryMQ"}, - {"name": "Mauritania", "value": "countryMR"}, - {"name": "Mauritius", "value": "countryMU"}, - {"name": "Mayotte", "value": "countryYT"}, - {"name": "Mexico", "value": "countryMX"}, - {"name": "Micronesia, Federated States of", "value": "countryFM"}, - {"name": "Moldova, Republic of", "value": "countryMD"}, - {"name": "Monaco", "value": "countryMC"}, - {"name": "Mongolia", "value": "countryMN"}, - {"name": "Montserrat", "value": "countryMS"}, - {"name": "Morocco", "value": "countryMA"}, - {"name": "Mozambique", "value": "countryMZ"}, - {"name": "Myanmar", "value": "countryMM"}, - {"name": "Namibia", "value": "countryNA"}, - {"name": "Nauru", "value": "countryNR"}, - {"name": "Nepal", "value": "countryNP"}, - {"name": "Netherlands", "value": "countryNL"}, - {"name": "Netherlands Antilles", "value": "countryAN"}, - {"name": "New Caledonia", "value": "countryNC"}, - {"name": "New Zealand", "value": "countryNZ"}, - {"name": "Nicaragua", "value": "countryNI"}, - {"name": "Niger", "value": "countryNE"}, - {"name": "Nigeria", "value": "countryNG"}, - {"name": "Niue", "value": "countryNU"}, - {"name": "Norfolk Island", "value": "countryNF"}, - {"name": "Northern Mariana Islands", "value": "countryMP"}, - {"name": "Norway", "value": "countryNO"}, - {"name": "Oman", "value": "countryOM"}, - {"name": "Pakistan", "value": "countryPK"}, - {"name": "Palau", "value": "countryPW"}, - {"name": "Palestinian Territory", "value": "countryPS"}, - {"name": "Panama", "value": "countryPA"}, - {"name": "Papua New Guinea", "value": "countryPG"}, - {"name": "Paraguay", "value": "countryPY"}, - {"name": "Peru", "value": "countryPE"}, - {"name": "Philippines", "value": "countryPH"}, - {"name": "Pitcairn", "value": "countryPN"}, - {"name": "Poland", "value": "countryPL"}, - {"name": "Portugal", "value": "countryPT"}, - {"name": "Puerto Rico", "value": "countryPR"}, - {"name": "Qatar", "value": "countryQA"}, - {"name": "Reunion", "value": "countryRE"}, - {"name": "Romania", "value": "countryRO"}, - {"name": "Russian Federation", "value": "countryRU"}, - {"name": "Rwanda", "value": "countryRW"}, - {"name": "Saint Helena", "value": "countrySH"}, - {"name": "Saint Kitts and Nevis", "value": "countryKN"}, - {"name": "Saint Lucia", "value": "countryLC"}, - {"name": "Saint Pierre and Miquelon", "value": "countryPM"}, - {"name": "Saint Vincent and the Grenadines", "value": "countryVC"}, - {"name": "Samoa", "value": "countryWS"}, - {"name": "San Marino", "value": "countrySM"}, - {"name": "Sao Tome and Principe", "value": "countryST"}, - {"name": "Saudi Arabia", "value": "countrySA"}, - {"name": "Senegal", "value": "countrySN"}, - {"name": "Serbia and Montenegro", "value": "countryCS"}, - {"name": "Seychelles", "value": "countrySC"}, - {"name": "Sierra Leone", "value": "countrySL"}, - {"name": "Singapore", "value": "countrySG"}, - {"name": "Slovakia", "value": "countrySK"}, - {"name": "Slovenia", "value": "countrySI"}, - {"name": "Solomon Islands", "value": "countrySB"}, - {"name": "Somalia", "value": "countrySO"}, - {"name": "South Africa", "value": "countryZA"}, + "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": "countryGS"}, - {"name": "Spain", "value": "countryES"}, - {"name": "Sri Lanka", "value": "countryLK"}, - {"name": "Sudan", "value": "countrySD"}, - {"name": "Suriname", "value": "countrySR"}, - {"name": "Svalbard and Jan Mayen", "value": "countrySJ"}, - {"name": "Swaziland", "value": "countrySZ"}, - {"name": "Sweden", "value": "countrySE"}, - {"name": "Switzerland", "value": "countryCH"}, - {"name": "Syrian Arab Republic", "value": "countrySY"}, - {"name": "Taiwan", "value": "countryTW"}, - {"name": "Tajikistan", "value": "countryTJ"}, - {"name": "Tanzania, United Republic of", "value": "countryTZ"}, - {"name": "Thailand", "value": "countryTH"}, - {"name": "Togo", "value": "countryTG"}, - {"name": "Tokelau", "value": "countryTK"}, - {"name": "Tonga", "value": "countryTO"}, - {"name": "Trinidad and Tobago", "value": "countryTT"}, - {"name": "Tunisia", "value": "countryTN"}, - {"name": "Turkey", "value": "countryTR"}, - {"name": "Turkmenistan", "value": "countryTM"}, - {"name": "Turks and Caicos Islands", "value": "countryTC"}, - {"name": "Tuvalu", "value": "countryTV"}, - {"name": "Uganda", "value": "countryUG"}, - {"name": "Ukraine", "value": "countryUA"}, - {"name": "United Arab Emirates", "value": "countryAE"}, - {"name": "United Kingdom", "value": "countryUK"}, - {"name": "United States", "value": "countryUS"}, - {"name": "United States Minor Outlying Islands", "value": "countryUM"}, - {"name": "Uruguay", "value": "countryUY"}, - {"name": "Uzbekistan", "value": "countryUZ"}, - {"name": "Vanuatu", "value": "countryVU"}, - {"name": "Venezuela", "value": "countryVE"}, - {"name": "Vietnam", "value": "countryVN"}, - {"name": "Virgin Islands, British", "value": "countryVG"}, - {"name": "Virgin Islands, U.S.", "value": "countryVI"}, - {"name": "Wallis and Futuna", "value": "countryWF"}, - {"name": "Western Sahara", "value": "countryEH"}, - {"name": "Yemen", "value": "countryYE"}, - {"name": "Yugoslavia", "value": "countryYU"}, - {"name": "Zambia", "value": "countryZM"}, - {"name": "Zimbabwe", "value": "countryZW"} + "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"} ] diff --git a/app/static/settings/translations.json b/app/static/settings/translations.json index cef9adc..4797d0d 100644 --- a/app/static/settings/translations.json +++ b/app/static/settings/translations.json @@ -2,8 +2,7 @@ "lang_en": { "search": "Search", "config": "Configuration", - "config-country": "Filter Results by Country", - "config-country-help": "Note: If enabled, a website will only appear in the search results if it is *hosted* in the selected country.", + "config-country": "Set Country", "config-lang": "Interface Language", "config-lang-search": "Search Language", "config-near": "Near", @@ -35,6 +34,8 @@ "light": "light", "dark": "dark", "system": "system", + "ratelimit": "Instance has been ratelimited", + "continue-search": "Continue your search with ", "all": "All", "images": "Images", "maps": "Maps", @@ -45,8 +46,7 @@ "lang_nl": { "search": "Zoeken", "config": "Instellingen", - "config-country": "Filter zoek resultaten bij land", - "config-country-help": "Let op: Als je dit aanzet zal alleen website die gehost worden in het land weergegeven worden.", + "config-country": "Land instellen", "config-lang": "Taal instellingen", "config-lang-search": "Zoek taal", "config-near": "Dichtbij", @@ -77,13 +77,14 @@ "translate": "vertalen", "light": "helder", "dark": "donker", - "system": "systeeminstellingen" + "system": "systeeminstellingen", + "ratelimit": "Instantie is beperkt in snelheid", + "continue-search": "Ga verder met zoeken met " }, "lang_de": { "search": "Suchen", "config": "Einstellungen", - "config-country": "Ergebnisse nach Land filtern", - "config-country-help": "Hinweis: Wenn aktiv, wird eine Webseite nur angezeigt, wenn sie auch in dem jeweiligen Land *gehosted* wird.", + "config-country": "Land einstellen", "config-lang": "Oberflächen-Sprache", "config-lang-search": "Such-Sprache", "config-near": "In der Nähe von", @@ -114,13 +115,14 @@ "translate": "Übersetzen", "light": "hell", "dark": "dunkel", - "system": "Systemeinstellung" + "system": "Systemeinstellung", + "ratelimit": "Instanz wurde ratenbegrenzt", + "continue-search": "Setzen Sie Ihre Suche fort mit " }, "lang_es": { "search": "Buscar", "config": "Configuración", - "config-country": "Filtrar Resultados por País", - "config-country-help": "Nota: Si está habilitado, un sitio web solo aparecerá en los resultados de búsqueda si está alojado en ese país.", + "config-country": "Establecer País", "config-lang": "Idioma de Interfaz", "config-lang-search": "Idioma de Búsqueda", "config-near": "Cerca", @@ -151,13 +153,14 @@ "translate": "traducir", "light": "brillante", "dark": "oscuro", - "system": "configuración del sistema" + "system": "configuración del sistema", + "ratelimit": "La instancia ha sido ratelimited", + "continue-search": "Continúe su búsqueda con " }, "lang_it": { "search": "Cerca", "config": "Impostazioni", - "config-country": "Filtra risultati per paese", - "config-country-help": "Nota: se abilitato, il sito sarà presente tra i risultati se e soltanto se il server risiede nel paese selezionato", + "config-country": "Imposta Paese", "config-lang": "Lingua dell'interfaccia", "config-lang-search": "Lingua della ricerca", "config-near": "Vicino", @@ -188,13 +191,14 @@ "translate": "tradurre", "light": "luminoso", "dark": "notte", - "system": "impostazioni di sistema" + "system": "impostazioni di sistema", + "ratelimit": "L'istanza è stata limitata alla velocità", + "continue-search": "Continua la tua ricerca con " }, "lang_pt": { "search": "Pesquisar", "config": "Configuração", - "config-country": "Filtrar Resultados por País", - "config-country-help": "Observação: Se ativado, um site só aparecerá nos resultados da pesquisa se estiver *hospedado* no país selecionado.", + "config-country": "Definir País", "config-lang": "Idioma da Interface", "config-lang-search": "Idioma da Pesquisa", "config-near": "Perto", @@ -225,13 +229,52 @@ "translate": "traduzir", "light": "brilhante", "dark": "escuro", - "system": "configuração de sistema" + "system": "configuração de sistema", + "ratelimit": "A instância foi limitada pela taxa", + "continue-search": "Continue sua pesquisa com " + }, + "lang_ru": { + "search": "Поиск", + "config": "Настройка", + "config-country": "Установить страну", + "config-lang": "Язык интерфейса", + "config-lang-search": "Язык поиска", + "config-near": "Около", + "config-near-help": "Название города", + "config-block": "Блокировать", + "config-block-help": "Список сайтов, разделенный запятыми", + "config-block-title": "Блокировать по названию", + "config-block-title-help": "Используйте regex", + "config-block-url": "Блокировать по URL-адресу", + "config-block-url-help": "Используйте regex", + "config-theme": "Оформление", + "config-nojs": "Показывать ссылки NoJS", + "config-dark": "Темный режим", + "config-safe": "Безопасный поиск", + "config-alts": "Заменить ссылки на социальные сети", + "config-alts-help": "Замена ссылкок Twitter, YouTube, Instagram и т.д. на альтернативы, уважающие конфиденциальность.", + "config-new-tab": "Открывать ссылки в новой вкладке", + "config-images": "Поиск полноразмерных изображений", + "config-images-help": "(Экспериментально) Добавляет опцию 'Просмотр изображения' к поиску изображений в ПК-режиме. Это приведет к тому, что миниатюры изображений будут иметь более низкое разрешение.", + "config-tor": "Использовать Tor", + "config-get-only": "Только GET-запросы", + "config-url": "Корневой URL-адрес", + "config-css": "Пользовательский CSS", + "load": "Загрузить", + "apply": "Применить", + "save-as": "Сохранить как...", + "github-link": "Посмотреть в GitHub", + "translate": "перевести", + "light": "светлое", + "dark": "темное", + "system": "системное", + "ratelimit": "Число экземпляров ограничено", + "continue-search": "Продолжайте поиск с " }, "lang_zh-CN": { "search": "搜索", "config": "配置", - "config-country": "按国家过滤搜索结果", - "config-country-help": "注意:启用后,只有在所选国家*部署*的网站会出现在搜索结果中。", + "config-country": "设置国家", "config-lang": "界面语言", "config-lang-search": "搜索语言", "config-near": "接近", @@ -262,13 +305,14 @@ "translate": "翻译", "light": "明亮的", "dark": "黑暗的", - "system": "系统设置" + "system": "系统设置", + "ratelimit": "实例已被限速", + "continue-search": "继续搜索 " }, "lang_si": { "search": "සොයන්න", "config": "වින්‍යාසය", - "config-country": "රට අනුව ප්‍රතිඵල පෙරන්න", - "config-country-help": "සටහන: සබල කර ඇත්නම්, වියමන අඩවියක් සෙවුම් ප්‍රතිඵලවල දිස්වන්නේ එය තෝරාගත් රටෙහි සිට *සත්කාරකත්වය* දරන්නේ නම් පමණි.", + "config-country": "රට සකසන්න", "config-lang": "අතුරු මුහුණතෙහි භාෂාව", "config-lang-search": "සෙවුම් භාෂාව", "config-near": "ආසන්න", @@ -299,13 +343,14 @@ "translate": "පරිවර්තනය කරන්න", "light": "දීප්තිමත්", "dark": "අඳුරු", - "system": "පද්ධතිය" + "system": "පද්ධතිය", + "ratelimit": "උදාහරණය අනුපාත කර ඇත", + "continue-search": "සමඟ ඔබේ සෙවීම දිගටම කරගෙන යන්න" }, "lang_fr": { "search": "Chercher", "config": "Configuration", - "config-country": "Filter les Résultats par Pays", - "config-country-help": "Note : Si activé, un site web va uniquement apparaitre dans les résultat de la recherche si il est *hébérgé* dans le pays sélectionné.", + "config-country": "Définir le pays", "config-lang": "Langage de l'Interface", "config-lang-search": "Langage de Recherche", "config-near": "Proche", @@ -336,13 +381,14 @@ "translate": "Traduire", "light": "clair", "dark": "sombre", - "system": "système" + "system": "système", + "ratelimit": "Le débit de l'instance a été limité", + "continue-search": "Continuez votre recherche avec " }, "lang_fa": { "search": "جستجو", "config": "پیکربندی", - "config-country": "فیلتر نتایج بر اساس کشور", - "config-country-help": "توجه: در صورت فعال بودن، وبسایت تنها در صورتی نمایش داده می‌شود که *در کشور انتخابی میزبانی شده باشد*.", + "config-country": "کشور را تنظیم کنید", "config-lang": "زبان رابط کاربری", "config-lang-search": "زبان جستجو", "config-near": "نزدیک", @@ -373,13 +419,14 @@ "translate": "ترجمه", "light": "روشن", "dark": "تیره", - "system": "سیستم" + "system": "سیستم", + "ratelimit": "نمونه با نرخ محدود شده است", + "continue-search": "جستجوی خود را با " }, "lang_cs": { "search": "Hledat", "config": "Konfigurace", - "config-country": "Filtrovat výsledky podle země", - "config-country-help": "Poznámka: Pokud je povoleno, webová stránka se objeví ve výsledcích vyhledávání, pouze pokud je *hostována* ve vybrané zemi.", + "config-country": "Nastavte zemi", "config-lang": "Jazyk rozhraní", "config-lang-search": "Jazyk vyhledávání", "config-near": "Poblíž", @@ -410,13 +457,14 @@ "translate": "Přeložit", "light": "Světlý", "dark": "Tmavý", - "system": "Systémový" + "system": "Systémový", + "ratelimit": "Instance byla omezena sazbou", + "continue-search": "Pokračujte ve vyhledávání pomocí " }, "lang_zh-TW": { "search": "搜尋", "config": "設定", - "config-country": "依國家過濾結果", - "config-country-help": "注意:一經套用,只有在部署在指定國家內的網站會出現在搜尋結果中。", + "config-country": "設置國家", "config-lang": "界面語言", "config-lang-search": "搜尋語言", "config-near": "接近", @@ -447,13 +495,14 @@ "translate": "翻譯", "light": "明亮的", "dark": "黑暗的", - "system": "依系統" + "system": "依系統", + "ratelimit": "實例已被限速", + "continue-search": "繼續搜索 " }, "lang_bg": { "search": "Търсене", "config": "Конфигурация", - "config-country": "Филтрирай резултатите по държави", - "config-country-help": "Забележка: Ако това е разрешено, уебсайтoвете ще се показват в резултатите от търсенето, само ако са * хоствани * в избраната държава.", + "config-country": "Задайте държава", "config-lang": "Език на интерфейса", "config-lang-search": "Език за търсене", "config-near": "Близо до", @@ -484,13 +533,14 @@ "translate": "превод", "light": "светла", "dark": "тъмна", - "system": "системна" + "system": "системна", + "ratelimit": "Екземплярът е с ограничена скорост", + "continue-search": "Продължете търсенето си с " }, "lang_hi": { "search": "खोज", "config": "कॉन्फ़िगरेशन", - "config-country": "देश के अनुसार परिणाम फ़िल्टर करें", - "config-country-help": "नोट: यदि सक्षम है, तो कोई वेबसाइट खोज परिणामों में केवल तभी दिखाई देगी जब वह चयनित देश में *होस्ट* हो।", + "config-country": "देश सेट करें", "config-lang": "इंटरफ़ेस भाषा", "config-lang-search": "खोज की भाषा", "config-near": "पास", @@ -521,6 +571,46 @@ "translate": "अनुवाद करना", "light": "रोशनी", "dark": "अंधेरा", - "system": "प्रणाली" + "system": "प्रणाली", + "ratelimit": "इंस्टेंस को सीमित कर दिया गया है", + "continue-search": "के साथ अपनी खोज जारी रखें " + }, + "lang_ja": { + "search": "検索", + "config": "設定", + "config-country": "国を設定する", + "config-lang": "インタフェースの言語", + "config-lang-search": "検索する言語", + "config-near": "場所", + "config-near-help": "街の名前", + "config-block": "ブロック", + "config-block-help": "サイトのリストをコンマ区切りで入力", + "config-block-title": "タイトルでブロック", + "config-block-title-help": "正規表現を使用します", + "config-block-url": "でブロック", + "config-block-url-help": "正規表現を使用", + "config-theme": "テーマ", + "config-nojs": "非JSリンクを表示", + "config-dark": "ダークモード", + "config-safe": "セーフサーチ", + "config-alts": "ソーシャルメディアのリンクを置き換え", + "config-alts-help": "Twitter/YouTube/Instagramなどのリンクを、プライバシーを尊重した代替サイトに置き換えます。", + "config-new-tab": "新しいタブでリンクを開く", + "config-images": "フルサイズの画像を検索", + "config-images-help": "(実験的) デスクトップの画像検索に「画像を表示」オプションを追加します。これにより、画像検索結果のサムネイルの解像度が低くなります。", + "config-tor": "Torを使用", + "config-get-only": "GETリクエストのみ", + "config-url": "ルートURL", + "config-css": "カスタムCSS", + "load": "読み込み", + "apply": "反映", + "save-as": "名前を付けて保存", + "github-link": "Githubで確認", + "translate": "翻訳", + "light": "ライト", + "dark": "ダーク", + "system": "自動", + "ratelimit": "インスタンスはレート制限されています", + "continue-search": "で検索を続ける " } } diff --git a/app/templates/display.html b/app/templates/display.html index 4f2c109..e4eea3e 100644 --- a/app/templates/display.html +++ b/app/templates/display.html @@ -5,6 +5,7 @@ + @@ -33,13 +34,11 @@ {% endif %} {{ response|safe }} - - +{% include 'footer.html' %} +{% if autocomplete_enabled == '1' %} + +{% endif %} + diff --git a/app/templates/error.html b/app/templates/error.html index efa3f79..687327f 100644 --- a/app/templates/error.html +++ b/app/templates/error.html @@ -1,6 +1,40 @@ -

Error

-
-

- Error: "{{ error_message|safe }}" -

-Return Home +{% if config.theme %} + {% if config.theme == 'system' %} + + {% else %} + + {% endif %} +{% else %} + +{% endif %} + + + +
+

Error

+

+ {{ error_message|safe }} +

+
+

+ {% if blocked is defined %} +

{{ translation['continue-search'] }} Farside!

+ Whoogle: +
+ + {{farside}}/whoogle/search?q={{query}}{{params}} + +

+ Searx: +
+ + {{farside}}/searx/search?q={{query}} + +
+ {% endif %} +

+ Return Home +
diff --git a/app/templates/footer.html b/app/templates/footer.html new file mode 100644 index 0000000..ed66e42 --- /dev/null +++ b/app/templates/footer.html @@ -0,0 +1,9 @@ + diff --git a/app/templates/index.html b/app/templates/index.html index cf8f342..a05f3b4 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -17,10 +17,13 @@ - + {% if autocomplete_enabled == '1' %} + + {% endif %} + {% if config.theme %} {% if config.theme == 'system' %}